Skip to content

Commit f618d93

Browse files
authored
feat: MCP tool annotations, typed write outputs, and documentation (#54)
* feat: add MCP tool annotations and typed write outputs Support MCP ToolAnnotations (ReadOnlyHint, DestructiveHint, IdempotentHint, OpenWorldHint) with three-level override chain: per-registration, toolkit-level, and defaults. Add typed output structs for write tools and ListConnections. Ref: txn2/mcp-data-platform#102 * docs: document MCP tool annotations and typed write outputs Update CLAUDE.md, README, doc site (tools reference, API reference, configuration, quickstart, MCP protocol concepts) with annotation override API, default annotation table, and write tool output types. Ref: txn2/mcp-data-platform#102
1 parent bb33166 commit f618d93

27 files changed

+671
-65
lines changed

CLAUDE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,19 @@ Tool descriptions can be customized at three levels of priority:
139139

140140
Descriptions can also be set via config file (`toolkit.descriptions` section) or the `Descriptions` field in `server.Options`.
141141

142+
## Annotation Overrides
143+
144+
MCP tool annotations (behavior hints per the MCP specification) follow the same three-level priority:
145+
146+
1. **Per-registration** (highest): `toolkit.RegisterWith(server, tools.ToolSearch, tools.WithAnnotation(&mcp.ToolAnnotations{...}))`
147+
2. **Toolkit-level**: `tools.NewToolkit(client, cfg, tools.WithAnnotations(map[tools.ToolName]*mcp.ToolAnnotations{...}))`
148+
3. **Default**: Built-in annotations from `pkg/tools/annotations.go`
149+
150+
Default annotations for all 19 tools:
151+
152+
- **Read tools** (12): `ReadOnlyHint: true`, `IdempotentHint: true`, `OpenWorldHint: false`
153+
- **Write tools** (7): `DestructiveHint: false`, `IdempotentHint: true`, `OpenWorldHint: false`
154+
142155
## Extensions Package (`pkg/extensions/`)
143156

144157
Optional middleware and config file support. All extensions are opt-in.

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ toolkit := tools.NewToolkit(datahubClient, tools.Config{},
9292
)
9393
```
9494

95+
#### Customizing Tool Annotations
96+
97+
Override [MCP tool annotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#annotations) (behavior hints for AI clients):
98+
99+
```go
100+
toolkit := tools.NewToolkit(datahubClient, tools.Config{},
101+
tools.WithAnnotations(map[tools.ToolName]*mcp.ToolAnnotations{
102+
tools.ToolSearch: {ReadOnlyHint: true, OpenWorldHint: boolPtr(true)},
103+
}),
104+
)
105+
```
106+
107+
All 19 tools ship with default annotations: read tools are marked `ReadOnlyHint: true`, write tools are marked `DestructiveHint: false` and `IdempotentHint: true`.
108+
95109
#### Extensions (Logging, Metrics, Error Hints)
96110

97111
Enable optional middleware via the extensions package:

docs/concepts/mcp-protocol.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Tools are functions that AI assistants can call. Each tool has:
5757
| Name | Unique identifier |
5858
| Description | What the tool does |
5959
| Input Schema | Parameters the tool accepts |
60+
| Annotations | Behavior hints (read-only, destructive, idempotent) |
6061
| Handler | Function that executes the tool |
6162

6263
Example tool definition:
@@ -86,6 +87,19 @@ Example tool definition:
8687
}
8788
```
8889

90+
### Tool Annotations
91+
92+
Tool annotations are optional metadata that describe a tool's behavior to AI clients. mcp-datahub sets annotations on all 19 tools:
93+
94+
| Annotation | Description |
95+
|------------|-------------|
96+
| `ReadOnlyHint` | Tool only reads data (all 12 read tools) |
97+
| `DestructiveHint` | Tool may destructively update (false for all write tools) |
98+
| `IdempotentHint` | Repeated calls produce the same result (all tools) |
99+
| `OpenWorldHint` | Tool interacts with external entities beyond the server (false for all tools) |
100+
101+
MCP clients can use these hints to make informed decisions, such as auto-approving read-only tools or prompting for confirmation before write operations.
102+
89103
### Transport
90104

91105
MCP supports multiple transport mechanisms:

docs/library/quickstart.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ toolkit := tools.NewToolkit(datahubClient, tools.Config{},
108108
)
109109
```
110110

111+
## With Annotation Overrides
112+
113+
Customize MCP tool annotations (behavior hints for AI clients):
114+
115+
```go
116+
toolkit := tools.NewToolkit(datahubClient, tools.Config{},
117+
tools.WithAnnotations(map[tools.ToolName]*mcp.ToolAnnotations{
118+
tools.ToolSearch: {ReadOnlyHint: true, OpenWorldHint: boolPtr(true)},
119+
}),
120+
)
121+
```
122+
123+
All tools ship with sensible defaults (read tools marked read-only, write tools marked non-destructive and idempotent). See the [Tools API Reference](../reference/tools-api.md#withannotations) for the full annotation API.
124+
111125
## With Extensions
112126

113127
Enable built-in middleware for logging, metrics, and error hints:

docs/reference/configuration.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,34 @@ Description priority (highest to lowest):
180180
2. Toolkit-level override via `WithDescriptions()`
181181
3. Built-in default description
182182

183+
## Annotation Overrides
184+
185+
Customize [MCP tool annotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#annotations) (behavior hints for AI clients):
186+
187+
```go
188+
toolkit := tools.NewToolkit(datahubClient, tools.Config{},
189+
tools.WithAnnotations(map[tools.ToolName]*mcp.ToolAnnotations{
190+
tools.ToolSearch: {ReadOnlyHint: true, OpenWorldHint: boolPtr(true)},
191+
}),
192+
)
193+
```
194+
195+
Or override a single tool at registration time:
196+
197+
```go
198+
toolkit.RegisterWith(server, tools.ToolSearch,
199+
tools.WithAnnotation(&mcp.ToolAnnotations{ReadOnlyHint: true}),
200+
)
201+
```
202+
203+
Annotation priority (highest to lowest):
204+
205+
1. Per-registration override via `WithAnnotation()`
206+
2. Toolkit-level override via `WithAnnotations()`
207+
3. Built-in default annotations
208+
209+
All 19 tools ship with defaults: read tools are `ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: false`; write tools are `DestructiveHint: false, IdempotentHint: true, OpenWorldHint: false`.
210+
183211
## Extensions Configuration
184212

185213
The `extensions` package provides built-in middleware and config file support.

docs/reference/tools-api.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,61 @@ toolkit.RegisterWith(server, tools.ToolSearch,
153153
2. Toolkit-level `WithDescriptions()` map
154154
3. Built-in default description (lowest)
155155

156+
### WithAnnotations
157+
158+
Overrides MCP tool annotations at the toolkit level.
159+
160+
```go
161+
func WithAnnotations(anns map[ToolName]*mcp.ToolAnnotations) ToolkitOption
162+
```
163+
164+
**Example:**
165+
166+
```go
167+
toolkit := tools.NewToolkit(datahubClient, config,
168+
tools.WithAnnotations(map[tools.ToolName]*mcp.ToolAnnotations{
169+
tools.ToolSearch: {ReadOnlyHint: true, OpenWorldHint: boolPtr(true)},
170+
}),
171+
)
172+
```
173+
174+
### WithAnnotation
175+
176+
Overrides the annotations for a single tool at registration time.
177+
178+
```go
179+
func WithAnnotation(ann *mcp.ToolAnnotations) ToolOption
180+
```
181+
182+
**Example:**
183+
184+
```go
185+
toolkit.RegisterWith(server, tools.ToolSearch,
186+
tools.WithAnnotation(&mcp.ToolAnnotations{ReadOnlyHint: true}),
187+
)
188+
```
189+
190+
**Annotation Priority:**
191+
192+
1. Per-registration `WithAnnotation()` (highest)
193+
2. Toolkit-level `WithAnnotations()` map
194+
3. Built-in default annotations (lowest)
195+
196+
### DefaultAnnotations
197+
198+
Returns the default annotations for a tool by name. Returns nil for unknown tool names.
199+
200+
```go
201+
func DefaultAnnotations(name ToolName) *mcp.ToolAnnotations
202+
```
203+
204+
**Default Annotations:**
205+
206+
| Tool Category | ReadOnlyHint | DestructiveHint | IdempotentHint | OpenWorldHint |
207+
|---------------|:------------:|:---------------:|:--------------:|:-------------:|
208+
| Read tools (12) | `true` | _(default)_ | `true` | `false` |
209+
| Write tools (7) | `false` | `false` | `true` | `false` |
210+
156211
## Tool Names
157212

158213
Available tool name constants:
@@ -261,6 +316,74 @@ Creates an error result.
261316
func ErrorResult(msg string) *mcp.CallToolResult
262317
```
263318

319+
## Write Tool Output Types
320+
321+
Write tools return typed output structs as the second return value from their handler functions. These provide structured access to operation results.
322+
323+
### UpdateDescriptionOutput
324+
325+
```go
326+
type UpdateDescriptionOutput struct {
327+
URN string `json:"urn"`
328+
Aspect string `json:"aspect"`
329+
Action string `json:"action"`
330+
}
331+
```
332+
333+
### AddTagOutput / RemoveTagOutput
334+
335+
```go
336+
type AddTagOutput struct {
337+
URN string `json:"urn"`
338+
Tag string `json:"tag"`
339+
Aspect string `json:"aspect"`
340+
Action string `json:"action"`
341+
}
342+
343+
type RemoveTagOutput struct {
344+
URN string `json:"urn"`
345+
Tag string `json:"tag"`
346+
Aspect string `json:"aspect"`
347+
Action string `json:"action"`
348+
}
349+
```
350+
351+
### AddGlossaryTermOutput / RemoveGlossaryTermOutput
352+
353+
```go
354+
type AddGlossaryTermOutput struct {
355+
URN string `json:"urn"`
356+
Term string `json:"term"`
357+
Aspect string `json:"aspect"`
358+
Action string `json:"action"`
359+
}
360+
361+
type RemoveGlossaryTermOutput struct {
362+
URN string `json:"urn"`
363+
Term string `json:"term"`
364+
Aspect string `json:"aspect"`
365+
Action string `json:"action"`
366+
}
367+
```
368+
369+
### AddLinkOutput / RemoveLinkOutput
370+
371+
```go
372+
type AddLinkOutput struct {
373+
URN string `json:"urn"`
374+
URL string `json:"url"`
375+
Aspect string `json:"aspect"`
376+
Action string `json:"action"`
377+
}
378+
379+
type RemoveLinkOutput struct {
380+
URN string `json:"urn"`
381+
URL string `json:"url"`
382+
Aspect string `json:"aspect"`
383+
Action string `json:"action"`
384+
}
385+
```
386+
264387
## Integration Package
265388

266389
The `integration` package provides interfaces for enterprise integration.

docs/server/tools.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@
22

33
mcp-datahub provides 19 MCP tools for interacting with DataHub (12 read + 7 write).
44

5+
## Tool Annotations
6+
7+
All tools include [MCP tool annotations](https://modelcontextprotocol.io/specification/2025-03-26/server/tools#annotations) that describe their behavior to AI clients:
8+
9+
| Hint | Read Tools | Write Tools | Description |
10+
|------|:----------:|:-----------:|-------------|
11+
| `ReadOnlyHint` | `true` | `false` | Whether the tool only reads data |
12+
| `DestructiveHint` | _(default)_ | `false` | Whether the tool may destructively update |
13+
| `IdempotentHint` | `true` | `true` | Whether repeated calls produce the same result |
14+
| `OpenWorldHint` | `false` | `false` | Whether the tool interacts with external entities |
15+
16+
These annotations help MCP clients make informed decisions about tool invocation (e.g., auto-approving read-only tools). Library users can override annotations per-tool or per-toolkit; see the [Tools API Reference](../reference/tools-api.md#withannotations).
17+
518
## Multi-Server Support
619

720
All tools accept an optional `connection` parameter to target a specific DataHub server in multi-server environments. Use `datahub_list_connections` to discover available connections.

pkg/tools/annotations.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package tools
2+
3+
import "github.com/modelcontextprotocol/go-sdk/mcp"
4+
5+
// boolPtr returns a pointer to a bool value.
6+
func boolPtr(b bool) *bool {
7+
return &b
8+
}
9+
10+
// defaultAnnotations holds the default annotations for each built-in tool.
11+
// These follow the MCP specification:
12+
// - ReadOnlyHint (bool, default false): tool does not modify state
13+
// - DestructiveHint (*bool, default true): tool may destructively update
14+
// - IdempotentHint (bool, default false): repeated calls produce same result
15+
// - OpenWorldHint (*bool, default true): tool interacts with external entities
16+
var defaultAnnotations = map[ToolName]*mcp.ToolAnnotations{
17+
// Read-only tools
18+
ToolSearch: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
19+
ToolGetEntity: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
20+
ToolGetSchema: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
21+
ToolGetLineage: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
22+
ToolGetColumnLineage: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
23+
ToolGetQueries: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
24+
ToolGetGlossaryTerm: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
25+
ToolListTags: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
26+
ToolListDomains: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
27+
ToolListDataProducts: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
28+
ToolGetDataProduct: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
29+
ToolListConnections: {ReadOnlyHint: true, IdempotentHint: true, OpenWorldHint: boolPtr(false)},
30+
31+
// Write tools
32+
ToolUpdateDescription: {DestructiveHint: boolPtr(false), IdempotentHint: true, OpenWorldHint: boolPtr(false)},
33+
ToolAddTag: {DestructiveHint: boolPtr(false), IdempotentHint: true, OpenWorldHint: boolPtr(false)},
34+
ToolRemoveTag: {DestructiveHint: boolPtr(false), IdempotentHint: true, OpenWorldHint: boolPtr(false)},
35+
ToolAddGlossaryTerm: {DestructiveHint: boolPtr(false), IdempotentHint: true, OpenWorldHint: boolPtr(false)},
36+
ToolRemoveGlossaryTerm: {DestructiveHint: boolPtr(false), IdempotentHint: true, OpenWorldHint: boolPtr(false)},
37+
ToolAddLink: {DestructiveHint: boolPtr(false), IdempotentHint: true, OpenWorldHint: boolPtr(false)},
38+
ToolRemoveLink: {DestructiveHint: boolPtr(false), IdempotentHint: true, OpenWorldHint: boolPtr(false)},
39+
}
40+
41+
// DefaultAnnotations returns the default annotations for a tool.
42+
// Returns nil for unknown tool names.
43+
func DefaultAnnotations(name ToolName) *mcp.ToolAnnotations {
44+
return defaultAnnotations[name]
45+
}
46+
47+
// getAnnotations resolves the annotations for a tool using the priority chain:
48+
// 1. Per-registration override (cfg.annotations) — highest priority
49+
// 2. Toolkit-level override (t.annotations) — medium priority
50+
// 3. Default annotations — lowest priority.
51+
func (t *Toolkit) getAnnotations(name ToolName, cfg *toolConfig) *mcp.ToolAnnotations {
52+
// Per-registration override (highest priority)
53+
if cfg != nil && cfg.annotations != nil {
54+
return cfg.annotations
55+
}
56+
57+
// Toolkit-level override (medium priority)
58+
if ann, ok := t.annotations[name]; ok {
59+
return ann
60+
}
61+
62+
// Default annotations (lowest priority)
63+
return defaultAnnotations[name]
64+
}

0 commit comments

Comments
 (0)