How to Connect Claude Code to a Custom MCP Server, Step by Step
The short answer: write a server that speaks the Model Context Protocol over stdio, register it with one command, and confirm it
NexaSphere Team
Author

The short answer: write a server that speaks the Model Context Protocol over stdio, register it with one command, and confirm it loaded.
# 1. your server runs as a normal process
python /abs/path/to/server.py
# 2. tell Claude Code about it
claude mcp add changelog -- python /abs/path/to/server.py
# 3. verify inside a session
/mcp
That is the whole loop. Everything below is the part that decides whether the server is actually useful once it is connected, which is where most of the real work lives.
What Claude Code needs from your server
MCP is a client/server protocol. Claude Code is the client. Your server exposes some combination of tools (functions the model can call), resources (read-only data the model can pull in), and prompts (reusable templates the user can invoke). In practice, tools carry almost all of the weight.
Servers talk over one of two transport styles:
- stdio: Claude Code launches your server as a child process and speaks JSON-RPC over stdin and stdout. This is the default for anything running on your own machine.
- HTTP: your server is already running somewhere and Claude Code connects to a URL. Use this for shared or hosted servers.
Start with stdio. It has no ports, no auth, and no deployment story, so you can get a working tool in about ten minutes.
Step 1: Write the smallest server that does one real thing
Resist the urge to expose fifteen tools on day one. Pick a single job you actually repeat by hand.
Python, using the official SDK:
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("changelog")
@mcp.tool()
def recent_releases(repo: str, limit: int = 5) -> str:
"""Return the most recent release titles and dates for a repo.
Args:
repo: owner/name, for example "acme/widgets"
limit: how many releases to return, newest first
"""
rows = fetch_releases(repo, limit) # your code
return "\n".join(f"{r.tag} ({r.date}): {r.title}" for r in rows)
if __name__ == "__main__":
mcp.run()
TypeScript, same idea:
// server.ts
const server = new McpServer({ name: "changelog", version: "1.0.0" });
server.tool(
"recent_releases",
"Return the most recent release titles and dates for a repo.",
{ repo: z.string().describe('owner/name, e.g. "acme/widgets"'),
limit: z.number().default(5) },
async ({ repo, limit }) => ({
content: [{ type: "text", text: await fetchReleases(repo, limit) }],
})
);
await server.connect(new StdioServerTransport());
One rule that will save you an afternoon: on stdio, never write to stdout. Stdout is the protocol channel. A stray print() or console.log corrupts the JSON-RPC stream and the server will fail to connect with an error that does not point at your logging. Log to stderr instead.
Step 2: Test it before Claude Code ever sees it
Debugging a broken server through a chat interface is miserable. Use the MCP Inspector, which gives you a browser UI that lists your tools and lets you call them with real arguments.
npx @modelcontextprotocol/inspector python /abs/path/to/server.py
If your tool does not appear in the Inspector, it will not appear in Claude Code. Fix it here.
Step 3: Register the server
The claude mcp add command takes a name, then --, then the exact command used to launch the process:
claude mcp add changelog -- python /abs/path/to/server.py
Use absolute paths. Claude Code does not necessarily launch the server from the directory you are sitting in, and a relative path is the single most common reason a server never starts.
If your project uses a package manager that owns the environment, put it in the launch command:
claude mcp add changelog -- uv run --directory /abs/path/to/project server.py
Secrets go through environment variables, not into the code:
claude mcp add changelog -e GITHUB_TOKEN=ghp_xxx -- python /abs/path/to/server.py
Pick the right scope
Scope decides who gets the server and where the config is written.
-s local(the default): just you, just this project. Good for experiments and anything holding a personal token.-s project: writes a.mcp.jsonfile at the repo root that you commit. Everyone who checks out the repo gets the server. Claude Code asks each person for approval before running a project-scoped server, which is the correct behavior given that a config file is telling their machine to execute a command.-s user: available to you across every project on the machine. Good for general utilities.
For a team server, project scope plus environment variables is the pattern that holds up. Commit the .mcp.json, keep the credentials out of it, and document which variables the server expects.
Step 4: Connect to a remote server instead
If the server is already hosted, skip the process management entirely:
claude mcp add --transport http notes https://mcp.example.com/mcp
There is also an SSE transport for older servers. Authenticated servers either accept a header you pass at registration time or run an OAuth flow that Claude Code triggers from the /mcp menu when you first connect. If the server requires interactive authorization, you have to complete that in an interactive session, not a scripted one.
Step 5: Verify, then make the tools legible
Inside a session, run /mcp. You should see the server listed as connected, with its tools. Outside a session, claude mcp list shows every configured server and claude mcp get <name> shows the resolved command and environment for one of them. If a server is failing, claude --debug prints the startup output, including whatever your server wrote to stderr.
Connection is the easy half. The half that determines whether the server is worth having is the tool descriptions.
The model chooses tools by reading their names, descriptions, and parameter schemas. Nothing else. A tool named query described as "runs a query" will get called at the wrong times and skipped at the right ones. Write descriptions the way you would write them for a competent new hire who has never seen your system:
- Say what the tool returns, not just what it does.
- Say when not to use it, if there is an adjacent tool that overlaps.
- Give parameter examples in the schema descriptions, especially for anything with a format (identifiers, date ranges, enums).
- Return structured, compact text. Dumping a raw 200 KB JSON blob into the context window is a real cost, and the model reads a formatted summary far more reliably.
A useful discipline: after adding a tool, start a fresh session and ask for the task in the words you would naturally use. If the model does not reach for your tool, the description is wrong. That is a faster signal than any test suite.
The failures that account for most of the pain
- Relative paths. Always absolute, for the script and for anything it opens.
- Stdout pollution. Any library that prints on import will break a stdio server. Log to stderr.
- The wrong interpreter. The server runs with whatever
pythonornoderesolves to in the launch environment, which may not be your shell's virtualenv. Point at the interpreter directly or wrap it in your package manager's run command. - Silent auth failures. If your tool swallows a 401 and returns an empty string, the model will confidently report there is no data. Return errors as errors, with the actual message.
A security note worth taking seriously
An MCP server is arbitrary code that runs on your machine with your permissions, and its tool descriptions become instructions the model reads. Both halves matter. Only add servers whose source you have read or whose publisher you trust. Give the server the narrowest credential that does the job, not your personal access token with full scopes. Prefer read-only tools by default and make anything destructive require an explicit, obvious argument. If a server pulls in third-party content, treat that content as untrusted input, because a model reading it will treat text as text.
FAQ
Do I need to restart Claude Code after adding a server?
Yes, for a new session to pick it up. The config is read at startup, so add the server, then start a session and check /mcp.
Where does the configuration actually live?
Local and user scoped servers go into your Claude Code configuration file outside the repo. Project scoped servers go into .mcp.json at the repo root, which is designed to be committed.
Can I use one server across multiple projects?
Yes. Register it with -s user and it is available everywhere on that machine.
Tools versus resources: which should I build? Build tools. Resources are for read-only data the user or model explicitly attaches, and they are useful for reference material. Anything that takes parameters or performs an action should be a tool.
Why does the model ignore my tool? Almost always the description. Rewrite it to state the concrete output and the situation it applies to, then retest with a natural-language request in a fresh session.
How many tools should one server expose? Fewer than you want to. Every tool definition consumes context and adds a chance of a wrong selection. Three sharp tools beat twelve overlapping ones.
Free tool
Find any chat in seconds, across ChatGPT, Claude and Gemini
Search every conversation you have ever had, in one place, without scrolling the sidebar. Free, and it works on the chats you already have.
Works on your existing chats. No account needed.
Related Posts
Aider Alternative: The Best Free Terminal AI Coding Assistants in 2026
September 3, 2026
Cline vs Roo Code in 2026: Which Open Source AI Coding Agent Should You Actually Run?
September 2, 2026
Windsurf vs Cursor for Solo Developers in 2026: Pick Cursor, Unless You Value Flow Over Control
September 1, 2026