@@ -21,6 +21,7 @@ import (
2121 "sync"
2222 "time"
2323
24+ "github.com/mark3labs/mcp-go/mcp"
2425 "github.com/mark3labs/mcp-go/server"
2526
2627 "github.com/stacklok/toolhive/pkg/audit"
@@ -491,6 +492,24 @@ func New(
491492 srv .handleSessionRegistration (ctx , session )
492493 })
493494
495+ // Register OnBeforeListTools hook for lazy session tool injection.
496+ //
497+ // When a session is reconstructed from Redis on a different pod (cross-pod sharing),
498+ // the SDK's per-session tool store is empty because OnRegisterSession only fires
499+ // during Initialize, which the client doesn't re-send to pod B. This hook lazily
500+ // injects the tools from the VMCP session manager into the ephemeral SDK session
501+ // before handleListTools reads from the per-session tool store.
502+ hooks .AddBeforeListTools (func (ctx context.Context , _ any , _ * mcp.ListToolsRequest ) {
503+ srv .lazyInjectSessionTools (ctx )
504+ })
505+
506+ // Register OnBeforeCallTool hook for the same reason as OnBeforeListTools.
507+ // A client may call a tool directly without first calling tools/list, so we
508+ // also need to ensure the tool handlers are registered before the call is routed.
509+ hooks .AddBeforeCallTool (func (ctx context.Context , _ any , _ * mcp.CallToolRequest ) {
510+ srv .lazyInjectSessionTools (ctx )
511+ })
512+
494513 // Disarm the close-on-error guard: Server is fully constructed.
495514 closeStorageOnErr = false
496515 return srv , nil
@@ -1008,6 +1027,40 @@ func setSessionToolsDirect(session server.ClientSession, tools []server.ServerTo
10081027 return nil
10091028}
10101029
1030+ // lazyInjectSessionTools injects tools into the SDK ephemeral session for sessions
1031+ // that were reconstructed from Redis on a different pod (cross-pod session sharing).
1032+ //
1033+ // When a client connects to pod B with an existing session ID (established on pod A),
1034+ // the SDK creates an ephemeral session with no tools because OnRegisterSession only fires
1035+ // during Initialize, which the client doesn't re-send to pod B. This method is called
1036+ // from OnBeforeListTools and OnBeforeCallTool hooks to lazily inject the tools before
1037+ // the SDK handler reads from the per-session tool store.
1038+ //
1039+ // For sessions initialized on this pod (normal case), tools are already in the store
1040+ // (set by setSessionToolsDirect during OnRegisterSession); this method is a no-op.
1041+ func (s * Server ) lazyInjectSessionTools (ctx context.Context ) {
1042+ sess := server .ClientSessionFromContext (ctx )
1043+ if sess == nil {
1044+ return
1045+ }
1046+ sessionWithTools , ok := sess .(server.SessionWithTools )
1047+ if ! ok {
1048+ return
1049+ }
1050+ if len (sessionWithTools .GetSessionTools ()) > 0 {
1051+ return // tools already registered (normal pod-local case)
1052+ }
1053+ sessionID := sess .SessionID ()
1054+ adaptedTools , err := s .vmcpSessionMgr .GetAdaptedTools (sessionID )
1055+ if err != nil || len (adaptedTools ) == 0 {
1056+ slog .Debug ("lazyInjectSessionTools: no tools available for session" , "session_id" , sessionID )
1057+ return
1058+ }
1059+ if err := setSessionToolsDirect (sess , adaptedTools ); err != nil {
1060+ slog .Warn ("lazyInjectSessionTools: failed to inject tools" , "session_id" , sessionID , "error" , err )
1061+ }
1062+ }
1063+
10111064// handleSessionRegistration processes a new MCP session registration.
10121065// It fires AFTER the session is registered in the SDK.
10131066func (s * Server ) handleSessionRegistration (
0 commit comments