v0.50.0 c4ec31f

jsonrpc.go RawHistoryFolder

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)) }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close