Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions docs/en/resources/tools/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 16 additions & 12 deletions internal/tools/mongodb/mongodbaggregate/mongodbaggregate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 14 additions & 10 deletions internal/tools/mongodb/mongodbdeletemany/mongodbdeletemany.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 14 additions & 10 deletions internal/tools/mongodb/mongodbdeleteone/mongodbdeleteone.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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{
Expand Down
Loading
Loading