返回 DeepSeek-Reasonix
mcp.go
根目录 / internal / control / mcp.go
1 package control
2
3 import (
4 "context"
5 "fmt"
6 "sync"
7 "time"
8
9 "reasonix/internal/plugin"
10 "reasonix/internal/tool"
11 )
12
13 // mcpManager owns the session's live tool/plugin surface: the MCP plugin Host
14 // (live server connections), the tool Registry the executor reads each turn, and
15 // the session-scoped context a hot-added stdio server binds its subprocess to.
16 // Like approvalManager it holds the live plumbing behind its own lock, off c.mu —
17 // the Controller keeps the config-facing orchestration (persisting reasonix.toml
18 // on add/remove, building specs from entries).
19 //
20 // mu guards the lazy host creation and host-pointer reads. The registry is
21 // internally thread-safe (its own RWMutex) and pluginCtx is write-once, so the
22 // lock is held only briefly — never across the host's network/subprocess I/O.
23 // host is either injected at construction (the desktop shared-host path) or
24 // created lazily on the first connect; once set it never reverts to nil.
25 type mcpManager struct {
26 mu sync.Mutex
27 host *plugin.Host
28 reg *tool.Registry
29 pluginCtx context.Context
30 }
31
32 func newMcpManager(host *plugin.Host, reg *tool.Registry, pluginCtx context.Context) mcpManager {
33 return mcpManager{host: host, reg: reg, pluginCtx: pluginCtx}
34 }
35
36 // hostRef returns the live plugin host (nil until one is injected or lazily
37 // created), for the SessionAPI Host() accessor and the nil-safe read wrappers.
38 func (m *mcpManager) hostRef() *plugin.Host {
39 m.mu.Lock()
40 defer m.mu.Unlock()
41 return m.host
42 }
43
44 // connectSpec connects (or attaches to an already-connected) MCP server and
45 // registers its tools, replacing any prior tools under the same prefix. Returns
46 // the tool count. The host's network/subprocess I/O runs off mu.
47 func (m *mcpManager) connectSpec(s plugin.Spec) (int, error) {
48 m.mu.Lock()
49 if m.host == nil {
50 m.host = plugin.NewHost()
51 }
52 host, ctx, reg := m.host, m.pluginCtx, m.reg
53 m.mu.Unlock()
54
55 tools, err := host.Add(ctx, s)
56 if err != nil {
57 if !plugin.IsServerAlreadyConnected(err) {
58 return 0, err
59 }
60 toolsCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
61 defer cancel()
62 tools, err = host.ToolsFor(toolsCtx, s.Name)
63 if err != nil {
64 return 0, err
65 }
66 }
67 if reg != nil {
68 reg.ResumePrefix(plugin.ToolPrefix(s.Name))
69 reg.RemovePrefix(plugin.ToolPrefix(s.Name))
70 for _, t := range tools {
71 reg.Add(t)
72 }
73 }
74 return len(tools), nil
75 }
76
77 // registerSpecOnDemand restores one enabled server into this session's tool
78 // registry without starting a disconnected process. A live shared-host client
79 // is reused immediately; otherwise cached lazy tools (or one connect stub on a
80 // cache miss) start the server only when the model makes the first real call.
81 func (m *mcpManager) registerSpecOnDemand(s plugin.Spec) (int, error) {
82 m.mu.Lock()
83 if m.host == nil {
84 m.host = plugin.NewHost()
85 }
86 host, ctx, reg := m.host, m.pluginCtx, m.reg
87 m.mu.Unlock()
88
89 var tools []tool.Tool
90 if host.HasClient(s.Name) {
91 toolsCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
92 defer cancel()
93 var err error
94 tools, err = host.ToolsFor(toolsCtx, s.Name)
95 if err != nil {
96 return 0, err
97 }
98 } else {
99 cached, _ := plugin.LoadCachedSchemaForSpec(s)
100 tools = plugin.LazyToolset(s, cached, host, reg, ctx, false)
101 }
102 if reg != nil {
103 prefix := plugin.ToolPrefix(s.Name)
104 reg.ResumePrefix(prefix)
105 reg.RemovePrefix(prefix)
106 for _, t := range tools {
107 reg.Add(t)
108 }
109 }
110 return len(tools), nil
111 }
112
113 // disconnect drops a live server and its tools from the registry. Reports whether
114 // a live server was removed.
115 func (m *mcpManager) disconnect(name string) bool {
116 host := m.hostRef()
117 if host == nil {
118 return false
119 }
120 prefix, ok := host.Remove(name)
121 if ok {
122 if reg := m.registry(); reg != nil {
123 reg.RemovePrefix(prefix)
124 }
125 }
126 return ok
127 }
128
129 // removeToolPrefix drops a server's tools from the registry without touching the
130 // host — the placeholder / not-connected path. Returns the number removed.
131 func (m *mcpManager) removeToolPrefix(name string) int {
132 reg := m.registry()
133 if reg == nil {
134 return 0
135 }
136 return reg.RemovePrefix(plugin.ToolPrefix(name))
137 }
138
139 // suspendToolPrefix hides a server's tools from this session's registry while a
140 // shared host keeps the client alive for sibling sessions.
141 func (m *mcpManager) suspendToolPrefix(name string) bool {
142 reg := m.registry()
143 if reg == nil {
144 return false
145 }
146 reg.SuspendPrefix(plugin.ToolPrefix(name))
147 return true
148 }
149
150 // registerTool adds a built-in tool to the live registry (e.g. the slash-command
151 // tool rebuilt by ReloadCommands). No-op when no registry is bound.
152 func (m *mcpManager) registerTool(t tool.Tool) {
153 if reg := m.registry(); reg != nil {
154 reg.Add(t)
155 }
156 }
157
158 // registry returns the shared tool registry under mu (write-once, but read under
159 // the lock for consistency with the host pointer).
160 func (m *mcpManager) registry() *tool.Registry {
161 m.mu.Lock()
162 defer m.mu.Unlock()
163 return m.reg
164 }
165
166 // serverNames lists the live server names (nil when no host is connected).
167 func (m *mcpManager) serverNames() []string {
168 if h := m.hostRef(); h != nil {
169 return h.ServerNames()
170 }
171 return nil
172 }
173
174 // hasServer reports whether a server is live.
175 func (m *mcpManager) hasServer(name string) bool {
176 for _, n := range m.serverNames() {
177 if n == name {
178 return true
179 }
180 }
181 return false
182 }
183
184 // prompts lists the live MCP prompts (nil when no host is connected).
185 func (m *mcpManager) prompts() []plugin.Prompt {
186 if h := m.hostRef(); h != nil {
187 return h.Prompts()
188 }
189 return nil
190 }
191
192 // failures lists the recorded MCP startup failures (nil when no host).
193 func (m *mcpManager) failures() []plugin.Failure {
194 if h := m.hostRef(); h != nil {
195 return h.Failures()
196 }
197 return nil
198 }
199
200 // readResource reads an MCP resource. Errors when no host is connected.
201 func (m *mcpManager) readResource(ctx context.Context, server, uri string) (string, error) {
202 h := m.hostRef()
203 if h == nil {
204 return "", fmt.Errorf("no MCP servers connected")
205 }
206 return h.ReadResource(ctx, server, uri)
207 }
208
208 lines GO