Coding Agents in Practice (3/5) — Building MCP Servers: Spec, Examples, Debugging
Cursor, Claude Code, and VS Code Agent all adopted the Model Context Protocol. Build a server yourself to understand it.
ํต์ฌ ์์ฝ
- MCP is a standard protocol for LLM agents to access external tools, resources, and prompts
- Primary sources: modelcontextprotocol.io spec, Anthropic Claude Code MCP docs
- Three core capabilities: Tools (functions the agent calls) / Resources (read-only data) / Prompts (reusable templates)
- Debugging is
mcp inspector+ stderr logging — that's the de facto standard - Pitfall: a single stray
print()to stdout can break the entire JSON-RPC channel
1. The problem MCP solves
Each agent had its own tool-integration mechanism: Claude Code had one tool spec, Cursor had its own extensions, VS Code had a separate API. The same integration had to be built three times.
MCP standardizes this: write one MCP server, and it works in every MCP-supporting agent (Claude Code, Cursor, VS Code Agent, Continue, etc.). As of 2026 it is effectively the default.
Protocol essentials: JSON-RPC 2.0 + three capabilities (Tools / Resources / Prompts) + three transports (stdio / SSE / Streamable HTTP).
2. The three capabilities
2.1 Tools — Functions the agent calls
The most-used capability. The agent takes action.
- Examples: search_database(query), create_jira_ticket(title, body), run_shell(cmd).
- Definition: name + JSON Schema for arguments + short description.
- Returns text or structured data.
2.2 Resources — Read-only data
Resources the agent reads but does not change.
- Examples: file contents, DB rows, configuration values, API responses.
- Identified by URI: file:///path, db://table/row/123, config://app/setting.
- Unlike tools, no side effects are guaranteed (safe for agents to pre-load).
2.3 Prompts — Reusable templates
Frequent task patterns exposed as prompt templates. - Examples: "review this code," "explain this SQL," "draft a PR message for this diff." - Can be triggered as slash commands.
3. Minimum example — A Python MCP server in 30 lines
Using the mcp Python SDK. This is a real, working MCP server:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("hello-mcp")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the sum."""
return a + b
@mcp.tool()
def greet(name: str) -> str:
"""Greet a person by name."""
return f"Hello, {name}!"
@mcp.resource("config://app/version")
def app_version() -> str:
"""Read-only application version."""
return "1.0.0"
if __name__ == "__main__":
mcp.run(transport="stdio")
Register with Claude Code:
claude mcp add hello-mcp --command "python /abs/path/server.py"
That single command makes add, greet, and config://app/version auto-discovered inside the session.
4. Debugging — Looking inside an invisible channel
MCP's biggest pitfall is that stdout is the protocol channel. Under stdio transport, JSON-RPC messages flow through stdout — adding print("debug") immediately corrupts the channel.
Four debugging rules:
1. Never write to stdout. Logs go to sys.stderr or a file.
2. Use the MCP Inspector — npx @modelcontextprotocol/inspector python server.py opens a GUI to invoke tools/resources/prompts and see responses.
3. Raise errors. Don't swallow with try/except — raising propagates the error to the client.
4. Validate JSON Schema. The SDK auto-generates argument schemas, but inspect complex types yourself.
Common errors: - "server disconnected": process died at startup. Check stderr. - "tool not found": registered but typo or stale cache after signature change. - "invalid arguments": client sent JSON that violates the schema. Use Inspector to inspect the exact argument shape.
5. Choosing a transport
| Transport | Use case | Notes |
|---|---|---|
| stdio | Local server, single user | Simplest. Client spawns server as a child process |
| SSE / Streamable HTTP | Remote server, multi-user | HTTP-based with auth. For data centers / internal services |
Start with stdio. Move to HTTP only when remote use becomes a real requirement.
6. Security checklist — Before trusting an MCP server
Third-party MCP servers carry arbitrary code-execution authority. A tool call is a function execution, and the function can do anything. Before connecting an external server:
- Read the source. Open the GitHub repository and check what system calls the server makes.
- Permission scope. File system access, network, external API keys — what does it require?
- Isolation. Use containers, VMs, or separate OS users when possible.
- Audit logging. Record every tool invocation.
Use catalogs like awesome-mcp-servers as a starting point, but run all four checks before any production connection.
7. At a glance
| Step | Core | Watch out for |
|---|---|---|
| Design | Separate Tools / Resources / Prompts | Side-effecting → Tool, side-effect-free → Resource |
| Implementation | mcp SDK + @mcp.tool() decorator |
Never print to stdout |
| Registration | claude mcp add <name> --command ... |
Use absolute paths |
| Debugging | MCP Inspector + stderr logs | "server disconnected" → check stderr |
| Operation | Permission scope + isolation + audit | External MCP = arbitrary code execution |
Next up
Part 4/5: Multi-Agent Patterns — Orchestrator and Specialist Separation. If MCP integrates external tools, multi-agent patterns are how you split internal work.
References
- Model Context Protocol, Specification — modelcontextprotocol.io (verified 2026-05-05).
- Anthropic, Using MCP with Claude Code — code.claude.com/docs/mcp (verified 2026-05-05).
awesome-mcp-servers— github.com/punkpeye/awesome-mcp-servers.
This is part 3/5 of the Coding Agents in Practice series.
๋๊ธ
๋๊ธ ์ฐ๊ธฐ