From 92b6983c1cff905d1fbc39e5a85c8d0371d35e1f Mon Sep 17 00:00:00 2001 From: Xin <5097752+imfing@users.noreply.github.com> Date: Tue, 1 Jul 2025 17:41:22 +0100 Subject: [PATCH] mcp: add ResourceLink type (#76) This PR introduces the ResourceLink type to represent links to resources. The new type includes the following fields: `uri`, `name`, `title`, `description`, `mimeType`, `size`, and `_meta`. This implementation conforms to the `ResourceLink` schema introduced in the [MCP schema 2025-06-18](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/1cfdf1e7a8aa5065b7e3cb3e35e653df9542e0de/schema/2025-06-18/schema.json#L1826). Note: `ResourceLink` extends from the `Resource` type in the MCP schema. As such, it inherits support for optional fields like `title`, `_meta`, etc. Thus, `URI`, `Name`, `Title`, `Description`, and `Size` are added to the `wireContent` For reference, the [TypeScript SDK test suite](https://github.com/modelcontextprotocol/typescript-sdk/blob/fb4d07f0eed99685c230160ad4ff39578fe9d05f/src/types.test.ts#L43) provides examples demonstrating valid ResourceLink objects with all optional fields populated. https://modelcontextprotocol.io/specification/2025-06-18/server/tools#resource-links - [ResourceLink JSON Schema](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/1cfdf1e7a8aa5065b7e3cb3e35e653df9542e0de/schema/2025-06-18/schema.json#L1826) Signed-off-by: Xin Fu Fixes #14. --- mcp/content.go | 58 +++++++++++++++++++++++++++++++++++++++------ mcp/content_test.go | 18 ++++++++++++++ 2 files changed, 69 insertions(+), 7 deletions(-) diff --git a/mcp/content.go b/mcp/content.go index 43f7fdc..ed7f6f9 100644 --- a/mcp/content.go +++ b/mcp/content.go @@ -10,10 +10,8 @@ import ( "fmt" ) -// A Content is a [TextContent], [ImageContent], [AudioContent] or -// [EmbeddedResource]. -// -// TODO(rfindley): add ResourceLink. +// A Content is a [TextContent], [ImageContent], [AudioContent], +// [ResourceLink], or [EmbeddedResource]. type Content interface { MarshalJSON() ([]byte, error) fromWire(*wireContent) @@ -91,6 +89,43 @@ func (c *AudioContent) fromWire(wire *wireContent) { c.Annotations = wire.Annotations } +// ResourceLink is a link to a resource +type ResourceLink struct { + URI string + Name string + Title string + Description string + MIMEType string + Size *int64 + Meta Meta + Annotations *Annotations +} + +func (c *ResourceLink) MarshalJSON() ([]byte, error) { + return json.Marshal(&wireContent{ + Type: "resource_link", + URI: c.URI, + Name: c.Name, + Title: c.Title, + Description: c.Description, + MIMEType: c.MIMEType, + Size: c.Size, + Meta: c.Meta, + Annotations: c.Annotations, + }) +} + +func (c *ResourceLink) fromWire(wire *wireContent) { + c.URI = wire.URI + c.Name = wire.Name + c.Title = wire.Title + c.Description = wire.Description + c.MIMEType = wire.MIMEType + c.Size = wire.Size + c.Meta = wire.Meta + c.Annotations = wire.Annotations +} + // EmbeddedResource contains embedded resources. type EmbeddedResource struct { Resource *ResourceContents @@ -151,17 +186,22 @@ func (r ResourceContents) MarshalJSON() ([]byte, error) { } // wireContent is the wire format for content. -// It represents the protocol types TextContent, ImageContent, AudioContent -// and EmbeddedResource. +// It represents the protocol types TextContent, ImageContent, AudioContent, +// ResourceLink, and EmbeddedResource. // The Type field distinguishes them. In the protocol, each type has a constant // value for the field. -// At most one of Text, Data, and Resource is non-zero. +// At most one of Text, Data, Resource, and URI is non-zero. type wireContent struct { Type string `json:"type"` Text string `json:"text,omitempty"` MIMEType string `json:"mimeType,omitempty"` Data []byte `json:"data,omitempty"` Resource *ResourceContents `json:"resource,omitempty"` + URI string `json:"uri,omitempty"` + Name string `json:"name,omitempty"` + Title string `json:"title,omitempty"` + Description string `json:"description,omitempty"` + Size *int64 `json:"size,omitempty"` Meta Meta `json:"_meta,omitempty"` Annotations *Annotations `json:"annotations,omitempty"` } @@ -195,6 +235,10 @@ func contentFromWire(wire *wireContent, allow map[string]bool) (Content, error) v := new(AudioContent) v.fromWire(wire) return v, nil + case "resource_link": + v := new(ResourceLink) + v.fromWire(wire) + return v, nil case "resource": v := new(EmbeddedResource) v.fromWire(wire) diff --git a/mcp/content_test.go b/mcp/content_test.go index 5e6c6d0..5ee6f66 100644 --- a/mcp/content_test.go +++ b/mcp/content_test.go @@ -82,6 +82,24 @@ func TestContent(t *testing.T) { }, `{"type":"resource","resource":{"uri":"file://foo","mimeType":"text","text":"abc"},"_meta":{"key":"value"},"annotations":{"priority":1}}`, }, + { + &mcp.ResourceLink{ + URI: "file:///path/to/file.txt", + Name: "file.txt", + }, + `{"type":"resource_link","uri":"file:///path/to/file.txt","name":"file.txt"}`, + }, + { + &mcp.ResourceLink{ + URI: "https://example.com/resource", + Name: "Example Resource", + Title: "A comprehensive example resource", + Description: "This resource demonstrates all fields", + MIMEType: "text/plain", + Meta: mcp.Meta{"custom": "metadata"}, + }, + `{"type":"resource_link","mimeType":"text/plain","uri":"https://example.com/resource","name":"Example Resource","title":"A comprehensive example resource","description":"This resource demonstrates all fields","_meta":{"custom":"metadata"}}`, + }, } for _, test := range tests {