diff --git a/docs/en/resources/tools/_index.md b/docs/en/resources/tools/_index.md index 43a72427f77..a1099779470 100644 --- a/docs/en/resources/tools/_index.md +++ b/docs/en/resources/tools/_index.md @@ -279,4 +279,57 @@ tools: - other-auth-service ``` +## Tool Annotations + +Tool annotations provide semantic metadata that helps MCP clients understand tool +behavior. These hints enable clients to make better decisions about tool usage +and provide appropriate user experiences. + +### Available Annotations + +| **annotation** | **type** | **default** | **description** | +|--------------------|:-----------:|:-----------:|------------------------------------------------------------------------| +| readOnlyHint | bool | false | Tool only reads data, no modifications to the environment. | +| destructiveHint | bool | true | Tool may create, update, or delete data. | +| idempotentHint | bool | false | Repeated calls with same arguments have no additional effect. | +| openWorldHint | bool | true | Tool interacts with external entities beyond its local environment. | + +### Specifying Annotations + +Annotations can be specified in YAML tool configuration: + +```yaml +tools: + my_query_tool: + kind: mongodb-find-one + source: my-mongodb + description: Find a single document + database: mydb + collection: users + annotations: + readOnlyHint: true + idempotentHint: true +``` + +### Default Annotations + +If not specified, tools use sensible defaults based on their operation type: + +- **Read operations** (find, aggregate, list): `readOnlyHint: true` +- **Write operations** (insert, update, delete): `destructiveHint: true`, `readOnlyHint: false` + +### MCP Client Response + +Annotations appear in the `tools/list` MCP response: + +```json +{ + "name": "my_query_tool", + "description": "Find a single document", + "annotations": { + "readOnlyHint": true + } +} +``` + ## Kinds of tools diff --git a/internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go b/internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go index 00bf5641aa4..2710ed6915f 100644 --- a/internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go +++ b/internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go @@ -49,17 +49,18 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - PipelinePayload string `yaml:"pipelinePayload" validate:"required"` - PipelineParams parameters.Parameters `yaml:"pipelineParams" validate:"required"` - Canonical bool `yaml:"canonical"` - ReadOnly bool `yaml:"readOnly"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + PipelinePayload string `yaml:"pipelinePayload" validate:"required"` + PipelineParams parameters.Parameters `yaml:"pipelineParams" validate:"required"` + Canonical bool `yaml:"canonical"` + ReadOnly bool `yaml:"readOnly"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -80,8 +81,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ diff --git a/internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.go b/internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.go index 266ac5e9823..d21fec427af 100644 --- a/internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.go +++ b/internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.go @@ -97,6 +97,94 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-aggregate + source: my-instance + description: test description + database: test_db + collection: test_coll + readOnly: true + pipelinePayload: | + [{ $match: { name: {{json .name}} }}] + pipelineParams: + - name: name + type: string + description: filter name + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || !*mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be true for read-only tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-aggregate + source: my-instance + description: test description + database: test_db + collection: test_coll + readOnly: true + pipelinePayload: | + [{ $match: { name: {{json .name}} }}] + pipelineParams: + - name: name + type: string + description: filter name + annotations: + readOnlyHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go b/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go index 29a2a334950..4ca404c5ab3 100644 --- a/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go +++ b/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go @@ -49,15 +49,16 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - FilterPayload string `yaml:"filterPayload" validate:"required"` - FilterParams parameters.Parameters `yaml:"filterParams"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + FilterPayload string `yaml:"filterPayload" validate:"required"` + FilterParams parameters.Parameters `yaml:"filterParams"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -84,8 +85,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ diff --git a/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany_test.go b/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany_test.go index e5861949cf9..264cfb909ae 100644 --- a/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany_test.go +++ b/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany_test.go @@ -95,6 +95,95 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-delete-many + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.DestructiveHint == nil || !*mcpManifest.Annotations.DestructiveHint { + t.Error("expected destructiveHint to be true for destructive tool") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || *mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be false for destructive tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-delete-many + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + annotations: + destructiveHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go b/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go index 2d761d83ede..f0197b71ecf 100644 --- a/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go +++ b/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go @@ -49,15 +49,16 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - FilterPayload string `yaml:"filterPayload" validate:"required"` - FilterParams parameters.Parameters `yaml:"filterParams"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + FilterPayload string `yaml:"filterPayload" validate:"required"` + FilterParams parameters.Parameters `yaml:"filterParams"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -84,8 +85,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ diff --git a/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone_test.go b/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone_test.go index 4aa99da297b..821eb7d3b71 100644 --- a/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone_test.go +++ b/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone_test.go @@ -95,6 +95,95 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-delete-one + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.DestructiveHint == nil || !*mcpManifest.Annotations.DestructiveHint { + t.Error("expected destructiveHint to be true for destructive tool") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || *mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be false for destructive tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-delete-one + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + annotations: + destructiveHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/mongodb/mongodbfind/mongodbfind.go b/internal/tools/mongodb/mongodbfind/mongodbfind.go index 12ccd846361..87344388f01 100644 --- a/internal/tools/mongodb/mongodbfind/mongodbfind.go +++ b/internal/tools/mongodb/mongodbfind/mongodbfind.go @@ -52,20 +52,21 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - FilterPayload string `yaml:"filterPayload" validate:"required"` - FilterParams parameters.Parameters `yaml:"filterParams"` - ProjectPayload string `yaml:"projectPayload"` - ProjectParams parameters.Parameters `yaml:"projectParams"` - SortPayload string `yaml:"sortPayload"` - SortParams parameters.Parameters `yaml:"sortParams"` - Limit int64 `yaml:"limit"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + FilterPayload string `yaml:"filterPayload" validate:"required"` + FilterParams parameters.Parameters `yaml:"filterParams"` + ProjectPayload string `yaml:"projectPayload"` + ProjectParams parameters.Parameters `yaml:"projectParams"` + SortPayload string `yaml:"sortPayload"` + SortParams parameters.Parameters `yaml:"sortParams"` + Limit int64 `yaml:"limit"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -96,8 +97,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ diff --git a/internal/tools/mongodb/mongodbfind/mongodbfind_test.go b/internal/tools/mongodb/mongodbfind/mongodbfind_test.go index 6b56d9c7c4f..6eaf31b0f0c 100644 --- a/internal/tools/mongodb/mongodbfind/mongodbfind_test.go +++ b/internal/tools/mongodb/mongodbfind/mongodbfind_test.go @@ -105,6 +105,94 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-find + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + limit: 10 + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || !*mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be true for read-only tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-find + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + limit: 10 + annotations: + readOnlyHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/mongodb/mongodbfindone/mongodbfindone.go b/internal/tools/mongodb/mongodbfindone/mongodbfindone.go index e9f1555c669..725d7fd6452 100644 --- a/internal/tools/mongodb/mongodbfindone/mongodbfindone.go +++ b/internal/tools/mongodb/mongodbfindone/mongodbfindone.go @@ -51,17 +51,18 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - FilterPayload string `yaml:"filterPayload" validate:"required"` - FilterParams parameters.Parameters `yaml:"filterParams"` - ProjectPayload string `yaml:"projectPayload"` - ProjectParams parameters.Parameters `yaml:"projectParams"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + FilterPayload string `yaml:"filterPayload" validate:"required"` + FilterParams parameters.Parameters `yaml:"filterParams"` + ProjectPayload string `yaml:"projectPayload"` + ProjectParams parameters.Parameters `yaml:"projectParams"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -88,8 +89,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ diff --git a/internal/tools/mongodb/mongodbfindone/mongodbfindone_test.go b/internal/tools/mongodb/mongodbfindone/mongodbfindone_test.go index a8d5b9bfc05..7bdfcb843b1 100644 --- a/internal/tools/mongodb/mongodbfindone/mongodbfindone_test.go +++ b/internal/tools/mongodb/mongodbfindone/mongodbfindone_test.go @@ -100,6 +100,92 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-find-one + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || !*mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be true for read-only tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-find-one + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + annotations: + readOnlyHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany.go b/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany.go index 0dc11a230b6..99f55e32c8d 100644 --- a/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany.go +++ b/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany.go @@ -50,14 +50,15 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - Canonical bool `yaml:"canonical"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + Canonical bool `yaml:"canonical"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -79,8 +80,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ Config: cfg, diff --git a/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany_test.go b/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany_test.go index 19ac3ce0c11..67e7e3b0c6c 100644 --- a/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany_test.go +++ b/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany_test.go @@ -129,6 +129,83 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-insert-many + source: my-instance + description: test description + database: test_db + collection: test_coll + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.DestructiveHint == nil || !*mcpManifest.Annotations.DestructiveHint { + t.Error("expected destructiveHint to be true for destructive tool") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || *mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be false for destructive tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-insert-many + source: my-instance + description: test description + database: test_db + collection: test_coll + annotations: + destructiveHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/mongodb/mongodbinsertone/mongodbinsertone.go b/internal/tools/mongodb/mongodbinsertone/mongodbinsertone.go index a93589c053b..00f004aa52d 100644 --- a/internal/tools/mongodb/mongodbinsertone/mongodbinsertone.go +++ b/internal/tools/mongodb/mongodbinsertone/mongodbinsertone.go @@ -50,14 +50,15 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - Canonical bool `yaml:"canonical"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + Canonical bool `yaml:"canonical"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -79,8 +80,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ diff --git a/internal/tools/mongodb/mongodbinsertone/mongodbinsertone_test.go b/internal/tools/mongodb/mongodbinsertone/mongodbinsertone_test.go index a61dde20b0b..d4c174feb9e 100644 --- a/internal/tools/mongodb/mongodbinsertone/mongodbinsertone_test.go +++ b/internal/tools/mongodb/mongodbinsertone/mongodbinsertone_test.go @@ -129,6 +129,83 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-insert-one + source: my-instance + description: test description + database: test_db + collection: test_coll + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.DestructiveHint == nil || !*mcpManifest.Annotations.DestructiveHint { + t.Error("expected destructiveHint to be true for destructive tool") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || *mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be false for destructive tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-insert-one + source: my-instance + description: test description + database: test_db + collection: test_coll + annotations: + destructiveHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany.go b/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany.go index b80bc4972e4..f8c00ba76f8 100644 --- a/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany.go +++ b/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany.go @@ -48,19 +48,20 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - FilterPayload string `yaml:"filterPayload" validate:"required"` - FilterParams parameters.Parameters `yaml:"filterParams"` - UpdatePayload string `yaml:"updatePayload" validate:"required"` - UpdateParams parameters.Parameters `yaml:"updateParams" validate:"required"` - Canonical bool `yaml:"canonical"` - Upsert bool `yaml:"upsert"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + FilterPayload string `yaml:"filterPayload" validate:"required"` + FilterParams parameters.Parameters `yaml:"filterParams"` + UpdatePayload string `yaml:"updatePayload" validate:"required"` + UpdateParams parameters.Parameters `yaml:"updateParams" validate:"required"` + Canonical bool `yaml:"canonical"` + Upsert bool `yaml:"upsert"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -87,8 +88,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ diff --git a/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany_test.go b/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany_test.go index b7353afe08f..a254ff40f18 100644 --- a/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany_test.go +++ b/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany_test.go @@ -226,6 +226,107 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-update-many + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .filter_name}} } + filterParams: + - name: filter_name + type: string + description: filter name + updatePayload: | + { $set: { name: {{json .new_name}} } } + updateParams: + - name: new_name + type: string + description: name to set + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.DestructiveHint == nil || !*mcpManifest.Annotations.DestructiveHint { + t.Error("expected destructiveHint to be true for destructive tool") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || *mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be false for destructive tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-update-many + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .filter_name}} } + filterParams: + - name: filter_name + type: string + description: filter name + updatePayload: | + { $set: { name: {{json .new_name}} } } + updateParams: + - name: new_name + type: string + description: name to set + annotations: + destructiveHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/mongodb/mongodbupdateone/mongodbupdateone.go b/internal/tools/mongodb/mongodbupdateone/mongodbupdateone.go index d3236992e84..b0a811985a0 100644 --- a/internal/tools/mongodb/mongodbupdateone/mongodbupdateone.go +++ b/internal/tools/mongodb/mongodbupdateone/mongodbupdateone.go @@ -48,20 +48,20 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Kind string `yaml:"kind" validate:"required"` - Source string `yaml:"source" validate:"required"` - AuthRequired []string `yaml:"authRequired" validate:"required"` - Description string `yaml:"description" validate:"required"` - Database string `yaml:"database" validate:"required"` - Collection string `yaml:"collection" validate:"required"` - FilterPayload string `yaml:"filterPayload" validate:"required"` - FilterParams parameters.Parameters `yaml:"filterParams"` - UpdatePayload string `yaml:"updatePayload" validate:"required"` - UpdateParams parameters.Parameters `yaml:"updateParams" validate:"required"` - - Canonical bool `yaml:"canonical"` - Upsert bool `yaml:"upsert"` + Name string `yaml:"name" validate:"required"` + Kind string `yaml:"kind" validate:"required"` + Source string `yaml:"source" validate:"required"` + AuthRequired []string `yaml:"authRequired" validate:"required"` + Description string `yaml:"description" validate:"required"` + Database string `yaml:"database" validate:"required"` + Collection string `yaml:"collection" validate:"required"` + FilterPayload string `yaml:"filterPayload" validate:"required"` + FilterParams parameters.Parameters `yaml:"filterParams"` + UpdatePayload string `yaml:"updatePayload" validate:"required"` + UpdateParams parameters.Parameters `yaml:"updateParams" validate:"required"` + Canonical bool `yaml:"canonical"` + Upsert bool `yaml:"upsert"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -88,8 +88,11 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } + // Add default annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations) // finish tool setup return Tool{ diff --git a/internal/tools/mongodb/mongodbupdateone/mongodbupdateone_test.go b/internal/tools/mongodb/mongodbupdateone/mongodbupdateone_test.go index b450892fb2d..0d544abf1dd 100644 --- a/internal/tools/mongodb/mongodbupdateone/mongodbupdateone_test.go +++ b/internal/tools/mongodb/mongodbupdateone/mongodbupdateone_test.go @@ -232,6 +232,107 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + ctx, err := testutils.ContextWithNewLogger() + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + + t.Run("default annotations", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-update-one + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + updatePayload: | + { $set : { item: {{json .item}} } } + updateParams: + - name: item + type: string + description: item to set + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.DestructiveHint == nil || !*mcpManifest.Annotations.DestructiveHint { + t.Error("expected destructiveHint to be true for destructive tool") + } + if mcpManifest.Annotations.ReadOnlyHint == nil || *mcpManifest.Annotations.ReadOnlyHint { + t.Error("expected readOnlyHint to be false for destructive tool") + } + }) + + t.Run("custom annotations from YAML", func(t *testing.T) { + in := ` + tools: + test_tool: + kind: mongodb-update-one + source: my-instance + description: test description + database: test_db + collection: test_coll + filterPayload: | + { name: {{json .name}} } + filterParams: + - name: name + type: string + description: filter name + updatePayload: | + { $set : { item: {{json .item}} } } + updateParams: + - name: item + type: string + description: item to set + annotations: + destructiveHint: true + idempotentHint: true + ` + got := struct { + Tools server.ToolConfigs `yaml:"tools"` + }{} + err := yaml.UnmarshalContext(ctx, testutils.FormatYaml(in), &got) + if err != nil { + t.Fatalf("unable to unmarshal: %s", err) + } + + tool, err := got.Tools["test_tool"].Initialize(nil) + if err != nil { + t.Fatalf("unable to initialize: %s", err) + } + + mcpManifest := tool.McpManifest() + if mcpManifest.Annotations == nil { + t.Fatal("expected annotations to be set") + } + if mcpManifest.Annotations.IdempotentHint == nil || !*mcpManifest.Annotations.IdempotentHint { + t.Error("expected idempotentHint from YAML to be applied") + } + }) +} + func TestFailParseFromYamlMongoQuery(t *testing.T) { ctx, err := testutils.ContextWithNewLogger() if err != nil { diff --git a/internal/tools/tools.go b/internal/tools/tools.go index daadf385b72..2183b1cd698 100644 --- a/internal/tools/tools.go +++ b/internal/tools/tools.go @@ -75,6 +75,33 @@ type ToolAnnotations struct { ReadOnlyHint *bool `json:"readOnlyHint,omitempty" yaml:"readOnlyHint,omitempty"` } +// NewReadOnlyAnnotations creates default annotations for a read-only tool. +// Use this for tools that only query/fetch data without side effects. +func NewReadOnlyAnnotations() *ToolAnnotations { + readOnly := true + return &ToolAnnotations{ReadOnlyHint: &readOnly} +} + +// NewDestructiveAnnotations creates default annotations for a destructive tool. +// Use this for tools that create, update, or delete data. +func NewDestructiveAnnotations() *ToolAnnotations { + readOnly := false + destructive := true + return &ToolAnnotations{ + ReadOnlyHint: &readOnly, + DestructiveHint: &destructive, + } +} + +// GetAnnotationsOrDefault returns the provided annotations if non-nil, +// otherwise returns the result of calling defaultFn. +func GetAnnotationsOrDefault(annotations *ToolAnnotations, defaultFn func() *ToolAnnotations) *ToolAnnotations { + if annotations != nil { + return annotations + } + return defaultFn() +} + type AccessToken string func (token AccessToken) ParseBearerToken() (string, error) {