diff --git a/mcp/tools.go b/mcp/tools.go index 31f375f7e..3ab8f6ed4 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -643,7 +643,9 @@ func (t Tool) MarshalJSON() ([]byte, error) { m["outputSchema"] = t.OutputSchema } - m["annotations"] = t.Annotations + if t.Annotations.HasAny() { + m["annotations"] = t.Annotations + } if t.DeferLoading { m["defer_loading"] = t.DeferLoading @@ -777,6 +779,15 @@ type ToolAnnotation struct { OpenWorldHint *bool `json:"openWorldHint,omitempty"` } +// HasAny reports whether any annotation field was explicitly set. +func (a ToolAnnotation) HasAny() bool { + return a.Title != "" || + a.ReadOnlyHint != nil || + a.DestructiveHint != nil || + a.IdempotentHint != nil || + a.OpenWorldHint != nil +} + // ToolOption is a function that configures a Tool. // It provides a flexible way to set various properties of a Tool using the functional options pattern. type ToolOption func(*Tool) @@ -944,6 +955,15 @@ func WithRawOutputSchema(schema json.RawMessage) ToolOption { } } +// WithoutDefaultAnnotations clears the default annotation hints initialized by +// NewTool so the annotations field is omitted from JSON unless later options +// set annotation values explicitly. +func WithoutDefaultAnnotations() ToolOption { + return func(t *Tool) { + t.Annotations = ToolAnnotation{} + } +} + // WithToolAnnotation adds optional hints about the Tool. func WithToolAnnotation(annotation ToolAnnotation) ToolOption { return func(t *Tool) { diff --git a/mcp/tools_additional_test.go b/mcp/tools_additional_test.go index 4ce723d9e..d84f314bc 100644 --- a/mcp/tools_additional_test.go +++ b/mcp/tools_additional_test.go @@ -294,6 +294,96 @@ func TestToolAnnotations(t *testing.T) { }) } +func TestToolAnnotationsMarshalJSON(t *testing.T) { + rawSchema := json.RawMessage(`{"type":"object","properties":{}}`) + + tests := []struct { + name string + tool Tool + wantAnnotations bool + wantContains string + wantNotContains string + checkAnnotations func(t *testing.T, annotations map[string]any) + }{ + { + name: "NewTool includes default annotations", + tool: NewTool("test", WithDescription("desc")), + wantAnnotations: true, + checkAnnotations: func(t *testing.T, annotations map[string]any) { + t.Helper() + assert.Equal(t, false, annotations["readOnlyHint"]) + assert.Equal(t, true, annotations["destructiveHint"]) + }, + }, + { + name: "WithoutDefaultAnnotations omits annotations field", + tool: NewTool("test", + WithoutDefaultAnnotations(), + WithDescription("desc"), + ), + wantAnnotations: false, + wantNotContains: `"annotations"`, + }, + { + name: "WithoutDefaultAnnotations with explicit hint includes annotations", + tool: NewTool("test", + WithoutDefaultAnnotations(), + WithReadOnlyHintAnnotation(true), + ), + wantAnnotations: true, + checkAnnotations: func(t *testing.T, annotations map[string]any) { + t.Helper() + assert.Equal(t, true, annotations["readOnlyHint"]) + assert.NotContains(t, annotations, "destructiveHint") + }, + }, + { + name: "NewToolWithRawSchema omits annotations when unset", + tool: NewToolWithRawSchema("raw-tool", "Raw tool", rawSchema), + wantAnnotations: false, + wantNotContains: `"annotations"`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := json.Marshal(tt.tool) + require.NoError(t, err) + + if tt.wantContains != "" { + assert.Contains(t, string(data), tt.wantContains) + } + if tt.wantNotContains != "" { + assert.NotContains(t, string(data), tt.wantNotContains) + } + + var parsed map[string]any + require.NoError(t, json.Unmarshal(data, &parsed)) + + annotations, ok := parsed["annotations"].(map[string]any) + if tt.wantAnnotations { + require.True(t, ok) + if tt.checkAnnotations != nil { + tt.checkAnnotations(t, annotations) + } + return + } + + _, hasAnnotations := parsed["annotations"] + assert.False(t, hasAnnotations) + }) + } + + t.Run("zero-value ToolAnnotation HasAny is false", func(t *testing.T) { + assert.False(t, ToolAnnotation{}.HasAny()) + }) + + t.Run("ToolAnnotation HasAny detects explicit fields", func(t *testing.T) { + assert.True(t, ToolAnnotation{Title: "x"}.HasAny()) + assert.True(t, ToolAnnotation{ReadOnlyHint: ToBoolPtr(false)}.HasAny()) + }) +} + // Test Tool with both InputSchema and OutputSchema func TestToolWithBothSchemas(t *testing.T) {