Adding a New MCP Tool
Steps
- Add a tool name constant in
internal/service/service.go - Create request/response types in
internal/service/types.go - Implement the service in
internal/service/(new file or add to existing) - Create a markdown definition in
internal/service/definitions/— this is whatMakeToolDefinitionsreads - Add method to
Svcinterface ininternal/server/mcp/handler.go - Add handler in
handler.go - Register tool in
registerToolsinmcp.go - Generate mocks:
go generate ./... - Write tests
1. Tool name constant
Add a constant in internal/service/service.go:
const MyNewTool = "my_new_tool"2. Request/Response types
Define in internal/service/types.go:
type MyNewToolRequest struct {
Param1 string `json:"param1" validate:"required" jsonschema:"required,Description of param1"`
}
type MyNewToolResponse struct {
Result string `json:"result"`
}3. Service implementation
Create internal/service/my_new_tool.go or add to an existing service file. Follow the standard service pattern: validate → lookup → execute → return:
func (s *Service) MyNewTool(ctx context.Context, req MyNewToolRequest) (MyNewToolResponse, error) {
if err := s.validateRequest(req); err != nil {
return MyNewToolResponse{}, NewLLMError(validationFailedErrCode, err.Error())
}
// business logic
return MyNewToolResponse{Result: "ok"}, nil
}4. Markdown definition
Create internal/service/definitions/my_new_tool.md. This file is read by MakeToolDefinitions() and embedded into the binary. The frontmatter name: field must match the constant:
---
name: my_new_tool
---
# my_new_tool
Description of the tool.
## Parameters
| Parameter | Type | Description |
|-----------|------|-------------|
| `param1` | string | Description |The MakeToolDefinitions() function in tools.go reads all .md files from the embedded definitions/ directory, parses the YAML frontmatter for the name field, and uses the body as the tool description. The instruction.md file is treated specially — it becomes the system instruction for the LLM.
5. Svc Interface
Add a method to the composed Svc interface in handler.go:
type Svc interface {
// ... existing methods
MyNewTool(ctx context.Context, req service.MyNewToolRequest) (service.MyNewToolResponse, error)
}6. Handler
Add a handler method on handler in handler.go. The handler delegates to the service and wraps the result in StructuredContent:
func (h *handler) handleMyNewTool(
ctx context.Context,
_ *sdkmcp.CallToolRequest,
req service.MyNewToolRequest,
) (*sdkmcp.CallToolResult, any, error) {
resp, err := h.service.MyNewTool(ctx, req)
if err != nil {
return nil, nil, err
}
return &sdkmcp.CallToolResult{
StructuredContent: resp,
}, nil, nil
}7. Registration
Register the tool in the registerTools function in mcp.go. Add an entry to the toolRegistrations map:
service.MyNewTool: {
addTool[service.MyNewToolRequest](mcpServer, h.handleMyNewTool),
true, // false if the tool is mutable (like invoke or auth)
},The registerTools function signature is:
func registerTools(mcpServer *sdkmcp.Server, tools []service.Tool, h handler) {It iterates over the tool definitions returned by MakeToolDefinitions() and registers each one with its typed handler. The toolRegistrations map connects tool name constants to their handlers.