From faddd764aaf3d404e39899d5c31c7632e5046e07 Mon Sep 17 00:00:00 2001 From: Suraj Bobade Date: Thu, 13 Nov 2025 03:22:44 +0530 Subject: [PATCH] Implement SEP-973 (#570) mcp/protocol: Implement SEP-973 - Define Icon structure, which includes source, mimeType and sizes. - sizes is any array of strings. refer https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1531 - Support setting websiteUrl, icons for mcp.Implementation - Support setting icons for mcp.Prompt - Support setting icons for mcp.Tool - Support setting icons for mcp.Resource Fixes #552 --- examples/server/everything/main.go | 68 +++++- examples/server/everything/mcp.png | Bin 0 -> 2586 bytes mcp/conformance_test.go | 55 ++++- mcp/content.go | 5 + mcp/content_test.go | 53 ++++- mcp/protocol.go | 29 +++ .../spec-sep-973-additional-metadata.txtar | 216 ++++++++++++++++++ 7 files changed, 417 insertions(+), 9 deletions(-) create mode 100644 examples/server/everything/mcp.png create mode 100644 mcp/testdata/conformance/server/spec-sep-973-additional-metadata.txtar diff --git a/examples/server/everything/main.go b/examples/server/everything/main.go index 0b81919..f255ab4 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 0000000000000000000000000000000000000000..8e08571d326bba9110ea3ced50e6769d0e100b21 GIT binary patch literal 2586 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