1package mcp
2
3import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "time"
8)
9
10// cancelNotifyTimeout bounds one best-effort notifications/cancelled send.
11const cancelNotifyTimeout = 2 * time.Second
12
13// shouldSendCancel reports whether abandoning a request of this method may be
14// announced with notifications/cancelled. The MCP lifecycle forbids cancelling
15// initialize; an abandoned initialize just fails and closes the connection.
16func shouldSendCancel(method string) bool { return method != "initialize" }
17
18// scrubRPCError returns a copy of e whose message is safe to print.
19func (r *redactor) scrubRPCError(e *RPCError) *RPCError {
20 if e == nil {
21 return nil
22 }
23 cp := *e
24 cp.Message = r.scrubOneLine(e.Message)
25 return &cp
26}
27
28// message is one JSON-RPC 2.0 frame: request, notification, or response.
29type message struct {
30 JSONRPC string `json:"jsonrpc"`
31 ID json.RawMessage `json:"id,omitempty"`
32 Method string `json:"method,omitempty"`
33 Params json.RawMessage `json:"params,omitempty"`
34 Result json.RawMessage `json:"result,omitempty"`
35 Error *RPCError `json:"error,omitempty"`
36}
37
38func (m message) isResponse() bool { return m.Method == "" && len(m.ID) > 0 }
39func (m message) isRequest() bool { return m.Method != "" && len(m.ID) > 0 }
40
41// RPCError is a JSON-RPC error object returned by a server.
42type RPCError struct {
43 Code int `json:"code"`
44 Message string `json:"message"`
45 Data json.RawMessage `json:"data,omitempty"`
46}
47
48func (e *RPCError) Error() string { return fmt.Sprintf("json-rpc error %d: %s", e.Code, e.Message) }
49
50const (
51 codeMethodNotFound = -32601
52 codeInternalError = -32603
53)
54
55// incomingHandler processes a server-initiated request or notification. For
56// requests it returns the result or an error; for notifications the return
57// values are ignored.
58type incomingHandler func(msg message) (any, *RPCError)
59
60// transport moves JSON-RPC frames to one server. request blocks until the
61// response with id arrives, ctx ends, or the connection fails. Frames that
62// arrive while waiting (notifications and server requests) are delivered to
63// the handler installed by setHandler.
64type transport interface {
65 request(ctx context.Context, id int64, method string, params any) (json.RawMessage, error)
66 notify(ctx context.Context, method string, params any) error
67 setHandler(incomingHandler)
68 // setProtocolVersion is called after initialize negotiates a version.
69 setProtocolVersion(string)
70 // done is closed when the connection is unusable.
71 done() <-chan struct{}
72 close() error
73}
74
75func encodeRequest(id int64, method string, params any) ([]byte, error) {
76 frame := map[string]any{"jsonrpc": "2.0", "id": id, "method": method}
77 if params != nil {
78 frame["params"] = params
79 }
80 return json.Marshal(frame)
81}
82
83func encodeNotification(method string, params any) ([]byte, error) {
84 frame := map[string]any{"jsonrpc": "2.0", "method": method}
85 if params != nil {
86 frame["params"] = params
87 }
88 return json.Marshal(frame)
89}
90
91func encodeResponse(id json.RawMessage, result any, rpcErr *RPCError) ([]byte, error) {
92 frame := map[string]any{"jsonrpc": "2.0", "id": id}
93 if rpcErr != nil {
94 frame["error"] = rpcErr
95 } else {
96 if result == nil {
97 result = struct{}{}
98 }
99 frame["result"] = result
100 }
101 return json.Marshal(frame)
102}
103
104// parseFrames decodes either one JSON-RPC object or a batch array.
105func parseFrames(raw []byte) ([]message, error) {
106 raw = trimSpace(raw)
107 if len(raw) == 0 {
108 return nil, nil
109 }
110 if raw[0] == '[' {
111 var batch []message
112 if err := json.Unmarshal(raw, &batch); err != nil {
113 return nil, err
114 }
115 return batch, nil
116 }
117 var msg message
118 if err := json.Unmarshal(raw, &msg); err != nil {
119 return nil, err
120 }
121 return []message{msg}, nil
122}
123
124func trimSpace(b []byte) []byte {
125 start, end := 0, len(b)
126 for start < end && (b[start] == ' ' || b[start] == '\t' || b[start] == '\r' || b[start] == '\n') {
127 start++
128 }
129 for end > start && (b[end-1] == ' ' || b[end-1] == '\t' || b[end-1] == '\r' || b[end-1] == '\n') {
130 end--
131 }
132 return b[start:end]
133}
134
135// idKey normalizes a JSON id for map lookup ("7" and 7 are distinct ids in
136// JSON-RPC, but zut only issues integers, so compare the canonical text).
137func idKey(id json.RawMessage) string { return string(trimSpace(id)) }