diff --git a/mcpcompat/server/server_internal_test.go b/mcpcompat/server/server_internal_test.go index 778cd8b..0cb2689 100644 --- a/mcpcompat/server/server_internal_test.go +++ b/mcpcompat/server/server_internal_test.go @@ -164,6 +164,34 @@ func TestSetSessionTools_NonObjectSchemaDoesNotPanic(t *testing.T) { assert.Contains(t, registered, "good-schema", "the well-formed sibling tool must still register") } +// TestSyncSessionResources_UnparseableURIDoesNotPanic verifies that an overlay +// resource whose URI fails url.Parse does not crash the session when synced onto +// the go-sdk server. go-sdk's AddResource panics on such a URI, so the +// panic must be avoided by the url.Parse pre-check in syncSessionResources and the offending +// resource skipped while other resources in the same overlay are still processed. +func TestSyncSessionResources_UnparseableURIDoesNotPanic(t *testing.T) { + t.Parallel() + s := NewMCPServer("s", "1") + srv, err := s.buildServer(nil, false) + require.NoError(t, err) + + const badName = "broken" + handler := func(context.Context, mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { return nil, nil } + cs := s.sessionFor("sid-bad-resource") + cs.SetSessionResources(map[string]ServerResource{ + badName: { + Resource: mcp.Resource{URI: "%zz", Name: badName}, + Handler: handler, + }, + "good": { + Resource: mcp.Resource{URI: "file:///good", Name: "good"}, + Handler: handler, + }, + }) + + assert.NotPanics(t, func() { s.syncSessionResources(srv, cs) }) +} + func TestHooks_Fire(t *testing.T) { t.Parallel() h := &Hooks{} diff --git a/mcpcompat/server/session.go b/mcpcompat/server/session.go index 1b912df..abade8a 100644 --- a/mcpcompat/server/session.go +++ b/mcpcompat/server/session.go @@ -5,6 +5,7 @@ package server import ( "context" + "net/url" "sync" "sync/atomic" @@ -472,23 +473,34 @@ func (s *MCPServer) addSessionTool(srv *gosdk.Server, gt *gosdk.Tool, h ToolHand // syncSessionResources adds the session's resource overlay onto its go-sdk // server. Resources are add-only here (ToolHive sets them once at registration). +// +// go-sdk's AddResource calls url.Parse on the resource URI and PANICS if it +// fails. This path can run on the live session goroutine, so the same check is +// run first and a resource with an unparseable URI is logged and skipped, so one +// bad URI does not take down the session. func (s *MCPServer) syncSessionResources(srv *gosdk.Server, cs *clientSession) { cs.mu.RLock() defer cs.mu.RUnlock() - for _, sr := range cs.resources { + for uri, sr := range cs.resources { gr := &gosdk.Resource{} if err := jsonConvert(sr.Resource, gr); err != nil { continue } + if _, err := url.Parse(gr.URI); err != nil { + if s.logger != nil { + s.logger.Warn("skipping per-session resource: unparseable URI", "uri", uri, "error", err) + } + continue + } srv.AddResource(gr, s.wrapServerResource(sr)) } } // syncSessionResourceTemplates adds the session's resource-template overlay onto // its go-sdk server. Templates are add-only here (ToolHive sets them once at -// registration), mirroring syncSessionResources. Unlike AddResource, go-sdk's -// AddResourceTemplate parses the URI template and PANICS if it is invalid or not -// absolute; this path can run on the live session goroutine, so each template is +// registration), mirroring syncSessionResources. Like AddResource, go-sdk's +// AddResourceTemplate PANICS on a bad URI (here, an invalid or non-absolute URI +// template); this path can run on the live session goroutine, so each template is // registered via addSessionResourceTemplate, which recovers such a panic, logs // it, and skips the offending template so one bad template does not take down // the session. @@ -540,7 +552,7 @@ func (s *MCPServer) addSessionResourceTemplate( // without any shim-side dispatch. Prompts are add-only here (ToolHive sets them // once at registration). Unlike AddTool, go-sdk's AddPrompt performs no // schema/URI validation and cannot panic, so no recover wrapper is needed -// (mirroring syncSessionResources rather than the tool path). +// (unlike the tool, resource and resource-template paths). func (s *MCPServer) syncSessionPrompts(srv *gosdk.Server, cs *clientSession) { cs.mu.RLock() defer cs.mu.RUnlock()