From 4a9ce899ce9955027d21f3bbd8645e7baba45f54 Mon Sep 17 00:00:00 2001 From: triepod-ai <199543909+triepod-ai@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:36:01 -0600 Subject: [PATCH 1/2] feat: Add tool annotations to scaffolding templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add MCP tool annotations support to all framework templates (Go, TypeScript, Python, Java) to help LLMs better understand tool behavior. Changes by framework: **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 This change benefits all users who scaffold new MCP servers with kmcp, ensuring their generated projects include proper tool annotations from the start. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- .../frameworks/golang/templates/go.mod.tmpl | 2 +- .../internal/tools/all_tools.go.tmpl | 3 +- .../templates/internal/tools/echo.go.tmpl | 4 ++ .../templates/internal/tools/tool.go.tmpl | 4 ++ .../main/java/com/example/MCPServer.java.tmpl | 39 ++++++++++++++++--- .../java/com/example/tools/Echo.java.tmpl | 16 +++++--- .../java/com/example/tools/Tool.java.tmpl | 26 +++++++++---- .../com/example/tools/ToolTemplate.java.tmpl | 17 +++++--- .../python/templates/src/tools/echo.py.tmpl | 9 ++++- .../python/templates/src/tools/tool.py.tmpl | 9 ++++- .../typescript/templates/src/server.ts.tmpl | 6 +++ .../templates/src/tools/echo.ts.tmpl | 4 ++ .../templates/src/tools/tool.ts.tmpl | 4 ++ .../templates/src/types/index.ts.tmpl | 2 + 14 files changed, 118 insertions(+), 27 deletions(-) diff --git a/pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl b/pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl index 6762922..7af0b80 100644 --- a/pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl +++ b/pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl @@ -2,6 +2,6 @@ module {{.GoModuleName}} go 1.23.0 -require github.com/modelcontextprotocol/go-sdk v0.2.0 +require github.com/modelcontextprotocol/go-sdk v1.2.0 require github.com/yosida95/uritemplate/v3 v3.0.2 // indirect diff --git a/pkg/cli/internal/frameworks/golang/templates/internal/tools/all_tools.go.tmpl b/pkg/cli/internal/frameworks/golang/templates/internal/tools/all_tools.go.tmpl index 94e5b67..2d73172 100644 --- a/pkg/cli/internal/frameworks/golang/templates/internal/tools/all_tools.go.tmpl +++ b/pkg/cli/internal/frameworks/golang/templates/internal/tools/all_tools.go.tmpl @@ -15,12 +15,13 @@ var toolsToAdd []func(server *mcp.Server) func registerTool[I, O any](tool MCPTool[I, O]) { toolsToAdd = append(toolsToAdd, func(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{Name: tool.Name, Description: tool.Description}, tool.Handler) + mcp.AddTool(server, &mcp.Tool{Name: tool.Name, Description: tool.Description, Annotations: tool.Annotations}, tool.Handler) }) } type MCPTool[I, O any] struct { Name string Description string + Annotations *mcp.ToolAnnotations Handler func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[I]) (*mcp.CallToolResultFor[O], error) } diff --git a/pkg/cli/internal/frameworks/golang/templates/internal/tools/echo.go.tmpl b/pkg/cli/internal/frameworks/golang/templates/internal/tools/echo.go.tmpl index 29daca9..aee6b07 100644 --- a/pkg/cli/internal/frameworks/golang/templates/internal/tools/echo.go.tmpl +++ b/pkg/cli/internal/frameworks/golang/templates/internal/tools/echo.go.tmpl @@ -21,6 +21,10 @@ func Echo() MCPTool[EchoParams, EchoResult] { return MCPTool[EchoParams, EchoResult]{ Name: "echo", Description: "Echoes a message back to the user.", + Annotations: &mcp.ToolAnnotations{ + Title: "Echo Message", + ReadOnlyHint: true, + }, Handler: func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[EchoParams]) (*mcp.CallToolResultFor[EchoResult], error) { echoMessage := "Echo: " + params.Arguments.Message result := &mcp.CallToolResultFor[EchoResult]{ diff --git a/pkg/cli/internal/frameworks/golang/templates/internal/tools/tool.go.tmpl b/pkg/cli/internal/frameworks/golang/templates/internal/tools/tool.go.tmpl index 0227b80..a459cde 100644 --- a/pkg/cli/internal/frameworks/golang/templates/internal/tools/tool.go.tmpl +++ b/pkg/cli/internal/frameworks/golang/templates/internal/tools/tool.go.tmpl @@ -9,6 +9,10 @@ func init() { registerTool(MCPTool[{{.ClassName}}Params, {{.ClassName}}Result]{ Name: "{{.ToolName}}", Description: "{{.Description}}", + Annotations: &mcp.ToolAnnotations{ + Title: "{{.ToolNameTitle}}", + ReadOnlyHint: true, // Set to false if tool modifies state + }, Handler: func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[{{.ClassName}}Params]) (*mcp.CallToolResultFor[{{.ClassName}}Result], error) { result := run{{.ClassName}}(params.Arguments) return &mcp.CallToolResultFor[{{.ClassName}}Result]{ diff --git a/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/MCPServer.java.tmpl b/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/MCPServer.java.tmpl index 0a8cfa0..960186e 100644 --- a/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/MCPServer.java.tmpl +++ b/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/MCPServer.java.tmpl @@ -393,6 +393,29 @@ public class MCPServer { toolNode.put("name", tool.getName()); toolNode.put("description", tool.getDescription()); toolNode.set("inputSchema", convertToJsonSchema(tool.getInputSchema())); + + // Add tool annotations if available + McpSchema.ToolAnnotations annotations = tool.getAnnotations(); + if (annotations != null) { + ObjectNode annotationsNode = objectMapper.createObjectNode(); + if (annotations.title() != null) { + annotationsNode.put("title", annotations.title()); + } + if (annotations.readOnlyHint() != null) { + annotationsNode.put("readOnlyHint", annotations.readOnlyHint()); + } + if (annotations.destructiveHint() != null) { + annotationsNode.put("destructiveHint", annotations.destructiveHint()); + } + if (annotations.idempotentHint() != null) { + annotationsNode.put("idempotentHint", annotations.idempotentHint()); + } + if (annotations.openWorldHint() != null) { + annotationsNode.put("openWorldHint", annotations.openWorldHint()); + } + toolNode.set("annotations", annotationsNode); + } + toolsArray.add(toolNode); } @@ -576,12 +599,18 @@ Content-Type: application/json .serverInfo(serverName, "1.0.0"); for (Tool tool : tools.values()) { + McpSchema.Tool.Builder toolBuilder = McpSchema.Tool.builder() + .name(tool.getName()) + .description(tool.getDescription()) + .inputSchema(tool.getInputSchema()); + + // Add tool annotations if available + if (tool.getAnnotations() != null) { + toolBuilder.annotations(tool.getAnnotations()); + } + serverBuilder = serverBuilder.tool( - McpSchema.Tool.builder() - .name(tool.getName()) - .description(tool.getDescription()) - .inputSchema(tool.getInputSchema()) - .build(), + toolBuilder.build(), (exchange, args) -> { try { Object result = tool.execute(args); diff --git a/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Echo.java.tmpl b/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Echo.java.tmpl index c337d94..66cff71 100644 --- a/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Echo.java.tmpl +++ b/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Echo.java.tmpl @@ -1,29 +1,35 @@ package com.example.tools; +import io.modelcontextprotocol.spec.McpSchema; import java.util.Map; import java.util.List; import com.example.tools.ToolConfig; /** * Echo tool for {{.ProjectName}} MCP server. - * + * * This is an example tool showing the basic structure for MCP tools. * Each tool should implement the Tool interface and be registered in Tools.java. */ public class Echo implements Tool { - + @Override public String getName() { return "echo"; } - + @Override public String getDescription() { return "Echo a message back to the client"; } - + + @Override + public McpSchema.ToolAnnotations getAnnotations() { + return new McpSchema.ToolAnnotations("Echo Message", true, null, null, null); + } + @Override - public io.modelcontextprotocol.spec.McpSchema.JsonSchema getInputSchema() { + public McpSchema.JsonSchema getInputSchema() { return new io.modelcontextprotocol.spec.McpSchema.JsonSchema( "object", java.util.Map.of( diff --git a/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Tool.java.tmpl b/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Tool.java.tmpl index e43dc5a..41f0165 100644 --- a/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Tool.java.tmpl +++ b/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/Tool.java.tmpl @@ -5,36 +5,46 @@ import java.util.Map; /** * Interface for MCP tools. - * + * * All tools should implement this interface and be registered in the Tools class. * This interface provides the basic structure for MCP tools with input schema support. */ public interface Tool { /** * Get the name of the tool. - * + * * @return the tool name */ String getName(); - + /** * Get the description of the tool. - * + * * @return the tool description */ String getDescription(); - + /** * Get the input schema for the tool. * This defines the expected parameters and their types. - * + * * @return the input schema as a JsonSchema */ McpSchema.JsonSchema getInputSchema(); - + + /** + * Get the tool annotations for MCP clients. + * Annotations provide hints about tool behavior (read-only, destructive, etc.). + * + * @return the tool annotations, or null if not specified + */ + default McpSchema.ToolAnnotations getAnnotations() { + return null; + } + /** * Execute the tool with the given parameters. - * + * * @param parameters the input parameters for the tool * @return the result of the tool execution * @throws Exception if the tool execution fails diff --git a/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/ToolTemplate.java.tmpl b/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/ToolTemplate.java.tmpl index 605b8dd..108ace4 100644 --- a/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/ToolTemplate.java.tmpl +++ b/pkg/cli/internal/frameworks/java/templates/src/main/java/com/example/tools/ToolTemplate.java.tmpl @@ -1,5 +1,6 @@ package com.example.tools; +import io.modelcontextprotocol.spec.McpSchema; import java.util.Map; import java.util.List; import com.example.tools.ToolConfig; @@ -8,23 +9,29 @@ import com.example.tools.ToolConfig; * {{if .ToolNameTitle}}{{.ToolNameTitle}} tool{{else}}Example tool{{end}} for MCP server.{{if .Description}} {{.Description}}{{end}} - * + * * This is a template tool. Replace this implementation with your tool logic. */ public class {{if .ToolNamePascalCase}}{{.ToolNamePascalCase}}{{else}}Tool{{end}} implements Tool { - + @Override public String getName() { return "{{if .ToolName}}{{.ToolName}}{{else}}tool{{end}}"; } - + @Override public String getDescription() { return "{{if .ToolNameTitle}}{{.ToolNameTitle}}{{else}}Example{{end}} tool implementation"; } - + + @Override + public McpSchema.ToolAnnotations getAnnotations() { + // Set readOnlyHint to false if this tool modifies state + return new McpSchema.ToolAnnotations("{{if .ToolNameTitle}}{{.ToolNameTitle}}{{else}}Tool{{end}}", true, null, null, null); + } + @Override - public io.modelcontextprotocol.spec.McpSchema.JsonSchema getInputSchema() { + public McpSchema.JsonSchema getInputSchema() { return new io.modelcontextprotocol.spec.McpSchema.JsonSchema( "object", java.util.Map.of( diff --git a/pkg/cli/internal/frameworks/python/templates/src/tools/echo.py.tmpl b/pkg/cli/internal/frameworks/python/templates/src/tools/echo.py.tmpl index 6c09d6d..8f4c249 100644 --- a/pkg/cli/internal/frameworks/python/templates/src/tools/echo.py.tmpl +++ b/pkg/cli/internal/frameworks/python/templates/src/tools/echo.py.tmpl @@ -4,11 +4,18 @@ This is an example tool showing the basic structure for FastMCP tools. Each tool file should contain a function decorated with @mcp.tool(). """ +from mcp.types import ToolAnnotations + from core.server import mcp from core.utils import get_tool_config -@mcp.tool() +@mcp.tool( + annotations=ToolAnnotations( + title="Echo Message", + readOnlyHint=True, + ), +) def echo(message: str) -> str: """Echo a message back to the client. diff --git a/pkg/cli/internal/frameworks/python/templates/src/tools/tool.py.tmpl b/pkg/cli/internal/frameworks/python/templates/src/tools/tool.py.tmpl index 85134d0..b554e93 100644 --- a/pkg/cli/internal/frameworks/python/templates/src/tools/tool.py.tmpl +++ b/pkg/cli/internal/frameworks/python/templates/src/tools/tool.py.tmpl @@ -3,11 +3,18 @@ {{.description}}{{end}} """ +from mcp.types import ToolAnnotations + from core.server import mcp from core.utils import get_tool_config -@mcp.tool() +@mcp.tool( + annotations=ToolAnnotations( + title="{{if .ToolNameTitle}}{{.ToolNameTitle}}{{else}}Tool{{end}}", + readOnlyHint=True, # Set to False if tool modifies state + ), +) def {{if .ToolName}}{{.ToolName}}{{else}}tool{{end}}(message: str) -> str: """{{if .ToolNameTitle}}{{.ToolNameTitle}}{{else}}Example{{end}} tool implementation. diff --git a/pkg/cli/internal/frameworks/typescript/templates/src/server.ts.tmpl b/pkg/cli/internal/frameworks/typescript/templates/src/server.ts.tmpl index 3011a92..b497fde 100644 --- a/pkg/cli/internal/frameworks/typescript/templates/src/server.ts.tmpl +++ b/pkg/cli/internal/frameworks/typescript/templates/src/server.ts.tmpl @@ -24,6 +24,7 @@ export async function createServer(): Promise { name: tool.name, description: tool.description, inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as any, + annotations: tool.annotations, })), }; }); @@ -77,11 +78,16 @@ export async function createHttpServer(): Promise { name: 'health', description: 'Health check', inputSchema: { type: 'object', properties: {}, additionalProperties: false }, + annotations: { + title: 'Health Check', + readOnlyHint: true, + }, }, ...allTools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: zodToJsonSchema(tool.inputSchema, { strictUnions: true }) as any, + annotations: tool.annotations, })), ], }; diff --git a/pkg/cli/internal/frameworks/typescript/templates/src/tools/echo.ts.tmpl b/pkg/cli/internal/frameworks/typescript/templates/src/tools/echo.ts.tmpl index e53df17..a5d0b80 100644 --- a/pkg/cli/internal/frameworks/typescript/templates/src/tools/echo.ts.tmpl +++ b/pkg/cli/internal/frameworks/typescript/templates/src/tools/echo.ts.tmpl @@ -8,6 +8,10 @@ const echo = { name: 'echo', description: 'Echo back the provided message', inputSchema: echoSchema, + annotations: { + title: 'Echo Message', + readOnlyHint: true, + }, handler: async (params: z.infer) => { return { message: `Echo: ${params.message}`, diff --git a/pkg/cli/internal/frameworks/typescript/templates/src/tools/tool.ts.tmpl b/pkg/cli/internal/frameworks/typescript/templates/src/tools/tool.ts.tmpl index 0f7bd1a..3c4d7fb 100644 --- a/pkg/cli/internal/frameworks/typescript/templates/src/tools/tool.ts.tmpl +++ b/pkg/cli/internal/frameworks/typescript/templates/src/tools/tool.ts.tmpl @@ -10,6 +10,10 @@ const {{.ToolNameCamelCase}} = { name: '{{.ToolName}}', description: '{{.Description}}', inputSchema: {{.ToolNameCamelCase}}Schema, + annotations: { + title: '{{.ToolNameTitle}}', + readOnlyHint: true, // Set to false if tool modifies state + }, handler: async (params: z.infer) => { // Implement your tool logic here // Example: diff --git a/pkg/cli/internal/frameworks/typescript/templates/src/types/index.ts.tmpl b/pkg/cli/internal/frameworks/typescript/templates/src/types/index.ts.tmpl index 54caf9b..55fe1f0 100644 --- a/pkg/cli/internal/frameworks/typescript/templates/src/types/index.ts.tmpl +++ b/pkg/cli/internal/frameworks/typescript/templates/src/types/index.ts.tmpl @@ -1,4 +1,5 @@ import { z } from 'zod'; +import type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'; /** * Common type definitions for the MCP server. @@ -8,6 +9,7 @@ export interface Tool { name: string; description: string; inputSchema: z.ZodSchema; + annotations?: ToolAnnotations; handler: (params: any) => Promise; } From 01119b9b8c525a574774dd3dc926866d538b50b7 Mon Sep 17 00:00:00 2001 From: triepod-ai <199543909+triepod-ai@users.noreply.github.com> Date: Fri, 2 Jan 2026 10:43:12 -0600 Subject: [PATCH 2/2] fix: Revert Go template changes (SDK v1.2.0 has breaking API changes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Go SDK v1.2.0 has a completely different API compared to v0.2.0 that the templates were designed for. The migration requires updating all Go templates including main.go, not just the tool files. This reverts the Go-specific changes while keeping annotations for: - TypeScript (@modelcontextprotocol/sdk) - Python (FastMCP with mcp.types.ToolAnnotations) - Java (io.modelcontextprotocol.sdk) The Go templates will need a separate PR to migrate to the v1.2.0 API before annotations can be added. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl | 2 +- .../golang/templates/internal/tools/all_tools.go.tmpl | 3 +-- .../frameworks/golang/templates/internal/tools/echo.go.tmpl | 4 ---- .../frameworks/golang/templates/internal/tools/tool.go.tmpl | 4 ---- 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl b/pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl index 7af0b80..6762922 100644 --- a/pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl +++ b/pkg/cli/internal/frameworks/golang/templates/go.mod.tmpl @@ -2,6 +2,6 @@ module {{.GoModuleName}} go 1.23.0 -require github.com/modelcontextprotocol/go-sdk v1.2.0 +require github.com/modelcontextprotocol/go-sdk v0.2.0 require github.com/yosida95/uritemplate/v3 v3.0.2 // indirect diff --git a/pkg/cli/internal/frameworks/golang/templates/internal/tools/all_tools.go.tmpl b/pkg/cli/internal/frameworks/golang/templates/internal/tools/all_tools.go.tmpl index 2d73172..94e5b67 100644 --- a/pkg/cli/internal/frameworks/golang/templates/internal/tools/all_tools.go.tmpl +++ b/pkg/cli/internal/frameworks/golang/templates/internal/tools/all_tools.go.tmpl @@ -15,13 +15,12 @@ var toolsToAdd []func(server *mcp.Server) func registerTool[I, O any](tool MCPTool[I, O]) { toolsToAdd = append(toolsToAdd, func(server *mcp.Server) { - mcp.AddTool(server, &mcp.Tool{Name: tool.Name, Description: tool.Description, Annotations: tool.Annotations}, tool.Handler) + mcp.AddTool(server, &mcp.Tool{Name: tool.Name, Description: tool.Description}, tool.Handler) }) } type MCPTool[I, O any] struct { Name string Description string - Annotations *mcp.ToolAnnotations Handler func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[I]) (*mcp.CallToolResultFor[O], error) } diff --git a/pkg/cli/internal/frameworks/golang/templates/internal/tools/echo.go.tmpl b/pkg/cli/internal/frameworks/golang/templates/internal/tools/echo.go.tmpl index aee6b07..29daca9 100644 --- a/pkg/cli/internal/frameworks/golang/templates/internal/tools/echo.go.tmpl +++ b/pkg/cli/internal/frameworks/golang/templates/internal/tools/echo.go.tmpl @@ -21,10 +21,6 @@ func Echo() MCPTool[EchoParams, EchoResult] { return MCPTool[EchoParams, EchoResult]{ Name: "echo", Description: "Echoes a message back to the user.", - Annotations: &mcp.ToolAnnotations{ - Title: "Echo Message", - ReadOnlyHint: true, - }, Handler: func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[EchoParams]) (*mcp.CallToolResultFor[EchoResult], error) { echoMessage := "Echo: " + params.Arguments.Message result := &mcp.CallToolResultFor[EchoResult]{ diff --git a/pkg/cli/internal/frameworks/golang/templates/internal/tools/tool.go.tmpl b/pkg/cli/internal/frameworks/golang/templates/internal/tools/tool.go.tmpl index a459cde..0227b80 100644 --- a/pkg/cli/internal/frameworks/golang/templates/internal/tools/tool.go.tmpl +++ b/pkg/cli/internal/frameworks/golang/templates/internal/tools/tool.go.tmpl @@ -9,10 +9,6 @@ func init() { registerTool(MCPTool[{{.ClassName}}Params, {{.ClassName}}Result]{ Name: "{{.ToolName}}", Description: "{{.Description}}", - Annotations: &mcp.ToolAnnotations{ - Title: "{{.ToolNameTitle}}", - ReadOnlyHint: true, // Set to false if tool modifies state - }, Handler: func(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[{{.ClassName}}Params]) (*mcp.CallToolResultFor[{{.ClassName}}Result], error) { result := run{{.ClassName}}(params.Arguments) return &mcp.CallToolResultFor[{{.ClassName}}Result]{