Build MCP Servers: Using MCP Python SDK
Table of Contents
It’s time to build a simple MCP server. We’ll start by looking into the official MCP Python SDK. We are primarily focusing on MCP Tools. I will show you how to define them, various ways of running the server (who doesn’t want choices?) and how that will look like in the MCP Inspector.
While self-contained, being familiar with the MCP Inspector can be super valuable. Not sure why or what the MCP I am talking about? My MCP Introduction might help with that.
Install
pip install mcp[cli]
Simple MCP Server
The MCP Python SDK makes it easy to define tools.
# server.py
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(name='Example MCP Server')
@mcp.tool()
async def add_numbers(a: float, b: float) -> float:
"""Add two numbers."""
return a + b
if __name__ == "__main__":
mcp.run()
To run the server we will look more closely at the following options…
Running the server via mcp.run()
Run the server via:
python server.py
That will load server.py
with __name__
being set to __main__
. i.e. it will also execute mcp.run()
which by default starts the server in stdio
mode.
Therefore that is equivalent to passing in stdio
explicitly:
mcp.run(transport="stdio")
Due to the nature of stdio
, this will need to be started by the MCP client / host.
You can pass in other modes via the transport
parameter, e.g.:
A Stateful Server using the Streamable HTTP transport mode:
mcp = FastMCP("Streamable HTTP: Stateful Server")
mcp.run(transport="streamable-http")
Connect client to: http://localhost:8000/mcp
Or a Stateless Server also using the Streamable HTTP transport mode:
mcp = FastMCP(
"Streamable HTTP: Stateless Server",
stateless_http=True
)
mcp.run(transport="streamable-http")
Also available under: http://localhost:8000/mcp
You can also disable SSE support by setting json_response
to True
:
mcp = FastMCP(
"Streamable HTTP: Stateless Server (no SSE)",
stateless_http=True,
json_response=True
)
mcp.run(transport="streamable-http")
And finally using the now deprecated SSE transport mode:
mcp = FastMCP("Deprecated Server-Sent Events(SSE)")
mcp.run(transport="sse")
The SSE endpoint is available under: http://localhost:8000/sse
You could also configure host
and port
by passing them in to FastMCP
, e.g.:
mcp = FastMCP("Example MCP Server", host='0.0.0.0', port=8080)
mcp.run(transport="streamable-http")
Running the server via mcp run
Run the server via:
mcp run server.py
This will load server.py
and look for a global of type FastMCP
. It would then run the server, similar to how you’d use mcp.run()
.
You can also select the global variable of the MCP server, e.g. mcp
:
mcp run server.py:mcp
The transport can also be selected via --transport
parameter. e.g.:
mcp run --transport=streamable-http server.py:mcp
There doesn’t seem to be an option to set the host
or port
via the cli
.
Running the server via mcp dev
Run the MCP Inspector via:
mcp dev server.py
The inpector will then be available under http://127.0.0.1:6274
.
In the inspector, the following configuation will be pre-populated:
Configuration Option | Value |
---|---|
Transport Type | STDIO |
Command | uv |
Arguments | run --with mcp mcp run server.py |
That means it uses uv to run mcp run server.py
in an isolated virtual environment with mcp installed.
If this works for you, great.
You could also amend the configuration in the inspector to say:
Configuration Option | Value |
---|---|
Transport Type | STDIO |
Command | mcp |
Arguments | run server.py |
Instead of using mcp dev
to start the inspector, you can just start it directly via:
npx @modelcontextprotocol/inspector
Personally, the mcp dev
seems to add a well intended abstraction that unfortunately becomes more difficult to reason about.
Instead it should focus on auto-reloading like you may be used to with FastAPI apps.
Interacting with MCP Server using the Inspector
The MCP Inspector helps you to test your MCP Server before integrating it into an AI Agent.
Connect to MCP Server
If you started the above server in Streamable HTTP
transport mode then the following configuration should work:
Configuration Option | Value |
---|---|
Transport Type | Streamable HTTP |
URL | http://localhost:8000/mcp |
Alternatively, using the stdio
transport mode:
Configuration Option | Value |
---|---|
Transport Type | STDIO |
Command | mcp |
Arguments | run server.py |
List and Run Tools
Under Tools
click on List Tools
and you should see the following tool listed:
add_numbers
When you click on it, you can run it:
Tools schema generated by mcp
The tools schema is important as it describes the available tools - to users as well as AI. In this case the schema includes the correct input type integer
and other fields you would expect:
{
"tools": [
{
"name": "add_numbers",
"description": "Add two numbers.",
"inputSchema": {
"type": "object",
"properties": {
"a": {
"title": "A",
"type": "integer"
},
"b": {
"title": "B",
"type": "integer"
}
},
"required": [
"a",
"b"
],
"title": "add_numbersArguments"
}
}
]
}
You can get the schema by looking at the tools/list
response in the MCP Inspector.
Integrate into FastAPI app
You can also mount an MCP server into an existing FastAPI app:
# server.py
import contextlib
from fastapi import FastAPI
from mcp.server.fastmcp import FastMCP
mcp = FastMCP(
name='Example MCP Server',
stateless_http=True,
json_response=True
)
@mcp.tool()
async def add_numbers(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@contextlib.asynccontextmanager
async def lifespan(app: FastAPI):
async with contextlib.AsyncExitStack() as stack:
await stack.enter_async_context(mcp.session_manager.run())
yield
app = FastAPI(lifespan=lifespan)
app.mount("/example", mcp.streamable_http_app())
This then allows you to start the server like you’d start any other Fast API server. e.g. using the FastAPI CLI or using uvicorn directly.
For example:
fastapi run --port=8000 server.py
Streamable HTTP
will be available under: http://127.0.0.1:8000/example/mcp
Using an MCP Factory and Unit Testing
Examples code often look simple. But real applications usually have configuration and dependencies. At the same time we want to avoid global variables, not least to help with TDD. One way to achieve that is by using a factory method that is creating an instance of FastMCP. I will show you how that pattern looks like, including unit tests.
MCP Factory: server.py
from dataclasses import dataclass
import os
from mcp.server.fastmcp import FastMCP
@dataclass
class AppConfig:
name: str
def get_app_config() -> AppConfig:
return AppConfig(name=os.getenv("MCP_NAME", "Demo"))
def create_mcp_for_config(config: AppConfig) -> FastMCP:
mcp = FastMCP(config.name)
@mcp.tool()
def add_numbers(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
return mcp
def create_mcp() -> FastMCP:
return create_mcp_for_config(get_app_config())
MCP Factory: cli.py
import logging
import os
from .server import create_mcp
def main() -> None:
mcp = create_mcp()
mcp.run(
transport=os.getenv( # type: ignore[arg-type]
'MCP_TRANSPORT',
'streamable-http'
)
)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()
MCP Factory: Start the Server
You can then start the server in Streamable HTTP
mode:
MCP_NAME='My MCP Server' \
MCP_TRANSPORT=streamable-http \
python -m cli
The MCP_NAME
demonstrates some app configuration, overriding the name of the MCP server.
Unlike uvicorn
, the mcp
CLI doesn’t support factory methods. That is why we need to start the server using mcp.run
.
In this example we used environment variables for the configuration for simplicity.
MCP Factory: Unit Testing
Now if you were searching the MCP Python SDK for “unit test” you might get: … (well, nothing)
But I promised you some unit test example:
import pytest
from mcp.types import TextContent
from server import AppConfig, create_mcp_for_config
class TestMcp:
def test_should_set_name(self):
mcp = create_mcp_for_config(
config=AppConfig(name="TestApp")
)
assert mcp.name == "TestApp"
@pytest.mark.asyncio
async def test_should_add_two_numbers(self):
mcp = create_mcp_for_config(
config=AppConfig(name="TestApp")
)
result = await mcp.call_tool(
"add_numbers",
arguments={"a": 1, "b": 2}
)
assert result
assert isinstance(result[0], TextContent)
assert result[0].text == '3'
Dependencies needed: pytest
and pytest-asyncio
Expanding on the tests using TDD will likely lead to more separation of concerns.
Code
You can find self contained examples code in my python-examples
repo, under python_examples/ai/mcp/mcp_python_sdk
.
Conclusion
There you go. I have shown you a simple MCP server and how to run it. Using mcp.run()
is probably the most simple and yet flexible way to do so. For advanced setups, the FastAPI integration may have advantages.
Next we will look at FastMCP v2.