Skip to content

Commit 732b97f

Browse files
authored
mcp: add syntax and scheme validation to AddResourceTemplate (#253)
Add template validation and scheme check in `AddResourceTemplate`.
1 parent 46ba813 commit 732b97f

File tree

2 files changed

+50
-1
lines changed

2 files changed

+50
-1
lines changed

mcp/server.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
2323
"github.com/modelcontextprotocol/go-sdk/internal/util"
2424
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
25+
"github.com/yosida95/uritemplate/v3"
2526
)
2627

2728
const DefaultPageSize = 1000
@@ -229,7 +230,19 @@ func (s *Server) RemoveResources(uris ...string) {
229230
func (s *Server) AddResourceTemplate(t *ResourceTemplate, h ResourceHandler) {
230231
s.changeAndNotify(notificationResourceListChanged, &ResourceListChangedParams{},
231232
func() bool {
232-
// TODO: check template validity.
233+
// Validate the URI template syntax
234+
_, err := uritemplate.New(t.URITemplate)
235+
if err != nil {
236+
panic(fmt.Errorf("URI template %q is invalid: %w", t.URITemplate, err))
237+
}
238+
// Ensure the URI template has a valid scheme
239+
u, err := url.Parse(t.URITemplate)
240+
if err != nil {
241+
panic(err) // url.Parse includes the URI in the error
242+
}
243+
if !u.IsAbs() {
244+
panic(fmt.Errorf("URI template %q needs a scheme", t.URITemplate))
245+
}
233246
s.resourceTemplates.add(&serverResourceTemplate{t, h})
234247
return true
235248
})

mcp/server_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,42 @@ func TestServerCapabilities(t *testing.T) {
373373
}
374374
}
375375

376+
func TestServerAddResourceTemplate(t *testing.T) {
377+
tests := []struct {
378+
name string
379+
template string
380+
expectPanic bool
381+
}{
382+
{"ValidFileTemplate", "file:///{a}/{b}", false},
383+
{"ValidCustomScheme", "myproto:///{a}", false},
384+
{"MissingScheme1", "://example.com/{path}", true},
385+
{"MissingScheme2", "/api/v1/users/{id}", true},
386+
{"EmptyVariable", "file:///{}/{b}", true},
387+
{"UnclosedVariable", "file:///{a", true},
388+
}
389+
390+
for _, tt := range tests {
391+
t.Run(tt.name, func(t *testing.T) {
392+
rt := ResourceTemplate{URITemplate: tt.template}
393+
394+
defer func() {
395+
if r := recover(); r != nil {
396+
if !tt.expectPanic {
397+
t.Errorf("%s: unexpected panic: %v", tt.name, r)
398+
}
399+
} else {
400+
if tt.expectPanic {
401+
t.Errorf("%s: expected panic but did not panic", tt.name)
402+
}
403+
}
404+
}()
405+
406+
s := NewServer(testImpl, nil)
407+
s.AddResourceTemplate(&rt, nil)
408+
})
409+
}
410+
}
411+
376412
// TestServerSessionkeepaliveCancelOverwritten is to verify that `ServerSession.keepaliveCancel` is assigned exactly once,
377413
// ensuring that only a single goroutine is responsible for the session's keepalive ping mechanism.
378414
func TestServerSessionkeepaliveCancelOverwritten(t *testing.T) {

0 commit comments

Comments
 (0)