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