diff --git a/examples/server/everything/main.go b/examples/server/everything/main.go index 0b81919f..f255ab49 100644 --- a/examples/server/everything/main.go +++ b/examples/server/everything/main.go @@ -7,6 +7,7 @@ package main import ( "context" + "encoding/base64" "flag" "fmt" "log" @@ -50,19 +51,28 @@ func main() { CompletionHandler: complete, // support completions by setting this handler } - server := mcp.NewServer(&mcp.Implementation{Name: "everything"}, opts) + // Optionally add an icon to the server implementation. + icons, err := iconToBase64DataURL("./mcp.png") + if err != nil { + log.Fatalf("failed to read icon: %v", err) + } + + server := mcp.NewServer(&mcp.Implementation{Name: "everything", WebsiteURL: "https://example.com", Icons: icons}, opts) // Add tools that exercise different features of the protocol. mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, contentTool) - mcp.AddTool(server, &mcp.Tool{Name: "greet (structured)"}, structuredTool) // returns structured output - mcp.AddTool(server, &mcp.Tool{Name: "ping"}, pingingTool) // performs a ping - mcp.AddTool(server, &mcp.Tool{Name: "log"}, loggingTool) // performs a log - mcp.AddTool(server, &mcp.Tool{Name: "sample"}, samplingTool) // performs sampling - mcp.AddTool(server, &mcp.Tool{Name: "elicit"}, elicitingTool) // performs elicitation - mcp.AddTool(server, &mcp.Tool{Name: "roots"}, rootsTool) // lists roots + mcp.AddTool(server, &mcp.Tool{Name: "greet (structured)"}, structuredTool) // returns structured output + mcp.AddTool(server, &mcp.Tool{Name: "greet (with Icons)", Icons: icons}, structuredTool) // tool with icons + mcp.AddTool(server, &mcp.Tool{Name: "greet (content with ResourceLink)"}, resourceLinkContentTool(icons)) // tool that returns content with a resource link + mcp.AddTool(server, &mcp.Tool{Name: "ping"}, pingingTool) // performs a ping + mcp.AddTool(server, &mcp.Tool{Name: "log"}, loggingTool) // performs a log + mcp.AddTool(server, &mcp.Tool{Name: "sample"}, samplingTool) // performs sampling + mcp.AddTool(server, &mcp.Tool{Name: "elicit"}, elicitingTool) // performs elicitation + mcp.AddTool(server, &mcp.Tool{Name: "roots"}, rootsTool) // lists roots // Add a basic prompt. server.AddPrompt(&mcp.Prompt{Name: "greet"}, prompt) + server.AddPrompt(&mcp.Prompt{Name: "greet (with Icons)", Icons: icons}, prompt) // greet prompt with icons // Add an embedded resource. server.AddResource(&mcp.Resource{ @@ -70,6 +80,20 @@ func main() { MIMEType: "text/plain", URI: "embedded:info", }, embeddedResource) + server.AddResource(&mcp.Resource{ // text resource with icons + Name: "info (with Icons)", + MIMEType: "text/plain", + URI: "embedded:info", + Icons: icons, + }, embeddedResource) + + // Add a resource template. + server.AddResourceTemplate(&mcp.ResourceTemplate{ + Name: "Resource template (with Icon)", + MIMEType: "text/plain", + URITemplate: "http://example.com/~{resource_name}/", + Icons: icons, + }, embeddedResource) // Serve over stdio, or streamable HTTP if -http is set. if *httpAddr != "" { @@ -140,6 +164,23 @@ func contentTool(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp }, nil, nil } +// resourceLinkContentTool returns a ResourceLink content with icons. +func resourceLinkContentTool(icons []mcp.Icon) func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) { + return func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.ResourceLink{ + Name: "greeting", + Title: "A friendly greeting", + MIMEType: "text/plain", + URI: "data:text/plain,Hi%20" + url.PathEscape(args.Name), + Icons: icons, + }, + }, + }, nil, nil + } +} + type result struct { Message string `json:"message" jsonschema:"the message to convey"` } @@ -222,3 +263,16 @@ func complete(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResul }, }, nil } + +func iconToBase64DataURL(path string) ([]mcp.Icon, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + return []mcp.Icon{{ + Source: "data:image/png;base64," + base64.StdEncoding.EncodeToString(data), + MIMEType: "image/png", + Sizes: []string{"48x48"}, + Theme: "light", // or "dark" or empty + }}, nil +} diff --git a/examples/server/everything/mcp.png b/examples/server/everything/mcp.png new file mode 100644 index 00000000..8e08571d Binary files /dev/null and b/examples/server/everything/mcp.png differ diff --git a/mcp/conformance_test.go b/mcp/conformance_test.go index 3393efcb..b2801dbc 100644 --- a/mcp/conformance_test.go +++ b/mcp/conformance_test.go @@ -111,6 +111,19 @@ func structuredTool(ctx context.Context, req *CallToolRequest, args *structuredI return nil, &structuredOutput{"Ack " + args.In}, nil } +func contentTool(ctx context.Context, req *CallToolRequest, args *structuredInput) (*CallToolResult, any, error) { + return &CallToolResult{ + Content: []Content{ + &ResourceLink{ + Name: "Example Resource Link with Icons", + MIMEType: "text/plain", + URI: "https://example.com/resource/" + args.In, + Icons: []Icon{iconObj}, + }, + }, + }, nil, nil +} + type tomorrowInput struct { Now time.Time } @@ -135,12 +148,26 @@ func incTool(_ context.Context, _ *CallToolRequest, args incInput) (*CallToolRes return nil, incOutput{args.X + 1}, nil } +var iconObj = Icon{ + Source: "foobar", + MIMEType: "image/png", + Sizes: []string{"48x48", "96x96"}, + Theme: "light", +} + // runServerTest runs the server conformance test. // It must be executed in a synctest bubble. func runServerTest(t *testing.T, test *conformanceTest) { ctx := t.Context() // Construct the server based on features listed in the test. - s := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil) + impl := &Implementation{Name: "testServer", Version: "v1.0.0"} + + if test.name == "spec-sep-973-additional-metadata.txtar" { + impl.Icons = []Icon{iconObj} + impl.WebsiteURL = "https://github.com/modelcontextprotocol/go-sdk" + } + + s := NewServer(impl, nil) for _, tn := range test.tools { switch tn { case "greet": @@ -148,6 +175,12 @@ func runServerTest(t *testing.T, test *conformanceTest) { Name: "greet", Description: "say hi", }, sayHi) + case "greetWithIcon": + AddTool(s, &Tool{ + Name: "greetWithIcon", + Description: "say hi", + Icons: []Icon{iconObj}, + }, sayHi) case "structured": AddTool(s, &Tool{Name: "structured"}, structuredTool) case "tomorrow": @@ -159,6 +192,12 @@ func runServerTest(t *testing.T, test *conformanceTest) { } inSchema.Properties["x"].Default = json.RawMessage(`6`) AddTool(s, &Tool{Name: "inc", InputSchema: inSchema}, incTool) + case "contentTool": + AddTool(s, &Tool{ + Name: "contentTool", + Title: "contentTool", + Description: "return resourceLink content with Icon", + }, contentTool) default: t.Fatalf("unknown tool %q", tn) } @@ -167,6 +206,13 @@ func runServerTest(t *testing.T, test *conformanceTest) { switch pn { case "code_review": s.AddPrompt(codeReviewPrompt, codReviewPromptHandler) + case "code_reviewWithIcon": + s.AddPrompt(&Prompt{ + Name: "code_review", + Description: "do a code review", + Arguments: []*PromptArgument{{Name: "Code", Required: true}}, + Icons: []Icon{iconObj}, + }, codReviewPromptHandler) default: t.Fatalf("unknown prompt %q", pn) } @@ -177,6 +223,13 @@ func runServerTest(t *testing.T, test *conformanceTest) { s.AddResource(resource1, readHandler) case "info": s.AddResource(resource3, handleEmbeddedResource) + case "infoWithIcon": + s.AddResource(&Resource{ + Name: "info", + MIMEType: "text/plain", + URI: "embedded:info", + Icons: []Icon{iconObj}, + }, handleEmbeddedResource) default: t.Fatalf("unknown resource %q", rn) } diff --git a/mcp/content.go b/mcp/content.go index e53cad14..fb1a0d1e 100644 --- a/mcp/content.go +++ b/mcp/content.go @@ -130,6 +130,8 @@ type ResourceLink struct { Size *int64 Meta Meta Annotations *Annotations + // Icons for the resource link, if any. + Icons []Icon `json:"icons,omitempty"` } func (c *ResourceLink) MarshalJSON() ([]byte, error) { @@ -143,6 +145,7 @@ func (c *ResourceLink) MarshalJSON() ([]byte, error) { Size: c.Size, Meta: c.Meta, Annotations: c.Annotations, + Icons: c.Icons, }) } @@ -155,6 +158,7 @@ func (c *ResourceLink) fromWire(wire *wireContent) { c.Size = wire.Size c.Meta = wire.Meta c.Annotations = wire.Annotations + c.Icons = wire.Icons } // EmbeddedResource contains embedded resources. @@ -237,6 +241,7 @@ type wireContent struct { Size *int64 `json:"size,omitempty"` Meta Meta `json:"_meta,omitempty"` Annotations *Annotations `json:"annotations,omitempty"` + Icons []Icon `json:"icons,omitempty"` } func contentsFromWire(wires []*wireContent, allow map[string]bool) ([]Content, error) { diff --git a/mcp/content_test.go b/mcp/content_test.go index 9366b0d4..ae13c307 100644 --- a/mcp/content_test.go +++ b/mcp/content_test.go @@ -121,8 +121,9 @@ func TestContent(t *testing.T) { Description: "This resource demonstrates all fields", MIMEType: "text/plain", Meta: mcp.Meta{"custom": "metadata"}, + Icons: []mcp.Icon{{Source: "foobar", MIMEType: "image/png", Sizes: []string{"48x48"}, Theme: "light"}}, }, - `{"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"}}`, + `{"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"},"icons":[{"src":"foobar","mimeType":"image/png","sizes":["48x48"],"theme":"light"}]}`, }, } @@ -192,3 +193,53 @@ func TestEmbeddedResource(t *testing.T) { } } } + +// TestContentUnmarshal tests that unmarshaling JSON into various Content types +// works correctly, including when the Content fields are initially nil. +func TestContentUnmarshal(t *testing.T) { + valInt64 := int64(24) + tests := []struct { + name string + json string + content mcp.Content + expectContent mcp.Content + }{ + { + name: "ResourceLink", + json: `{"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"},"icons":[{"src":"foobar","mimeType":"image/png","sizes":["48x48"],"theme":"light"}], "size":24,"annotations":{"audience":["user","assistant"],"lastModified":"2025-01-12T15:00:58Z","priority":0.5}}`, + content: &mcp.ResourceLink{}, + expectContent: &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"}, + Size: &valInt64, + Annotations: &mcp.Annotations{Audience: []mcp.Role{"user", "assistant"}, LastModified: "2025-01-12T15:00:58Z", Priority: 0.5}, + Icons: []mcp.Icon{{Source: "foobar", MIMEType: "image/png", Sizes: []string{"48x48"}, Theme: "light"}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test that unmarshaling doesn't panic on nil Content fields + defer func() { + if r := recover(); r != nil { + t.Errorf("UnmarshalJSON panicked: %v", r) + } + }() + + err := json.Unmarshal([]byte(tt.json), tt.content) + if err != nil { + t.Errorf("UnmarshalJSON failed: %v", err) + } + + // Verify that the Content field was properly populated + if cmp.Diff(tt.expectContent, tt.content) != "" { + t.Errorf("Content is not equal: %v", cmp.Diff(tt.expectContent, tt.content)) + } + }) + } +} diff --git a/mcp/protocol.go b/mcp/protocol.go index 1312dfbd..4d7fce08 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -658,6 +658,23 @@ type ProgressNotificationParams struct { func (*ProgressNotificationParams) isParams() {} +// Icon provides visual identifiers for their resources, tools, prompts, and implementations +// See [/specification/draft/basic/index#icons] for notes on icons + +// TODO(iamsurajbobade): update specification url from draft. +type Icon struct { + // Source is A URI pointing to the icon resource (required). This can be: + // - An HTTP/HTTPS URL pointing to an image file + // - A data URI with base64-encoded image data + Source string `json:"src"` + // Optional MIME type if the server's type is missing or generic + MIMEType string `json:"mimeType,omitempty"` + // Optional size specification (e.g., ["48x48"], ["any"] for scalable formats like SVG, or ["48x48", "96x96"] for multiple sizes) + Sizes []string `json:"sizes,omitempty"` + // Optional Theme of the icon, e.g., "light" or "dark" + Theme string `json:"theme,omitempty"` +} + // A prompt or prompt template that the server offers. type Prompt struct { // See [specification/2025-06-18/basic/index#general-fields] for notes on _meta @@ -673,6 +690,8 @@ type Prompt struct { // Intended for UI and end-user contexts — optimized to be human-readable and // easily understood, even by those unfamiliar with domain-specific terminology. Title string `json:"title,omitempty"` + // Icons for the prompt, if any. + Icons []Icon `json:"icons,omitempty"` } // Describes an argument that a prompt can accept. @@ -782,6 +801,8 @@ type Resource struct { Title string `json:"title,omitempty"` // The URI of this resource. URI string `json:"uri"` + // Icons for the resource, if any. + Icons []Icon `json:"icons,omitempty"` } type ResourceListChangedParams struct { @@ -822,6 +843,8 @@ type ResourceTemplate struct { // A URI template (according to RFC 6570) that can be used to construct resource // URIs. URITemplate string `json:"uriTemplate"` + // Icons for the resource template, if any. + Icons []Icon `json:"icons,omitempty"` } // The sender or recipient of messages and data in a conversation. @@ -948,6 +971,8 @@ type Tool struct { // If not provided, Annotations.Title should be used for display if present, // otherwise Name. Title string `json:"title,omitempty"` + // Icons for the tool, if any. + Icons []Icon `json:"icons,omitempty"` } // Additional properties describing a Tool to clients. @@ -1090,6 +1115,10 @@ type Implementation struct { // easily understood, even by those unfamiliar with domain-specific terminology. Title string `json:"title,omitempty"` Version string `json:"version"` + // WebsiteURL for the server, if any. + WebsiteURL string `json:"websiteUrl,omitempty"` + // Icons for the Server, if any. + Icons []Icon `json:"icons,omitempty"` } // Present if the server supports argument autocompletion suggestions. diff --git a/mcp/testdata/conformance/server/spec-sep-973-additional-metadata.txtar b/mcp/testdata/conformance/server/spec-sep-973-additional-metadata.txtar new file mode 100644 index 00000000..985bff8f --- /dev/null +++ b/mcp/testdata/conformance/server/spec-sep-973-additional-metadata.txtar @@ -0,0 +1,217 @@ +Check behavior of server with Icons and websiteUrl metadata added as part of SEP-973 specification. + +check modelcontextprotocol/go-sdk/issues/552 for more details. + +Checks following: +- If client sends protocolVersion as "draft", server responds with same +- Test setting websiteUrl, icons for mcp.Implementation +- Test setting icons for mcp.Prompt +- Test setting icons for mcp.Tool +- Test setting icons for mcp.Resource +- Test if tool call returning ResourceLink with icons works as expected + + +-- tools -- +greetWithIcon +contentTool + +-- prompts -- +code_reviewWithIcon + +-- resources -- +infoWithIcon + +-- client -- +{ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2025-06-18", + "capabilities": {}, + "clientInfo": { "name": "ExampleClient", "version": "1.0.0" } + } +} +{ "jsonrpc": "2.0", "method": "notifications/initialized" } +{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" } +{ "jsonrpc": "2.0", "id": 3, "method": "resources/list" } +{ "jsonrpc": "2.0", "id": 4, "method": "prompts/list" } +{ + "jsonrpc": "2.0", + "id": 5, + "method": "tools/call", + "params": { + "name": "contentTool", + "arguments": { + "In": "resource1" + } + } +} + +-- server -- +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "capabilities": { + "logging": {}, + "prompts": { + "listChanged": true + }, + "resources": { + "listChanged": true + }, + "tools": { + "listChanged": true + } + }, + "protocolVersion": "2025-06-18", + "serverInfo": { + "name": "testServer", + "version": "v1.0.0", + "websiteUrl": "https://github.com/modelcontextprotocol/go-sdk", + "icons": [ + { + "src": "foobar", + "mimeType": "image/png", + "sizes": [ + "48x48", + "96x96" + ], + "theme": "light" + } + ] + } + } +} +{ + "jsonrpc": "2.0", + "id": 2, + "result": { + "tools": [ + { + "description": "return resourceLink content with Icon", + "inputSchema": { + "type": "object", + "required": [ + "In" + ], + "properties": { + "In": { + "type": "string", + "description": "the input" + } + }, + "additionalProperties": false + }, + "name": "contentTool", + "title": "contentTool" + }, + { + "description": "say hi", + "inputSchema": { + "type": "object", + "required": [ + "Name" + ], + "properties": { + "Name": { + "type": "string" + } + }, + "additionalProperties": false + }, + "name": "greetWithIcon", + "icons": [ + { + "src": "foobar", + "mimeType": "image/png", + "sizes": [ + "48x48", + "96x96" + ], + "theme": "light" + } + ] + } + ] + } +} +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "resources": [ + { + "mimeType": "text/plain", + "name": "info", + "uri": "embedded:info", + "icons": [ + { + "src": "foobar", + "mimeType": "image/png", + "sizes": [ + "48x48", + "96x96" + ], + "theme": "light" + } + ] + } + ] + } +} +{ + "jsonrpc": "2.0", + "id": 4, + "result": { + "prompts": [ + { + "arguments": [ + { + "name": "Code", + "required": true + } + ], + "description": "do a code review", + "name": "code_review", + "icons": [ + { + "src": "foobar", + "mimeType": "image/png", + "sizes": [ + "48x48", + "96x96" + ], + "theme": "light" + } + ] + } + ] + } +} +{ + "jsonrpc": "2.0", + "id": 5, + "result": { + "content": [ + { + "type": "resource_link", + "mimeType": "text/plain", + "uri": "https://example.com/resource/resource1", + "name": "Example Resource Link with Icons", + "icons": [ + { + "src": "foobar", + "mimeType": "image/png", + "sizes": [ + "48x48", + "96x96" + ], + "theme": "light" + } + ] + } + ] + } +}