mcp-server-dev
MCP Server Development Skill
Context
You are building MCP servers for LocalCowork, a desktop AI agent where the LLM calls pre-built tools via MCP — it never writes code. Each server is an independent process communicating via JSON-RPC over stdio.
The PRD (docs/PRD.md) is the product source of truth.
The tool registry (docs/mcp-tool-registry.yaml) defines exact tool signatures.
The pattern doc (docs/patterns/mcp-server-pattern.md) is the canonical template.
Before You Start
Read these files in order — they give you the complete picture:
docs/mcp-tool-registry.yaml— find the server you're building and read all its tool definitions (params, returns, confirmation, undo metadata).docs/PRD.mdSection 6 — read the use cases that reference this server to understand how tools will be composed in real workflows. The "Implementation Notes for Claude Code" in each UC are especially important.docs/patterns/mcp-server-pattern.md— the canonical implementation pattern with templates for both TypeScript and Python.mcp-servers/_shared/— the base classes your server must extend.
Language Selection
Check docs/mcp-tool-registry.yaml for the server's designated language:
| Language | Servers |
|---|---|
| TypeScript | filesystem, calendar, email, task, data, audit, clipboard, system |
| Python | document, ocr, knowledge, meeting, security |
The language is non-negotiable — it's driven by dependency requirements (ADR-002).
Implementation Checklist
For each tool in the registry, work through these steps:
Step 1: Create the tool file
One tool per file. Place in mcp-servers/<server>/src/tools/<tool_name>.ts (or .py).
Step 2: Define typed params
Match the registry EXACTLY. Use zod (TypeScript) or pydantic (Python).
TypeScript example:
const paramsSchema = z.object({
path: z.string().describe('Absolute path to directory'),
recursive: z.boolean().optional().default(false),
filter: z.string().optional(),
});
Python example:
class Params(BaseModel):
path: str = Field(description="Path to image file")
language: Optional[str] = Field(default="eng", description="OCR language")
Step 3: Define typed returns
Match the registry return type. Create a result type/interface.
Step 4: Implement the logic
Follow the PRD implementation notes for this tool. Key principles:
- Use the shared Logger (never print/console.log)
- Use structured MCPError for errors (never throw raw exceptions)
- Validate paths are within sandbox before any filesystem access
- Return structured results that the model can parse
Step 5: Set confirmation metadata
Every tool declares two metadata flags:
confirmationRequired: does the Agent Core need user confirmation before executing?undoSupported: can this action be reversed via the undo stack?
These must match docs/mcp-tool-registry.yaml exactly.
Step 6: Write the unit test
Every tool gets a test file covering:
- Param validation: invalid types, missing required, extra params
- Happy path: correct input → expected output shape
- Error paths: file not found, permission denied, invalid input
- Sandbox enforcement: paths outside sandbox are rejected
- Metadata verification: confirmationRequired and undoSupported match registry
Step 7: Write the smoke test
Every tool also gets a smoke test — a FAST (<500ms) regression check. Use /add-smoke-test for the template, or create manually:
- TypeScript:
mcp-servers/<server>/tests/<tool_name>.smoke.test.ts - Python:
mcp-servers/<server>/tests/<tool_name>_smoke_test.py
Smoke tests check:
- Tool can be imported / instantiated
- Params schema accepts valid input
- Params schema rejects garbage input
- Confirmation/undo metadata matches registry
- (Non-destructive tools only) Basic execution doesn't crash
These are auto-discovered by ./scripts/smoke-test.sh and run before every push.
Step 8: Lint and type check
Before moving to the next tool:
- TypeScript:
npx tsc --noEmitmust pass - Python:
mypy --strict src/must pass
Step 9: Run smoke tests
After all tools for the server are done, verify the whole server passes:
./scripts/smoke-test.sh --server <server-name>
Server Entry Point
After all tools are implemented, create the entry point:
- TypeScript:
src/index.ts— imports all tools, registers them, starts JSON-RPC - Python:
src/__init__.py— imports all tools, registers them, starts JSON-RPC
The entry point uses the shared MCPServer base class from _shared/.
Validation
After implementing all tools, run the validation command:
/validate-server <server-name>
This checks every tool against the PRD spec and reports any mismatches.
Common Pitfalls
-
Don't deviate from the registry signatures. Even if a param seems unnecessary, include it — the model's tool definitions are auto-generated from the registry.
-
Don't combine multiple tools into one file. One file per tool is how the project stays navigable across 68+ tools.
-
Don't skip the sandbox check on filesystem tools. Every path must be validated against the user's granted directories.
-
Don't use console.log or print. Always use the shared Logger. The audit system depends on structured log output.
-
Don't forget to handle the case where the model sends bad arguments. The local LLM may occasionally hallucinate params. Validation should catch this gracefully and return a helpful error, not crash.