326fddae
spike: validate channel mechanism, fix meta schema and capability
a73x 2026-04-29 05:41
Two corrections from end-to-end smoke against Claude Code 2.1.121:
1. MCP server must declare experimental.claude/channel capability in
the initialize response, or Claude Code silently skips channel
registration ("server did not declare claude/channel capability").
2. notifications/claude/channel meta is Record<string, string>; non-
string values (e.g. line as number) are silently rejected by Zod
validation. Line is now stringified.
The original spec and Task 10/11 plan code had line as number, citing
"don't use string-typed integers." Reverted: schema requires string.
diff --git a/docs/superpowers/plans/2026-04-28-claudealong-filewatch-mcp.md b/docs/superpowers/plans/2026-04-28-claudealong-filewatch-mcp.md index e85ed06..7cf4641 100644 --- a/docs/superpowers/plans/2026-04-28-claudealong-filewatch-mcp.md +++ b/docs/superpowers/plans/2026-04-28-claudealong-filewatch-mcp.md @@ -45,10 +45,11 @@ func (w *Watcher) Start(ctx context.Context) error func (w *Watcher) Close() error // internal/mcp // All fields are strings: schema is Record<string, string>. type ChannelMeta struct { Source string // always "filewatch" File string Line int Line string // stringified line number ReplyTo string // "fw-XXXXXXXX" } type Server struct{ /* unexported */ } @@ -1662,8 +1663,8 @@ func TestSendChannelEmitsCorrectFrame(t *testing.T) { t.Errorf("meta wrong: %#v", meta) } // line should be a number, not a string if l, ok := meta["line"].(float64); !ok || l != 42 { t.Errorf("line = %v (%T), want 42 (number)", meta["line"], meta["line"]) if s, ok := meta["line"].(string); !ok || s != "42" { t.Errorf("line = %v (%T), want \"42\" (string)", meta["line"], meta["line"]) } } ``` @@ -1688,10 +1689,12 @@ import ( "sync" ) // Note: all meta fields are strings — Claude Code's schema is // Record<string, string> and silently drops non-string values. type ChannelMeta struct { Source string `json:"source"` File string `json:"file"` Line int `json:"line"` Line string `json:"line"` ReplyTo string `json:"replyTo"` } @@ -1761,9 +1764,21 @@ func (s *Server) Run(ctx context.Context) error { var result any switch method { case "initialize": // Echo the client's protocolVersion so we accept whatever they speak. params, _ := req["params"].(map[string]any) protoVer, _ := params["protocolVersion"].(string) if protoVer == "" { protoVer = "2024-11-05" } // experimental.claude/channel capability is REQUIRED — Claude Code // silently skips channel registration without it. result = map[string]any{ "protocolVersion": "2024-11-05", "capabilities": map[string]any{}, "protocolVersion": protoVer, "capabilities": map[string]any{ "experimental": map[string]any{ "claude/channel": map[string]any{}, }, }, "serverInfo": map[string]any{ "name": "filewatch-mcp", "version": "0.1.0", @@ -1827,6 +1842,7 @@ import ( "log" "os" "os/signal" "strconv" "syscall" "time" @@ -1903,7 +1919,7 @@ func handle(ctx context.Context, path string, srv *mcp.Server) { if err := srv.SendChannel(ctx, mcp.ChannelMeta{ Source: "filewatch", File: path, Line: m.Line, Line: strconv.Itoa(m.Line), ReplyTo: id, }, m.Text); err != nil { log.Printf("send: %v", err) diff --git a/docs/superpowers/specs/2026-04-28-claude-channel-filewatch-design.md b/docs/superpowers/specs/2026-04-28-claude-channel-filewatch-design.md index 48cda21..a499997 100644 --- a/docs/superpowers/specs/2026-04-28-claude-channel-filewatch-design.md +++ b/docs/superpowers/specs/2026-04-28-claude-channel-filewatch-design.md @@ -82,7 +82,24 @@ Consequences: ### `internal/mcp` - MCP stdio server using `github.com/modelcontextprotocol/go-sdk`. - For each fired (untagged) marker, emits one outbound notification: - **Init handshake must declare the channel capability**, or Claude Code silently skips registration: ```json { "jsonrpc": "2.0", "id": 0, "result": { "protocolVersion": "<echo client's>", "capabilities": { "experimental": { "claude/channel": {} } }, "serverInfo": { "name": "filewatch-mcp", "version": "..." } } } ``` - For each fired (untagged) marker, emits one outbound notification. **All `meta` values must be strings** — Claude Code's schema is `Record<string, string>`; non-string values are silently rejected by validation. ```json { @@ -93,7 +110,7 @@ Consequences: "meta": { "source": "filewatch", "file": "src/foo.ts", "line": 42, "line": "42", "replyTo": "fw-a1b2c3d4" } } @@ -102,6 +119,7 @@ Consequences: Claude renders this as `<channel source="filewatch" file="src/foo.ts" line="42" replyTo="fw-a1b2c3d4">@claude can you write a test for foo</channel>`. The `replyTo` UUID matches the bracketed tag stamped into the file. - No reply tool exposed in v1. Claude's `Edit` to remove the marker is the implicit ack. - Confirmed via spike on Claude Code v2.1.121 with `--dangerously-load-development-channels server:<name>`. ### `internal/ignore` - `.gitignore` parsing via `github.com/sabhiram/go-gitignore`.