Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when creating a new MCP (Model Context Protocol) server, extending an existing one, or debugging tool discoverability/performance. Guides through research → implementation → test → eval phases with TypeScript-first guidance matching our stack. Trigger on phrases like "build an MCP server", "expose X as an MCP tool", "write MCP tools for Y", "integrate Z via MCP".
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 65% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 76% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 82% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 98% | 0% |
Adapted from anthropics/skills/mcp-builder. MCP-server quality is measured by how well it lets LLMs accomplish real-world tasks — not by endpoint count.
stdio for local tools, Streamable HTTP (stateless JSON) for remote@modelcontextprotocol/sdkAPI coverage vs. workflow tools. Balance comprehensive endpoint coverage with specialized workflow shortcuts. Default to coverage unless you have a clear reason — agents compose basic tools well; workflow tools ossify.
Tool naming & discoverability. Consistent prefix + action verb. Examples:
github_create_issue, github_list_reposgitlab_search_issues, gitlab_close_mrContext management. Return focused, paginated data. Agents suffer when a single tool call floods context.
Actionable error messages. Errors must guide the next action:
❌ "Invalid input"
✅ "Field 'project_id' is required. Call gitlab_list_projects to enumerate available IDs."https://modelcontextprotocol.io/sitemap.xml.md to any page URL for markdown (e.g. https://modelcontextprotocol.io/specification/draft.md)Focus on: tool definitions, resource definitions, transport mechanisms.
https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.mdhttps://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.mdFetch via WebFetch only when needed — don't dump entire docs into context upfront.
Before writing a line of implementation code, choose a hosting pattern. The wrong choice cannot be refactored cheaply once tooling is wired.
≤ 5 tools AND latency-critical (<50ms tool resolution)?
│
├─ Yes → tools share the SDK process AND no external auth required?
│ │
│ ├─ Yes → In-process @tool decorator (single-process, sub-ms resolution)
│ └─ No → Stdio MCP Server
│
└─ No → Stdio MCP Server
(≥ 6 tools, external auth, language/runtime mismatch, long-lived process)Use create_sdk_mcp_server when your tools live entirely inside the SDK process and you need the lowest possible latency. Source reference: examples/mcp_calculator.py L11–99.
pythonfrom claude_agent_sdk import tool, create_sdk_mcp_server @tool(name="add", description="Add two numbers", input_schema={"a": int, "b": int}) async def add(args): return {"content": [{"type": "text", "text": str(args["a"] + args["b"])}]} server = create_sdk_mcp_server(name="calc", version="1.0.0", tools=[add])
Our default stack uses McpServer.registerTool() from @modelcontextprotocol/sdk. The inline Zod schema is parsed at registration time — no separate schema file needed for small tool sets.
typescriptimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; const server = new McpServer({ name: 'calc', version: '1.0.0' }); server.registerTool( 'add', { title: 'Add two numbers', inputSchema: { a: z.number(), b: z.number() }, }, async ({ a, b }) => ({ content: [{ type: 'text', text: String(a + b) }], }), );
readOnlyHint and destructiveHintAnnotations are first-class SDK metadata that Claude and downstream hooks use for permission decisions. Set them on every tool:
typescriptserver.registerTool( 'delete-file', { title: 'Delete a file', inputSchema: { path: z.string() }, annotations: { readOnlyHint: false, destructiveHint: true }, }, handler, );
readOnlyHint: true — signals the tool only reads state; Claude can call it freely without a permission prompt.destructiveHint: true — signals irreversible side effects; our pre-bash-destructive-guard hook and agents/security-reviewer.md both elevate review priority for tools carrying this flag. Any tool that deletes, overwrites, or mutates shared state must set this.destructiveHint: true on a destructive tool is a known pitfall — see the "Common pitfalls" table below.| Aspect | In-Process @tool | Stdio MCP Server | |--------|-----------------|------------------| | Tool count | ≤ 5 | 6+ | | Latency | Sub-ms resolution | 5–50 ms IPC overhead | | Auth complexity | Shares SDK auth | Separate auth context | | Language constraint | Must match SDK | Any runtime | | Process isolation | None (in-SDK) | Full (separate child) | | Lifecycle | Bound to SDK session | Long-lived independent |
For the stdio MCP server implementation path (≥ 6 tools, external auth, or language mismatch), continue with Phase 2 — Implementation below, which covers project structure, core infrastructure, and the full TypeScript stdio setup.
If a future capability is MCP-only (no good native CLI of its own), the token-frugal path skips full .mcp.json wiring:
mcporter generate-cli <server> --bundle mints a schema-baked standalone CLI for one MCP server (real flags: --compile, --bundler rolldown|bun, --output <path>, --include-tools <csv> / --exclude-tools <csv>). A driver skill dispatches it via Bash, writes deterministic JSON to a run-dir, and the orchestrator parses from disk — never via prompt context, so token cost stays flat. This is the same pattern skills/playwright-driver/SKILL.md and skills/peekaboo-driver/SKILL.md already use (dispatch CLI → write AX-snapshot/JSON → parse from disk).mcporter call <server>.<tool> (also: mcporter call --server <s> --tool <t> --args '{...}') invokes a single MCP tool without a standing .mcp.json entry — a concrete implementation of the projects-baseline MCP-002 discipline ("no cargo-cult .mcp.json"). Note: MCP-002 is a baseline cross-repo reference, not a local mandate in this repo.MCPJungle context: this repo's local MCP layer is session-orchestrator declared in .mcp.json (a bash-based server). Baseline MCP aggregation is MCPJungle (machine-level gateway, not wired here). mcporter is therefore an alternative recipe for MCP-only drivers, not a drop-in for MCPJungle — it is optional (skills/repo-audit/SKILL.md Category 9 already uses it with graceful-degrade). Only reach for this pattern when a real MCP-only-driver need arises; this is a forward-looking planning note.
mcp-server-name/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts (server entry, transport wiring)
│ ├── tools/ (one file per tool or tool group)
│ ├── schemas.ts (shared Zod schemas)
│ └── client.ts (API client with auth + error handling)
└── README.md (setup + config)Build once, reuse everywhere:
For each tool:
Input schema — Zod, with descriptions per field:
tsz.object({ projectId: z.string().describe("GitLab project ID. Call gitlab_list_projects to discover."), state: z.enum(["opened", "closed", "all"]).default("opened"), });
Output schema — define outputSchema where possible; use structuredContent in tool responses (TS SDK feature). This helps downstream agents parse results.
Annotations — set all four:
readOnlyHint: true/falsedestructiveHint: true/falseidempotentHint: true/falseopenWorldHint: true/falseThese inform Claude's hook decisions (destructive-guard, permission prompts).
Implementation — async/await for I/O; errors must surface with enough context for the LLM to fix them.
tsgo --noEmit or tsc --noEmit cleanbashpnpm build # or npm run build in non-pnpm projects npx @modelcontextprotocol/inspector # interactive testing UI
Walk through every tool in the Inspector. If a tool can fail, trigger the failure and verify the error message is actionable.
Create 10 evaluation questions. An MCP server without evals is a guess, not a deliverable.
Each question must be:
xml<evaluation> <qa_pair> <question>Which GitLab project in group 'X' has the highest number of open issues labeled 'bug'?</question> <answer>project-name-here</answer> </qa_pair> </evaluation>
Run the eval via: Claude-with-MCP-server on each question, compare output to expected answer. Any eval below 80% accuracy signals tool-design problems (usually: unclear descriptions, missing pagination, or bad error messages).
| Pitfall | Fix | |---------|-----| | Tool returns 10k rows, agent context blows up | Add pagination + default page size | | Agent can't figure out auth failure | Error message: "Set ENV_VAR_NAME — current value is empty" | | Tool name collision across MCP servers | Always prefix with service name | | Destructive tools without destructiveHint: true | Breaks our destructive-guard hook | | Async errors swallowed | Wrap every handler in try/catch that returns structured error |
Upstream reference material (worth reading once, not mirroring here):
Other measured skills in the registry, with their headline benchmark lift.