Scaffold a new Model Context Protocol (MCP) server in TypeScript using the proven five-layer architecture. Use when the user wants to build an MCP server, expose a piece of software to AI agents (Claude Code, Cursor, Cline, Windsurf), connect a tool to an LLM, or asks "how do I make X accessible to AI?". Also use when starting any new agent-tool integration where the target software has its own scripting API, CLI, or HTTP surface.
Every MCP server, regardless of target software, has the same skeleton:
src/index.ts) — stdio entry point, lifecycle, cleanup.src/tool-definitions.ts) — the contract the AI reads to decide what to call. The single most important file.src/tool-router.ts, src/handlers/*.ts) — request dispatch, validation, response shaping.src/executor.ts, scripts/*) — how you actually talk to the target. 80% of the work.src/utils.ts) — path checks, execFile over exec, tool filtering.Ask: how does the target software let me automate it?
| Target offers | Bridge pattern | Example |
|---|---|---|
| Scripting API (Lua, GDScript, Python) | Subprocess + script-injection dispatcher in the target's language | Aseprite (Lua), Godot (GDScript) |
| REST / HTTP API | fetch calls from handlers | Any web service |
| Rich CLI | execFile directly | FFmpeg, ImageMagick |
| Socket / TCP / WebSocket server | Persistent connection, manage state in ServerContext | Godot interactive mode |
| Native SDK | Direct import if Node-compatible | SQLite, native modules |
If two patterns apply, prefer the lower-state one (subprocess > persistent socket). State means lifecycle management.
your-mcp/
src/
index.ts # Layer 1
context.ts # Shared state
tool-definitions.ts # Layer 2
tool-router.ts # Layer 3
executor.ts # Layer 4
utils.ts # Layer 5
handlers/
<domain>-handlers.ts
scripts/
operations.<lua|gd|py> # Layer 4 if bridge needs it
package.json
tsconfig.json
Single production dependency: @modelcontextprotocol/sdk. Everything else (TypeScript, ESLint, Vitest) is dev-only.
Do not define 50 tools up front. Pick the simplest useful operation (get_version, create_file), wire it through all five layers, run the inspector, watch the AI call it. Once one tool works, adding more is mechanical.
npx @anthropic/mcp-inspector build/index.js
If the tool list reads as confusing to you, it reads as confusing to the AI.
The description is the only documentation the AI has. Three rules:
enum: ["RGB", "Grayscale", "Indexed"] constrains the AI to valid values.Every handler does four things, in this order:
typescriptasync function handleX(args, ctx) {const params = normalizeParameters(args); // snake_case → camelCasevalidatePath(params.inputPath); // securityconst result = await executeOperation(...); // call the bridgereturn { content: [{ type: "text", text: result.stdout }], isError: false };}
Group handlers by domain. Each file 3–8 functions, none over ~25 KB. The codebase stays maintainable at 60+ tools.
execFile, never exec. Arguments as array — no shell interpretation, no injection through file names... before passing to the bridge.MCP_TOOLSETS, MCP_EXCLUDE_TOOLS, MCP_READ_ONLY so consumers can lock down what the AI sees. The filter runs before the tool list is exposed.tool-definitions.tshandlers/<domain>-handlers.tsHANDLER_MAP in tool-router.tsDocument this in CONTRIBUTING.md so future contributors don't drift.
Based on How to Build an MCP Server — reference implementations at godot-mcp and aseprite-mcp.