Skip to content
Merged
53 changes: 53 additions & 0 deletions docs/en/resources/tools/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
26 changes: 14 additions & 12 deletions internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 12 additions & 10 deletions internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 {
Expand Down
22 changes: 12 additions & 10 deletions internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 {
Expand Down
34 changes: 18 additions & 16 deletions internal/tools/mongodb/mongodbfind/mongodbfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{
Expand Down
24 changes: 24 additions & 0 deletions internal/tools/mongodb/mongodbfind/mongodbfind_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading