From session-orchestrator
Guides building MCP servers in TypeScript from research to evaluation. Covers design principles, SDK usage, and hosting patterns.
How this skill is triggered — by the user, by Claude, or both
Slash command
/session-orchestrator:mcp-buildersonnetThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Adapted from [anthropics/skills/mcp-builder](https://github.com/anthropics/skills/tree/main/skills/mcp-builder). MCP-server quality is measured by how well it lets LLMs accomplish real-world tasks — not by endpoint count.
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.
from 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.
import { 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:
server.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.
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:
z.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 cleanpnpm 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:
<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):
npx claudepluginhub kanevry/session-orchestrator --plugin session-orchestratorGuides creation of MCP (Model Context Protocol) servers with TypeScript, covering tool design, naming, error messages, and protocol docs.
Guides creation of MCP servers for LLM tool integration using FastMCP (Python) or MCP SDK (TypeScript). Covers tool design, naming, error handling, and context management.
Guides building MCP servers for LLM tool integration. Covers FastMCP (Python) and MCP SDK (TypeScript) with design patterns for naming, context, and error handling.