diff --git a/docs/en/resources/tools/_index.md b/docs/en/resources/tools/_index.md index 753997a681ab..19c7096fc68f 100644 --- a/docs/en/resources/tools/_index.md +++ b/docs/en/resources/tools/_index.md @@ -312,4 +312,57 @@ authRequired: - 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 519e79b69622..db657897e988 100644 --- a/internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go +++ b/internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go @@ -51,17 +51,18 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" 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"` + Type string `yaml:"type" 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 @@ -83,7 +84,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) } // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations) + 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 a9adf7f272c8..bfaf54d57c16 100644 --- a/internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.go +++ b/internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbaggregate" "github.com/googleapis/genai-toolbox/internal/util/parameters" @@ -93,6 +94,29 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for read-only tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewReadOnlyAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != true { + t.Error("expected readOnlyHint to be true") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customReadOnly := false + custom := &tools.ToolAnnotations{ReadOnlyHint: &customReadOnly} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewReadOnlyAnnotations) + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected custom readOnlyHint to be false") + } + }) +} + 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 8f67aec41f45..046433df307d 100644 --- a/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go +++ b/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go @@ -51,15 +51,16 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" 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"` + Type string `yaml:"type" 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 @@ -87,7 +88,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) } // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + 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 af9ee4933e4d..aa7eb10e8deb 100644 --- a/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany_test.go +++ b/internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbdeletemany" "github.com/googleapis/genai-toolbox/internal/util/parameters" @@ -90,6 +91,32 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for destructive tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewDestructiveAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != true { + t.Error("expected destructiveHint to be true") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected readOnlyHint to be false") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customDestructive := false + custom := &tools.ToolAnnotations{DestructiveHint: &customDestructive} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewDestructiveAnnotations) + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != false { + t.Error("expected custom destructiveHint to be false") + } + }) +} + 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 55f855c7e030..7d06e8923adf 100644 --- a/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go +++ b/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go @@ -51,15 +51,16 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" 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"` + Type string `yaml:"type" 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 @@ -87,7 +88,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) } // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + 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 3f395c631593..4dd97ff605e0 100644 --- a/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone_test.go +++ b/internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbdeleteone" "github.com/googleapis/genai-toolbox/internal/util/parameters" @@ -90,6 +91,32 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for destructive tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewDestructiveAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != true { + t.Error("expected destructiveHint to be true") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected readOnlyHint to be false") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customDestructive := false + custom := &tools.ToolAnnotations{DestructiveHint: &customDestructive} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewDestructiveAnnotations) + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != false { + t.Error("expected custom destructiveHint to be false") + } + }) +} + 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 9da5a8b30e2d..5e5eb06b04b2 100644 --- a/internal/tools/mongodb/mongodbfind/mongodbfind.go +++ b/internal/tools/mongodb/mongodbfind/mongodbfind.go @@ -53,20 +53,21 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" 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"` + Type string `yaml:"type" 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 @@ -97,8 +98,9 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } - // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + // Create MCP manifest with annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations) + 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 dcc25914148b..2caf9282b7c8 100644 --- a/internal/tools/mongodb/mongodbfind/mongodbfind_test.go +++ b/internal/tools/mongodb/mongodbfind/mongodbfind_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbfind" "github.com/googleapis/genai-toolbox/internal/util/parameters" @@ -100,6 +101,29 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for read-only tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewReadOnlyAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != true { + t.Error("expected readOnlyHint to be true") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customReadOnly := false + custom := &tools.ToolAnnotations{ReadOnlyHint: &customReadOnly} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewReadOnlyAnnotations) + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected custom readOnlyHint to be false") + } + }) +} + 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 f75af4328a21..4fefd71f4799 100644 --- a/internal/tools/mongodb/mongodbfindone/mongodbfindone.go +++ b/internal/tools/mongodb/mongodbfindone/mongodbfindone.go @@ -53,17 +53,18 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" 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"` + Type string `yaml:"type" 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 @@ -90,8 +91,9 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) paramManifest = make([]parameters.ParameterManifest, 0) } - // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + // Create MCP manifest with annotations + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations) + 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 ce23e01e11b3..cb4c19702a8b 100644 --- a/internal/tools/mongodb/mongodbfindone/mongodbfindone_test.go +++ b/internal/tools/mongodb/mongodbfindone/mongodbfindone_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbfindone" "github.com/googleapis/genai-toolbox/internal/util/parameters" @@ -95,6 +96,29 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for read-only tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewReadOnlyAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != true { + t.Error("expected readOnlyHint to be true") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customReadOnly := false + custom := &tools.ToolAnnotations{ReadOnlyHint: &customReadOnly} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewReadOnlyAnnotations) + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected custom readOnlyHint to be false") + } + }) +} + 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 0de1cc8de413..82d06c6b7fe9 100644 --- a/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany.go +++ b/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany.go @@ -51,14 +51,15 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" 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"` + Type string `yaml:"type" 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 @@ -81,7 +82,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) } // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + 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 e965e056d9c6..824cc3c7922b 100644 --- a/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany_test.go +++ b/internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbinsertmany" "github.com/google/go-cmp/cmp" @@ -124,6 +125,32 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for destructive tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewDestructiveAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != true { + t.Error("expected destructiveHint to be true") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected readOnlyHint to be false") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customDestructive := false + custom := &tools.ToolAnnotations{DestructiveHint: &customDestructive} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewDestructiveAnnotations) + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != false { + t.Error("expected custom destructiveHint to be false") + } + }) +} + 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 3fc9260a512f..b707d9248b37 100644 --- a/internal/tools/mongodb/mongodbinsertone/mongodbinsertone.go +++ b/internal/tools/mongodb/mongodbinsertone/mongodbinsertone.go @@ -51,14 +51,15 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" 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"` + Type string `yaml:"type" 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 @@ -81,7 +82,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) } // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + 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 ace38f2d78a1..84b420b1cbd0 100644 --- a/internal/tools/mongodb/mongodbinsertone/mongodbinsertone_test.go +++ b/internal/tools/mongodb/mongodbinsertone/mongodbinsertone_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbinsertone" "github.com/google/go-cmp/cmp" @@ -124,6 +125,32 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for destructive tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewDestructiveAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != true { + t.Error("expected destructiveHint to be true") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected readOnlyHint to be false") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customDestructive := false + custom := &tools.ToolAnnotations{DestructiveHint: &customDestructive} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewDestructiveAnnotations) + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != false { + t.Error("expected custom destructiveHint to be false") + } + }) +} + 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 7e34e5238441..8bdb42f08065 100644 --- a/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany.go +++ b/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany.go @@ -50,19 +50,20 @@ type compatibleSource interface { } type Config struct { - Name string `yaml:"name" validate:"required"` - Type string `yaml:"type" 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"` + Type string `yaml:"type" 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 @@ -90,7 +91,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) } // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + 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 f3fcf468d560..bd01672dbc44 100644 --- a/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany_test.go +++ b/internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbupdatemany" "github.com/googleapis/genai-toolbox/internal/util/parameters" @@ -221,6 +222,32 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for destructive tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewDestructiveAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != true { + t.Error("expected destructiveHint to be true") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected readOnlyHint to be false") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customDestructive := false + custom := &tools.ToolAnnotations{DestructiveHint: &customDestructive} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewDestructiveAnnotations) + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != false { + t.Error("expected custom destructiveHint to be false") + } + }) +} + 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 6369e08a911a..c89a1420689d 100644 --- a/internal/tools/mongodb/mongodbupdateone/mongodbupdateone.go +++ b/internal/tools/mongodb/mongodbupdateone/mongodbupdateone.go @@ -62,8 +62,9 @@ type Config struct { UpdatePayload string `yaml:"updatePayload" validate:"required"` UpdateParams parameters.Parameters `yaml:"updateParams" validate:"required"` - Canonical bool `yaml:"canonical"` - Upsert bool `yaml:"upsert"` + Canonical bool `yaml:"canonical"` + Upsert bool `yaml:"upsert"` + Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"` } // validate interface @@ -91,7 +92,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error) } // Create MCP manifest - mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil) + annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations) + 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 87202485f7b0..380acf759ba3 100644 --- a/internal/tools/mongodb/mongodbupdateone/mongodbupdateone_test.go +++ b/internal/tools/mongodb/mongodbupdateone/mongodbupdateone_test.go @@ -18,6 +18,7 @@ import ( "strings" "testing" + "github.com/googleapis/genai-toolbox/internal/tools" "github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbupdateone" "github.com/googleapis/genai-toolbox/internal/util/parameters" @@ -227,6 +228,32 @@ func TestParseFromYamlMongoQuery(t *testing.T) { } +func TestAnnotations(t *testing.T) { + // Test default annotations for destructive tool + t.Run("default annotations", func(t *testing.T) { + annotations := tools.GetAnnotationsOrDefault(nil, tools.NewDestructiveAnnotations) + if annotations == nil { + t.Fatal("expected non-nil annotations") + } + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != true { + t.Error("expected destructiveHint to be true") + } + if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false { + t.Error("expected readOnlyHint to be false") + } + }) + + // Test custom annotations override default + t.Run("custom annotations", func(t *testing.T) { + customDestructive := false + custom := &tools.ToolAnnotations{DestructiveHint: &customDestructive} + annotations := tools.GetAnnotationsOrDefault(custom, tools.NewDestructiveAnnotations) + if annotations.DestructiveHint == nil || *annotations.DestructiveHint != false { + t.Error("expected custom destructiveHint to be false") + } + }) +} + 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 93f2654c854e..82428a17800d 100644 --- a/internal/tools/tools.go +++ b/internal/tools/tools.go @@ -76,6 +76,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) {