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
This commit is contained in:
Suraj Bobade 2025-11-13 03:22:44 +05:30 • committed by GitHub
commit faddd764aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 417 additions and 9 deletions

View file

@ -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
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -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)
}

View file

@ -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) {

View file

@ -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))
}
})
}
}

View file

@ -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.

View file

@ -0,0 +1,216 @@
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:
- 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"
}
]
}
]
}
}