Skip to content

Commit b76516b

Browse files
authored
feat: add default icons for all tools (#39)
Add Icons field support to tool registrations with a 3-level priority chain: per-registration override, toolkit-level override, and built-in default. All 9 tools now include a default S3 SVG icon in tools/list responses. New WithIcons() toolkit option and WithIcon() tool option allow consumers to override icons at registration time.
1 parent f1550f7 commit b76516b

14 files changed

Lines changed: 166 additions & 0 deletions

icons/s3.svg

Lines changed: 6 additions & 0 deletions
Loading

pkg/tools/connections.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func (t *Toolkit) registerListConnectionsTool(server *mcp.Server, cfg *toolConfi
3636
Name: t.toolName(ToolListConnections),
3737
Description: t.getDescription(ToolListConnections, cfg),
3838
Annotations: t.getAnnotations(ToolListConnections, cfg),
39+
Icons: t.getIcons(ToolListConnections, cfg),
3940
}, func(ctx context.Context, req *mcp.CallToolRequest, input ListConnectionsInput) (*mcp.CallToolResult, *ListConnectionsResult, error) {
4041
result, out, err := wrappedHandler(ctx, req, input)
4142
if typed, ok := out.(*ListConnectionsResult); ok {

pkg/tools/copy_object.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func (t *Toolkit) registerCopyObjectTool(server *mcp.Server, cfg *toolConfig) {
3535
Name: t.toolName(ToolCopyObject),
3636
Description: t.getDescription(ToolCopyObject, cfg),
3737
Annotations: t.getAnnotations(ToolCopyObject, cfg),
38+
Icons: t.getIcons(ToolCopyObject, cfg),
3839
}, func(ctx context.Context, req *mcp.CallToolRequest, input CopyObjectInput) (*mcp.CallToolResult, *CopyObjectResult, error) {
3940
result, out, err := wrappedHandler(ctx, req, input)
4041
if typed, ok := out.(*CopyObjectResult); ok {

pkg/tools/delete_object.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func (t *Toolkit) registerDeleteObjectTool(server *mcp.Server, cfg *toolConfig)
2929
Name: t.toolName(ToolDeleteObject),
3030
Description: t.getDescription(ToolDeleteObject, cfg),
3131
Annotations: t.getAnnotations(ToolDeleteObject, cfg),
32+
Icons: t.getIcons(ToolDeleteObject, cfg),
3233
}, func(ctx context.Context, req *mcp.CallToolRequest, input DeleteObjectInput) (*mcp.CallToolResult, *DeleteObjectResult, error) {
3334
result, out, err := wrappedHandler(ctx, req, input)
3435
if typed, ok := out.(*DeleteObjectResult); ok {

pkg/tools/get_object.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func (t *Toolkit) registerGetObjectTool(server *mcp.Server, cfg *toolConfig) {
4242
Name: t.toolName(ToolGetObject),
4343
Description: t.getDescription(ToolGetObject, cfg),
4444
Annotations: t.getAnnotations(ToolGetObject, cfg),
45+
Icons: t.getIcons(ToolGetObject, cfg),
4546
}, func(ctx context.Context, req *mcp.CallToolRequest, input GetObjectInput) (*mcp.CallToolResult, *GetObjectResult, error) {
4647
result, out, err := wrappedHandler(ctx, req, input)
4748
if typed, ok := out.(*GetObjectResult); ok {

pkg/tools/get_object_metadata.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func (t *Toolkit) registerGetObjectMetadataTool(server *mcp.Server, cfg *toolCon
3434
Name: t.toolName(ToolGetObjectMetadata),
3535
Description: t.getDescription(ToolGetObjectMetadata, cfg),
3636
Annotations: t.getAnnotations(ToolGetObjectMetadata, cfg),
37+
Icons: t.getIcons(ToolGetObjectMetadata, cfg),
3738
}, func(
3839
ctx context.Context, req *mcp.CallToolRequest, input GetObjectMetadataInput,
3940
) (*mcp.CallToolResult, *GetObjectMetadataResult, error) {

pkg/tools/icons.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package tools
2+
3+
import "github.com/modelcontextprotocol/go-sdk/mcp"
4+
5+
// defaultIcons holds the default icon for all S3 tools.
6+
// Uses a GitHub-hosted SVG from the mcp-s3 repository.
7+
var defaultIcons = []mcp.Icon{{
8+
Source: "https://raw.githubusercontent.com/txn2/mcp-s3/main/icons/s3.svg",
9+
MIMEType: "image/svg+xml",
10+
}}
11+
12+
// DefaultIcons returns the default icons for S3 tools.
13+
func DefaultIcons() []mcp.Icon {
14+
return defaultIcons
15+
}
16+
17+
// getIcons resolves the icons for a tool using the priority chain:
18+
// 1. Per-registration override (cfg.icons) — highest priority
19+
// 2. Toolkit-level override (t.icons) — medium priority
20+
// 3. Default icons — lowest priority.
21+
func (t *Toolkit) getIcons(name ToolName, cfg *toolConfig) []mcp.Icon {
22+
// Per-registration override (highest priority)
23+
if cfg != nil && cfg.icons != nil {
24+
return cfg.icons
25+
}
26+
27+
// Toolkit-level override (medium priority)
28+
if t.icons != nil {
29+
if icons, ok := t.icons[name]; ok {
30+
return icons
31+
}
32+
}
33+
34+
// Default icons (lowest priority)
35+
return defaultIcons
36+
}

pkg/tools/icons_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package tools
2+
3+
import (
4+
"testing"
5+
6+
"github.com/modelcontextprotocol/go-sdk/mcp"
7+
)
8+
9+
func TestDefaultIcons(t *testing.T) {
10+
icons := DefaultIcons()
11+
if len(icons) != 1 {
12+
t.Fatalf("expected 1 default icon, got %d", len(icons))
13+
}
14+
if icons[0].Source == "" {
15+
t.Error("default icon source should not be empty")
16+
}
17+
if icons[0].MIMEType != "image/svg+xml" {
18+
t.Errorf("expected MIME type image/svg+xml, got %s", icons[0].MIMEType)
19+
}
20+
}
21+
22+
func TestGetIcons_DefaultPriority(t *testing.T) {
23+
tk := NewToolkit(nil)
24+
icons := tk.getIcons(ToolListBuckets, nil)
25+
if len(icons) != 1 {
26+
t.Fatalf("expected 1 icon, got %d", len(icons))
27+
}
28+
if icons[0].Source != defaultIcons[0].Source {
29+
t.Errorf("expected default icon source, got %s", icons[0].Source)
30+
}
31+
}
32+
33+
func TestGetIcons_ToolkitOverride(t *testing.T) {
34+
customIcons := []mcp.Icon{{Source: "https://example.com/custom.svg", MIMEType: "image/svg+xml"}}
35+
tk := NewToolkit(nil, WithIcons(map[ToolName][]mcp.Icon{
36+
ToolListBuckets: customIcons,
37+
}))
38+
39+
// ToolListBuckets should get the override
40+
icons := tk.getIcons(ToolListBuckets, nil)
41+
if len(icons) != 1 {
42+
t.Fatalf("expected 1 icon, got %d", len(icons))
43+
}
44+
if icons[0].Source != "https://example.com/custom.svg" {
45+
t.Errorf("expected custom icon source, got %s", icons[0].Source)
46+
}
47+
48+
// ToolGetObject should get the default
49+
icons = tk.getIcons(ToolGetObject, nil)
50+
if icons[0].Source != defaultIcons[0].Source {
51+
t.Errorf("expected default icon source for ToolGetObject, got %s", icons[0].Source)
52+
}
53+
}
54+
55+
func TestGetIcons_PerRegistrationOverride(t *testing.T) {
56+
tk := NewToolkit(nil, WithIcons(map[ToolName][]mcp.Icon{
57+
ToolListBuckets: {{Source: "https://example.com/toolkit.svg"}},
58+
}))
59+
60+
// Per-registration should take highest priority
61+
cfg := &toolConfig{
62+
icons: []mcp.Icon{{Source: "https://example.com/registration.svg"}},
63+
}
64+
icons := tk.getIcons(ToolListBuckets, cfg)
65+
if len(icons) != 1 {
66+
t.Fatalf("expected 1 icon, got %d", len(icons))
67+
}
68+
if icons[0].Source != "https://example.com/registration.svg" {
69+
t.Errorf("expected per-registration icon source, got %s", icons[0].Source)
70+
}
71+
}
72+
73+
func TestGetIcons_NilConfig(t *testing.T) {
74+
tk := NewToolkit(nil)
75+
icons := tk.getIcons(ToolListBuckets, nil)
76+
if len(icons) == 0 {
77+
t.Fatal("expected default icons, got empty")
78+
}
79+
}
80+
81+
func TestGetIcons_EmptyToolConfig(t *testing.T) {
82+
tk := NewToolkit(nil)
83+
cfg := &toolConfig{}
84+
icons := tk.getIcons(ToolListBuckets, cfg)
85+
if len(icons) == 0 {
86+
t.Fatal("expected default icons, got empty")
87+
}
88+
if icons[0].Source != defaultIcons[0].Source {
89+
t.Errorf("expected default icon, got %s", icons[0].Source)
90+
}
91+
}

pkg/tools/list_buckets.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func (t *Toolkit) registerListBucketsTool(server *mcp.Server, cfg *toolConfig) {
3434
Name: t.toolName(ToolListBuckets),
3535
Description: t.getDescription(ToolListBuckets, cfg),
3636
Annotations: t.getAnnotations(ToolListBuckets, cfg),
37+
Icons: t.getIcons(ToolListBuckets, cfg),
3738
}, func(ctx context.Context, req *mcp.CallToolRequest, input ListBucketsInput) (*mcp.CallToolResult, *ListBucketsResult, error) {
3839
result, out, err := wrappedHandler(ctx, req, input)
3940
if typed, ok := out.(*ListBucketsResult); ok {

pkg/tools/list_objects.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func (t *Toolkit) registerListObjectsTool(server *mcp.Server, cfg *toolConfig) {
4343
Name: t.toolName(ToolListObjects),
4444
Description: t.getDescription(ToolListObjects, cfg),
4545
Annotations: t.getAnnotations(ToolListObjects, cfg),
46+
Icons: t.getIcons(ToolListObjects, cfg),
4647
}, func(ctx context.Context, req *mcp.CallToolRequest, input ListObjectsInput) (*mcp.CallToolResult, *ListObjectsResult, error) {
4748
result, out, err := wrappedHandler(ctx, req, input)
4849
if typed, ok := out.(*ListObjectsResult); ok {

0 commit comments

Comments
 (0)