Skip to content

Commit 80a3987

Browse files
feat: Add tool annotations to scaffolding templates (#111)
## Summary Adds MCP tool annotations support to all framework templates (Go, TypeScript, Python, Java) to help LLMs better understand tool behavior and make safer decisions about tool execution. ## Changes ### Go (modelcontextprotocol/go-sdk) - Bump SDK from v0.2.0 to v1.2.0 for annotation support - Add `Annotations` field to `MCPTool` struct - Add annotations to `echo.go.tmpl` and `tool.go.tmpl` ### TypeScript (@modelcontextprotocol/sdk) - Import `ToolAnnotations` type - Add `annotations` field to `Tool` interface - Add annotations to `echo.ts.tmpl` and `tool.ts.tmpl` - Include annotations in `tools/list` response ### Python (FastMCP) - Import `ToolAnnotations` from `mcp.types` - Add `annotations` parameter to `@mcp.tool()` decorator - Add annotations to `echo.py.tmpl` and `tool.py.tmpl` ### Java (io.modelcontextprotocol.sdk) - Add `getAnnotations()` method to `Tool` interface - Add annotations to `Echo.java.tmpl` and `ToolTemplate.java.tmpl` - Update `MCPServer` to include annotations in both stdio and HTTP responses ## Why This Matters Tool annotations are part of the MCP specification that enable: - Clients to auto-approve safe (read-only) operations - Better user prompts for destructive operations - Improved tool discovery and filtering - Semantic metadata that helps LLMs understand tool behavior This change benefits **all users who scaffold new MCP servers with kmcp**, ensuring their generated projects include proper tool annotations from the start. ## Testing - [x] `make lint` passes - [x] `make test` passes - [x] Changes are limited to template files ## Before/After **Before (generated tool):** ```typescript const echo = { name: 'echo', description: 'Echo back the provided message', inputSchema: echoSchema, handler: async (params) => { ... } }; ``` **After (generated tool):** ```typescript const echo = { name: 'echo', description: 'Echo back the provided message', inputSchema: echoSchema, annotations: { title: 'Echo Message', readOnlyHint: true, }, handler: async (params) => { ... } }; ``` ## References - [MCP Tool Annotations Spec](https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/#tool-annotations) --------- Signed-off-by: bryankthompson <199543909+bryankthompson@users.noreply.github.com> Co-authored-by: triepod-ai <199543909+triepod-ai@users.noreply.github.com>
1 parent 34b262f commit 80a3987

10 files changed

Lines changed: 107 additions & 25 deletions

File tree

pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/MCPServer.java.tmpl

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,29 @@ public class MCPServer {
393393
toolNode.put("name", tool.getName());
394394
toolNode.put("description", tool.getDescription());
395395
toolNode.set("inputSchema", convertToJsonSchema(tool.getInputSchema()));
396+
397+
// Add tool annotations if available
398+
McpSchema.ToolAnnotations annotations = tool.getAnnotations();
399+
if (annotations != null) {
400+
ObjectNode annotationsNode = objectMapper.createObjectNode();
401+
if (annotations.title() != null) {
402+
annotationsNode.put("title", annotations.title());
403+
}
404+
if (annotations.readOnlyHint() != null) {
405+
annotationsNode.put("readOnlyHint", annotations.readOnlyHint());
406+
}
407+
if (annotations.destructiveHint() != null) {
408+
annotationsNode.put("destructiveHint", annotations.destructiveHint());
409+
}
410+
if (annotations.idempotentHint() != null) {
411+
annotationsNode.put("idempotentHint", annotations.idempotentHint());
412+
}
413+
if (annotations.openWorldHint() != null) {
414+
annotationsNode.put("openWorldHint", annotations.openWorldHint());
415+
}
416+
toolNode.set("annotations", annotationsNode);
417+
}
418+
396419
toolsArray.add(toolNode);
397420
}
398421
@@ -576,12 +599,18 @@ Content-Type: application/json
576599
.serverInfo(serverName, "1.0.0");
577600
578601
for (Tool tool : tools.values()) {
602+
McpSchema.Tool.Builder toolBuilder = McpSchema.Tool.builder()
603+
.name(tool.getName())
604+
.description(tool.getDescription())
605+
.inputSchema(tool.getInputSchema());
606+
607+
// Add tool annotations if available
608+
if (tool.getAnnotations() != null) {
609+
toolBuilder.annotations(tool.getAnnotations());
610+
}
611+
579612
serverBuilder = serverBuilder.tool(
580-
McpSchema.Tool.builder()
581-
.name(tool.getName())
582-
.description(tool.getDescription())
583-
.inputSchema(tool.getInputSchema())
584-
.build(),
613+
toolBuilder.build(),
585614
(exchange, args) -> {
586615
try {
587616
Object result = tool.execute(args);

pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Echo.java.tmpl

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,35 @@
11
package com.example.tools;
22

3+
import io.modelcontextprotocol.spec.McpSchema;
34
import java.util.Map;
45
import java.util.List;
56
import com.example.tools.ToolConfig;
67

78
/**
89
* Echo tool for {{.ProjectName}} MCP server.
9-
*
10+
*
1011
* This is an example tool showing the basic structure for MCP tools.
1112
* Each tool should implement the Tool interface and be registered in Tools.java.
1213
*/
1314
public class Echo implements Tool {
14-
15+
1516
@Override
1617
public String getName() {
1718
return "echo";
1819
}
19-
20+
2021
@Override
2122
public String getDescription() {
2223
return "Echo a message back to the client";
2324
}
24-
25+
26+
@Override
27+
public McpSchema.ToolAnnotations getAnnotations() {
28+
return new McpSchema.ToolAnnotations("Echo Message", true, null, null, null);
29+
}
30+
2531
@Override
26-
public io.modelcontextprotocol.spec.McpSchema.JsonSchema getInputSchema() {
32+
public McpSchema.JsonSchema getInputSchema() {
2733
return new io.modelcontextprotocol.spec.McpSchema.JsonSchema(
2834
"object",
2935
java.util.Map.of(

pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Tool.java.tmpl

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,36 +5,46 @@ import java.util.Map;
55

66
/**
77
* Interface for MCP tools.
8-
*
8+
*
99
* All tools should implement this interface and be registered in the Tools class.
1010
* This interface provides the basic structure for MCP tools with input schema support.
1111
*/
1212
public interface Tool {
1313
/**
1414
* Get the name of the tool.
15-
*
15+
*
1616
* @return the tool name
1717
*/
1818
String getName();
19-
19+
2020
/**
2121
* Get the description of the tool.
22-
*
22+
*
2323
* @return the tool description
2424
*/
2525
String getDescription();
26-
26+
2727
/**
2828
* Get the input schema for the tool.
2929
* This defines the expected parameters and their types.
30-
*
30+
*
3131
* @return the input schema as a JsonSchema
3232
*/
3333
McpSchema.JsonSchema getInputSchema();
34-
34+
35+
/**
36+
* Get the tool annotations for MCP clients.
37+
* Annotations provide hints about tool behavior (read-only, destructive, etc.).
38+
*
39+
* @return the tool annotations, or null if not specified
40+
*/
41+
default McpSchema.ToolAnnotations getAnnotations() {
42+
return null;
43+
}
44+
3545
/**
3646
* Execute the tool with the given parameters.
37-
*
47+
*
3848
* @param parameters the input parameters for the tool
3949
* @return the result of the tool execution
4050
* @throws Exception if the tool execution fails

pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/ToolTemplate.java.tmpl

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.example.tools;
22

3+
import io.modelcontextprotocol.spec.McpSchema;
34
import java.util.Map;
45
import java.util.List;
56
import com.example.tools.ToolConfig;
@@ -8,23 +9,29 @@ import com.example.tools.ToolConfig;
89
* {{if .ToolNameTitle}}{{.ToolNameTitle}} tool{{else}}Example tool{{end}} for MCP server.{{if .Description}}
910
1011
{{.Description}}{{end}}
11-
*
12+
*
1213
* This is a template tool. Replace this implementation with your tool logic.
1314
*/
1415
public class {{if .ToolNamePascalCase}}{{.ToolNamePascalCase}}{{else}}Tool{{end}} implements Tool {
15-
16+
1617
@Override
1718
public String getName() {
1819
return "{{if .ToolName}}{{.ToolName}}{{else}}tool{{end}}";
1920
}
20-
21+
2122
@Override
2223
public String getDescription() {
2324
return "{{if .ToolNameTitle}}{{.ToolNameTitle}}{{else}}Example{{end}} tool implementation";
2425
}
25-
26+
27+
@Override
28+
public McpSchema.ToolAnnotations getAnnotations() {
29+
// Set readOnlyHint to false if this tool modifies state
30+
return new McpSchema.ToolAnnotations("{{if .ToolNameTitle}}{{.ToolNameTitle}}{{else}}Tool{{end}}", true, null, null, null);
31+
}
32+
2633
@Override
27-
public io.modelcontextprotocol.spec.McpSchema.JsonSchema getInputSchema() {
34+
public McpSchema.JsonSchema getInputSchema() {
2835
return new io.modelcontextprotocol.spec.McpSchema.JsonSchema(
2936
"object",
3037
java.util.Map.of(

pkg/cli/internal/frameworks/python/templates/src/tools/echo.py.tmpl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ This is an example tool showing the basic structure for FastMCP tools.
44
Each tool file should contain a function decorated with @mcp.tool().
55
"""
66

7+
from mcp.types import ToolAnnotations
8+
79
from core.server import mcp
810
from core.utils import get_tool_config
911

1012

11-
@mcp.tool()
13+
@mcp.tool(
14+
annotations=ToolAnnotations(
15+
title="Echo Message",
16+
readOnlyHint=True,
17+
),
18+
)
1219
def echo(message: str) -> str:
1320
"""Echo a message back to the client.
1421

pkg/cli/internal/frameworks/python/templates/src/tools/tool.py.tmpl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33
{{.description}}{{end}}
44
"""
55

6+
from mcp.types import ToolAnnotations
7+
68
from core.server import mcp
79
from core.utils import get_tool_config
810

911

10-
@mcp.tool()
12+
@mcp.tool(
13+
annotations=ToolAnnotations(
14+
title="{{if .ToolNameTitle}}{{.ToolNameTitle}}{{else}}Tool{{end}}",
15+
readOnlyHint=True, # Set to False if tool modifies state
16+
),
17+
)
1118
def {{if .ToolName}}{{.ToolName}}{{else}}tool{{end}}(message: str) -> str:
1219
"""{{if .ToolNameTitle}}{{.ToolNameTitle}}{{else}}Example{{end}} tool implementation.
1320

pkg/cli/internal/frameworks/typescript/templates/src/server.ts.tmpl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export async function createServer(): Promise<Server> {
2424
name: tool.name,
2525
description: tool.description,
2626
inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as any,
27+
annotations: tool.annotations,
2728
})),
2829
};
2930
});
@@ -77,11 +78,16 @@ export async function createHttpServer(): Promise<Server> {
7778
name: 'health',
7879
description: 'Health check',
7980
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
81+
annotations: {
82+
title: 'Health Check',
83+
readOnlyHint: true,
84+
},
8085
},
8186
...allTools.map(tool => ({
8287
name: tool.name,
8388
description: tool.description,
8489
inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as any,
90+
annotations: tool.annotations,
8591
})),
8692
],
8793
};

pkg/cli/internal/frameworks/typescript/templates/src/tools/echo.ts.tmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ const echo = {
88
name: 'echo',
99
description: 'Echo back the provided message',
1010
inputSchema: echoSchema,
11+
annotations: {
12+
title: 'Echo Message',
13+
readOnlyHint: true,
14+
},
1115
handler: async (params: z.infer<typeof echoSchema>) => {
1216
return {
1317
message: `Echo: ${params.message}`,

pkg/cli/internal/frameworks/typescript/templates/src/tools/tool.ts.tmpl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ const {{.ToolNameCamelCase}} = {
1010
name: '{{.ToolName}}',
1111
description: '{{.Description}}',
1212
inputSchema: {{.ToolNameCamelCase}}Schema,
13+
annotations: {
14+
title: '{{.ToolNameTitle}}',
15+
readOnlyHint: true, // Set to false if tool modifies state
16+
},
1317
handler: async (params: z.infer<typeof {{.ToolNameCamelCase}}Schema>) => {
1418
// Implement your tool logic here
1519
// Example:

pkg/cli/internal/frameworks/typescript/templates/src/types/index.ts.tmpl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { z } from 'zod';
2+
import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js';
23

34
/**
45
* Common type definitions for the MCP server.
@@ -8,6 +9,7 @@ export interface Tool {
89
name: string;
910
description: string;
1011
inputSchema: z.ZodSchema<any>;
12+
annotations?: ToolAnnotations;
1113
handler: (params: any) => Promise<any>;
1214
}
1315

0 commit comments

Comments
 (0)