internal/mcp/mcp.go
Ref: Size: 3.6 KiB
package mcp
import (
"context"
"encoding/json"
"io"
"os"
"sync"
)
// channelInstructions is advertised in the initialize response so Claude
// Code adds it to the session prompt. Tells Claude how to handle filewatch
// channel messages.
const channelInstructions = `Channel messages arrive when a developer writes @claude in a code comment. ` +
`params.content is the marker text starting with "@claude". ` +
`params.meta has file (path), line (1-indexed line number, as a string), ` +
`and replyTo (e.g. "fw-a1b2c3d4") — the UUID already stamped into the comment as @claude[<replyTo>]. ` +
`To handle a message: read the file at meta.file around line meta.line, ` +
`do what the developer asked (answer in-session for questions, edit code for tasks), ` +
`then use Edit to remove the entire @claude[<replyTo>] marker line so the file returns to a clean state. ` +
`There is no reply tool — line removal is the ack.`
// ChannelMeta is the meta block of a notifications/claude/channel notification.
// All 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 string `json:"line"`
ReplyTo string `json:"replyTo"`
}
type Server struct {
in io.Reader
out io.Writer
mu sync.Mutex // serializes writes to out
}
func New() *Server {
return &Server{in: os.Stdin, out: os.Stdout}
}
// NewWithIO is for tests.
func NewWithIO(in io.Reader, out io.Writer) *Server {
return &Server{in: in, out: out}
}
func (s *Server) SendChannel(ctx context.Context, meta ChannelMeta, content string) error {
frame := map[string]any{
"jsonrpc": "2.0",
"method": "notifications/claude/channel",
"params": map[string]any{
"content": content,
"meta": map[string]any{
"source": meta.Source,
"file": meta.File,
"line": meta.Line,
"replyTo": meta.ReplyTo,
},
},
}
s.mu.Lock()
defer s.mu.Unlock()
return json.NewEncoder(s.out).Encode(frame)
}
// Run drives the JSON-RPC request loop. Reads requests on s.in; responds to
// initialize with the required experimental.claude/channel capability; replies
// with method-not-found for any other request. Notifications (no id) are
// ignored. Returns nil on EOF or context cancellation.
func (s *Server) Run(ctx context.Context) error {
dec := json.NewDecoder(s.in)
for {
if ctx.Err() != nil {
return nil
}
var req map[string]any
if err := dec.Decode(&req); err != nil {
if err == io.EOF {
return nil
}
return err
}
method, _ := req["method"].(string)
id := req["id"]
if id == nil {
continue // notification, ignore
}
switch method {
case "initialize":
params, _ := req["params"].(map[string]any)
protoVer, _ := params["protocolVersion"].(string)
if protoVer == "" {
protoVer = "2024-11-05"
}
s.writeResult(id, 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",
},
"instructions": channelInstructions,
})
default:
s.writeError(id, -32601, "method not found")
}
}
}
func (s *Server) writeResult(id, result any) {
s.mu.Lock()
defer s.mu.Unlock()
_ = json.NewEncoder(s.out).Encode(map[string]any{
"jsonrpc": "2.0", "id": id, "result": result,
})
}
func (s *Server) writeError(id any, code int, msg string) {
s.mu.Lock()
defer s.mu.Unlock()
_ = json.NewEncoder(s.out).Encode(map[string]any{
"jsonrpc": "2.0", "id": id, "error": map[string]any{"code": code, "message": msg},
})
}