返回 DeepSeek-Reasonix
mcp_concurrency.go
根目录 / internal / agent / mcp_concurrency.go
1 package agent
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "sync"
8
9 "reasonix/internal/config"
10 )
11
12 // MCP concurrency policies. A server is parallel unless something says
13 // otherwise, which preserves the shared-Host performance tradeoff.
14 const (
15 MCPConcurrencyParallel = "parallel"
16 MCPConcurrencySerial = "serial"
17 )
18
19 // knownStatefulMCPServers are servers whose tools mutate session state the
20 // protocol does not model — an open page, a selected tab, a cursor. They may
21 // even declare readOnly, because nothing is written to disk, yet two children
22 // interleaving on the one shared process still corrupt each other's run.
23 // Matching is by substring so vendor prefixes and versions still hit.
24 var knownStatefulMCPServers = []string{
25 "browser",
26 "playwright",
27 "puppeteer",
28 "chrome",
29 "chromium",
30 "selenium",
31 }
32
33 // mcpServerIsSerial reports whether calls to this server must not overlap.
34 // Explicit configuration always wins; the built-in list is only a conservative
35 // default for servers known to carry session state.
36 func mcpServerIsSerial(entry config.PluginEntry) bool {
37 switch strings.ToLower(strings.TrimSpace(entry.Concurrency)) {
38 case MCPConcurrencySerial:
39 return true
40 case MCPConcurrencyParallel:
41 return false
42 }
43 name := strings.ToLower(strings.TrimSpace(entry.Name))
44 if name == "" {
45 return false
46 }
47 for _, known := range knownStatefulMCPServers {
48 if strings.Contains(name, known) {
49 return true
50 }
51 }
52 return false
53 }
54
55 // serverIsSerial resolves the policy for a configured server.
56 func (r *MCPCapabilityRuntime) serverIsSerial(server string) bool {
57 if r == nil {
58 return false
59 }
60 r.mu.RLock()
61 configured, ok := r.servers[strings.TrimSpace(server)]
62 r.mu.RUnlock()
63 return ok && mcpServerIsSerial(configured.entry)
64 }
65
66 // mcpServerGates holds one gate per serialized server. It lives on the session
67 // runtime because the process whose state the calls interleave on is shared at
68 // exactly that scope.
69 type mcpServerGates struct {
70 mu sync.Mutex
71 m map[string]chan struct{}
72 }
73
74 func (g *mcpServerGates) gate(server string) chan struct{} {
75 g.mu.Lock()
76 defer g.mu.Unlock()
77 if g.m == nil {
78 g.m = map[string]chan struct{}{}
79 }
80 if _, ok := g.m[server]; !ok {
81 g.m[server] = make(chan struct{}, 1)
82 }
83 return g.m[server]
84 }
85
86 // withServerGate runs one call with exclusive access to a stateful server.
87 // Parallel servers run straight through, so the common path is unchanged, and a
88 // queued call still honours its own cancellation instead of pinning the session
89 // behind a stuck server.
90 func (r *MCPCapabilityRuntime) withServerGate(ctx context.Context, server string, execute func() error) error {
91 if r == nil || !r.serverIsSerial(server) {
92 return execute()
93 }
94 gate := r.gates.gate(server)
95 select {
96 case gate <- struct{}{}:
97 defer func() { <-gate }()
98 return execute()
99 case <-ctx.Done():
100 return fmt.Errorf("waiting for exclusive access to MCP server %q: %w", server, ctx.Err())
101 }
102 }
103
103 lines GO