Scaffold an MCP server
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.
The five layers
Every MCP server, regardless of target software, has the same skeleton:
- Server & Transport (
src/index.ts) — stdio entry point, lifecycle, cleanup. - Tool Definitions (
src/tool-definitions.ts) — the contract the AI reads to decide what to call. The single most important file. - Router & Handlers (
src/tool-router.ts,src/handlers/*.ts) — request dispatch, validation, response shaping. - The Bridge (
src/executor.ts,scripts/*) — how you actually talk to the target. 80% of the work. - Security & Validation (
src/utils.ts) — path checks,execFileoverexec, tool filtering.
Step 1: Identify the bridge — this decides everything else
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.
Step 2: Scaffold the files
your-mcp/src/index.ts # Layer 1context.ts # Shared statetool-definitions.ts # Layer 2tool-router.ts # Layer 3executor.ts # Layer 4utils.ts # Layer 5handlers/<domain>-handlers.tsscripts/operations.<lua|gd|py> # Layer 4 if bridge needs itpackage.jsontsconfig.json
Single production dependency: @modelcontextprotocol/sdk. Everything else (TypeScript, ESLint, Vitest) is dev-only.
Step 3: Write ONE tool end to end before anything else
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.
Tool description rules
The description is the only documentation the AI has. Three rules:
- Precise verbs. "Create a new sprite file with the specified dimensions and save it to the given path" beats "Sprite creation tool".
- Enums over strings.
enum: ["RGB", "Grayscale", "Indexed"]constrains the AI to valid values. - Mark required vs optional clearly. If a parameter has a sensible default, make it optional and document the default in the description.
Handler skeleton
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.
Security non-negotiables
execFile, neverexec. Arguments as array — no shell interpretation, no injection through file names.- Path validation on every path parameter. Reject anything containing
..before passing to the bridge. - Process isolation. One subprocess per operation. If the target crashes on bad input, the MCP keeps running and the AI retries.
- Tool filtering via env vars. Support
MCP_TOOLSETS,MCP_EXCLUDE_TOOLS,MCP_READ_ONLYso consumers can lock down what the AI sees. The filter runs before the tool list is exposed.
Adding a new tool — four touch points
- Tool definition in
tool-definitions.ts - Handler function in
handlers/<domain>-handlers.ts - Entry in
HANDLER_MAPintool-router.ts - Operation in the bridge script (if Layer 4 uses one)
Document this in CONTRIBUTING.md so future contributors don't drift.
Source
Based on How to Build an MCP Server — reference implementations at godot-mcp and aseprite-mcp.
