diff --git a/examples/custom_context/main.go b/examples/custom_context/main.go index e41ab8db7..938011eff 100644 --- a/examples/custom_context/main.go +++ b/examples/custom_context/main.go @@ -110,6 +110,7 @@ func NewMCPServer() *MCPServer { server.WithToolCapabilities(true), ) mcpServer.AddTool(mcp.NewTool("make_authenticated_request", + mcp.WithTitle("Authenticated HTTP Request Tool"), mcp.WithDescription("Makes an authenticated request"), mcp.WithString("message", mcp.Description("Message to echo"), diff --git a/examples/dynamic_path/main.go b/examples/dynamic_path/main.go index 80d96789a..868f8eeab 100644 --- a/examples/dynamic_path/main.go +++ b/examples/dynamic_path/main.go @@ -19,7 +19,7 @@ func main() { mcpServer := server.NewMCPServer("dynamic-path-example", "1.0.0") // Add a trivial tool for demonstration - mcpServer.AddTool(mcp.NewTool("echo"), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { + mcpServer.AddTool(mcp.NewTool("echo", mcp.WithTitle("Echo Message Tool")), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { return mcp.NewToolResultText(fmt.Sprintf("Echo: %v", req.GetArguments()["message"])), nil }) diff --git a/examples/everything/main.go b/examples/everything/main.go index 5489220c3..7440ab38d 100644 --- a/examples/everything/main.go +++ b/examples/everything/main.go @@ -74,11 +74,13 @@ func NewMCPServer() *server.MCPServer { mcpServer.AddResource(mcp.NewResource("test://static/resource", "Static Resource", mcp.WithMIMEType("text/plain"), + mcp.WithResourceTitle("Static Text Resource"), ), handleReadResource) mcpServer.AddResourceTemplate( mcp.NewResourceTemplate( "test://dynamic/resource/{id}", "Dynamic Resource", + mcp.WithTemplateTitle("Dynamic Resource Template"), ), handleResourceTemplate, ) @@ -90,9 +92,11 @@ func NewMCPServer() *server.MCPServer { mcpServer.AddPrompt(mcp.NewPrompt(string(SIMPLE), mcp.WithPromptDescription("A simple prompt"), + mcp.WithPromptTitle("Simple Prompt Example"), ), handleSimplePrompt) mcpServer.AddPrompt(mcp.NewPrompt(string(COMPLEX), mcp.WithPromptDescription("A complex prompt"), + mcp.WithPromptTitle("Complex Prompt with Arguments"), mcp.WithArgument("temperature", mcp.ArgumentDescription("The temperature parameter for generation"), mcp.RequiredArgument(), @@ -104,6 +108,7 @@ func NewMCPServer() *server.MCPServer { ), handleComplexPrompt) mcpServer.AddTool(mcp.NewTool(string(ECHO), mcp.WithDescription("Echoes back the input"), + mcp.WithTitle("Echo Tool"), mcp.WithString("message", mcp.Description("Message to echo"), mcp.Required(), @@ -111,12 +116,15 @@ func NewMCPServer() *server.MCPServer { ), handleEchoTool) mcpServer.AddTool( - mcp.NewTool("notify"), + mcp.NewTool("notify", + mcp.WithTitle("Send Notification"), + ), handleSendNotification, ) mcpServer.AddTool(mcp.NewTool(string(ADD), mcp.WithDescription("Adds two numbers"), + mcp.WithTitle("Number Addition Tool"), mcp.WithNumber("a", mcp.Description("First number"), mcp.Required(), @@ -131,6 +139,7 @@ func NewMCPServer() *server.MCPServer { mcp.WithDescription( "Demonstrates a long running operation with progress updates", ), + mcp.WithTitle("Long Running Operation Demo"), mcp.WithNumber("duration", mcp.Description("Duration of the operation in seconds"), mcp.DefaultNumber(10), @@ -161,6 +170,7 @@ func NewMCPServer() *server.MCPServer { // }, s.handleSampleLLMTool) mcpServer.AddTool(mcp.NewTool(string(GET_TINY_IMAGE), mcp.WithDescription("Returns the MCP_TINY_IMAGE"), + mcp.WithTitle("Tiny Image Provider"), ), handleGetTinyImageTool) mcpServer.AddNotificationHandler("notification", handleNotification) @@ -172,18 +182,23 @@ func generateResources() []mcp.Resource { resources := make([]mcp.Resource, 100) for i := 0; i < 100; i++ { uri := fmt.Sprintf("test://static/resource/%d", i+1) + resourceName := fmt.Sprintf("Resource %d", i+1) + resourceTitle := fmt.Sprintf("Generated Resource #%d", i+1) + if i%2 == 0 { - resources[i] = mcp.Resource{ - URI: uri, - Name: fmt.Sprintf("Resource %d", i+1), - MIMEType: "text/plain", - } + resources[i] = mcp.NewResource( + uri, + resourceName, + mcp.WithMIMEType("text/plain"), + mcp.WithResourceTitle(resourceTitle), + ) } else { - resources[i] = mcp.Resource{ - URI: uri, - Name: fmt.Sprintf("Resource %d", i+1), - MIMEType: "application/octet-stream", - } + resources[i] = mcp.NewResource( + uri, + resourceName, + mcp.WithMIMEType("application/octet-stream"), + mcp.WithResourceTitle(resourceTitle), + ) } } return resources diff --git a/examples/in_process/main.go b/examples/in_process/main.go index d01a5e808..0b842e553 100644 --- a/examples/in_process/main.go +++ b/examples/in_process/main.go @@ -28,6 +28,7 @@ func NewMCPServer() *server.MCPServer { server.WithToolCapabilities(true), ) mcpServer.AddTool(mcp.NewTool("dummy_tool", + mcp.WithTitle("Simple Demo Tool"), mcp.WithDescription("A dummy tool that returns foo bar"), ), handleDummyTool) diff --git a/examples/typed_tools/main.go b/examples/typed_tools/main.go index f9bd3c21e..31904233c 100644 --- a/examples/typed_tools/main.go +++ b/examples/typed_tools/main.go @@ -30,6 +30,7 @@ func main() { // Add tool with complex schema tool := mcp.NewTool("greeting", + mcp.WithTitle("Personalized Greeting Generator"), mcp.WithDescription("Generate a personalized greeting"), mcp.WithString("name", mcp.Required(), diff --git a/mcp/prompts.go b/mcp/prompts.go index a63a21450..007190800 100644 --- a/mcp/prompts.go +++ b/mcp/prompts.go @@ -45,6 +45,9 @@ type GetPromptResult struct { type Prompt struct { // The name of the prompt or prompt template. Name string `json:"name"` + // A human-friendly display name for the prompt. + // This is used for UI display purposes, while Name is used for programmatic identification. + Title string `json:"title,omitempty"` // An optional description of what this prompt provides Description string `json:"description,omitempty"` // A list of arguments to use for templating the prompt. @@ -57,6 +60,11 @@ func (p Prompt) GetName() string { return p.Name } +// GetTitle returns the display title for the prompt. +func (p Prompt) GetTitle() string { + return p.Title +} + // PromptArgument describes an argument that a prompt template can accept. // When a prompt includes arguments, clients must provide values for all // required arguments when making a prompts/get request. @@ -130,6 +138,14 @@ func WithPromptDescription(description string) PromptOption { } } +// WithPromptTitle sets the title field of the Prompt. +// This provides a human-readable display name for the prompt. +func WithPromptTitle(title string) PromptOption { + return func(p *Prompt) { + p.Title = title + } +} + // WithArgument adds an argument to the prompt's argument list. // The argument will be configured based on the provided options. func WithArgument(name string, opts ...ArgumentOption) PromptOption { diff --git a/mcp/resources.go b/mcp/resources.go index 07a59a322..d08a5322b 100644 --- a/mcp/resources.go +++ b/mcp/resources.go @@ -30,6 +30,14 @@ func WithResourceDescription(description string) ResourceOption { } } +// WithResourceTitle sets the title field of the Resource. +// This provides a human-readable display name for the resource. +func WithResourceTitle(title string) ResourceOption { + return func(r *Resource) { + r.Title = title + } +} + // WithMIMEType sets the MIME type for the Resource. // This should indicate the format of the resource's contents. func WithMIMEType(mimeType string) ResourceOption { @@ -78,6 +86,14 @@ func WithTemplateDescription(description string) ResourceTemplateOption { } } +// WithTemplateTitle sets the title field of the ResourceTemplate. +// This provides a human-readable display name for the resource template. +func WithTemplateTitle(title string) ResourceTemplateOption { + return func(t *ResourceTemplate) { + t.Title = title + } +} + // WithTemplateMIMEType sets the MIME type for the ResourceTemplate. // This should only be set if all resources matching this template will have the same type. func WithTemplateMIMEType(mimeType string) ResourceTemplateOption { diff --git a/mcp/tools.go b/mcp/tools.go index 3e3931b09..a555c9974 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -472,6 +472,9 @@ type ToolListChangedNotification struct { type Tool struct { // The name of the tool. Name string `json:"name"` + // A human-friendly display name for the tool. + // This is used for UI display purposes, while Name is used for programmatic identification. + Title string `json:"title,omitempty"` // A human-readable description of the tool. Description string `json:"description,omitempty"` // A JSON Schema object defining the expected parameters for the tool. @@ -487,14 +490,26 @@ func (t Tool) GetName() string { return t.Name } +// GetTitle returns the display title for the tool. +// It follows the precedence: direct title field → annotations.title → empty string. +func (t Tool) GetTitle() string { + if title := t.Title; title != "" { + return title + } + return t.Annotations.Title +} + // MarshalJSON implements the json.Marshaler interface for Tool. // It handles marshaling either InputSchema or RawInputSchema based on which is set. func (t Tool) MarshalJSON() ([]byte, error) { // Create a map to build the JSON structure - m := make(map[string]any, 3) + m := make(map[string]any, 4) - // Add the name and description + // Add the name and title m["name"] = t.Name + if t.Title != "" { + m["title"] = t.Title + } if t.Description != "" { m["description"] = t.Description } @@ -615,6 +630,14 @@ func WithDescription(description string) ToolOption { } } +// WithTitle sets the direct title field of the Tool. +// This title takes precedence over the annotation title when displaying the tool. +func WithTitle(title string) ToolOption { + return func(t *Tool) { + t.Title = title + } +} + // WithToolAnnotation adds optional hints about the Tool. func WithToolAnnotation(annotation ToolAnnotation) ToolOption { return func(t *Tool) { diff --git a/mcp/types.go b/mcp/types.go index d4f6132c8..2c6f355f6 100644 --- a/mcp/types.go +++ b/mcp/types.go @@ -641,6 +641,9 @@ type Resource struct { // // This can be used by clients to populate UI elements. Name string `json:"name"` + // A human-friendly display name for this resource. + // This is used for UI display purposes, while Name is used for programmatic identification. + Title string `json:"title,omitempty"` // A description of what this resource represents. // // This can be used by clients to improve the LLM's understanding of @@ -655,6 +658,11 @@ func (r Resource) GetName() string { return r.Name } +// GetTitle returns the display title for the resource. +func (r Resource) GetTitle() string { + return r.Title +} + // ResourceTemplate represents a template description for resources available // on the server. type ResourceTemplate struct { @@ -666,6 +674,9 @@ type ResourceTemplate struct { // // This can be used by clients to populate UI elements. Name string `json:"name"` + // A human-friendly display name for this resource template. + // This is used for UI display purposes, while Name is used for programmatic identification. + Title string `json:"title,omitempty"` // A description of what this template is for. // // This can be used by clients to improve the LLM's understanding of @@ -681,6 +692,11 @@ func (rt ResourceTemplate) GetName() string { return rt.Name } +// GetTitle returns the title of the resourceTemplate. +func (rt ResourceTemplate) GetTitle() string { + return rt.Title +} + // ResourceContents represents the contents of a specific resource or sub- // resource. type ResourceContents interface { @@ -1058,3 +1074,11 @@ type ServerResult any type Named interface { GetName() string } + +// BaseMetadata defines the interface for objects that have both programmatic names +// and human-friendly display titles. This enables consistent display name handling +// across different MCP object types. +type BaseMetadata interface { + Named + GetTitle() string +} diff --git a/mcp/types_test.go b/mcp/types_test.go index 526e1ac1e..2017e486c 100644 --- a/mcp/types_test.go +++ b/mcp/types_test.go @@ -68,3 +68,133 @@ func TestMetaMarshalling(t *testing.T) { }) } } + +// TestGetDisplayName tests the display name logic for all types +func TestGetDisplayName(t *testing.T) { + tests := []struct { + name string + meta BaseMetadata + expectedName string + }{ + // Tool tests + { + name: "tool with direct title", + meta: &Tool{ + Name: "test-tool", + Title: "Tool Title", + Annotations: ToolAnnotation{Title: "Annotation Title"}, + }, + expectedName: "Tool Title", + }, + { + name: "tool with annotation title only", + meta: &Tool{ + Name: "test-tool", + Annotations: ToolAnnotation{Title: "Annotation Title"}, + }, + expectedName: "Annotation Title", + }, + { + name: "tool falls back to name", + meta: &Tool{Name: "test-tool"}, + expectedName: "test-tool", + }, + + // Prompt tests + { + name: "prompt with title", + meta: &Prompt{ + Name: "test-prompt", + Title: "Prompt Title", + }, + expectedName: "Prompt Title", + }, + { + name: "prompt falls back to name", + meta: &Prompt{Name: "test-prompt"}, + expectedName: "test-prompt", + }, + + // Resource tests + { + name: "resource with title", + meta: &Resource{ + Name: "test-resource", + Title: "Resource Title", + }, + expectedName: "Resource Title", + }, + { + name: "resource falls back to name", + meta: &Resource{Name: "test-resource"}, + expectedName: "test-resource", + }, + + // ResourceTemplate tests + { + name: "resource template with title", + meta: &ResourceTemplate{ + Name: "test-template", + Title: "Template Title", + }, + expectedName: "Template Title", + }, + { + name: "resource template falls back to name", + meta: &ResourceTemplate{Name: "test-template"}, + expectedName: "test-template", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expectedName, GetDisplayName(tt.meta)) + }) + } +} + +// TestToolTitleSerialization tests that Tool title field is properly serialized +func TestToolTitleSerialization(t *testing.T) { + tool := Tool{ + Name: "test-tool", + Title: "Test Tool Title", + Description: "A test tool", + InputSchema: ToolInputSchema{ + Type: "object", + Properties: map[string]any{}, + }, + Annotations: ToolAnnotation{ + Title: "Annotation Title", + }, + } + + // Test serialization + data, err := json.Marshal(tool) + require.NoError(t, err) + + var result map[string]any + err = json.Unmarshal(data, &result) + require.NoError(t, err) + + assert.Equal(t, "test-tool", result["name"]) + assert.Equal(t, "Test Tool Title", result["title"]) + assert.Equal(t, "A test tool", result["description"]) + + annotations, ok := result["annotations"].(map[string]any) + require.True(t, ok) + assert.Equal(t, "Annotation Title", annotations["title"]) + + // Test deserialization + var deserializedTool Tool + err = json.Unmarshal(data, &deserializedTool) + require.NoError(t, err) + + assert.Equal(t, "test-tool", deserializedTool.Name) + assert.Equal(t, "Test Tool Title", deserializedTool.Title) + assert.Equal(t, "A test tool", deserializedTool.Description) + assert.Equal(t, "Annotation Title", deserializedTool.Annotations.Title) + + // Test GetTitle method + assert.Equal(t, "Test Tool Title", deserializedTool.GetTitle()) + assert.Equal(t, "Test Tool Title", GetDisplayName(&deserializedTool)) +} diff --git a/mcp/utils.go b/mcp/utils.go index 3e652efd7..67a48f880 100644 --- a/mcp/utils.go +++ b/mcp/utils.go @@ -57,6 +57,12 @@ var _ ServerResult = &ReadResourceResult{} var _ ServerResult = &CallToolResult{} var _ ServerResult = &ListToolsResult{} +// BaseMetadata types +var _ BaseMetadata = &Tool{} +var _ BaseMetadata = &Prompt{} +var _ BaseMetadata = &Resource{} +var _ BaseMetadata = &ResourceTemplate{} + // Helper functions for type assertions // asType attempts to cast the given interface to the given type @@ -817,3 +823,14 @@ func ParseStringMap(request CallToolRequest, key string, defaultValue map[string func ToBoolPtr(b bool) *bool { return &b } + +// GetDisplayName returns the best display name for a BaseMetadata object. +// It follows the precedence: GetTitle() → GetName(), providing a consistent +// way to get human-friendly names across all MCP object types. +func GetDisplayName(meta BaseMetadata) string { + if title := meta.GetTitle(); title != "" { + return title + } + + return meta.GetName() +}