Building Your First MCP Server for Claude Code

Teach Claude Code your world — your API, your database, your tools — with a Model Context Protocol server. It's smaller than you think.

Claude Code knows how to write code, read your files, and run commands. What it does not know is anything specific to your world — your internal API, your team's ticketing system, that weird database only you can reach. The Model Context Protocol (MCP) is how you teach it. An MCP server is a small program that exposes tools, data, or prompts to Claude Code through a standard interface, and building your first one is smaller than you think.

What MCP actually is

MCP is an open standard for connecting an AI client (like Claude Code) to external capabilities. A server speaks the protocol and offers up to three kinds of things:

  • Tools — callable functions Claude can invoke, each with an input schema. search_issues(query), deploy(service). Claude decides when to call them based on the task.
  • Resources — reference data Claude can pull in with an @ mention. @docs:file://api/auth. Not called, just attached to context.
  • Prompts — templated instructions that show up as / commands. /mcp__github__pr_review 123.

Most first servers are just a tool or two, so that is what we will build.

Registering a server with Claude Code

You add servers with claude mcp add. The two shapes you will use most:

# A local server that runs on your machine (stdio)
claude mcp add my-server -- python server.py

# A remote HTTP server
claude mcp add --transport http github https://api.githubcopilot.com/mcp/

The -- in the local form is load-bearing: everything after it is the command that launches your server. Leave it out and Claude Code tries to parse your server's flags as its own — a classic first-timer error.

Servers have a scope, set with --scope:

  • local (default) — just you, just this project
  • project — written to a .mcp.json file in the repo, shared with your team (they will be asked to approve it the first time, for safety)
  • user — just you, across all your projects

Manage them with claude mcp list, claude mcp get <name>, claude mcp remove <name>, or the /mcp command inside a session.

Transports: local vs remote

There are a few transports, but the choice is basically:

  • stdio — your server runs as a local process; Claude Code talks to it over standard in/out. Use this when the tool needs to touch your machine, your filesystem, or a database only you can reach.
  • HTTP — your server runs somewhere reachable over the network. This is the recommended choice for shared or cloud services, and it supports OAuth. (An older SSE transport exists but HTTP has superseded it.)

For your first server, stdio is the path of least resistance — no hosting, no auth, just a script.

The smallest useful server

Here is a complete stdio server in Python that exposes one tool, using the official MCP SDK:

from mcp.server import Server
from mcp.server.stdio import stdio_server

server = Server("my-server")

@server.list_tools()
async def list_tools():
    return [
        {
            "name": "greet",
            "description": "Greet someone by name",
            "inputSchema": {
                "type": "object",
                "properties": {"name": {"type": "string"}},
                "required": ["name"],
            },
        }
    ]

@server.call_tool()
async def call_tool(name, arguments):
    if name == "greet":
        return f"Hello, {arguments['name']}!"
    raise ValueError(f"Unknown tool: {name}")

if __name__ == "__main__":
    stdio_server(server).run()

That is the whole thing. Two handlers: one that lists the tools you offer (name, description, and a JSON Schema for the arguments), and one that runs a tool when Claude calls it. Register it with claude mcp add my-server -- python server.py, and Claude Code can now call greet.

The exact SDK surface differs slightly between the Python and TypeScript packages and evolves over time, so once you have the shape in your head, the official SDK (github.com/modelcontextprotocol) is the source of truth for current syntax. But the concept never changes: list what you offer, execute what gets called.

The description is the real interface

Here is the thing beginners underestimate: Claude chooses whether to call your tool based on its name and description, not its code. A tool called greet described as "Greet someone by name" will get used in exactly the situations that phrase implies. A vague description means the tool gets called at the wrong times, or never. Treat the description and the schema field descriptions as prompt engineering — because that is exactly what they are. This is the single highest-leverage part of an MCP server, and it lives entirely in strings, not logic.

Gotchas to save you time

  • Forgetting -- on a stdio server — everything after it is the launch command; without it, registration breaks.
  • An HTTP server needs "type": "http" if you configure it via JSON; a bare url is assumed to be stdio and errors out.
  • Project-scoped servers prompt for approval on first use — that is a security feature, not a bug.
  • ${VAR} expansion works inside .mcp.json for command, args, env, url, and headers, so you can keep secrets out of the committed file.
  • Stdio servers get CLAUDE_PROJECT_DIR in their environment — use it for project-relative paths instead of trusting the working directory.

Where to go from here

Once greet works, everything else is variations on the same theme: swap the toy handler for a real one that queries your database, hits your internal API, or reads your design system, and give it a crisp description. That is the whole trick to extending Claude Code — you are not modifying the model, you are handing it new verbs. And the first time you watch it reach for a tool you wrote, unprompted, to solve a problem in your domain, the protocol stops feeling like plumbing and starts feeling like a superpower.


Related reading