Skip to content

Commit faddd76

Browse files
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 modelcontextprotocol/modelcontextprotocol#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
1 parent 35dfcf2 commit faddd76

File tree

7 files changed

+417
-9
lines changed

7 files changed

+417
-9
lines changed

examples/server/everything/main.go

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package main
77

88
import (
99
"context"
10+
"encoding/base64"
1011
"flag"
1112
"fmt"
1213
"log"
@@ -50,26 +51,49 @@ func main() {
5051
CompletionHandler: complete, // support completions by setting this handler
5152
}
5253

53-
server := mcp.NewServer(&mcp.Implementation{Name: "everything"}, opts)
54+
// Optionally add an icon to the server implementation.
55+
icons, err := iconToBase64DataURL("./mcp.png")
56+
if err != nil {
57+
log.Fatalf("failed to read icon: %v", err)
58+
}
59+
60+
server := mcp.NewServer(&mcp.Implementation{Name: "everything", WebsiteURL: "https://example.com", Icons: icons}, opts)
5461

5562
// Add tools that exercise different features of the protocol.
5663
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, contentTool)
57-
mcp.AddTool(server, &mcp.Tool{Name: "greet (structured)"}, structuredTool) // returns structured output
58-
mcp.AddTool(server, &mcp.Tool{Name: "ping"}, pingingTool) // performs a ping
59-
mcp.AddTool(server, &mcp.Tool{Name: "log"}, loggingTool) // performs a log
60-
mcp.AddTool(server, &mcp.Tool{Name: "sample"}, samplingTool) // performs sampling
61-
mcp.AddTool(server, &mcp.Tool{Name: "elicit"}, elicitingTool) // performs elicitation
62-
mcp.AddTool(server, &mcp.Tool{Name: "roots"}, rootsTool) // lists roots
64+
mcp.AddTool(server, &mcp.Tool{Name: "greet (structured)"}, structuredTool) // returns structured output
65+
mcp.AddTool(server, &mcp.Tool{Name: "greet (with Icons)", Icons: icons}, structuredTool) // tool with icons
66+
mcp.AddTool(server, &mcp.Tool{Name: "greet (content with ResourceLink)"}, resourceLinkContentTool(icons)) // tool that returns content with a resource link
67+
mcp.AddTool(server, &mcp.Tool{Name: "ping"}, pingingTool) // performs a ping
68+
mcp.AddTool(server, &mcp.Tool{Name: "log"}, loggingTool) // performs a log
69+
mcp.AddTool(server, &mcp.Tool{Name: "sample"}, samplingTool) // performs sampling
70+
mcp.AddTool(server, &mcp.Tool{Name: "elicit"}, elicitingTool) // performs elicitation
71+
mcp.AddTool(server, &mcp.Tool{Name: "roots"}, rootsTool) // lists roots
6372

6473
// Add a basic prompt.
6574
server.AddPrompt(&mcp.Prompt{Name: "greet"}, prompt)
75+
server.AddPrompt(&mcp.Prompt{Name: "greet (with Icons)", Icons: icons}, prompt) // greet prompt with icons
6676

6777
// Add an embedded resource.
6878
server.AddResource(&mcp.Resource{
6979
Name: "info",
7080
MIMEType: "text/plain",
7181
URI: "embedded:info",
7282
}, embeddedResource)
83+
server.AddResource(&mcp.Resource{ // text resource with icons
84+
Name: "info (with Icons)",
85+
MIMEType: "text/plain",
86+
URI: "embedded:info",
87+
Icons: icons,
88+
}, embeddedResource)
89+
90+
// Add a resource template.
91+
server.AddResourceTemplate(&mcp.ResourceTemplate{
92+
Name: "Resource template (with Icon)",
93+
MIMEType: "text/plain",
94+
URITemplate: "http://example.com/~{resource_name}/",
95+
Icons: icons,
96+
}, embeddedResource)
7397

7498
// Serve over stdio, or streamable HTTP if -http is set.
7599
if *httpAddr != "" {
@@ -140,6 +164,23 @@ func contentTool(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp
140164
}, nil, nil
141165
}
142166

167+
// resourceLinkContentTool returns a ResourceLink content with icons.
168+
func resourceLinkContentTool(icons []mcp.Icon) func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
169+
return func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
170+
return &mcp.CallToolResult{
171+
Content: []mcp.Content{
172+
&mcp.ResourceLink{
173+
Name: "greeting",
174+
Title: "A friendly greeting",
175+
MIMEType: "text/plain",
176+
URI: "data:text/plain,Hi%20" + url.PathEscape(args.Name),
177+
Icons: icons,
178+
},
179+
},
180+
}, nil, nil
181+
}
182+
}
183+
143184
type result struct {
144185
Message string `json:"message" jsonschema:"the message to convey"`
145186
}
@@ -222,3 +263,16 @@ func complete(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResul
222263
},
223264
}, nil
224265
}
266+
267+
func iconToBase64DataURL(path string) ([]mcp.Icon, error) {
268+
data, err := os.ReadFile(path)
269+
if err != nil {
270+
return nil, err
271+
}
272+
return []mcp.Icon{{
273+
Source: "data:image/png;base64," + base64.StdEncoding.EncodeToString(data),
274+
MIMEType: "image/png",
275+
Sizes: []string{"48x48"},
276+
Theme: "light", // or "dark" or empty
277+
}}, nil
278+
}

examples/server/everything/mcp.png

2.53 KB
Loading

mcp/conformance_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,19 @@ func structuredTool(ctx context.Context, req *CallToolRequest, args *structuredI
111111
return nil, &structuredOutput{"Ack " + args.In}, nil
112112
}
113113

114+
func contentTool(ctx context.Context, req *CallToolRequest, args *structuredInput) (*CallToolResult, any, error) {
115+
return &CallToolResult{
116+
Content: []Content{
117+
&ResourceLink{
118+
Name: "Example Resource Link with Icons",
119+
MIMEType: "text/plain",
120+
URI: "https://example.com/resource/" + args.In,
121+
Icons: []Icon{iconObj},
122+
},
123+
},
124+
}, nil, nil
125+
}
126+
114127
type tomorrowInput struct {
115128
Now time.Time
116129
}
@@ -135,19 +148,39 @@ func incTool(_ context.Context, _ *CallToolRequest, args incInput) (*CallToolRes
135148
return nil, incOutput{args.X + 1}, nil
136149
}
137150

151+
var iconObj = Icon{
152+
Source: "foobar",
153+
MIMEType: "image/png",
154+
Sizes: []string{"48x48", "96x96"},
155+
Theme: "light",
156+
}
157+
138158
// runServerTest runs the server conformance test.
139159
// It must be executed in a synctest bubble.
140160
func runServerTest(t *testing.T, test *conformanceTest) {
141161
ctx := t.Context()
142162
// Construct the server based on features listed in the test.
143-
s := NewServer(&Implementation{Name: "testServer", Version: "v1.0.0"}, nil)
163+
impl := &Implementation{Name: "testServer", Version: "v1.0.0"}
164+
165+
if test.name == "spec-sep-973-additional-metadata.txtar" {
166+
impl.Icons = []Icon{iconObj}
167+
impl.WebsiteURL = "https://github.com/modelcontextprotocol/go-sdk"
168+
}
169+
170+
s := NewServer(impl, nil)
144171
for _, tn := range test.tools {
145172
switch tn {
146173
case "greet":
147174
AddTool(s, &Tool{
148175
Name: "greet",
149176
Description: "say hi",
150177
}, sayHi)
178+
case "greetWithIcon":
179+
AddTool(s, &Tool{
180+
Name: "greetWithIcon",
181+
Description: "say hi",
182+
Icons: []Icon{iconObj},
183+
}, sayHi)
151184
case "structured":
152185
AddTool(s, &Tool{Name: "structured"}, structuredTool)
153186
case "tomorrow":
@@ -159,6 +192,12 @@ func runServerTest(t *testing.T, test *conformanceTest) {
159192
}
160193
inSchema.Properties["x"].Default = json.RawMessage(`6`)
161194
AddTool(s, &Tool{Name: "inc", InputSchema: inSchema}, incTool)
195+
case "contentTool":
196+
AddTool(s, &Tool{
197+
Name: "contentTool",
198+
Title: "contentTool",
199+
Description: "return resourceLink content with Icon",
200+
}, contentTool)
162201
default:
163202
t.Fatalf("unknown tool %q", tn)
164203
}
@@ -167,6 +206,13 @@ func runServerTest(t *testing.T, test *conformanceTest) {
167206
switch pn {
168207
case "code_review":
169208
s.AddPrompt(codeReviewPrompt, codReviewPromptHandler)
209+
case "code_reviewWithIcon":
210+
s.AddPrompt(&Prompt{
211+
Name: "code_review",
212+
Description: "do a code review",
213+
Arguments: []*PromptArgument{{Name: "Code", Required: true}},
214+
Icons: []Icon{iconObj},
215+
}, codReviewPromptHandler)
170216
default:
171217
t.Fatalf("unknown prompt %q", pn)
172218
}
@@ -177,6 +223,13 @@ func runServerTest(t *testing.T, test *conformanceTest) {
177223
s.AddResource(resource1, readHandler)
178224
case "info":
179225
s.AddResource(resource3, handleEmbeddedResource)
226+
case "infoWithIcon":
227+
s.AddResource(&Resource{
228+
Name: "info",
229+
MIMEType: "text/plain",
230+
URI: "embedded:info",
231+
Icons: []Icon{iconObj},
232+
}, handleEmbeddedResource)
180233
default:
181234
t.Fatalf("unknown resource %q", rn)
182235
}

mcp/content.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ type ResourceLink struct {
130130
Size *int64
131131
Meta Meta
132132
Annotations *Annotations
133+
// Icons for the resource link, if any.
134+
Icons []Icon `json:"icons,omitempty"`
133135
}
134136

135137
func (c *ResourceLink) MarshalJSON() ([]byte, error) {
@@ -143,6 +145,7 @@ func (c *ResourceLink) MarshalJSON() ([]byte, error) {
143145
Size: c.Size,
144146
Meta: c.Meta,
145147
Annotations: c.Annotations,
148+
Icons: c.Icons,
146149
})
147150
}
148151

@@ -155,6 +158,7 @@ func (c *ResourceLink) fromWire(wire *wireContent) {
155158
c.Size = wire.Size
156159
c.Meta = wire.Meta
157160
c.Annotations = wire.Annotations
161+
c.Icons = wire.Icons
158162
}
159163

160164
// EmbeddedResource contains embedded resources.
@@ -237,6 +241,7 @@ type wireContent struct {
237241
Size *int64 `json:"size,omitempty"`
238242
Meta Meta `json:"_meta,omitempty"`
239243
Annotations *Annotations `json:"annotations,omitempty"`
244+
Icons []Icon `json:"icons,omitempty"`
240245
}
241246

242247
func contentsFromWire(wires []*wireContent, allow map[string]bool) ([]Content, error) {

mcp/content_test.go

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,9 @@ func TestContent(t *testing.T) {
121121
Description: "This resource demonstrates all fields",
122122
MIMEType: "text/plain",
123123
Meta: mcp.Meta{"custom": "metadata"},
124+
Icons: []mcp.Icon{{Source: "foobar", MIMEType: "image/png", Sizes: []string{"48x48"}, Theme: "light"}},
124125
},
125-
`{"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"}}`,
126+
`{"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"}]}`,
126127
},
127128
}
128129

@@ -192,3 +193,53 @@ func TestEmbeddedResource(t *testing.T) {
192193
}
193194
}
194195
}
196+
197+
// TestContentUnmarshal tests that unmarshaling JSON into various Content types
198+
// works correctly, including when the Content fields are initially nil.
199+
func TestContentUnmarshal(t *testing.T) {
200+
valInt64 := int64(24)
201+
tests := []struct {
202+
name string
203+
json string
204+
content mcp.Content
205+
expectContent mcp.Content
206+
}{
207+
{
208+
name: "ResourceLink",
209+
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}}`,
210+
content: &mcp.ResourceLink{},
211+
expectContent: &mcp.ResourceLink{
212+
URI: "https://example.com/resource",
213+
Name: "Example Resource",
214+
Title: "A comprehensive example resource",
215+
Description: "This resource demonstrates all fields",
216+
MIMEType: "text/plain",
217+
// Meta: mcp.Meta{"custom": "metadata"},
218+
Size: &valInt64,
219+
Annotations: &mcp.Annotations{Audience: []mcp.Role{"user", "assistant"}, LastModified: "2025-01-12T15:00:58Z", Priority: 0.5},
220+
Icons: []mcp.Icon{{Source: "foobar", MIMEType: "image/png", Sizes: []string{"48x48"}, Theme: "light"}},
221+
},
222+
},
223+
}
224+
225+
for _, tt := range tests {
226+
t.Run(tt.name, func(t *testing.T) {
227+
// Test that unmarshaling doesn't panic on nil Content fields
228+
defer func() {
229+
if r := recover(); r != nil {
230+
t.Errorf("UnmarshalJSON panicked: %v", r)
231+
}
232+
}()
233+
234+
err := json.Unmarshal([]byte(tt.json), tt.content)
235+
if err != nil {
236+
t.Errorf("UnmarshalJSON failed: %v", err)
237+
}
238+
239+
// Verify that the Content field was properly populated
240+
if cmp.Diff(tt.expectContent, tt.content) != "" {
241+
t.Errorf("Content is not equal: %v", cmp.Diff(tt.expectContent, tt.content))
242+
}
243+
})
244+
}
245+
}

mcp/protocol.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,23 @@ type ProgressNotificationParams struct {
658658

659659
func (*ProgressNotificationParams) isParams() {}
660660

661+
// Icon provides visual identifiers for their resources, tools, prompts, and implementations
662+
// See [/specification/draft/basic/index#icons] for notes on icons
663+
//
664+
// TODO(iamsurajbobade): update specification url from draft.
665+
type Icon struct {
666+
// Source is A URI pointing to the icon resource (required). This can be:
667+
// - An HTTP/HTTPS URL pointing to an image file
668+
// - A data URI with base64-encoded image data
669+
Source string `json:"src"`
670+
// Optional MIME type if the server's type is missing or generic
671+
MIMEType string `json:"mimeType,omitempty"`
672+
// Optional size specification (e.g., ["48x48"], ["any"] for scalable formats like SVG, or ["48x48", "96x96"] for multiple sizes)
673+
Sizes []string `json:"sizes,omitempty"`
674+
// Optional Theme of the icon, e.g., "light" or "dark"
675+
Theme string `json:"theme,omitempty"`
676+
}
677+
661678
// A prompt or prompt template that the server offers.
662679
type Prompt struct {
663680
// See [specification/2025-06-18/basic/index#general-fields] for notes on _meta
@@ -673,6 +690,8 @@ type Prompt struct {
673690
// Intended for UI and end-user contexts — optimized to be human-readable and
674691
// easily understood, even by those unfamiliar with domain-specific terminology.
675692
Title string `json:"title,omitempty"`
693+
// Icons for the prompt, if any.
694+
Icons []Icon `json:"icons,omitempty"`
676695
}
677696

678697
// Describes an argument that a prompt can accept.
@@ -782,6 +801,8 @@ type Resource struct {
782801
Title string `json:"title,omitempty"`
783802
// The URI of this resource.
784803
URI string `json:"uri"`
804+
// Icons for the resource, if any.
805+
Icons []Icon `json:"icons,omitempty"`
785806
}
786807

787808
type ResourceListChangedParams struct {
@@ -822,6 +843,8 @@ type ResourceTemplate struct {
822843
// A URI template (according to RFC 6570) that can be used to construct resource
823844
// URIs.
824845
URITemplate string `json:"uriTemplate"`
846+
// Icons for the resource template, if any.
847+
Icons []Icon `json:"icons,omitempty"`
825848
}
826849

827850
// The sender or recipient of messages and data in a conversation.
@@ -948,6 +971,8 @@ type Tool struct {
948971
// If not provided, Annotations.Title should be used for display if present,
949972
// otherwise Name.
950973
Title string `json:"title,omitempty"`
974+
// Icons for the tool, if any.
975+
Icons []Icon `json:"icons,omitempty"`
951976
}
952977

953978
// Additional properties describing a Tool to clients.
@@ -1090,6 +1115,10 @@ type Implementation struct {
10901115
// easily understood, even by those unfamiliar with domain-specific terminology.
10911116
Title string `json:"title,omitempty"`
10921117
Version string `json:"version"`
1118+
// WebsiteURL for the server, if any.
1119+
WebsiteURL string `json:"websiteUrl,omitempty"`
1120+
// Icons for the Server, if any.
1121+
Icons []Icon `json:"icons,omitempty"`
10931122
}
10941123

10951124
// Present if the server supports argument autocompletion suggestions.

0 commit comments

Comments
 (0)