Skip to content

Commit 04c98f0

Browse files
feat: support RFC 6570 for resource templates (#54)
1 parent 7c4fe62 commit 04c98f0

File tree

6 files changed

+82
-18
lines changed

6 files changed

+82
-18
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ require (
1010
require (
1111
github.com/davecgh/go-spew v1.1.1 // indirect
1212
github.com/pmezard/go-difflib v1.0.0 // indirect
13+
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
1314
gopkg.in/yaml.v3 v3.0.1 // indirect
1415
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
66
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
77
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
88
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9+
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
10+
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
911
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
1012
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
1113
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

mcp/resources.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package mcp
22

3+
import "github.com/yosida95/uritemplate/v3"
4+
35
// ResourceOption is a function that configures a Resource.
46
// It provides a flexible way to set various properties of a Resource using the functional options pattern.
57
type ResourceOption func(*Resource)
@@ -60,7 +62,7 @@ type ResourceTemplateOption func(*ResourceTemplate)
6062
// Options are applied in order, allowing for flexible template configuration.
6163
func NewResourceTemplate(uriTemplate string, name string, opts ...ResourceTemplateOption) ResourceTemplate {
6264
template := ResourceTemplate{
63-
URITemplate: uriTemplate,
65+
URITemplate: uritemplate.MustNew(uriTemplate),
6466
Name: name,
6567
}
6668

mcp/types.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
// MCP is a protocol for communication between LLM-powered applications and their supporting services.
33
package mcp
44

5-
import "encoding/json"
5+
import (
6+
"encoding/json"
7+
8+
"github.com/yosida95/uritemplate/v3"
9+
)
610

711
/* JSON-RPC types */
812

@@ -442,7 +446,7 @@ type ResourceTemplate struct {
442446
Annotated
443447
// A URI template (according to RFC 6570) that can be used to construct
444448
// resource URIs.
445-
URITemplate string `json:"uriTemplate"`
449+
URITemplate *uritemplate.Template `json:"uriTemplate"`
446450
// A human-readable name for the type of resource this template refers to.
447451
//
448452
// This can be used by clients to populate UI elements.

server/server.go

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8-
"regexp"
98
"sort"
109
"sync"
1110
"sync/atomic"
1211

1312
"github.com/mark3labs/mcp-go/mcp"
13+
"github.com/yosida95/uritemplate/v3"
1414
)
1515

1616
// resourceEntry holds both a resource and its handler
@@ -496,7 +496,7 @@ func (s *MCPServer) AddResourceTemplate(
496496
}
497497
s.mu.Lock()
498498
defer s.mu.Unlock()
499-
s.resourceTemplates[template.URITemplate] = resourceTemplateEntry{
499+
s.resourceTemplates[template.URITemplate.Raw()] = resourceTemplateEntry{
500500
template: template,
501501
handler: handler,
502502
}
@@ -693,10 +693,17 @@ func (s *MCPServer) handleReadResource(
693693
// If no direct handler found, try matching against templates
694694
var matchedHandler ResourceTemplateHandlerFunc
695695
var matched bool
696-
for uriTemplate, entry := range s.resourceTemplates {
697-
if matchesTemplate(request.Params.URI, uriTemplate) {
696+
for _, entry := range s.resourceTemplates {
697+
template := entry.template
698+
if matchesTemplate(request.Params.URI, template.URITemplate) {
698699
matchedHandler = entry.handler
699700
matched = true
701+
matchedVars := template.URITemplate.Match(request.Params.URI)
702+
// Convert matched variables to a map
703+
request.Params.Arguments = make(map[string]interface{})
704+
for name, value := range matchedVars {
705+
request.Params.Arguments[name] = value.V
706+
}
700707
break
701708
}
702709
}
@@ -724,17 +731,8 @@ func (s *MCPServer) handleReadResource(
724731
}
725732

726733
// matchesTemplate checks if a URI matches a URI template pattern
727-
func matchesTemplate(uri string, template string) bool {
728-
// Convert template into a regex pattern
729-
pattern := template
730-
// Replace {name} with ([^/]+)
731-
pattern = regexp.QuoteMeta(pattern)
732-
pattern = regexp.MustCompile(`\\\{[^}]+\\\}`).
733-
ReplaceAllString(pattern, `([^/]+)`)
734-
pattern = "^" + pattern + "$"
735-
736-
matched, _ := regexp.MatchString(pattern, uri)
737-
return matched
734+
func matchesTemplate(uri string, template *uritemplate.Template) bool {
735+
return template.Regexp().MatchString(uri)
738736
}
739737

740738
func (s *MCPServer) handleListPrompts(

server/server_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -920,6 +920,63 @@ func TestMCPServer_Instructions(t *testing.T) {
920920
}
921921
}
922922

923+
func TestMCPServer_ResourceTemplates(t *testing.T) {
924+
server := NewMCPServer("test-server", "1.0.0",
925+
WithResourceCapabilities(true, true),
926+
WithPromptCapabilities(true),
927+
)
928+
929+
server.AddResourceTemplate(
930+
mcp.NewResourceTemplate(
931+
"test://{a}/test-resource{/b*}",
932+
"My Resource",
933+
),
934+
func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
935+
a := request.Params.Arguments["a"].([]string)
936+
b := request.Params.Arguments["b"].([]string)
937+
// Validate that the template arguments are passed correctly to the handler
938+
assert.Equal(t, []string{"something"}, a)
939+
assert.Equal(t, []string{"a", "b", "c"}, b)
940+
return []mcp.ResourceContents{
941+
mcp.TextResourceContents{
942+
URI: "test://something/test-resource/a/b/c",
943+
MIMEType: "text/plain",
944+
Text: "test content: " + a[0],
945+
},
946+
}, nil
947+
},
948+
)
949+
950+
message := `{
951+
"jsonrpc": "2.0",
952+
"id": 1,
953+
"method": "resources/read",
954+
"params": {
955+
"uri": "test://something/test-resource/a/b/c"
956+
}
957+
}`
958+
959+
t.Run("Get resource template", func(t *testing.T) {
960+
response := server.HandleMessage(
961+
context.Background(),
962+
[]byte(message),
963+
)
964+
assert.NotNil(t, response)
965+
966+
resp, ok := response.(mcp.JSONRPCResponse)
967+
assert.True(t, ok)
968+
// Validate that the resource values are returned correctly
969+
result, ok := resp.Result.(mcp.ReadResourceResult)
970+
assert.True(t, ok)
971+
assert.Len(t, result.Contents, 1)
972+
resultContent, ok := result.Contents[0].(mcp.TextResourceContents)
973+
assert.True(t, ok)
974+
assert.Equal(t, "test://something/test-resource/a/b/c", resultContent.URI)
975+
assert.Equal(t, "text/plain", resultContent.MIMEType)
976+
assert.Equal(t, "test content: something", resultContent.Text)
977+
})
978+
}
979+
923980
func createTestServer() *MCPServer {
924981
server := NewMCPServer("test-server", "1.0.0",
925982
WithResourceCapabilities(true, true),

0 commit comments

Comments
 (0)