返回 DeepSeek-Reasonix
protocol_inbound.go
根目录 / internal / plugin / protocol_inbound.go
1 package plugin
2
3 import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "net/url"
8 "path/filepath"
9 "strconv"
10 "strings"
11 "sync"
12
13 "reasonix/internal/tool"
14 )
15
16 // progressTransport is optional so lightweight transports used by embedders and
17 // tests remain valid. Native transports implement it and route a server's
18 // notifications/progress message to the matching tools/call context.
19 type progressTransport interface {
20 registerProgress(token string, sink tool.ProgressFunc) func()
21 }
22
23 type progressRouter struct {
24 mu sync.Mutex
25 sinks map[string]tool.ProgressFunc
26 }
27
28 func (r *progressRouter) registerProgress(token string, sink tool.ProgressFunc) func() {
29 if token == "" || sink == nil {
30 return func() {}
31 }
32 r.mu.Lock()
33 if r.sinks == nil {
34 r.sinks = map[string]tool.ProgressFunc{}
35 }
36 r.sinks[token] = sink
37 r.mu.Unlock()
38 return func() {
39 r.mu.Lock()
40 delete(r.sinks, token)
41 r.mu.Unlock()
42 }
43 }
44
45 func (r *progressRouter) dispatchProgress(params json.RawMessage) bool {
46 var p struct {
47 ProgressToken any `json:"progressToken"`
48 Progress *float64 `json:"progress"`
49 Total *float64 `json:"total"`
50 Message string `json:"message"`
51 }
52 if err := json.Unmarshal(params, &p); err != nil {
53 return false
54 }
55 token := progressTokenKey(p.ProgressToken)
56 if token == "" {
57 return false
58 }
59 r.mu.Lock()
60 sink := r.sinks[token]
61 r.mu.Unlock()
62 if sink == nil {
63 return false
64 }
65 sink(formatMCPProgress(p.Message, p.Progress, p.Total))
66 return true
67 }
68
69 func progressTokenKey(token any) string {
70 switch value := token.(type) {
71 case string:
72 return value
73 case float64:
74 return strconv.FormatFloat(value, 'f', -1, 64)
75 case json.Number:
76 return value.String()
77 default:
78 return ""
79 }
80 }
81
82 func formatMCPProgress(message string, progress, total *float64) string {
83 label := strings.TrimSpace(message)
84 if label == "" {
85 label = "MCP progress"
86 }
87 formatNumber := func(value float64) string {
88 return strconv.FormatFloat(value, 'f', -1, 64)
89 }
90 switch {
91 case progress != nil && total != nil:
92 return fmt.Sprintf("%s (%s/%s)\n", label, formatNumber(*progress), formatNumber(*total))
93 case progress != nil:
94 return fmt.Sprintf("%s (%s)\n", label, formatNumber(*progress))
95 default:
96 return label + "\n"
97 }
98 }
99
100 type mcpRoot struct {
101 URI string `json:"uri"`
102 Name string `json:"name,omitempty"`
103 }
104
105 func mcpRoots(workspaceRoot string) []mcpRoot {
106 root := strings.TrimSpace(workspaceRoot)
107 if root == "" {
108 return nil
109 }
110 abs, err := filepath.Abs(root)
111 if err != nil {
112 return nil
113 }
114 clean := filepath.Clean(abs)
115 path := filepath.ToSlash(clean)
116 fileURL := &url.URL{Scheme: "file"}
117 if strings.HasPrefix(path, "//") {
118 parts := strings.SplitN(strings.TrimPrefix(path, "//"), "/", 2)
119 fileURL.Host = parts[0]
120 if len(parts) == 2 {
121 fileURL.Path = "/" + parts[1]
122 } else {
123 fileURL.Path = "/"
124 }
125 } else {
126 if volume := filepath.VolumeName(clean); volume != "" && !strings.HasPrefix(path, "/") {
127 path = "/" + path
128 }
129 fileURL.Path = path
130 }
131 name := filepath.Base(clean)
132 if name == "." {
133 name = clean
134 }
135 return []mcpRoot{{URI: fileURL.String(), Name: name}}
136 }
137
138 type inboundMessage struct {
139 JSONRPC string `json:"jsonrpc"`
140 ID json.RawMessage `json:"id"`
141 Method string `json:"method"`
142 Params json.RawMessage `json:"params"`
143 }
144
145 func decodeInboundMessage(payload []byte) (inboundMessage, bool) {
146 var message inboundMessage
147 if err := json.Unmarshal(payload, &message); err != nil {
148 return inboundMessage{}, false
149 }
150 return message, true
151 }
152
153 func isNotificationID(id json.RawMessage) bool {
154 id = bytes.TrimSpace(id)
155 return len(id) == 0 || bytes.Equal(id, []byte("null"))
156 }
157
158 func serverRequestReply(id json.RawMessage, method string, roots []mcpRoot) any {
159 response := struct {
160 JSONRPC string `json:"jsonrpc"`
161 ID json.RawMessage `json:"id"`
162 Result any `json:"result,omitempty"`
163 Error *rpcError `json:"error,omitempty"`
164 }{JSONRPC: "2.0", ID: append(json.RawMessage(nil), id...)}
165 switch method {
166 case "ping":
167 response.Result = map[string]any{}
168 case "roots/list":
169 response.Result = map[string]any{"roots": roots}
170 default:
171 response.Error = &rpcError{Code: -32601, Message: "Method not found"}
172 }
173 return response
174 }
175
175 lines GO