返回 DeepSeek-Reasonix
mcp_interaction.go
根目录 / internal / acp / mcp_interaction.go
1 package acp
2
3 import (
4 "context"
5 "encoding/json"
6
7 "reasonix/internal/control"
8 "reasonix/internal/event"
9 "reasonix/internal/mcpinteraction"
10 )
11
12 const mcpInteractionMethod = "_reasonix.io/mcp/request_interaction"
13
14 func (s *updateSink) bindControllerPrompts(ctrl *control.Controller, interactions bool) {
15 s.bindApprove(ctrl.Approve)
16 s.bindAnswer(ctrl.AnswerQuestion)
17 s.bindMCPInteraction(interactions, ctrl.AnswerMCPInteractionChecked)
18 }
19
20 func (s *updateSink) emitPrompt(e event.Event) {
21 // Requests run off the synchronous event sink so user decisions can release
22 // the blocked controller. Each request remains bound to its originating turn.
23 switch e.Kind {
24 case event.ApprovalRequest:
25 go s.requestPermission(s.currentTurnContext(), e.Approval)
26 case event.AskRequest:
27 go s.requestAsk(s.currentTurnContext(), e.Ask)
28 case event.MCPInteractionRequest:
29 s.emitMCPInteraction(e.MCPInteraction)
30 }
31 }
32
33 // MCPInteractionCapability is negotiated explicitly by capable ACP clients.
34 // Older clients retain the core MCP profile and receive no reverse requests.
35 type MCPInteractionCapability struct {
36 Supported bool `json:"supported"`
37 SchemaVersion int `json:"schemaVersion"`
38 Method string `json:"method,omitempty"`
39 }
40
41 type MCPInteractionParams struct {
42 SessionID string `json:"sessionId"`
43 PromptID string `json:"promptId"`
44 TurnID string `json:"turnId"`
45 Server string `json:"server"`
46 Mode string `json:"mode"`
47 Message string `json:"message"`
48 RequestedSchema json.RawMessage `json:"requestedSchema,omitempty"`
49 URL string `json:"url,omitempty"`
50 ElicitationID string `json:"elicitationId,omitempty"`
51 }
52
53 type MCPInteractionResult struct {
54 Action string `json:"action"`
55 Content map[string]any `json:"content,omitempty"`
56 }
57
58 func clientMCPInteractionSupported(caps ClientCapabilities) bool {
59 vendor, ok := caps.Meta["reasonix.io"].(map[string]any)
60 if !ok {
61 return false
62 }
63 raw, err := json.Marshal(vendor["mcpInteraction"])
64 if err != nil {
65 return false
66 }
67 var capability MCPInteractionCapability
68 return json.Unmarshal(raw, &capability) == nil && capability.Supported && capability.SchemaVersion == 1
69 }
70
71 func (s *updateSink) bindMCPInteraction(supported bool, answer func(string, string, map[string]any) error) {
72 s.mu.Lock()
73 s.mcpInteractionSupported, s.answerMCPInteraction = supported, answer
74 s.mu.Unlock()
75 }
76
77 func (s *updateSink) emitMCPInteraction(req event.MCPInteraction) {
78 s.mu.Lock()
79 supported, answer, ctx := s.mcpInteractionSupported, s.answerMCPInteraction, s.turnCtx
80 s.mu.Unlock()
81 if answer == nil {
82 return
83 }
84 if ctx == nil {
85 ctx = context.Background()
86 }
87 // Capture the originating controller callback before dispatch. A late reply
88 // must never resolve a numerically identical prompt on a replacement controller.
89 go func() {
90 result := MCPInteractionResult{Action: mcpinteraction.ActionCancel}
91 if supported && ctx.Err() == nil && mcpinteraction.SanitizeURLMode(mcpinteraction.Request{Mode: req.Mode, URL: req.URL}) {
92 params := MCPInteractionParams{SessionID: s.sessionID, PromptID: req.ID, TurnID: req.TurnID,
93 Server: req.Server, Mode: req.Mode, Message: req.Message, RequestedSchema: req.RequestedSchema,
94 URL: req.URL, ElicitationID: req.ElicitationID}
95 raw, err := s.conn.Request(ctx, mcpInteractionMethod, params)
96 var response MCPInteractionResult
97 if err == nil && ctx.Err() == nil && json.Unmarshal(raw, &response) == nil {
98 switch response.Action {
99 case mcpinteraction.ActionAccept, mcpinteraction.ActionDecline, mcpinteraction.ActionCancel:
100 result = response
101 }
102 }
103 }
104 if result.Action != mcpinteraction.ActionAccept {
105 result.Content = nil
106 }
107 // The controller persists the decision before releasing the MCP waiter;
108 // a persistence error leaves the owning turn failed, never auto-approved.
109 _ = answer(req.ID, result.Action, result.Content)
110 }()
111 }
112
112 lines GO