| name | mcp-server-design |
| description | Design agent-friendly MCP servers with optimal UX. Use when building MCP tools, designing agent APIs, writing tool documentation, implementing error handling, or creating multi-agent coordination systems. |
MCP Server Design
Core Insight: Agents are NOT humans. Design for "agent theory of mind"—anticipate how agents will misuse, misunderstand, or misapply your tools, then build systems that guide them toward success rather than simply rejecting errors.
Why MCP? "Once you have something expressed as an MCP server it makes it easy to use with an LLM in a plug-and-play, self-documented way." — MCP provides standardized tool discovery and invocation.
The One Rule
Make the wrong thing impossible and the right thing obvious.
Every design decision should pass: "If an agent makes an obvious mistake, does this educate them or just fail?"
The 12 Core Principles
| # | Principle | Implementation |
|---|
| 1 | Anticipate Intent | Detect what agents MEANT, not just what they said |
| 2 | Fail Helpfully | Structured errors with suggestions, not stack traces |
| 3 | Intercept Early | Catch mistakes at environment level before tool calls |
| 4 | Forgive by Default | Auto-correct invalid inputs; educate in strict mode |
| 5 | Document for Agents | Do/Don't sections, examples, discovery hints |
| 6 | Scope Narrowly | No broadcast; force explicit, targeted actions |
| 7 | Provide Macros | Bundle common multi-step workflows for reliability |
| 8 | Precision Over Breadth | ≤7 tools per cluster; capability gating reduces context 70% |
| 9 | Workflow-First Design | Guide agents through multi-step tasks with next_actions |
| 10 | Defense in Depth | XSS sanitization, path traversal prevention, EMFILE recovery |
| 11 | Full Observability | Query tracking, slow query detection, cost logging |
| 12 | Graceful Degradation | Suggested tool calls in errors, concurrent creation idempotency |
THE PROMPT
Design an MCP server for [DOMAIN/PURPOSE].
Requirements:
- Tool cluster: [main operations]
- Target agents: [Claude Code, Codex, Gemini CLI, etc.]
- Coordination needs: [single-agent/multi-agent]
Follow mcp-server-design patterns:
1. Define mistake detection for common errors
2. Create structured error types with recovery hints
3. Write agent-friendly tool documentation
4. Implement input normalization and auto-correction
5. Design resources for discovery
6. Add validation scripts
Run checklist before finalizing.
Quick Reference: 65+ Design Patterns
Architecture Patterns
Error Handling Patterns
| Pattern | Description | Reference |
|---|
| ToolExecutionError | Structured errors with type/message/recoverable/data | ERROR-DESIGN.md |
| Exception Mapping | SQLAlchemy → NOT_FOUND, TypeError → hints | ERROR-DESIGN.md |
| Fuzzy Suggestions | SequenceMatcher for typo recovery | ERROR-DESIGN.md |
| EMFILE Recovery | Whitelist safe-to-retry tools, clear caches | ERROR-DESIGN.md |
| Suggested Tool Calls | recoverable=True + suggested_tool_calls payload | ERROR-DESIGN.md |
| IntegrityError Idempotency | Concurrent creation returns existing record | ERROR-DESIGN.md |
| Stale Resource Release | Multi-dimensional heuristics for abandoned reservations | ERROR-DESIGN.md |
Validation Patterns
Agent UX Patterns
Git Integration Patterns
Query Optimization Patterns
Installation Patterns
Database Patterns
LLM Integration Patterns
Testing Patterns
Workflow
Phase 1: Design
Phase 2: Anticipate Failures
Phase 3: Implement
Phase 4: Document
Phase 5: Install & Integrate
Phase 6: Validate
Tool Documentation Template
Every tool MUST have these sections:
"""
Brief one-liner description.
Discovery
---------
How to find required parameter values:
- project_key: Use `pwd` for absolute working directory
- agent_name: Use resource://agents/{project_key}
- thread_id: Use resource://threads/{project_key}
When to use
-----------
- Scenario 1 that triggers this tool
- Scenario 2
- NOT for: scenario that should use different tool
Parameters
----------
param_name : type
Description. MUST be [constraint]. (RECOMMENDED: [suggestion])
Returns
-------
dict
{
field1: type, # Description
field2: type, # Description
next_actions: list[str] # Suggested follow-up actions
}
Do / Don't
----------
Do:
- Specific positive guidance with reason
- Another best practice
Don't:
- Specific anti-pattern with consequence
- Another mistake to avoid
Examples
--------
Basic usage:
```json
{"jsonrpc":"2.0","id":"1","method":"tools/call","params":{"name":"tool_name","arguments":{...}}}
Common mistakes
- Mistake 1: explanation and fix
- Mistake 2: explanation and fix
Idempotency
- Safe to call multiple times. Returns existing record if already exists.
Edge cases
- If X is empty: [behavior]
- If Y not found: [behavior with suggestions]
"""
**Full template:** [TOOL-DOCUMENTATION.md](references/TOOL-DOCUMENTATION.md)
---
## Structured Error Design
```python
class ToolExecutionError(Exception):
def __init__(
self,
error_type: str, # Machine-parseable category
message: str, # Human-readable explanation
*,
recoverable: bool = True, # Should agent retry?
data: dict = None # Structured hints
):
self.error_type = error_type
self.recoverable = recoverable
self.data = data or {}
def to_payload(self) -> dict:
return {
"error": {
"type": self.error_type,
"message": str(self),
"recoverable": self.recoverable,
"data": self.data, # suggestions, fix_hint, available_options
}
}
Error types: BROADCAST_ATTEMPT, PROGRAM_NAME_AS_AGENT, CONFIGURATION_ERROR, NOT_FOUND, INVALID_ARGUMENT, FILE_RESERVATION_CONFLICT, EMFILE, TIMEOUT, etc.
Deep dive: ERROR-DESIGN.md
Mistake Detection System
Detect what agents MEANT when they make errors:
_KNOWN_PROGRAMS = frozenset({
"claude-code", "codex-cli", "cursor", "copilot", "gemini-cli",
"aider", "cline", "windsurf", "continue", "bolt", "devin", ...
})
_MODEL_PATTERNS = ("gpt-", "claude-", "opus", "sonnet", "haiku", "llama", "mistral", ...)
_BROADCAST_KEYWORDS = {"all", "*", "everyone", "@all", "@everyone", "team", "channel"}
_DESCRIPTIVE_SUFFIXES = ("agent", "bot", "worker", "handler", "migrator", "harmonizer", ...)
def _detect_mistake(value: str) -> tuple[str, str] | None:
"""Returns (error_type, helpful_message) or None."""
if value.lower() in _KNOWN_PROGRAMS:
return ("PROGRAM_NAME_AS_AGENT",
f"'' is a program name. Use 'program' parameter instead.")
(p value.lower() p _MODEL_PATTERNS):
(,
)
value.lower() _BROADCAST_KEYWORDS:
(,
)
(value.lower().endswith(s) s _DESCRIPTIVE_SUFFIXES):
(,
)
Full patterns: MISTAKE-DETECTION.md
Input Normalization
Auto-correct instead of rejecting:
MODES = {
"strict": "Reject invalid, return detailed error",
"coerce": "Auto-fix invalid, use valid result (DEFAULT)",
"always_auto": "Ignore user input, always auto-generate",
}
def normalize_timestamp(value: str) -> datetime:
"""Smart coercion for timestamps."""
normalized = value.strip()
normalized = normalized.replace("/", "-")
if normalized.endswith("Z"):
normalized = normalized[:-1] + "+00:00"
if not re.search(r"[Z+-]", normalized):
normalized += "+00:00"
return datetime.fromisoformat(normalized)
def sanitize_fts_query(query: str) -> str | None:
"""Clean FTS5 queries."""
query = re.sub(r"^\*+", "", query)
if query in ("*", "**", "."):
return None
return query
Deep dive: VALIDATION-PATTERNS.md
The "Fake CLI" Pattern
Agents often confuse MCP servers with CLI tools. Solution:
#!/usr/bin/env bash
cat <<'MSG'
+=====================================================================+
| This is NOT a CLI tool! |
| |
| It's an MCP server. Use the MCP tools directly: |
| - mcp__mcp-agent-mail__register_agent |
| - mcp__mcp-agent-mail__send_message |
| - mcp__mcp-agent-mail__fetch_inbox |
| |
| WRONG: mcp-agent-mail send --to BlueLake |
| RIGHT: Use MCP tools in your agent |
+=====================================================================+
MSG
exit 1
Create symlinks for all naming variations:
mcp-agent-mail
mcp_agent_mail
mcpagentmail
agentmail
Full patterns: INSTALLATION-PATTERNS.md
Resource Design for Discovery
Agents need to know WHERE to find parameter values:
RESOURCES = [
"resource://projects",
"resource://project/{slug}",
"resource://agents/{project_key}",
"resource://file_reservations/{slug}",
"resource://message/{id}",
"resource://thread/{thread_id}",
"resource://inbox/{agent}",
"resource://outbox/{agent}",
"resource://views/urgent-unread/{agent}",
"resource://views/ack-required/{agent}",
"resource://views/acks-stale/{agent}",
"resource://tooling/metrics",
"resource://tooling/recent",
"resource://tooling/capabilities",
]
Full patterns: RESOURCE-DESIGN.md
Macro Tools for Workflows
Bundle multi-step operations for smaller models:
@mcp.tool
def macro_start_session(project_key: str, program: str, model: str) -> dict:
"""
Boot a complete session in one call:
1. ensure_project(project_key)
2. register_agent(project_key, program, model)
3. fetch_inbox(project_key, agent_name)
Returns combined result with next_actions hints.
"""
project = await ensure_project(project_key)
agent = await register_agent(project_key, program, model)
inbox = await fetch_inbox(project_key, agent["name"])
return {
"project": project,
"agent": agent,
"inbox": inbox,
"next_actions": [
"Consider file_reservation_paths before editing",
"Check inbox for urgent messages",
"Renew file reservations in 30 minutes if still working"
]
}
Other macros:
macro_prepare_thread — Join thread with context
macro_file_reservation_cycle — Reserve → work → release
macro_contact_handshake — Request + approve + welcome
No Broadcast Philosophy
"Not every agent NEEDS to know everything. That would be distracting and waste context space."
Implementation:
if recipient.lower() in {"all", "*", "everyone", "@all"}:
raise ToolExecutionError(
"BROADCAST_ATTEMPT",
"Broadcast not supported. List specific recipient names. "
"This design is intentional: targeted communication is more efficient "
"and prevents context waste.",
recoverable=True,
data={
"available_recipients": await list_agents(project),
"philosophy": "Targeted > Broadcast for agent coordination"
}
)
Git Integration Best Practices
@contextmanager
def _git_repo(path: str) -> Generator[Repo, None, None]:
repo = Repo(path)
try:
yield repo
finally:
repo.close()
class _LRURepoCache:
def __init__(self, maxsize: int = 16):
self._cache = OrderedDict()
self._maxsize = maxsize
def get(self, key: str) -> Repo | None:
if key in self._cache:
self._cache.move_to_end(key)
return self._cache[key]
return None
def put(self, key: str, repo: Repo) -> None:
if len(self._cache) >= self._maxsize:
_, evicted = self._cache.popitem(last=False)
evicted.close()
self._cache[key] = repo
Full patterns: GIT-INTEGRATION.md
Checklist: Before Shipping
Error Handling
Documentation
Input Handling
Agent UX
Git Integration
Query Optimization
Testing
Reference Index
Quick Search
grep -ri "broadcast" .claude/skills/mcp-server-design/references/
grep -i "error_type" .claude/skills/mcp-server-design/references/
grep -A 10 "Do / Don't" .claude/skills/mcp-server-design/references/
grep -B 2 -A 20 "```python" .claude/skills/mcp-server-design/references/