Skip to content

Commit b7a5f80

Browse files
feat: Add tool annotations to MongoDB tools for improved LLM understanding (#2219)
## Summary Adds MCP tool annotations (`readOnlyHint`, `destructiveHint`) to all 9 MongoDB tools to help LLMs better understand tool behavior and make safer decisions. ## Changes | Tool | Annotation | |------|------------| | mongodb-find | `readOnlyHint: true` | | mongodb-find-one | `readOnlyHint: true` | | mongodb-aggregate | `readOnlyHint: true` | | mongodb-insert-one | `destructiveHint: true` | | mongodb-insert-many | `destructiveHint: true` | | mongodb-update-one | `destructiveHint: true` | | mongodb-update-many | `destructiveHint: true` | | mongodb-delete-one | `destructiveHint: true` | | mongodb-delete-many | `destructiveHint: true` | ## Implementation Each tool now: 1. Has an `Annotations` field in its Config struct for YAML configurability 2. Provides default annotations if not explicitly configured 3. Passes annotations to `GetMcpManifest()` instead of `nil` This follows the exact pattern established by the Looker tools (e.g., `lookergetconnectionschemas`, `lookerupdateprojectfile`). ## Why This Matters - **Semantic metadata**: Annotations provide information beyond just the tool description - **Safety signals**: `readOnlyHint` tells LLMs a tool is safe to call without side effects - **Destructive awareness**: `destructiveHint` signals LLMs should be more careful before executing - **Better tool selection**: LLMs can prioritize read-only tools for information gathering - **MCP compliance**: Follows the [MCP tool annotations specification](https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations) ## Testing - [ ] CI builds successfully - [ ] `tools/list` returns annotations in MCP response ## Files Changed - `internal/tools/mongodb/mongodbfind/mongodbfind.go` - `internal/tools/mongodb/mongodbfindone/mongodbfindone.go` - `internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go` - `internal/tools/mongodb/mongodbinsertone/mongodbinsertone.go` - `internal/tools/mongodb/mongodbinsertmany/mongodbinsertmany.go` - `internal/tools/mongodb/mongodbupdateone/mongodbupdateone.go` - `internal/tools/mongodb/mongodbupdatemany/mongodbupdatemany.go` - `internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go` - `internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: triepod-ai <199543909+triepod-ai@users.noreply.github.com> Co-authored-by: bryankthompson <199543909+bryankthompson@users.noreply.github.com> Co-authored-by: Wenxin Du <117315983+duwenxin99@users.noreply.github.com>
1 parent a554298 commit b7a5f80

20 files changed

Lines changed: 428 additions & 96 deletions

File tree

docs/en/resources/tools/_index.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,4 +312,57 @@ authRequired:
312312
- other-auth-service
313313
```
314314
315+
## Tool Annotations
316+
317+
Tool annotations provide semantic metadata that helps MCP clients understand tool
318+
behavior. These hints enable clients to make better decisions about tool usage
319+
and provide appropriate user experiences.
320+
321+
### Available Annotations
322+
323+
| **annotation** | **type** | **default** | **description** |
324+
|--------------------|:-----------:|:-----------:|------------------------------------------------------------------------|
325+
| readOnlyHint | bool | false | Tool only reads data, no modifications to the environment. |
326+
| destructiveHint | bool | true | Tool may create, update, or delete data. |
327+
| idempotentHint | bool | false | Repeated calls with same arguments have no additional effect. |
328+
| openWorldHint | bool | true | Tool interacts with external entities beyond its local environment. |
329+
330+
### Specifying Annotations
331+
332+
Annotations can be specified in YAML tool configuration:
333+
334+
```yaml
335+
tools:
336+
my_query_tool:
337+
kind: mongodb-find-one
338+
source: my-mongodb
339+
description: Find a single document
340+
database: mydb
341+
collection: users
342+
annotations:
343+
readOnlyHint: true
344+
idempotentHint: true
345+
```
346+
347+
### Default Annotations
348+
349+
If not specified, tools use sensible defaults based on their operation type:
350+
351+
- **Read operations** (find, aggregate, list): `readOnlyHint: true`
352+
- **Write operations** (insert, update, delete): `destructiveHint: true`, `readOnlyHint: false`
353+
354+
### MCP Client Response
355+
356+
Annotations appear in the `tools/list` MCP response:
357+
358+
```json
359+
{
360+
"name": "my_query_tool",
361+
"description": "Find a single document",
362+
"annotations": {
363+
"readOnlyHint": true
364+
}
365+
}
366+
```
367+
315368
## Kinds of tools

internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,17 +51,18 @@ type compatibleSource interface {
5151
}
5252

5353
type Config struct {
54-
Name string `yaml:"name" validate:"required"`
55-
Type string `yaml:"type" validate:"required"`
56-
Source string `yaml:"source" validate:"required"`
57-
AuthRequired []string `yaml:"authRequired" validate:"required"`
58-
Description string `yaml:"description" validate:"required"`
59-
Database string `yaml:"database" validate:"required"`
60-
Collection string `yaml:"collection" validate:"required"`
61-
PipelinePayload string `yaml:"pipelinePayload" validate:"required"`
62-
PipelineParams parameters.Parameters `yaml:"pipelineParams" validate:"required"`
63-
Canonical bool `yaml:"canonical"`
64-
ReadOnly bool `yaml:"readOnly"`
54+
Name string `yaml:"name" validate:"required"`
55+
Type string `yaml:"type" validate:"required"`
56+
Source string `yaml:"source" validate:"required"`
57+
AuthRequired []string `yaml:"authRequired" validate:"required"`
58+
Description string `yaml:"description" validate:"required"`
59+
Database string `yaml:"database" validate:"required"`
60+
Collection string `yaml:"collection" validate:"required"`
61+
PipelinePayload string `yaml:"pipelinePayload" validate:"required"`
62+
PipelineParams parameters.Parameters `yaml:"pipelineParams" validate:"required"`
63+
Canonical bool `yaml:"canonical"`
64+
ReadOnly bool `yaml:"readOnly"`
65+
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
6566
}
6667

6768
// validate interface
@@ -83,7 +84,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
8384
}
8485

8586
// Create MCP manifest
86-
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil)
87+
annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations)
88+
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations)
8789

8890
// finish tool setup
8991
return Tool{

internal/tools/mongodb/mongodbaggregate/mongodbaggregate_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"strings"
1919
"testing"
2020

21+
"github.com/googleapis/genai-toolbox/internal/tools"
2122
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbaggregate"
2223
"github.com/googleapis/genai-toolbox/internal/util/parameters"
2324

@@ -93,6 +94,29 @@ func TestParseFromYamlMongoQuery(t *testing.T) {
9394

9495
}
9596

97+
func TestAnnotations(t *testing.T) {
98+
// Test default annotations for read-only tool
99+
t.Run("default annotations", func(t *testing.T) {
100+
annotations := tools.GetAnnotationsOrDefault(nil, tools.NewReadOnlyAnnotations)
101+
if annotations == nil {
102+
t.Fatal("expected non-nil annotations")
103+
}
104+
if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != true {
105+
t.Error("expected readOnlyHint to be true")
106+
}
107+
})
108+
109+
// Test custom annotations override default
110+
t.Run("custom annotations", func(t *testing.T) {
111+
customReadOnly := false
112+
custom := &tools.ToolAnnotations{ReadOnlyHint: &customReadOnly}
113+
annotations := tools.GetAnnotationsOrDefault(custom, tools.NewReadOnlyAnnotations)
114+
if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false {
115+
t.Error("expected custom readOnlyHint to be false")
116+
}
117+
})
118+
}
119+
96120
func TestFailParseFromYamlMongoQuery(t *testing.T) {
97121
ctx, err := testutils.ContextWithNewLogger()
98122
if err != nil {

internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,16 @@ type compatibleSource interface {
5151
}
5252

5353
type Config struct {
54-
Name string `yaml:"name" validate:"required"`
55-
Type string `yaml:"type" validate:"required"`
56-
Source string `yaml:"source" validate:"required"`
57-
AuthRequired []string `yaml:"authRequired" validate:"required"`
58-
Description string `yaml:"description" validate:"required"`
59-
Database string `yaml:"database" validate:"required"`
60-
Collection string `yaml:"collection" validate:"required"`
61-
FilterPayload string `yaml:"filterPayload" validate:"required"`
62-
FilterParams parameters.Parameters `yaml:"filterParams"`
54+
Name string `yaml:"name" validate:"required"`
55+
Type string `yaml:"type" validate:"required"`
56+
Source string `yaml:"source" validate:"required"`
57+
AuthRequired []string `yaml:"authRequired" validate:"required"`
58+
Description string `yaml:"description" validate:"required"`
59+
Database string `yaml:"database" validate:"required"`
60+
Collection string `yaml:"collection" validate:"required"`
61+
FilterPayload string `yaml:"filterPayload" validate:"required"`
62+
FilterParams parameters.Parameters `yaml:"filterParams"`
63+
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
6364
}
6465

6566
// validate interface
@@ -87,7 +88,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
8788
}
8889

8990
// Create MCP manifest
90-
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil)
91+
annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations)
92+
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations)
9193

9294
// finish tool setup
9395
return Tool{

internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"strings"
1919
"testing"
2020

21+
"github.com/googleapis/genai-toolbox/internal/tools"
2122
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbdeletemany"
2223
"github.com/googleapis/genai-toolbox/internal/util/parameters"
2324

@@ -90,6 +91,32 @@ func TestParseFromYamlMongoQuery(t *testing.T) {
9091

9192
}
9293

94+
func TestAnnotations(t *testing.T) {
95+
// Test default annotations for destructive tool
96+
t.Run("default annotations", func(t *testing.T) {
97+
annotations := tools.GetAnnotationsOrDefault(nil, tools.NewDestructiveAnnotations)
98+
if annotations == nil {
99+
t.Fatal("expected non-nil annotations")
100+
}
101+
if annotations.DestructiveHint == nil || *annotations.DestructiveHint != true {
102+
t.Error("expected destructiveHint to be true")
103+
}
104+
if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false {
105+
t.Error("expected readOnlyHint to be false")
106+
}
107+
})
108+
109+
// Test custom annotations override default
110+
t.Run("custom annotations", func(t *testing.T) {
111+
customDestructive := false
112+
custom := &tools.ToolAnnotations{DestructiveHint: &customDestructive}
113+
annotations := tools.GetAnnotationsOrDefault(custom, tools.NewDestructiveAnnotations)
114+
if annotations.DestructiveHint == nil || *annotations.DestructiveHint != false {
115+
t.Error("expected custom destructiveHint to be false")
116+
}
117+
})
118+
}
119+
93120
func TestFailParseFromYamlMongoQuery(t *testing.T) {
94121
ctx, err := testutils.ContextWithNewLogger()
95122
if err != nil {

internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,16 @@ type compatibleSource interface {
5151
}
5252

5353
type Config struct {
54-
Name string `yaml:"name" validate:"required"`
55-
Type string `yaml:"type" validate:"required"`
56-
Source string `yaml:"source" validate:"required"`
57-
AuthRequired []string `yaml:"authRequired" validate:"required"`
58-
Description string `yaml:"description" validate:"required"`
59-
Database string `yaml:"database" validate:"required"`
60-
Collection string `yaml:"collection" validate:"required"`
61-
FilterPayload string `yaml:"filterPayload" validate:"required"`
62-
FilterParams parameters.Parameters `yaml:"filterParams"`
54+
Name string `yaml:"name" validate:"required"`
55+
Type string `yaml:"type" validate:"required"`
56+
Source string `yaml:"source" validate:"required"`
57+
AuthRequired []string `yaml:"authRequired" validate:"required"`
58+
Description string `yaml:"description" validate:"required"`
59+
Database string `yaml:"database" validate:"required"`
60+
Collection string `yaml:"collection" validate:"required"`
61+
FilterPayload string `yaml:"filterPayload" validate:"required"`
62+
FilterParams parameters.Parameters `yaml:"filterParams"`
63+
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
6364
}
6465

6566
// validate interface
@@ -87,7 +88,8 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
8788
}
8889

8990
// Create MCP manifest
90-
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil)
91+
annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewDestructiveAnnotations)
92+
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations)
9193

9294
// finish tool setup
9395
return Tool{

internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"strings"
1919
"testing"
2020

21+
"github.com/googleapis/genai-toolbox/internal/tools"
2122
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbdeleteone"
2223
"github.com/googleapis/genai-toolbox/internal/util/parameters"
2324

@@ -90,6 +91,32 @@ func TestParseFromYamlMongoQuery(t *testing.T) {
9091

9192
}
9293

94+
func TestAnnotations(t *testing.T) {
95+
// Test default annotations for destructive tool
96+
t.Run("default annotations", func(t *testing.T) {
97+
annotations := tools.GetAnnotationsOrDefault(nil, tools.NewDestructiveAnnotations)
98+
if annotations == nil {
99+
t.Fatal("expected non-nil annotations")
100+
}
101+
if annotations.DestructiveHint == nil || *annotations.DestructiveHint != true {
102+
t.Error("expected destructiveHint to be true")
103+
}
104+
if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false {
105+
t.Error("expected readOnlyHint to be false")
106+
}
107+
})
108+
109+
// Test custom annotations override default
110+
t.Run("custom annotations", func(t *testing.T) {
111+
customDestructive := false
112+
custom := &tools.ToolAnnotations{DestructiveHint: &customDestructive}
113+
annotations := tools.GetAnnotationsOrDefault(custom, tools.NewDestructiveAnnotations)
114+
if annotations.DestructiveHint == nil || *annotations.DestructiveHint != false {
115+
t.Error("expected custom destructiveHint to be false")
116+
}
117+
})
118+
}
119+
93120
func TestFailParseFromYamlMongoQuery(t *testing.T) {
94121
ctx, err := testutils.ContextWithNewLogger()
95122
if err != nil {

internal/tools/mongodb/mongodbfind/mongodbfind.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,21 @@ type compatibleSource interface {
5353
}
5454

5555
type Config struct {
56-
Name string `yaml:"name" validate:"required"`
57-
Type string `yaml:"type" validate:"required"`
58-
Source string `yaml:"source" validate:"required"`
59-
AuthRequired []string `yaml:"authRequired" validate:"required"`
60-
Description string `yaml:"description" validate:"required"`
61-
Database string `yaml:"database" validate:"required"`
62-
Collection string `yaml:"collection" validate:"required"`
63-
FilterPayload string `yaml:"filterPayload" validate:"required"`
64-
FilterParams parameters.Parameters `yaml:"filterParams"`
65-
ProjectPayload string `yaml:"projectPayload"`
66-
ProjectParams parameters.Parameters `yaml:"projectParams"`
67-
SortPayload string `yaml:"sortPayload"`
68-
SortParams parameters.Parameters `yaml:"sortParams"`
69-
Limit int64 `yaml:"limit"`
56+
Name string `yaml:"name" validate:"required"`
57+
Type string `yaml:"type" validate:"required"`
58+
Source string `yaml:"source" validate:"required"`
59+
AuthRequired []string `yaml:"authRequired" validate:"required"`
60+
Description string `yaml:"description" validate:"required"`
61+
Database string `yaml:"database" validate:"required"`
62+
Collection string `yaml:"collection" validate:"required"`
63+
FilterPayload string `yaml:"filterPayload" validate:"required"`
64+
FilterParams parameters.Parameters `yaml:"filterParams"`
65+
ProjectPayload string `yaml:"projectPayload"`
66+
ProjectParams parameters.Parameters `yaml:"projectParams"`
67+
SortPayload string `yaml:"sortPayload"`
68+
SortParams parameters.Parameters `yaml:"sortParams"`
69+
Limit int64 `yaml:"limit"`
70+
Annotations *tools.ToolAnnotations `yaml:"annotations,omitempty"`
7071
}
7172

7273
// validate interface
@@ -97,8 +98,9 @@ func (cfg Config) Initialize(srcs map[string]sources.Source) (tools.Tool, error)
9798
paramManifest = make([]parameters.ParameterManifest, 0)
9899
}
99100

100-
// Create MCP manifest
101-
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, nil)
101+
// Create MCP manifest with annotations
102+
annotations := tools.GetAnnotationsOrDefault(cfg.Annotations, tools.NewReadOnlyAnnotations)
103+
mcpManifest := tools.GetMcpManifest(cfg.Name, cfg.Description, cfg.AuthRequired, allParameters, annotations)
102104

103105
// finish tool setup
104106
return Tool{

internal/tools/mongodb/mongodbfind/mongodbfind_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"strings"
1919
"testing"
2020

21+
"github.com/googleapis/genai-toolbox/internal/tools"
2122
"github.com/googleapis/genai-toolbox/internal/tools/mongodb/mongodbfind"
2223
"github.com/googleapis/genai-toolbox/internal/util/parameters"
2324

@@ -100,6 +101,29 @@ func TestParseFromYamlMongoQuery(t *testing.T) {
100101

101102
}
102103

104+
func TestAnnotations(t *testing.T) {
105+
// Test default annotations for read-only tool
106+
t.Run("default annotations", func(t *testing.T) {
107+
annotations := tools.GetAnnotationsOrDefault(nil, tools.NewReadOnlyAnnotations)
108+
if annotations == nil {
109+
t.Fatal("expected non-nil annotations")
110+
}
111+
if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != true {
112+
t.Error("expected readOnlyHint to be true")
113+
}
114+
})
115+
116+
// Test custom annotations override default
117+
t.Run("custom annotations", func(t *testing.T) {
118+
customReadOnly := false
119+
custom := &tools.ToolAnnotations{ReadOnlyHint: &customReadOnly}
120+
annotations := tools.GetAnnotationsOrDefault(custom, tools.NewReadOnlyAnnotations)
121+
if annotations.ReadOnlyHint == nil || *annotations.ReadOnlyHint != false {
122+
t.Error("expected custom readOnlyHint to be false")
123+
}
124+
})
125+
}
126+
103127
func TestFailParseFromYamlMongoQuery(t *testing.T) {
104128
ctx, err := testutils.ContextWithNewLogger()
105129
if err != nil {

0 commit comments

Comments
 (0)