The Model Context Protocol (MCP) is an open standard that lets an AI agent call external tools and read external resources through a single, consistent interface. Instead of bolting a one-off integration onto every model, you write a small server once and any MCP-aware client can use it.
In this tutorial you will build exactly that: a tiny server that exposes one custom tool, word_count, which takes a piece of text and returns the number of words. It is deliberately harmless and self-contained, so you can focus on the mechanics rather than wiring up a real system. By the end you will have written the server, registered it with an agent, watched the agent call your tool, and learned the security basics you should apply before pointing anything at real data. Twenty minutes, start to finish.
1. Prerequisites
You need very little to get going. Make sure you have:
- A recent Python runtime, version 3.10 or newer. The MCP SDK relies on modern type hints and async features, so do not try this on an ancient interpreter.
- A way to manage a virtual environment. The examples use the standard
venvmodule, butuv,poetryorcondawork equally well. - An MCP-aware client to connect to. Several AI agent applications and IDEs now support MCP; we will keep the registration step illustrative because each client configures servers slightly differently.
That is all. No accounts, no API keys, no cloud anything. The whole exercise runs on your machine.
2. Set up the project and install the SDK
Create a fresh directory, set up an isolated environment, and install the official Python SDK. Working in a virtual environment keeps the SDK and its dependencies out of your global Python, which you will appreciate once you start running several servers.
mkdir wordcount-mcp && cd wordcount-mcp
# Create and activate a virtual environment
python -m venv .venv
source .venv/bin/activate # on Windows: .venv\Scripts\activate
# Install the Model Context Protocol SDK
pip install "mcp[cli]"
The mcp package gives you the protocol implementation and the FastMCP helper we are about to use; the optional [cli] extra adds a handy command-line runner for local testing. Once the install finishes, you are ready to write the server itself.
3. Write the server
This is the main part of the tutorial, and it is shorter than you might expect. Create a file called server.py. The pattern is consistent across every MCP server you will ever write: create a server object, decorate a plain function to expose it as a tool, and start the server so it speaks the protocol over a transport.
from mcp.server.fastmcp import FastMCP
# Create the server. The name is how clients will identify it.
mcp = FastMCP("wordcount")
@mcp.tool()
def word_count(text: str) -> int:
"""Count the number of words in a piece of text.
Args:
text: The text whose words should be counted.
Returns:
The number of whitespace-separated words.
"""
return len(text.split())
if __name__ == "__main__":
# Run the server, communicating over standard input/output.
mcp.run(transport="stdio")
Three things are doing the real work here:
- The tool name and description. The function name (
word_count) becomes the tool name, and the docstring becomes the description the agent reads to decide when to call it. Write that docstring as you would a prompt. A vague description means the agent calls your tool at the wrong moments, or never. - The typed input schema. The type hint
text: stris automatically turned into a JSON schema that tells the client exactly what arguments your tool accepts. The types tell the agent what to send, and let the runtime validate what comes back. - The transport.
mcp.run(transport="stdio")starts the server reading requests from standard input and writing responses to standard output. This is the simplest transport and the right choice for a local server the client launches as a subprocess.
A tool's description is the only thing an agent reads before deciding to call it. A clear, honest sentence about what the tool does (and what it does not do) is worth more than any amount of clever code behind it.
One caveat: MCP and its SDKs are evolving quickly, and the exact import paths, decorators and runner arguments can shift between versions. The shape shown here is accurate for the current Python SDK, but always check the current MCP documentation if something does not line up. An API rename is far more likely than a mistake on your part.
4. Register the server with your agent
A server on its own does nothing useful; the agent has to be told it exists. Most MCP clients keep a small configuration file listing the servers they should launch, each entry naming a command and its arguments. A typical entry looks like this:
{
"mcpServers": {
"wordcount": {
"command": "python",
"args": ["/absolute/path/to/wordcount-mcp/server.py"]
}
}
}
The client reads that entry, launches your server.py as a subprocess, and talks to it over stdio, the transport you configured in step 3. Use an absolute path to the script so the client can find it regardless of where it happens to be running from.
The exact mechanism varies by client. Some applications expose a settings panel where you paste this JSON; others have a command-line "add server" step; an IDE might keep the configuration in a workspace file. The principle is the same everywhere (name the server, give the command to start it), so once you have done it once, every client feels familiar. Check your client's documentation for where the file lives, then restart the client so it picks up the new entry.
5. Test it
With the server registered, open a conversation with your agent and ask it something that should trigger the tool. You do not call the tool by name; you describe the task and let the agent decide.
You: How many words are in this sentence:
"The quick brown fox jumps over the lazy dog"?
Agent: [calls tool word_count with text="The quick brown fox jumps over the lazy dog"]
[tool returns: 9]
Agent: That sentence contains 9 words.
Success looks like three things happening in sequence: the agent recognises that your tool is relevant, it calls word_count with a sensibly populated argument, and it uses the returned number in its reply. Most clients let you expand the tool call to inspect the exact arguments sent and the raw result returned. Do look, because that view is the best debugging aid you have.
If the agent never calls the tool, the usual culprit is the description: sharpen the docstring so it clearly states what the tool is for. If the server fails to start, run python server.py directly in your terminal and read the error. A missing dependency or a typo will show up immediately.
6. Make it actually useful
Counting words is a fine first tool, but the real value of MCP appears when a tool reaches into your systems. The same handful of lines you just wrote can wrap a query against your documentation, look up the current on-call engineer in your rota, fetch the status of a ticket, or summarise yesterday's deployment log. Once an agent can read your real context, it can answer from ground truth instead of guessing.
The mechanics do not change. You still define a function with a clear description and typed arguments; you simply put a real lookup inside it instead of text.split(). Start with read-only tools that fetch information, since they deliver most of the value with the least risk, and add anything that changes state only once you are confident in the pattern.
7. Security basics you must not skip
This is the part it is tempting to rush, and the part you will most regret rushing. An MCP server is a new doorway into whatever it can reach, and an AI agent will walk through that doorway in ways you did not anticipate. Treat the server as part of your attack surface from the very first commit.
- Least privilege. Give the server only the access it genuinely needs. A tool that reads tickets should hold read-only credentials scoped to tickets, not an admin token that happens to be lying around. If the server is compromised or the agent is misled, the blast radius is bounded by exactly this decision.
- No destructive actions without confirmation. Do not expose tools that delete, send, pay or deploy as fire-and-forget calls. If a tool can do something irreversible, require an explicit human confirmation step, or split it so the agent can only propose the action while a person approves it.
- Validate every input. The arguments arriving at your tool are effectively model output, and model output can be wrong, malformed or adversarial. Check types, ranges and formats before acting; never interpolate an argument straight into a shell command, a SQL query or a file path.
- Authenticate remote servers. The local stdio server in this tutorial is reachable only by the process that launched it. The moment you expose a server over the network, it needs authentication and transport encryption. An unauthenticated remote tool endpoint is an open invitation; treat it with the same seriousness as any other internet-facing service.
None of this is exotic. It is the same discipline you would apply to any service that touches production data, just applied a little earlier, because the caller on the other side reasons in natural language and does not share your instinct for caution.
8. Going further
You have built a working MCP server, exposed a custom tool, connected it to an agent and watched it run. You also know the security work that turns a toy into something you can trust near real systems. From here, the natural next steps are to expose resources (read-only data the agent can pull in as context, alongside the tools it can call), to return richer structured results, and to package the server so colleagues can install it without copying files by hand.
The best map for all of that is the official MCP documentation and the SDK's own examples, which stay current as the protocol matures. Build one more tool this week against a system you actually use (a docs search, an on-call lookup, a status check) and you will understand MCP far better than any tutorial can teach you. The first server is the hard one; every server after it is a variation on what you have already done.