·4 min read#mcp#how-to
Build an MCP Server in an Afternoon
MCP is for when Claude needs to do something at runtime that a prompt can't describe. That's the whole test. If the capability is knowledge, a procedure, a voice, a checklist, you want a skill, a SKILL.md that costs you twenty minutes and zero infrastructure. If the capability is a live database query, an authenticated API call, or a side effect you don't want expressed as arbitrary shell commands, you want an MCP server. Building the minimal version of one takes an afternoon, and most of that afternoon is deciding what the tool should return.
The 40-line server
An MCP server over stdio is a process that speaks JSON-RPC on stdin/stdout. Claude Code spawns it, asks it what tools it has, and calls them. You never touch the protocol directly; the official SDKs handle the handshake. Here's a complete server in Python using the mcp package's FastMCP wrapper:
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("dealflow")
@mcp.tool()
def lookup_company(name: str) -> str:
"""Look up a company in the local dealflow database.
Returns stage, last contact date, and notes."""
# Replace with your real data source: sqlite, an internal API, a CSV.
db = {
"acme": "Stage: term sheet. Last contact: 2026-07-02. Notes: waiting on cap table.",
}
return db.get(name.lower(), f"No record for {name}.")
if __name__ == "__main__":
mcp.run() # stdio transport by default
Install the dependency (pip install mcp or uv add mcp), run python server.py manually once to confirm it doesn't crash on import, and the server side is done. The docstring matters more than it looks: it's the tool description the model reads when deciding whether to call you. Write it like you're explaining the tool to a competent colleague, including what the return value looks like.
The TypeScript path is the same shape with @modelcontextprotocol/sdk: instantiate McpServer, register a tool with a Zod schema, connect a StdioServerTransport. Pick whichever language your data source already lives in.
Wiring it into Claude Code
Two options. For your own machine:
claude mcp add dealflow -- python /abs/path/to/server.py
For a project, so teammates get it automatically, put a .mcp.json at the repo root:
{
"mcpServers": {
"dealflow": {
"command": "python",
"args": ["server.py"]
}
}
}
Restart Claude Code, run /mcp, and you should see dealflow connected with one tool. Inside a session the tool surfaces as mcp__dealflow__lookup_company. Ask "what stage is Acme at?" and watch it get called. If the server shows as failed, the debugging loop is always the same: run the command yourself in a terminal, because 90% of MCP failures are the process dying on startup (missing dep, wrong Python, relative path resolved from the wrong cwd). Use absolute paths in the config and this class of bug disappears.
One protocol rule that bites people: on stdio transport, stdout belongs to JSON-RPC. If your server print()s a debug line, you've corrupted the stream. Log to stderr.
MCP versus a plain skill
This is where most people over-build. Claude Code plugins have four surfaces: skills, commands, hooks, and MCP servers. Skills win far more often than the excitement around MCP suggests.
| You need | Use |
|---|---|
| Domain knowledge, judgment, a writing voice, a procedure | Skill (SKILL.md + references/) |
| A repeatable prompt you trigger by name | Command |
| Something enforced on every edit or tool call | Hook |
| Live data, authenticated APIs, controlled side effects | MCP server |
A skill is a SKILL.md with YAML frontmatter (name, description) and optional reference files, loaded on demand when the description matches the task. It's inert text. It can't be down, can't have a dependency conflict, doesn't spawn a process, and costs context only when actually used. An MCP server is a running program: its tool definitions load into every session that has it enabled, it needs its runtime installed, and it can fail in ways a Markdown file cannot.
The honest decision rule: if Claude could accomplish the task with Bash and curl given the right instructions, you probably want a skill that contains those instructions, not a server. MCP earns its keep in three cases. First, credentials: the server holds the API key, so it never appears in context or in a shell command. Second, shaped access: you want Claude querying your CRM through three vetted read-only tools, not composing raw SQL. Third, statefulness: a connection pool, an OAuth refresh loop, anything that shouldn't be re-established per command.
Shipping it in a plugin
If the server is worth sharing, package it. A Claude Code plugin ships all four surfaces from one directory: .claude-plugin/plugin.json for metadata, skills/ for skills, and an .mcp.json inside the plugin for servers, with ${CLAUDE_PLUGIN_ROOT} available so paths resolve wherever the plugin lands. List it in a marketplace, which is just a marketplace.json naming your plugins and their sources. One sharp edge if you host the marketplace on your own domain: plugin sources referenced over a URL must be git-backed (a repo, or a git-subdir source pointing into one). A relative HTTP path in marketplace.json doesn't error, it silently fails at install time. Verified on Claude Code CLI 2.1.x.
Then anyone gets your server, plus the skills that teach Claude when to use it, with /plugin marketplace add <url> followed by /plugin install <name>@<marketplace>. Which is the real lesson of the afternoon: the server is the easy half. The judgment about when to call it, what the outputs mean, what to do next, that lives in prose. Ship both.
Install a person
installs.me turns your files, calendar and calls into a Claude Code plugin that thinks like you. Anyone installs it with two commands:
/plugin marketplace add https://installs.me/lautaro
/plugin install lautaro@lautaro-installs