返回 DeepSeek-Reasonix
sdk_errors.go
根目录 / internal / plugin / sdk_errors.go
1 package plugin
2
3 import (
4 "context"
5 "errors"
6 "io"
7 "strings"
8
9 mcpjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc"
10 mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
11 )
12
13 func isExplicitMCPSessionMissing(err error) bool {
14 if errors.Is(err, mcpsdk.ErrSessionMissing) {
15 return true
16 }
17 if !isMCPHTTPNotFound(err) || !hasMCPTransportRejection(err) {
18 return false
19 }
20 found := false
21 visitMCPRPCErrors(err, func(rpcErr *mcpjsonrpc.Error) {
22 message := strings.ToLower(strings.TrimSpace(rpcErr.Message))
23 for _, marker := range []string{
24 "session not found",
25 "session missing",
26 "session expired",
27 "invalid session",
28 "unknown session",
29 } {
30 if strings.Contains(message, marker) {
31 found = true
32 }
33 }
34 })
35 return found
36 }
37
38 // isMCPHTTPNotFound recognizes the status-only error emitted by newer Go MCP
39 // SDKs for a plain HTTP 404 when no session ID exists. It intentionally does
40 // not match arbitrary "not found" prose so a tool-level domain error cannot be
41 // mistaken for an endpoint or protocol mismatch.
42 func isMCPHTTPNotFound(err error) bool {
43 if err == nil {
44 return false
45 }
46 hasRPCError := false
47 visitMCPRPCErrors(err, func(*mcpjsonrpc.Error) {
48 hasRPCError = true
49 })
50 if hasRPCError && !hasMCPTransportRejection(err) {
51 return false
52 }
53 message := strings.ToLower(strings.TrimSpace(err.Error()))
54 return message == "not found" ||
55 strings.HasSuffix(message, ": not found") ||
56 strings.Contains(message, "http 404") ||
57 strings.Contains(message, "status 404")
58 }
59
60 func hasMCPTransportRejection(err error) bool {
61 found := false
62 visitMCPRPCErrors(err, func(rpcErr *mcpjsonrpc.Error) {
63 if rpcErr.Code == -32005 && strings.EqualFold(strings.TrimSpace(rpcErr.Message), "rejected by transport") {
64 found = true
65 }
66 })
67 return found
68 }
69
70 // visitMCPRPCErrors walks every concrete error-tree node because errors.As
71 // returns only the first matching RPC error and would hide transport evidence.
72 //
73 //nolint:errorlint // Direct inspection distinguishes server errors from the SDK transport sentinel.
74 func visitMCPRPCErrors(err error, visit func(*mcpjsonrpc.Error)) {
75 if err == nil {
76 return
77 }
78 if rpcErr, ok := err.(*mcpjsonrpc.Error); ok && rpcErr != nil {
79 visit(rpcErr)
80 }
81 switch wrapped := err.(type) {
82 case interface{ Unwrap() []error }:
83 for _, child := range wrapped.Unwrap() {
84 visitMCPRPCErrors(child, visit)
85 }
86 case interface{ Unwrap() error }:
87 visitMCPRPCErrors(wrapped.Unwrap(), visit)
88 }
89 }
90
91 func (t *sdkSessionTransport) isStreamableHTTPNotFound(err error) bool {
92 return canonicalMCPRuntimeTransport(t.spec.Type) == "streamable-http" && isMCPHTTPNotFound(err)
93 }
94
95 func isTerminalSDKError(err error) bool {
96 return errors.Is(err, mcpsdk.ErrConnectionClosed) || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)
97 }
98
99 func isAmbiguousTransportError(err error) bool {
100 if err == nil {
101 return false
102 }
103 message := strings.ToLower(err.Error())
104 for _, marker := range []string{"connection reset", "broken pipe", "connection aborted", "connection refused", "transport is closing"} {
105 if strings.Contains(message, marker) {
106 return true
107 }
108 }
109 return false
110 }
111
112 func classifySessionError(err error) SessionErrorKind {
113 if err == nil {
114 return SessionErrorNone
115 }
116 switch {
117 case isExplicitMCPSessionMissing(err):
118 return SessionErrorSessionMissing
119 case errors.Is(err, context.DeadlineExceeded):
120 return SessionErrorTimeout
121 case isTerminalSDKError(err):
122 return SessionErrorStreamClosed
123 }
124 lower := strings.ToLower(err.Error())
125 switch {
126 case strings.Contains(lower, "unauthorized"), strings.Contains(lower, "forbidden"), strings.Contains(lower, "authorize again"), strings.Contains(lower, "authentication"):
127 return SessionErrorAuthRequired
128 case strings.Contains(lower, "protocol version"), strings.Contains(lower, "method not found"):
129 return SessionErrorProtocol
130 default:
131 return SessionErrorTransport
132 }
133 }
134
135 type sanitizedMCPError struct {
136 message string
137 cause error
138 }
139
140 func (e *sanitizedMCPError) Error() string { return e.message }
141 func (e *sanitizedMCPError) Unwrap() error { return e.cause }
142
143 func (t *sdkSessionTransport) sanitizeError(err error, managed *managedMCPSession) error {
144 if err == nil {
145 return nil
146 }
147 sessionID := ""
148 if managed != nil && managed.session != nil {
149 sessionID = managed.session.ID()
150 }
151 return &sanitizedMCPError{message: t.safeErrorText(err, sessionID), cause: err}
152 }
153
154 func (t *sdkSessionTransport) safeErrorText(err error, sessionID string) string {
155 return redactMCPConfigValues(safeMCPErrorText(err, sessionID), t.spec)
156 }
157
158 func redactMCPConfigValues(message string, spec Spec) string {
159 values := make([]string, 0, len(spec.Headers)+len(spec.Env)+2)
160 values = append(values, spec.WorkspaceRoot, spec.Dir)
161 for _, value := range spec.Headers {
162 values = append(values, value)
163 }
164 for _, value := range spec.Env {
165 values = append(values, value)
166 }
167 for _, value := range values {
168 value = strings.TrimSpace(value)
169 if value != "" {
170 message = strings.ReplaceAll(message, value, "[redacted]")
171 }
172 }
173 return message
174 }
175
176 func safeMCPErrorText(err error, sessionID string) string {
177 if err == nil {
178 return ""
179 }
180 message := summarizeFailureError(err)
181 if sessionID != "" {
182 message = strings.ReplaceAll(message, sessionID, "[redacted]")
183 }
184 if index := strings.Index(strings.ToLower(message), "session id:"); index >= 0 {
185 start := index + len("session id:")
186 end := strings.IndexByte(message[start:], ')')
187 if end >= 0 {
188 message = message[:start] + " [redacted]" + message[start+end:]
189 }
190 }
191 return message
192 }
193
193 lines GO