Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions mcpcompat/server/server_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down
22 changes: 17 additions & 5 deletions mcpcompat/server/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package server

import (
"context"
"net/url"
"sync"
"sync/atomic"

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand Down
Loading