An MCP server is a small app that extends an AI model's capabilities by giving it access to custom tools, a particular set of data or workflows. It's based on the Model Context Protocol, which is an open standard for connecting AI apps with these external sources.
The most straightforward way to create an MCP server is to use the official SDK, implement a single function and mark it as a tool and then expose it through stdio (standard input/output) which you can register in Claude Code; it basically boils down to a single Python file with a single tool and connecting it end-to-end took us around 10 minutes.
Background
Generally, the Model Context Protocol defines two sides:
- The server — it's the app you write that you use to publish tools/data
- The client — for example Claude Code; it finds and calls available tools based on your permission
As for the main purpose of the Model Context Protocol — before it was introduced, every AI app needed its own custom integration with every tool; the Model Context Protocol replaces this with a single standard connector, so to say it's like USB-C for the AI world — you have a single standardised port instead of having to use a separate cable with every device. In terms of the protocol, a tool is just a function that the model can decide to call.
So if you want to build an MCP server, you do it when you want your model to have access to some resources you have (like your internal API or database for example) which aren't available through any of the already-published servers.
Let's have a look at a minimal example of what such server might look like — a single Python file with a single tool that returns the number of words, characters and lines in the input text.
Scaffold the project
To set up the project we used uv (a CLI for managing Python projects) and installed the official SDK:
uv init word-count-mcp
cd word-count-mcp
uv add "mcp[cli]"
-
uv init word-count-mcp— initialises a new project called "word-count-mcp" with an uv project file -
uv add "mcp[cli]"— adds the mcp package to the project as a dependency with its CLI extras; it also creates a virtual environment and a lockfile - Note that when you run
uv initfor a new project, it creates a sample main.py file which you can remove if you don't need it
Implement a tool
To implement the tool, create server.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("word-count")
@mcp.tool()
def word_count(text: str) -> dict:
"""Count the words, characters, and lines in a block of text.
Args:
text: The text to analyze.
"""
words = text.split()
return {
"words": len(words),
"characters": len(text),
"characters_no_spaces": len("".join(text.split())),
"lines": len(text.splitlines()),
}
if __name__ == "__main__":
mcp.run(transport="stdio")
These three lines do the entire job:
-
FastMCP("word-count")— creates an instance of the FastMCP class and names it "word-count" -
@mcp.tool()— annotates a function as a tool; the SDK reads its type hints and the docstring to build the tool's schema so the model knows what it does and what arguments it takes; if you don't provide any type hints, the schema will be ambiguous -
mcp.run(transport="stdio")— runs the server through stdio, which means that the server waits for requests via stdin and sends responses via stdout
Run it and connect to Claude Code
Now run it to make sure everything works as expected:
uv run server.py
When you run it, it blocks until it receives some input, which is expected as it's a stdio server and hence it does nothing until a client connects; stop it with Ctrl+C.
And then, from the project folder, register it in Claude Code:
claude mcp add word-count -- uv run --directory "$(pwd)" server.py
The part after -- is the command to run the server. claude mcp add registers it as a stdio server by default; the --directory "$(pwd)" bit is a uv flag that points it at the current directory, so no matter from which folder you start Claude Code, it'll work.
And here's the confirmation that we've added a new stdio server called "word-count" to our local config:
Added stdio MCP server word-count with command: uv run --directory /…/word-count-mcp server.py to local config
If you run claude mcp list afterwards, you can see that it's Connected, which means that Claude Code started the server, connected through the Model Context Protocol and received a valid list of tools from it.
word-count: uv run --directory /…/word-count-mcp server.py - ✔ Connected
Verify the tool call
So let's try using this tool now. Run Claude Code on your machine and ask it to use the word-count tool:
Use the word_count tool on this text: "MCP turns Claude into a client for your own tools"
Claude Code sees the word_count tool (full name mcp_word-count_word_count), asks for your permission and calls the server which returns its output as is, here are the counts for the sample text:
{
"words": 10,
"characters": 49,
"characters_no_spaces": 40,
"lines": 1
}
So you can see that it's a round-trip from Claude Code through the server to the Python function and back to Claude Code.
If you want, you can also test it with the MCP Inspector — the SDK's CLI launches it via mcp dev and it runs at localhost:6274, letting you explore tools and experiment with them by providing arguments and seeing their raw output.
uv run mcp dev server.py
It's very useful for testing servers before integrating them with AI apps like Claude Code.
Common pitfalls
But there are some common pitfalls worth looking out for when working with the Model Context Protocol:
- Never write to stdout in a stdio server — stdout is a channel for strictly-formatted messages to the client, if you write to it in your server, you break the stream and terminate the server; use stderr instead for logs; probably the most common beginner's mistake
- Choose the right transport — use stdio for local servers, HTTP for remote servers that are available via network; generally, tools are local so prefer stdio but don't assume it works by default
- Remember to type your arguments — the tool's schema is based on its type hints and docstring so if you omit the type hints, the schema becomes ambiguous which leads to suboptimal behaviour of the model; always provide type hints and add a one-line docstring for your tools as this is what the model sees
- Know the scope —
claude mcp addtakes-sto decide where the registration is saved: local (the default, which is why you saw “local config” above) = only for you in the current project, user = you across all projects, project = written to .mcp.json so you can commit it and share it with your team; use local to experiment, project for sharing - Restart after tool changes — when you connect a client (like Claude Code) with an MCP server, it reads the list of tools once and then doesn't ask again until you restart the connection
Is it worth building your own?
So… When should you create your own MCP server? Anytime you need the model to access some resources that only you have (like your internal API or a database). If it's a system that's popular enough (like GitHub), there's usually a standard server for it but if you want to give your model access to some custom thing, you need to build this bridge.
The barrier of entry is really low — it's just about defining a function and annotating it with a single decorator; if you can write Python, you can write an MCP tool. But the more advanced challenges appear once your tool actually starts using some systems — authentication, error handling, rate limits, reliability etc. This word-count tool is pretty benign as it's deterministic and inert but when your tool needs to connect with a database or spend money, the actual engineering work starts.
The thing is that there's a difference between "works" and "production-grade". If you want to build an MCP server that works, it takes 10 minutes; if you want to have a production-grade MCP server, it might take 10 months. This is the kind of work DSPLCE does — we help AI companies take their AI-built software the last mile. Have a look at the example on our GitHub, clone and connect it in a couple of minutes.
Top comments (0)