Implementing MCP Tools
Minimal MCP tool definition and server response:
JSON{ "capabilities": { "tools": { "listChanged": true } } }
JSON{ "name": "get_weather", "title": "Weather Information Provider", "description": "Get current weather information for a location", "inputSchema": { "type": "object", "properties": { "location": { "type": "string", "description": "City name or zip code" } }, "required": ["location"] } }
Tool call response (unstructured):
JSON{ "jsonrpc": "2.0", "id": 2, "result": { "resultType": "complete", "content": [ { "type": "text", "text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy" } ], "isError": false } }
Progress checklist for implementing an MCP tool-serving capability:
- Declare
toolscapability (listChanged: true/false) in server capabilities - Define each tool:
name,title,description,inputSchema, optionaloutputSchema, optionalicons, optionalannotations - Validate tool name against naming rules (see below)
- Implement
tools/listhandler: paginated, deterministic ordering, no per-connection variance (auth-scoped variance is allowed) - Implement
tools/callhandler: validate arguments againstinputSchema, execute, returncontentand/orstructuredContent - If
outputSchemadefined, ensurestructuredContentconforms and mirror it in atextcontent block for backwards compatibility - If tool requires additional user input mid-call, return
resultType: "input_required"withinputRequestsandrequestState; handle retry withinputResponses - If tool list can change at runtime, send
notifications/tools/list_changedto subscribed clients - If using Streamable HTTP transport and exposing routable parameters, add
x-mcp-headerannotations; validate constraints and reject invalid tools client-side
Example 1 — Tool with output schema:
Input: Tool get_weather_data with outputSchema requiring temperature, conditions, humidity.
Output:
JSON{ "jsonrpc": "2.0", "id": 5, "result": { "resultType": "complete", "content": [ { "type": "text", "text": "{\"temperature\": 22.5, \"conditions\": \"Partly cloudy\", \"humidity\": 65}" } ], "structuredContent": { "temperature": 22.5, "conditions": "Partly cloudy", "humidity": 65 } } }
Example 2 — Input-required flow (elicitation mid-call):
Input: Client calls get_weather for "New York"; server needs GitHub login first.
Output (server asks for input):
JSON{ "jsonrpc": "2.0", "id": 2, "result": { "resultType": "input_required", "inputRequests": { "github_login": { "method": "elicitation/create", "params": { "mode": "form", "message": "Please provide your GitHub username", "requestedSchema": { "type": "object", "properties": { "name": { "type": "string" } }, "required": ["name"] } } } }, "requestState": "eyJsb2NhdGlvbiI6Ik5ldyBZb3JrIn0..." } }
Client retries with a new JSON-RPC id, including inputResponses and requestState.
Example 3 — x-mcp-header for HTTP routing:
Input: Tool parameter region should be mirrored as an HTTP header for load-balancer routing.
Output:
JSON{ "region": { "type": "string", "x-mcp-header": "Region" } }
Call with "region": "us-west1" → client adds header Mcp-Param-Region: us-west1.
- Always include a human-in-the-loop confirmation before invoking tools with side effects; surface which tools are exposed and indicate invocation clearly in UI.
- Use deterministic ordering in
tools/listresponses to support caching and LLM prompt cache hits. - Prefer
{"type": "object", "additionalProperties": false}for parameterless tools (explicit, unambiguous). - Always mirror
structuredContentinto atextcontent block for backward compatibility with clients that don't parse structured output. - Keep tool names within 1–128 chars, using only
[A-Za-z0-9_.-]; treat names as case-sensitive. - When aggregating tools from multiple servers, disambiguate collisions with a server-identifier prefix — never rely on
serverInfo.namefor uniqueness. - Treat tool
annotationsas untrusted metadata unless sourced from a trusted server. - Never mark sensitive parameters (passwords, tokens, PII) with
x-mcp-header— header values are visible to intermediaries. - Validate
x-mcp-headerconstraints strictly (field-name syntax, no control chars, case-insensitive uniqueness, primitive types only, safe integer range) and reject/log non-conforming tools rather than failing the whole list.
- Don't vary the
tools/listresult per-connection or as a side effect of unrelated requests — only authorization-based filtering is allowed. - Don't reuse the same JSON-RPC
idwhen retrying atools/callafter aninput_requiredresponse — it MUST be different. - Don't apply
x-mcp-headertonumber-typed parameters or to non-statically-reachable schema properties — only primitives (string,boolean, safe-rangeinteger). - Don't skip
outputSchemavalidation on the server side — structured content that doesn't conform breaks client contracts. - Don't assume tools with no
listChangeddeclaration will never change — check the capability before relying on notifications. - Don't ignore
x-mcp-headerconstraint violations silently on Streamable HTTP transport — reject the whole tool definition fromtools/list, not just the bad header.