返回 DeepSeek-Reasonix
sdk_session.go
根目录 / internal / plugin / sdk_session.go
1 package plugin
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "runtime/debug"
9 "strings"
10 "sync"
11 "sync/atomic"
12 "time"
13
14 mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
15 "reasonix/internal/mcpdiag"
16 "reasonix/internal/mcpinteraction"
17 "reasonix/internal/tool"
18 )
19
20 // SessionState is the transport lifecycle state exposed to local diagnostics.
21 // It intentionally contains no endpoint, project path, or session identifier.
22 type SessionState string
23
24 const (
25 SessionStateConnecting SessionState = "connecting"
26 SessionStateListening SessionState = "listening"
27 SessionStateReady SessionState = "ready"
28 SessionStateReconnecting SessionState = "reconnecting"
29 SessionStateFailed SessionState = "failed"
30 SessionStateClosed SessionState = "closed"
31 )
32
33 // SessionErrorKind classifies failures without exposing transport secrets.
34 type SessionErrorKind string
35
36 const (
37 SessionErrorNone SessionErrorKind = ""
38 SessionErrorAuthRequired SessionErrorKind = "auth_required"
39 SessionErrorSessionMissing SessionErrorKind = "session_missing"
40 SessionErrorStreamClosed SessionErrorKind = "stream_closed"
41 SessionErrorTimeout SessionErrorKind = "timeout"
42 SessionErrorProtocol SessionErrorKind = "protocol"
43 SessionErrorTransport SessionErrorKind = "transport"
44 )
45
46 type sessionDiagnostics struct {
47 ProtocolVersion string
48 State SessionState
49 SessionIDPresent bool
50 ReconnectAttempts int
51 LastErrorKind SessionErrorKind
52 LastError string
53 }
54
55 type sessionDiagnosticsProvider interface {
56 sessionDiagnostics() sessionDiagnostics
57 }
58
59 type sdkEndpoint struct {
60 transport mcpsdk.Transport
61 close func()
62 startupStderr func() string
63 }
64
65 type managedMCPSession struct {
66 generation uint64
67 session *mcpsdk.ClientSession
68 endpoint sdkEndpoint
69 protocol string
70 }
71
72 type sessionBuild struct {
73 done chan struct{}
74 session *managedMCPSession
75 err error
76 }
77
78 // sdkSessionTransport is the single connection owner for one configured MCP
79 // server. The official SDK owns JSON-RPC correlation, cancellation, protocol
80 // negotiation, Streamable HTTP listening, and graceful protocol close.
81 // Reasonix owns product timeouts, process isolation, security policy, and
82 // failure-atomic session replacement.
83 type sdkSessionTransport struct {
84 name string
85 spec Spec
86 // profile fixes the client capability surface this connection declares.
87 // It comes from the Host and is immutable for the transport's lifetime.
88 profile HostProfile
89
90 lifeCtx context.Context
91 cancel context.CancelFunc
92
93 progress progressRouter
94 notifications notificationRouter
95 oauth *mcpOAuthClient
96
97 mu sync.Mutex
98 current *managedMCPSession
99 building *sessionBuild
100 nextGeneration uint64
101 closed bool
102 state SessionState
103 reconnectAttempts int
104 lastErrorKind SessionErrorKind
105 lastError string
106 autoReconnecting bool
107 reconnectDelays []time.Duration
108 lastStartupStderr string
109 endpointFactory func(context.Context) (sdkEndpoint, error)
110 wg sync.WaitGroup
111
112 legacyElicitationMu sync.Mutex
113 legacyElicitationNext uint64
114 legacyElicitation map[uint64]legacyElicitationCall
115 }
116
117 var defaultSessionReconnectDelays = []time.Duration{
118 time.Second,
119 2 * time.Second,
120 5 * time.Second,
121 10 * time.Second,
122 30 * time.Second,
123 }
124
125 var linkedMCPClientVersion atomic.Pointer[string]
126
127 // SetMCPClientVersion supplies the release version injected into an executable.
128 // Library and development builds fall back to module metadata or "dev".
129 func SetMCPClientVersion(version string) {
130 version = strings.TrimSpace(version)
131 if version == "" {
132 version = "dev"
133 }
134 linkedMCPClientVersion.Store(&version)
135 }
136
137 func mcpClientVersion() string {
138 if version := linkedMCPClientVersion.Load(); version != nil {
139 return *version
140 }
141 if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "" && info.Main.Version != "(devel)" {
142 return info.Main.Version
143 }
144 return "dev"
145 }
146
147 func newSDKSessionTransport(ctx context.Context, s Spec, profile HostProfile) (*sdkSessionTransport, error) {
148 if ctx == nil {
149 ctx = context.Background()
150 }
151 typeName := canonicalMCPRuntimeTransport(s.Type)
152 switch typeName {
153 case "stdio":
154 if strings.TrimSpace(s.Command) == "" {
155 return nil, fmt.Errorf("stdio plugin %q: command is required", s.Name)
156 }
157 case "streamable-http", "sse":
158 if err := validateMCPURL(s.Name, typeName, s.URL); err != nil {
159 return nil, err
160 }
161 default:
162 return nil, fmt.Errorf("unknown transport type %q (want stdio|http|sse)", s.Type)
163 }
164
165 var oauth *mcpOAuthClient
166 var err error
167 if typeName == "streamable-http" && !hasExplicitMCPAuth(s) {
168 oauth, err = newMCPOAuthClient(s.StateDir, s.OAuthHTTPClient)
169 if err != nil {
170 return nil, fmt.Errorf("http plugin %q: load OAuth state: %w", s.Name, err)
171 }
172 if oauth != nil && !sameCanonicalResource(oauth.state.Resource, s.URL) {
173 return nil, fmt.Errorf("http plugin %q: stored OAuth token belongs to a different MCP resource; clear authentication and authorize this endpoint", s.Name)
174 }
175 }
176
177 lifeCtx, cancel := context.WithCancel(ctx)
178 profile = profile.Normalize()
179 return &sdkSessionTransport{
180 name: s.Name,
181 spec: s,
182 profile: profile,
183 lifeCtx: lifeCtx,
184 cancel: cancel,
185 oauth: oauth,
186 state: SessionStateConnecting,
187 reconnectDelays: append([]time.Duration(nil), defaultSessionReconnectDelays...),
188 }, nil
189 }
190
191 func hasExplicitMCPAuth(s Spec) bool {
192 return mcpdiag.HasAuthConfig(s.Headers, s.Env, s.URL)
193 }
194
195 func (t *sdkSessionTransport) registerProgress(token string, sink tool.ProgressFunc) func() {
196 unregister := t.progress.registerProgress(token, sink)
197 var once sync.Once
198 return func() {
199 once.Do(func() {
200 // The SDK dispatches notifications independently from the response that
201 // completes a call. Keep the token briefly so a progress notification
202 // already read from the wire cannot lose a race with the response.
203 time.AfterFunc(time.Second, unregister)
204 })
205 }
206 }
207
208 func (t *sdkSessionTransport) registerNotification(method string, callback func(json.RawMessage)) func() {
209 return t.notifications.registerNotification(method, callback)
210 }
211
212 func (t *sdkSessionTransport) acquire(ctx context.Context) (*managedMCPSession, error) {
213 for {
214 t.mu.Lock()
215 if t.closed {
216 t.mu.Unlock()
217 return nil, mcpsdk.ErrConnectionClosed
218 }
219 if t.current != nil {
220 current := t.current
221 t.mu.Unlock()
222 return current, nil
223 }
224 if attempt := t.building; attempt != nil {
225 done := attempt.done
226 t.mu.Unlock()
227 select {
228 case <-ctx.Done():
229 return nil, ctx.Err()
230 case <-t.lifeCtx.Done():
231 return nil, mcpsdk.ErrConnectionClosed
232 case <-done:
233 if attempt.err != nil {
234 return nil, attempt.err
235 }
236 return attempt.session, nil
237 }
238 }
239
240 attempt := &sessionBuild{done: make(chan struct{})}
241 t.building = attempt
242 t.nextGeneration++
243 generation := t.nextGeneration
244 if generation == 1 {
245 t.state = SessionStateConnecting
246 } else {
247 t.state = SessionStateReconnecting
248 }
249 t.wg.Add(1)
250 t.mu.Unlock()
251 go t.runBuild(attempt, generation)
252 }
253 }
254
255 func (t *sdkSessionTransport) runBuild(attempt *sessionBuild, generation uint64) {
256 defer t.wg.Done()
257 buildCtx, cancel := context.WithTimeout(t.lifeCtx, t.spec.startupTimeout())
258 managed, buildErr := t.build(buildCtx, generation)
259 cancel()
260
261 t.mu.Lock()
262 if t.closed && managed != nil {
263 t.mu.Unlock()
264 closeManagedSession(managed)
265 t.mu.Lock()
266 managed = nil
267 buildErr = mcpsdk.ErrConnectionClosed
268 }
269 if buildErr == nil {
270 t.current = managed
271 t.state = SessionStateReady
272 t.reconnectAttempts = 0
273 t.lastErrorKind = SessionErrorNone
274 t.lastError = ""
275 } else {
276 t.state = SessionStateFailed
277 t.lastErrorKind = classifySessionError(buildErr)
278 t.lastError = t.safeErrorText(buildErr, "")
279 }
280 attempt.session = managed
281 attempt.err = buildErr
282 if t.building == attempt {
283 t.building = nil
284 }
285 close(attempt.done)
286 t.mu.Unlock()
287
288 if managed != nil {
289 t.watch(managed)
290 }
291 }
292
293 func (t *sdkSessionTransport) build(ctx context.Context, generation uint64) (*managedMCPSession, error) {
294 // Connect uses its context for the connection lifetime, not only for the
295 // handshake. Give the connection a session-scoped context and let the bounded
296 // build context cancel it only while Connect is still in flight.
297 sessionCtx, cancelSession := context.WithCancel(t.lifeCtx)
298 stopBuildCancel := context.AfterFunc(ctx, cancelSession)
299 endpoint, err := t.newEndpoint(sessionCtx)
300 if err != nil {
301 stopBuildCancel()
302 cancelSession()
303 return nil, err
304 }
305 closeEndpoint := endpoint.close
306 var closeOnce sync.Once
307 endpoint.close = func() {
308 closeOnce.Do(func() {
309 // Let the endpoint deliver EOF and reap its child before cancelling
310 // the command context, which otherwise kills it before the grace period.
311 if closeEndpoint != nil {
312 closeEndpoint()
313 }
314 cancelSession()
315 })
316 }
317
318 capabilities := &mcpsdk.ClientCapabilities{}
319 if len(mcpRoots(t.spec.WorkspaceRoot)) > 0 {
320 //nolint:staticcheck // Legacy MCP servers still require roots during the SDK deprecation window.
321 capabilities.RootsV2 = &mcpsdk.RootCapabilities{ListChanged: false}
322 }
323 profileCaps := t.profile.Capabilities()
324 var elicitationHandler func(context.Context, *mcpsdk.ElicitRequest) (*mcpsdk.ElicitResult, error)
325 if profileCaps.ElicitationForms || profileCaps.ElicitationURL {
326 declared := &mcpsdk.ElicitationCapabilities{}
327 if profileCaps.ElicitationForms {
328 declared.Form = &mcpsdk.FormElicitationCapabilities{}
329 }
330 if profileCaps.ElicitationURL {
331 declared.URL = &mcpsdk.URLElicitationCapabilities{}
332 }
333 capabilities.Elicitation = declared
334 elicitationHandler = t.handleElicitation
335 }
336 if profileCaps.AppsUI {
337 capabilities.AddExtension(AppsUIExtensionID, map[string]any{
338 "mimeTypes": []any{AppsMimeType},
339 })
340 }
341 client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "reasonix", Version: mcpClientVersion()}, &mcpsdk.ClientOptions{
342 Capabilities: capabilities,
343 ElicitationHandler: elicitationHandler,
344 ToolListChangedHandler: func(_ context.Context, req *mcpsdk.ToolListChangedRequest) {
345 t.dispatchSDKNotification(generation, "notifications/tools/list_changed", req.Params)
346 },
347 PromptListChangedHandler: func(_ context.Context, req *mcpsdk.PromptListChangedRequest) {
348 t.dispatchSDKNotification(generation, "notifications/prompts/list_changed", req.Params)
349 },
350 ResourceListChangedHandler: func(_ context.Context, req *mcpsdk.ResourceListChangedRequest) {
351 t.dispatchSDKNotification(generation, "notifications/resources/list_changed", req.Params)
352 },
353 ProgressNotificationHandler: func(_ context.Context, req *mcpsdk.ProgressNotificationClientRequest) {
354 t.dispatchSDKProgress(generation, req.Params)
355 },
356 })
357 if canonicalMCPRuntimeTransport(t.spec.Type) == "streamable-http" {
358 client.AddSendingMiddleware(asyncStreamableHTTPSubscriptions)
359 }
360 for _, root := range mcpRoots(t.spec.WorkspaceRoot) {
361 //nolint:staticcheck // Preserve the existing workspace-root contract for legacy MCP servers.
362 client.AddRoots(&mcpsdk.Root{URI: root.URI, Name: root.Name})
363 }
364
365 t.setStateIfBuilding(generation, SessionStateListening)
366 session, err := client.Connect(sessionCtx, endpoint.transport, nil)
367 if err != nil {
368 stopBuildCancel()
369 endpoint.close()
370 stderr := ""
371 if endpoint.startupStderr != nil {
372 stderr = endpoint.startupStderr()
373 }
374 if stderr != "" {
375 t.mu.Lock()
376 t.lastStartupStderr = stderr
377 t.mu.Unlock()
378 }
379 return nil, err
380 }
381 if !stopBuildCancel() || ctx.Err() != nil {
382 _ = session.Close()
383 endpoint.close()
384 if err := ctx.Err(); err != nil {
385 return nil, err
386 }
387 return nil, mcpsdk.ErrConnectionClosed
388 }
389 protocol := ""
390 if result := session.InitializeResult(); result != nil {
391 protocol = result.ProtocolVersion
392 }
393 return &managedMCPSession{
394 generation: generation,
395 session: session,
396 endpoint: endpoint,
397 protocol: protocol,
398 }, nil
399 }
400
401 func (t *sdkSessionTransport) setStateIfBuilding(generation uint64, state SessionState) {
402 t.mu.Lock()
403 if !t.closed && t.current == nil && t.nextGeneration == generation {
404 t.state = state
405 }
406 t.mu.Unlock()
407 }
408
409 // handleElicitation answers server-initiated elicitation. MCP 2026 middleware
410 // preserves the tools/call context; legacy push requests use the fail-closed,
411 // unambiguous active-call registry. Without one exact broker the request is
412 // cancelled — the model must never guess an answer or cross tabs.
413 func (t *sdkSessionTransport) handleElicitation(ctx context.Context, req *mcpsdk.ElicitRequest) (*mcpsdk.ElicitResult, error) {
414 if ctx == nil {
415 ctx = context.Background()
416 }
417 broker := mcpinteraction.FromContext(ctx)
418 decisionCtx := ctx
419 cleanup := func() {}
420 if broker == nil {
421 legacyBroker, callCtx, ok := t.unambiguousLegacyElicitation()
422 if !ok {
423 return &mcpsdk.ElicitResult{Action: mcpinteraction.ActionCancel}, nil
424 }
425 broker = legacyBroker
426 var cancel context.CancelFunc
427 decisionCtx, cancel = context.WithCancel(callCtx)
428 stop := context.AfterFunc(ctx, cancel)
429 cleanup = func() {
430 stop()
431 cancel()
432 }
433 }
434 defer cleanup()
435 interactReq := mcpinteraction.Request{
436 Server: t.name,
437 Mode: req.Params.Mode,
438 Message: req.Params.Message,
439 URL: req.Params.URL,
440 ElicitationID: req.Params.ElicitationID,
441 }
442 if req.Params.RequestedSchema != nil {
443 if raw, err := json.Marshal(req.Params.RequestedSchema); err == nil {
444 interactReq.RequestedSchema = raw
445 } else {
446 return nil, fmt.Errorf("encode elicitation schema: %w", err)
447 }
448 }
449 if !mcpinteraction.SanitizeURLMode(interactReq) {
450 return &mcpsdk.ElicitResult{Action: mcpinteraction.ActionCancel}, nil
451 }
452 res, err := broker.Interact(decisionCtx, interactReq)
453 if err != nil {
454 return nil, err
455 }
456 switch res.Action {
457 case mcpinteraction.ActionAccept, mcpinteraction.ActionDecline, mcpinteraction.ActionCancel:
458 default:
459 return nil, fmt.Errorf("invalid elicitation action %q", res.Action)
460 }
461 return &mcpsdk.ElicitResult{Action: res.Action, Content: res.Content}, nil
462 }
463
464 func (t *sdkSessionTransport) dispatchSDKNotification(generation uint64, method string, params any) {
465 if !t.generationActive(generation) {
466 return
467 }
468 payload, err := json.Marshal(params)
469 if err != nil {
470 return
471 }
472 t.notifications.dispatchNotification(method, payload)
473 }
474
475 func (t *sdkSessionTransport) dispatchSDKProgress(generation uint64, params any) {
476 if !t.generationActive(generation) {
477 return
478 }
479 payload, err := json.Marshal(params)
480 if err != nil {
481 return
482 }
483 t.progress.dispatchProgress(payload)
484 }
485
486 func (t *sdkSessionTransport) generationActive(generation uint64) bool {
487 t.mu.Lock()
488 current := t.current
489 valid := !t.closed && (current == nil && t.nextGeneration == generation || current != nil && current.generation == generation)
490 t.mu.Unlock()
491 return valid
492 }
493
494 func (t *sdkSessionTransport) watch(managed *managedMCPSession) {
495 t.wg.Go(func() {
496 t.handleSessionEnd(managed, managed.session.Wait())
497 })
498 }
499
500 func (t *sdkSessionTransport) handleSessionEnd(managed *managedMCPSession, err error) {
501 t.mu.Lock()
502 if t.closed || t.current != managed {
503 t.mu.Unlock()
504 return
505 }
506 t.current = nil
507 if managed.session.ID() == "" && (errors.Is(err, mcpsdk.ErrSessionMissing) || t.isStreamableHTTPNotFound(err)) {
508 t.state = SessionStateFailed
509 t.lastErrorKind = SessionErrorProtocol
510 t.lastError = t.safeErrorText(fmt.Errorf("MCP endpoint returned HTTP 404 without an established session: %w", err), "")
511 t.mu.Unlock()
512 if managed.endpoint.close != nil {
513 managed.endpoint.close()
514 }
515 return
516 }
517 t.state = SessionStateReconnecting
518 t.lastErrorKind = SessionErrorStreamClosed
519 t.lastError = t.safeErrorText(err, managed.session.ID())
520 t.mu.Unlock()
521 if managed.endpoint.close != nil {
522 managed.endpoint.close()
523 }
524 t.startAutoReconnect()
525 }
526
527 func (t *sdkSessionTransport) invalidate(managed *managedMCPSession) {
528 if managed == nil {
529 return
530 }
531 t.mu.Lock()
532 if t.current != managed {
533 t.mu.Unlock()
534 return
535 }
536 t.current = nil
537 t.state = SessionStateReconnecting
538 t.mu.Unlock()
539 closeManagedSession(managed)
540 }
541
542 func (t *sdkSessionTransport) startAutoReconnect() {
543 t.mu.Lock()
544 if t.closed || t.autoReconnecting || t.current != nil {
545 t.mu.Unlock()
546 return
547 }
548 t.autoReconnecting = true
549 delays := append([]time.Duration(nil), t.reconnectDelays...)
550 t.wg.Add(1)
551 t.mu.Unlock()
552
553 go func() {
554 defer t.wg.Done()
555 defer func() {
556 t.mu.Lock()
557 t.autoReconnecting = false
558 t.mu.Unlock()
559 }()
560 for index, delay := range delays {
561 if err := sleepContext(t.lifeCtx, delay); err != nil {
562 return
563 }
564 t.mu.Lock()
565 if t.closed || t.current != nil {
566 t.mu.Unlock()
567 return
568 }
569 t.reconnectAttempts = index + 1
570 t.state = SessionStateReconnecting
571 t.mu.Unlock()
572
573 attemptCtx, cancel := context.WithTimeout(t.lifeCtx, t.spec.startupTimeout())
574 _, err := t.acquire(attemptCtx)
575 cancel()
576 if err == nil {
577 return
578 }
579 }
580 t.mu.Lock()
581 if !t.closed && t.current == nil {
582 t.state = SessionStateFailed
583 }
584 t.mu.Unlock()
585 }()
586 }
587
588 func (t *sdkSessionTransport) noteRuntimeError(managed *managedMCPSession, kind SessionErrorKind, err error) {
589 t.mu.Lock()
590 if !t.closed && (managed == nil || t.current == managed) {
591 t.lastErrorKind = kind
592 sessionID := ""
593 if managed != nil {
594 sessionID = managed.session.ID()
595 }
596 t.lastError = t.safeErrorText(err, sessionID)
597 }
598 t.mu.Unlock()
599 }
600
601 func (t *sdkSessionTransport) clearRuntimeError(managed *managedMCPSession) {
602 t.mu.Lock()
603 if !t.closed && t.current == managed {
604 t.lastErrorKind = SessionErrorNone
605 t.lastError = ""
606 t.state = SessionStateReady
607 }
608 t.mu.Unlock()
609 }
610
611 func (t *sdkSessionTransport) sessionDiagnostics() sessionDiagnostics {
612 t.mu.Lock()
613 defer t.mu.Unlock()
614 d := sessionDiagnostics{
615 State: t.state,
616 ReconnectAttempts: t.reconnectAttempts,
617 LastErrorKind: t.lastErrorKind,
618 LastError: t.lastError,
619 }
620 if t.current != nil {
621 d.ProtocolVersion = t.current.protocol
622 d.SessionIDPresent = t.current.session.ID() != ""
623 }
624 return d
625 }
626
627 func (t *sdkSessionTransport) startupStderr() string {
628 t.mu.Lock()
629 defer t.mu.Unlock()
630 if t.current != nil && t.current.endpoint.startupStderr != nil {
631 return redactMCPConfigValues(t.current.endpoint.startupStderr(), t.spec)
632 }
633 return redactMCPConfigValues(t.lastStartupStderr, t.spec)
634 }
635
636 func (t *sdkSessionTransport) close() {
637 if t == nil {
638 return
639 }
640 t.mu.Lock()
641 if t.closed {
642 t.mu.Unlock()
643 return
644 }
645 t.closed = true
646 t.state = SessionStateClosed
647 current := t.current
648 t.current = nil
649 t.mu.Unlock()
650
651 t.progress.clear()
652 closeManagedSession(current)
653 t.cancel()
654 waitWithBudget(t.wg.Wait, closeWaitBudget)
655 }
656
657 func closeManagedSession(managed *managedMCPSession) {
658 if managed == nil {
659 return
660 }
661 done := make(chan struct{})
662 go func() {
663 _ = managed.session.Close()
664 close(done)
665 }()
666 select {
667 case <-done:
668 case <-time.After(2 * time.Second):
669 if managed.endpoint.close != nil {
670 managed.endpoint.close()
671 }
672 select {
673 case <-done:
674 case <-time.After(gracefulCloseWaitBudget):
675 }
676 }
677 if managed.endpoint.close != nil {
678 managed.endpoint.close()
679 }
680 }
681
682 func invokeSDKMethod(ctx context.Context, session *mcpsdk.ClientSession, method string, params any) (json.RawMessage, error) {
683 marshal := func(value any, err error) (json.RawMessage, error) {
684 if err != nil {
685 return nil, err
686 }
687 data, err := json.Marshal(value)
688 return json.RawMessage(data), err
689 }
690 decode := func(target any) error {
691 data, err := json.Marshal(params)
692 if err != nil {
693 return err
694 }
695 return json.Unmarshal(data, target)
696 }
697
698 switch method {
699 case "initialize":
700 return marshal(session.InitializeResult(), nil)
701 case "ping":
702 return marshal(map[string]any{}, session.Ping(ctx, nil))
703 case "tools/list":
704 items := make([]*mcpsdk.Tool, 0)
705 for item, err := range session.Tools(ctx, nil) {
706 if err != nil {
707 return nil, err
708 }
709 items = append(items, item)
710 }
711 return marshal(map[string]any{"tools": items}, nil)
712 case "tools/call":
713 var typed mcpsdk.CallToolParams
714 if err := decode(&typed); err != nil {
715 return nil, err
716 }
717 return marshal(session.CallTool(ctx, &typed))
718 case "prompts/list":
719 items := make([]*mcpsdk.Prompt, 0)
720 for item, err := range session.Prompts(ctx, nil) {
721 if err != nil {
722 return nil, err
723 }
724 items = append(items, item)
725 }
726 return marshal(map[string]any{"prompts": items}, nil)
727 case "prompts/get":
728 var typed mcpsdk.GetPromptParams
729 if err := decode(&typed); err != nil {
730 return nil, err
731 }
732 return marshal(session.GetPrompt(ctx, &typed))
733 case "resources/list":
734 items := make([]*mcpsdk.Resource, 0)
735 for item, err := range session.Resources(ctx, nil) {
736 if err != nil {
737 return nil, err
738 }
739 items = append(items, item)
740 }
741 return marshal(map[string]any{"resources": items}, nil)
742 case "resources/read":
743 var typed mcpsdk.ReadResourceParams
744 if err := decode(&typed); err != nil {
745 return nil, err
746 }
747 return marshal(session.ReadResource(ctx, &typed))
748 default:
749 return nil, fmt.Errorf("unsupported MCP method %q", method)
750 }
751 }
752
753 func safeToReplayMCPMethod(method string) bool {
754 switch method {
755 case "initialize", "ping", "tools/list", "prompts/list", "prompts/get", "resources/list", "resources/read":
756 return true
757 default:
758 return false
759 }
760 }
761
761 lines GO