Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
32 changes: 32 additions & 0 deletions examples/everything/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,41 @@ func NewMCPServer() *server.MCPServer {

mcpServer.AddNotificationHandler("notification", handleNotification)

mcpServer.AddTool(mcp.NewTool("get_resource_link",
mcp.WithDescription("Returns a resource link example"),
mcp.WithString("resource_type",
mcp.Description("Type of resource to link to"),
mcp.DefaultString("document")),
), handleGetResourceLinkTool)

return mcpServer
}

func handleGetResourceLinkTool(
ctx context.Context,
request mcp.CallToolRequest,
) (*mcp.CallToolResult, error) {
resourceType := request.GetString("resource_type", "document")
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("Here's a link to a %s resource:", resourceType),
},
mcp.NewResourceLink(
fmt.Sprintf("file:///example/%s.pdf", resourceType),
fmt.Sprintf("Sample %s", resourceType),
fmt.Sprintf("A sample %s for demonstration", resourceType),
"application/pdf",
),
mcp.TextContent{
Type: "text",
Text: "You can access this resource using the provided URI.",
},
},
}, nil
}

func generateResources() []mcp.Resource {
resources := make([]mcp.Resource, 100)
for i := 0; i < 100; i++ {
Expand Down
66 changes: 66 additions & 0 deletions mcp/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,72 @@ func (r CallToolRequest) RequireBoolSlice(key string) ([]bool, error) {
return nil, fmt.Errorf("required argument %q not found", key)
}

// MarshalJSON implements custom JSON marshaling for CallToolResult
func (r CallToolResult) MarshalJSON() ([]byte, error) {
m := make(map[string]any)

// Marshal Meta if present
if r.Meta != nil {
m["_meta"] = r.Meta
}

// Marshal Content array
content := make([]any, len(r.Content))
for i, c := range r.Content {
content[i] = c
}
m["content"] = content

// Marshal IsError if true
if r.IsError {
m["isError"] = r.IsError
}

return json.Marshal(m)
}

// UnmarshalJSON implements custom JSON unmarshaling for CallToolResult
func (r *CallToolResult) UnmarshalJSON(data []byte) error {
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return err
}

// Unmarshal Meta
if meta, ok := raw["_meta"]; ok {
if metaMap, ok := meta.(map[string]any); ok {
r.Meta = metaMap
}
}

// Unmarshal Content array
if contentRaw, ok := raw["content"]; ok {
if contentArray, ok := contentRaw.([]any); ok {
r.Content = make([]Content, len(contentArray))
for i, item := range contentArray {
itemBytes, err := json.Marshal(item)
if err != nil {
return err
}
content, err := UnmarshalContent(itemBytes)
if err != nil {
return err
}
r.Content[i] = content
}
}
}

// Unmarshal IsError
if isError, ok := raw["isError"]; ok {
if isErrorBool, ok := isError.(bool); ok {
r.IsError = isErrorBool
}
}

return nil
}

// ToolListChangedNotification is an optional notification from the server to
// the client, informing it that the list of tools it offers has changed. This may
// be issued by servers without any previous subscription from the client.
Expand Down
43 changes: 43 additions & 0 deletions mcp/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1058,3 +1058,46 @@ type ServerResult any
type Named interface {
GetName() string
}

// MarshalJSON implements custom JSON marshaling for Content interface
func MarshalContent(content Content) ([]byte, error) {
return json.Marshal(content)
}

// UnmarshalContent implements custom JSON unmarshaling for Content interface
func UnmarshalContent(data []byte) (Content, error) {
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}

contentType, ok := raw["type"].(string)
if !ok {
return nil, fmt.Errorf("missing or invalid type field")
}

switch contentType {
case "text":
var content TextContent
err := json.Unmarshal(data, &content)
return content, err
case "image":
var content ImageContent
err := json.Unmarshal(data, &content)
return content, err
case "audio":
var content AudioContent
err := json.Unmarshal(data, &content)
return content, err
case "resource_link":
var content ResourceLink
err := json.Unmarshal(data, &content)
return content, err
case "resource":
var content EmbeddedResource
err := json.Unmarshal(data, &content)
return content, err
default:
return nil, fmt.Errorf("unknown content type: %s", contentType)
}
}
68 changes: 68 additions & 0 deletions mcp/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,71 @@ func TestMetaMarshalling(t *testing.T) {
})
}
}

func TestResourceLinkSerialization(t *testing.T) {
resourceLink := NewResourceLink(
"file:///example/document.pdf",
"Sample Document",
"A sample document for testing",
"application/pdf",
)

// Test marshaling
data, err := json.Marshal(resourceLink)
require.NoError(t, err)

// Test unmarshaling
var unmarshaled ResourceLink
err = json.Unmarshal(data, &unmarshaled)
require.NoError(t, err)

// Verify fields
assert.Equal(t, "resource_link", unmarshaled.Type)
assert.Equal(t, "file:///example/document.pdf", unmarshaled.URI)
assert.Equal(t, "Sample Document", unmarshaled.Name)
assert.Equal(t, "A sample document for testing", unmarshaled.Description)
assert.Equal(t, "application/pdf", unmarshaled.MIMEType)
}

func TestCallToolResultWithResourceLink(t *testing.T) {
result := &CallToolResult{
Content: []Content{
TextContent{
Type: "text",
Text: "Here's a resource link:",
},
NewResourceLink(
"file:///example/test.pdf",
"Test Document",
"A test document",
"application/pdf",
),
},
IsError: false,
}

// Test marshaling
data, err := json.Marshal(result)
require.NoError(t, err)

// Test unmarshalling
var unmarshalled CallToolResult
err = json.Unmarshal(data, &unmarshalled)
require.NoError(t, err)

// Verify content
require.Len(t, unmarshalled.Content, 2)

// Check first content (TextContent)
textContent, ok := unmarshalled.Content[0].(TextContent)
require.True(t, ok)
assert.Equal(t, "text", textContent.Type)
assert.Equal(t, "Here's a resource link:", textContent.Text)

// Check second content (ResourceLink)
resourceLink, ok := unmarshalled.Content[1].(ResourceLink)
require.True(t, ok)
assert.Equal(t, "resource_link", resourceLink.Type)
assert.Equal(t, "file:///example/test.pdf", resourceLink.URI)
assert.Equal(t, "Test Document", resourceLink.Name)
}
80 changes: 80 additions & 0 deletions www/docs/pages/servers/tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,86 @@ func handleMultiContentTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.
}
```

### Resource Links

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:

```go
func handleGetResourceLinkTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
resourceID, err := req.RequireString("resource_id")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Create a resource link pointing to an existing resource
uri := fmt.Sprintf("file://documents/%s", resourceID)
resourceLink := mcp.NewResourceLink(uri, "name", "resource name", "file")
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent("Found the requested document:"),
resourceLink,
},
}, nil
}
```

### Mixed Content with Resource Links

You can combine different content types including resource links in a single tool result:

```go
func handleSearchDocumentsTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
query, err := req.RequireString("query")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

// Simulate document search
foundDocs := []string{"doc1.pdf", "doc2.txt", "doc3.md"}

content := []mcp.Content{
mcp.NewTextContent(fmt.Sprintf("Found %d documents matching '%s':", len(foundDocs), query)),
}

// Add resource links for each found document
for _, doc := range foundDocs {
uri := fmt.Sprintf("file://documents/%s", doc)
docType := strings.Split(doc, ".")
resourceLink := mcp.NewResourceLink(uri, docType[0], "file", docType[1])
content = append(content, resourceLink)
}

return &mcp.CallToolResult{
Content: content,
}, nil
}
```

### Resource Link with Annotations

Resource links can include additional metadata through annotations:

```go
func handleGetAnnotatedResourceTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
docType := req.GetString("type", "general")
// Create resource link with annotations
annotated := mcp.Annotated{
Annotations: &mcp.Annotations{
Audience: []mcp.Role{mcp.RoleUser},
},
}
url := "file://documents/test.pdf"
resourceLink := mcp.NewResourceLink(url, "test", "doc", docType)
resourceLink.Annotated = annotated
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent("Here's the important document you requested:"),
resourceLink,
},
}, nil
}
```

### Error Results

```go
Expand Down