Skip to content

Commit e859847

Browse files
authored
feat: support in tool result handling & update example (#467)
* feat:support in tool result handling & update example Signed-off-by: FanOne <294350394@qq.com> * feat:add doc about resource link Signed-off-by: FanOne <294350394@qq.com> * feat:add desc about ResourceLink in doc * feat: add desc about resource link in docs Signed-off-by: FanOne <294350394@qq.com> --------- Signed-off-by: FanOne <294350394@qq.com>
1 parent 9c352bd commit e859847

File tree

5 files changed

+304
-0
lines changed

5 files changed

+304
-0
lines changed

examples/everything/main.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,41 @@ func NewMCPServer() *server.MCPServer {
165165

166166
mcpServer.AddNotificationHandler("notification", handleNotification)
167167

168+
mcpServer.AddTool(mcp.NewTool("get_resource_link",
169+
mcp.WithDescription("Returns a resource link example"),
170+
mcp.WithString("resource_type",
171+
mcp.Description("Type of resource to link to"),
172+
mcp.DefaultString("document")),
173+
), handleGetResourceLinkTool)
174+
168175
return mcpServer
169176
}
170177

178+
func handleGetResourceLinkTool(
179+
ctx context.Context,
180+
request mcp.CallToolRequest,
181+
) (*mcp.CallToolResult, error) {
182+
resourceType := request.GetString("resource_type", "document")
183+
return &mcp.CallToolResult{
184+
Content: []mcp.Content{
185+
mcp.TextContent{
186+
Type: "text",
187+
Text: fmt.Sprintf("Here's a link to a %s resource:", resourceType),
188+
},
189+
mcp.NewResourceLink(
190+
fmt.Sprintf("file:///example/%s.pdf", resourceType),
191+
fmt.Sprintf("Sample %s", resourceType),
192+
fmt.Sprintf("A sample %s for demonstration", resourceType),
193+
"application/pdf",
194+
),
195+
mcp.TextContent{
196+
Type: "text",
197+
Text: "You can access this resource using the provided URI.",
198+
},
199+
},
200+
}, nil
201+
}
202+
171203
func generateResources() []mcp.Resource {
172204
resources := make([]mcp.Resource, 100)
173205
for i := 0; i < 100; i++ {

mcp/tools.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,72 @@ func (r CallToolRequest) RequireBoolSlice(key string) ([]bool, error) {
461461
return nil, fmt.Errorf("required argument %q not found", key)
462462
}
463463

464+
// MarshalJSON implements custom JSON marshaling for CallToolResult
465+
func (r CallToolResult) MarshalJSON() ([]byte, error) {
466+
m := make(map[string]any)
467+
468+
// Marshal Meta if present
469+
if r.Meta != nil {
470+
m["_meta"] = r.Meta
471+
}
472+
473+
// Marshal Content array
474+
content := make([]any, len(r.Content))
475+
for i, c := range r.Content {
476+
content[i] = c
477+
}
478+
m["content"] = content
479+
480+
// Marshal IsError if true
481+
if r.IsError {
482+
m["isError"] = r.IsError
483+
}
484+
485+
return json.Marshal(m)
486+
}
487+
488+
// UnmarshalJSON implements custom JSON unmarshaling for CallToolResult
489+
func (r *CallToolResult) UnmarshalJSON(data []byte) error {
490+
var raw map[string]any
491+
if err := json.Unmarshal(data, &raw); err != nil {
492+
return err
493+
}
494+
495+
// Unmarshal Meta
496+
if meta, ok := raw["_meta"]; ok {
497+
if metaMap, ok := meta.(map[string]any); ok {
498+
r.Meta = metaMap
499+
}
500+
}
501+
502+
// Unmarshal Content array
503+
if contentRaw, ok := raw["content"]; ok {
504+
if contentArray, ok := contentRaw.([]any); ok {
505+
r.Content = make([]Content, len(contentArray))
506+
for i, item := range contentArray {
507+
itemBytes, err := json.Marshal(item)
508+
if err != nil {
509+
return err
510+
}
511+
content, err := UnmarshalContent(itemBytes)
512+
if err != nil {
513+
return err
514+
}
515+
r.Content[i] = content
516+
}
517+
}
518+
}
519+
520+
// Unmarshal IsError
521+
if isError, ok := raw["isError"]; ok {
522+
if isErrorBool, ok := isError.(bool); ok {
523+
r.IsError = isErrorBool
524+
}
525+
}
526+
527+
return nil
528+
}
529+
464530
// ToolListChangedNotification is an optional notification from the server to
465531
// the client, informing it that the list of tools it offers has changed. This may
466532
// be issued by servers without any previous subscription from the client.

mcp/types.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,3 +1063,46 @@ type ServerResult any
10631063
type Named interface {
10641064
GetName() string
10651065
}
1066+
1067+
// MarshalJSON implements custom JSON marshaling for Content interface
1068+
func MarshalContent(content Content) ([]byte, error) {
1069+
return json.Marshal(content)
1070+
}
1071+
1072+
// UnmarshalContent implements custom JSON unmarshaling for Content interface
1073+
func UnmarshalContent(data []byte) (Content, error) {
1074+
var raw map[string]any
1075+
if err := json.Unmarshal(data, &raw); err != nil {
1076+
return nil, err
1077+
}
1078+
1079+
contentType, ok := raw["type"].(string)
1080+
if !ok {
1081+
return nil, fmt.Errorf("missing or invalid type field")
1082+
}
1083+
1084+
switch contentType {
1085+
case "text":
1086+
var content TextContent
1087+
err := json.Unmarshal(data, &content)
1088+
return content, err
1089+
case "image":
1090+
var content ImageContent
1091+
err := json.Unmarshal(data, &content)
1092+
return content, err
1093+
case "audio":
1094+
var content AudioContent
1095+
err := json.Unmarshal(data, &content)
1096+
return content, err
1097+
case "resource_link":
1098+
var content ResourceLink
1099+
err := json.Unmarshal(data, &content)
1100+
return content, err
1101+
case "resource":
1102+
var content EmbeddedResource
1103+
err := json.Unmarshal(data, &content)
1104+
return content, err
1105+
default:
1106+
return nil, fmt.Errorf("unknown content type: %s", contentType)
1107+
}
1108+
}

mcp/types_test.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,73 @@ func TestMetaMarshalling(t *testing.T) {
6868
})
6969
}
7070
}
71+
72+
func TestResourceLinkSerialization(t *testing.T) {
73+
resourceLink := NewResourceLink(
74+
"file:///example/document.pdf",
75+
"Sample Document",
76+
"A sample document for testing",
77+
"application/pdf",
78+
)
79+
80+
// Test marshaling
81+
data, err := json.Marshal(resourceLink)
82+
require.NoError(t, err)
83+
84+
// Test unmarshaling
85+
var unmarshaled ResourceLink
86+
err = json.Unmarshal(data, &unmarshaled)
87+
require.NoError(t, err)
88+
89+
// Verify fields
90+
assert.Equal(t, "resource_link", unmarshaled.Type)
91+
assert.Equal(t, "file:///example/document.pdf", unmarshaled.URI)
92+
assert.Equal(t, "Sample Document", unmarshaled.Name)
93+
assert.Equal(t, "A sample document for testing", unmarshaled.Description)
94+
assert.Equal(t, "application/pdf", unmarshaled.MIMEType)
95+
}
96+
97+
func TestCallToolResultWithResourceLink(t *testing.T) {
98+
result := &CallToolResult{
99+
Content: []Content{
100+
TextContent{
101+
Type: "text",
102+
Text: "Here's a resource link:",
103+
},
104+
NewResourceLink(
105+
"file:///example/test.pdf",
106+
"Test Document",
107+
"A test document",
108+
"application/pdf",
109+
),
110+
},
111+
IsError: false,
112+
}
113+
114+
// Test marshaling
115+
data, err := json.Marshal(result)
116+
require.NoError(t, err)
117+
118+
// Test unmarshalling
119+
var unmarshalled CallToolResult
120+
err = json.Unmarshal(data, &unmarshalled)
121+
require.NoError(t, err)
122+
123+
// Verify content
124+
require.Len(t, unmarshalled.Content, 2)
125+
126+
// Check first content (TextContent)
127+
textContent, ok := unmarshalled.Content[0].(TextContent)
128+
require.True(t, ok)
129+
assert.Equal(t, "text", textContent.Type)
130+
assert.Equal(t, "Here's a resource link:", textContent.Text)
131+
132+
// Check second content (ResourceLink)
133+
resourceLink, ok := unmarshalled.Content[1].(ResourceLink)
134+
require.True(t, ok)
135+
assert.Equal(t, "resource_link", resourceLink.Type)
136+
assert.Equal(t, "file:///example/test.pdf", resourceLink.URI)
137+
assert.Equal(t, "Test Document", resourceLink.Name)
138+
assert.Equal(t, "A test document", resourceLink.Description)
139+
assert.Equal(t, "application/pdf", resourceLink.MIMEType)
140+
}

www/docs/pages/servers/tools.mdx

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,99 @@ func handleMultiContentTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.
529529
}
530530
```
531531

532+
### Resource Links
533+
534+
Tools can return resource links that reference other resources in your MCP server. This is useful when you want to point to existing data without duplicating content:
535+
536+
```go
537+
func handleGetResourceLinkTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
538+
resourceID, err := req.RequireString("resource_id")
539+
if err != nil {
540+
return mcp.NewToolResultError(err.Error()), nil
541+
}
542+
543+
// Create a resource link pointing to an existing resource
544+
uri := fmt.Sprintf("file://documents/%s", resourceID)
545+
resourceLink := mcp.NewResourceLink(uri, "Document", "The requested document", "application/pdf")
546+
return &mcp.CallToolResult{
547+
Content: []mcp.Content{
548+
mcp.NewTextContent("Found the requested document:"),
549+
resourceLink,
550+
},
551+
}, nil
552+
}
553+
```
554+
555+
### Mixed Content with Resource Links
556+
557+
You can combine different content types including resource links in a single tool result:
558+
559+
```go
560+
func handleSearchDocumentsTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
561+
query, err := req.RequireString("query")
562+
if err != nil {
563+
return mcp.NewToolResultError(err.Error()), nil
564+
}
565+
566+
// Simulate document search
567+
foundDocs := []string{"doc1.pdf", "doc2.txt", "doc3.md"}
568+
569+
content := []mcp.Content{
570+
mcp.NewTextContent(fmt.Sprintf("Found %d documents matching '%s':", len(foundDocs), query)),
571+
}
572+
573+
// Add resource links for each found document
574+
for _, doc := range foundDocs {
575+
uri := fmt.Sprintf("file://documents/%s", doc)
576+
parts := strings.SplitN(doc, ".", 2)
577+
name := parts[0]
578+
mimeType := "application/octet-stream" // default
579+
if len(parts) > 1 {
580+
// Map extension to MIME type (simplified)
581+
switch parts[1] {
582+
case "pdf":
583+
mimeType = "application/pdf"
584+
case "txt":
585+
mimeType = "text/plain"
586+
case "md":
587+
mimeType = "text/markdown"
588+
}
589+
}
590+
resourceLink := mcp.NewResourceLink(uri, name, fmt.Sprintf("Document: %s", doc), mimeType)
591+
content = append(content, resourceLink)
592+
}
593+
594+
return &mcp.CallToolResult{
595+
Content: content,
596+
}, nil
597+
}
598+
```
599+
600+
### Resource Link with Annotations
601+
602+
Resource links can include additional metadata through annotations:
603+
604+
```go
605+
func handleGetAnnotatedResourceTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
606+
docType := req.GetString("type", "general")
607+
// Create resource link with annotations
608+
annotated := mcp.Annotated{
609+
Annotations: &mcp.Annotations{
610+
Audience: []mcp.Role{mcp.RoleUser},
611+
},
612+
}
613+
url := "file://documents/test.pdf"
614+
resourceLink := mcp.NewResourceLink(url, "Test Document", fmt.Sprintf("A %s document", docType), "application/pdf")
615+
resourceLink.Annotated = annotated
616+
return &mcp.CallToolResult{
617+
Content: []mcp.Content{
618+
mcp.NewTextContent("Here's the important document you requested:"),
619+
resourceLink,
620+
},
621+
}, nil
622+
}
623+
```
624+
532625
### Error Results
533626

534627
```go

0 commit comments

Comments
 (0)