Skip to content

Commit dd458b4

Browse files
committed
feat: Add title field support for human-friendly display names
- Implement BaseMetadata interface for consistent title handling - Add Title field to Tool, Prompt, Resource, ResourceTemplate - Add GetDisplayName() helper with title → annotations.title → name fallback - Add WithTitle/WithPromptTitle/WithResourceTitle/WithTemplateTitle options - Fix Tool JSON serialization to include title field - Add comprehensive tests and update examples - Fully backward compatible Resolves Task #418
1 parent 0fdb197 commit dd458b4

File tree

6 files changed

+228
-2
lines changed

6 files changed

+228
-2
lines changed

mcp/prompts.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ type GetPromptResult struct {
4545
type Prompt struct {
4646
// The name of the prompt or prompt template.
4747
Name string `json:"name"`
48+
// A human-friendly display name for the prompt.
49+
// This is used for UI display purposes, while Name is used for programmatic identification.
50+
Title string `json:"title,omitempty"`
4851
// An optional description of what this prompt provides
4952
Description string `json:"description,omitempty"`
5053
// A list of arguments to use for templating the prompt.
@@ -57,6 +60,11 @@ func (p Prompt) GetName() string {
5760
return p.Name
5861
}
5962

63+
// GetTitle returns the display title for the prompt.
64+
func (p Prompt) GetTitle() string {
65+
return p.Title
66+
}
67+
6068
// PromptArgument describes an argument that a prompt template can accept.
6169
// When a prompt includes arguments, clients must provide values for all
6270
// required arguments when making a prompts/get request.
@@ -130,6 +138,14 @@ func WithPromptDescription(description string) PromptOption {
130138
}
131139
}
132140

141+
// WithPromptTitle sets the title field of the Prompt.
142+
// This provides a human-readable display name for the prompt.
143+
func WithPromptTitle(title string) PromptOption {
144+
return func(p *Prompt) {
145+
p.Title = title
146+
}
147+
}
148+
133149
// WithArgument adds an argument to the prompt's argument list.
134150
// The argument will be configured based on the provided options.
135151
func WithArgument(name string, opts ...ArgumentOption) PromptOption {

mcp/resources.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ func WithResourceDescription(description string) ResourceOption {
3030
}
3131
}
3232

33+
// WithResourceTitle sets the title field of the Resource.
34+
// This provides a human-readable display name for the resource.
35+
func WithResourceTitle(title string) ResourceOption {
36+
return func(r *Resource) {
37+
r.Title = title
38+
}
39+
}
40+
3341
// WithMIMEType sets the MIME type for the Resource.
3442
// This should indicate the format of the resource's contents.
3543
func WithMIMEType(mimeType string) ResourceOption {
@@ -78,6 +86,14 @@ func WithTemplateDescription(description string) ResourceTemplateOption {
7886
}
7987
}
8088

89+
// WithTemplateTitle sets the title field of the ResourceTemplate.
90+
// This provides a human-readable display name for the resource template.
91+
func WithTemplateTitle(title string) ResourceTemplateOption {
92+
return func(t *ResourceTemplate) {
93+
t.Title = title
94+
}
95+
}
96+
8197
// WithTemplateMIMEType sets the MIME type for the ResourceTemplate.
8298
// This should only be set if all resources matching this template will have the same type.
8399
func WithTemplateMIMEType(mimeType string) ResourceTemplateOption {

mcp/tools.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,9 @@ type ToolListChangedNotification struct {
472472
type Tool struct {
473473
// The name of the tool.
474474
Name string `json:"name"`
475+
// A human-friendly display name for the tool.
476+
// This is used for UI display purposes, while Name is used for programmatic identification.
477+
Title string `json:"title,omitempty"`
475478
// A human-readable description of the tool.
476479
Description string `json:"description,omitempty"`
477480
// A JSON Schema object defining the expected parameters for the tool.
@@ -487,14 +490,26 @@ func (t Tool) GetName() string {
487490
return t.Name
488491
}
489492

493+
// GetTitle returns the display title for the tool.
494+
// It follows the precedence: direct title field → annotations.title → empty string.
495+
func (t Tool) GetTitle() string {
496+
if title := t.Title; title != "" {
497+
return title
498+
}
499+
return t.Annotations.Title
500+
}
501+
490502
// MarshalJSON implements the json.Marshaler interface for Tool.
491503
// It handles marshaling either InputSchema or RawInputSchema based on which is set.
492504
func (t Tool) MarshalJSON() ([]byte, error) {
493505
// Create a map to build the JSON structure
494-
m := make(map[string]any, 3)
506+
m := make(map[string]any, 4)
495507

496-
// Add the name and description
508+
// Add the name and title
497509
m["name"] = t.Name
510+
if t.Title != "" {
511+
m["title"] = t.Title
512+
}
498513
if t.Description != "" {
499514
m["description"] = t.Description
500515
}
@@ -615,6 +630,14 @@ func WithDescription(description string) ToolOption {
615630
}
616631
}
617632

633+
// WithTitle sets the direct title field of the Tool.
634+
// This title takes precedence over the annotation title when displaying the tool.
635+
func WithTitle(title string) ToolOption {
636+
return func(t *Tool) {
637+
t.Title = title
638+
}
639+
}
640+
618641
// WithToolAnnotation adds optional hints about the Tool.
619642
func WithToolAnnotation(annotation ToolAnnotation) ToolOption {
620643
return func(t *Tool) {

mcp/types.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,9 @@ type Resource struct {
641641
//
642642
// This can be used by clients to populate UI elements.
643643
Name string `json:"name"`
644+
// A human-friendly display name for this resource.
645+
// This is used for UI display purposes, while Name is used for programmatic identification.
646+
Title string `json:"title,omitempty"`
644647
// A description of what this resource represents.
645648
//
646649
// This can be used by clients to improve the LLM's understanding of
@@ -655,6 +658,11 @@ func (r Resource) GetName() string {
655658
return r.Name
656659
}
657660

661+
// GetTitle returns the display title for the resource.
662+
func (r Resource) GetTitle() string {
663+
return r.Title
664+
}
665+
658666
// ResourceTemplate represents a template description for resources available
659667
// on the server.
660668
type ResourceTemplate struct {
@@ -666,6 +674,9 @@ type ResourceTemplate struct {
666674
//
667675
// This can be used by clients to populate UI elements.
668676
Name string `json:"name"`
677+
// A human-friendly display name for this resource template.
678+
// This is used for UI display purposes, while Name is used for programmatic identification.
679+
Title string `json:"title,omitempty"`
669680
// A description of what this template is for.
670681
//
671682
// This can be used by clients to improve the LLM's understanding of
@@ -681,6 +692,11 @@ func (rt ResourceTemplate) GetName() string {
681692
return rt.Name
682693
}
683694

695+
// GetTitle returns the title of the resourceTemplate.
696+
func (rt ResourceTemplate) GetTitle() string {
697+
return rt.Title
698+
}
699+
684700
// ResourceContents represents the contents of a specific resource or sub-
685701
// resource.
686702
type ResourceContents interface {
@@ -1058,3 +1074,11 @@ type ServerResult any
10581074
type Named interface {
10591075
GetName() string
10601076
}
1077+
1078+
// BaseMetadata defines the interface for objects that have both programmatic names
1079+
// and human-friendly display titles. This enables consistent display name handling
1080+
// across different MCP object types.
1081+
type BaseMetadata interface {
1082+
Named
1083+
GetTitle() string
1084+
}

mcp/types_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,133 @@ func TestMetaMarshalling(t *testing.T) {
6868
})
6969
}
7070
}
71+
72+
// TestGetDisplayName tests the display name logic for all types
73+
func TestGetDisplayName(t *testing.T) {
74+
tests := []struct {
75+
name string
76+
meta BaseMetadata
77+
expectedName string
78+
}{
79+
// Tool tests
80+
{
81+
name: "tool with direct title",
82+
meta: &Tool{
83+
Name: "test-tool",
84+
Title: "Tool Title",
85+
Annotations: ToolAnnotation{Title: "Annotation Title"},
86+
},
87+
expectedName: "Tool Title",
88+
},
89+
{
90+
name: "tool with annotation title only",
91+
meta: &Tool{
92+
Name: "test-tool",
93+
Annotations: ToolAnnotation{Title: "Annotation Title"},
94+
},
95+
expectedName: "Annotation Title",
96+
},
97+
{
98+
name: "tool falls back to name",
99+
meta: &Tool{Name: "test-tool"},
100+
expectedName: "test-tool",
101+
},
102+
103+
// Prompt tests
104+
{
105+
name: "prompt with title",
106+
meta: &Prompt{
107+
Name: "test-prompt",
108+
Title: "Prompt Title",
109+
},
110+
expectedName: "Prompt Title",
111+
},
112+
{
113+
name: "prompt falls back to name",
114+
meta: &Prompt{Name: "test-prompt"},
115+
expectedName: "test-prompt",
116+
},
117+
118+
// Resource tests
119+
{
120+
name: "resource with title",
121+
meta: &Resource{
122+
Name: "test-resource",
123+
Title: "Resource Title",
124+
},
125+
expectedName: "Resource Title",
126+
},
127+
{
128+
name: "resource falls back to name",
129+
meta: &Resource{Name: "test-resource"},
130+
expectedName: "test-resource",
131+
},
132+
133+
// ResourceTemplate tests
134+
{
135+
name: "resource template with title",
136+
meta: &ResourceTemplate{
137+
Name: "test-template",
138+
Title: "Template Title",
139+
},
140+
expectedName: "Template Title",
141+
},
142+
{
143+
name: "resource template falls back to name",
144+
meta: &ResourceTemplate{Name: "test-template"},
145+
expectedName: "test-template",
146+
},
147+
}
148+
149+
for _, tt := range tests {
150+
t.Run(tt.name, func(t *testing.T) {
151+
assert.Equal(t, tt.expectedName, GetDisplayName(tt.meta))
152+
})
153+
}
154+
}
155+
156+
// TestToolTitleSerialization tests that Tool title field is properly serialized
157+
func TestToolTitleSerialization(t *testing.T) {
158+
tool := Tool{
159+
Name: "test-tool",
160+
Title: "Test Tool Title",
161+
Description: "A test tool",
162+
InputSchema: ToolInputSchema{
163+
Type: "object",
164+
Properties: map[string]any{},
165+
},
166+
Annotations: ToolAnnotation{
167+
Title: "Annotation Title",
168+
},
169+
}
170+
171+
// Test serialization
172+
data, err := json.Marshal(tool)
173+
require.NoError(t, err)
174+
175+
var result map[string]any
176+
err = json.Unmarshal(data, &result)
177+
require.NoError(t, err)
178+
179+
assert.Equal(t, "test-tool", result["name"])
180+
assert.Equal(t, "Test Tool Title", result["title"])
181+
assert.Equal(t, "A test tool", result["description"])
182+
183+
annotations, ok := result["annotations"].(map[string]any)
184+
require.True(t, ok)
185+
assert.Equal(t, "Annotation Title", annotations["title"])
186+
187+
// Test deserialization
188+
var deserializedTool Tool
189+
err = json.Unmarshal(data, &deserializedTool)
190+
require.NoError(t, err)
191+
192+
assert.Equal(t, "test-tool", deserializedTool.Name)
193+
assert.Equal(t, "Test Tool Title", deserializedTool.Title)
194+
assert.Equal(t, "A test tool", deserializedTool.Description)
195+
assert.Equal(t, "Annotation Title", deserializedTool.Annotations.Title)
196+
197+
// Test GetTitle method
198+
assert.Equal(t, "Test Tool Title", deserializedTool.GetTitle())
199+
assert.Equal(t, "Test Tool Title", GetDisplayName(&deserializedTool))
200+
}

mcp/utils.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ var _ ServerResult = &ReadResourceResult{}
5757
var _ ServerResult = &CallToolResult{}
5858
var _ ServerResult = &ListToolsResult{}
5959

60+
// BaseMetadata types
61+
var _ BaseMetadata = &Tool{}
62+
var _ BaseMetadata = &Prompt{}
63+
var _ BaseMetadata = &Resource{}
64+
var _ BaseMetadata = &ResourceTemplate{}
65+
6066
// Helper functions for type assertions
6167

6268
// asType attempts to cast the given interface to the given type
@@ -817,3 +823,14 @@ func ParseStringMap(request CallToolRequest, key string, defaultValue map[string
817823
func ToBoolPtr(b bool) *bool {
818824
return &b
819825
}
826+
827+
// GetDisplayName returns the best display name for a BaseMetadata object.
828+
// It follows the precedence: GetTitle() → GetName(), providing a consistent
829+
// way to get human-friendly names across all MCP object types.
830+
func GetDisplayName(meta BaseMetadata) string {
831+
if title := meta.GetTitle(); title != "" {
832+
return title
833+
}
834+
835+
return meta.GetName()
836+
}

0 commit comments

Comments
 (0)