Test a Python MCP Server Locally With uv, Step by Step
Nikhil Tiwari
MCP Playground
π TL;DR
- Create the project with
uv init --no-package, thenuv add "mcp[cli]". Current uv makes asrc/package layout without the flag. - The Python SDK is now 2.x.
FastMCPwas renamed: importMCPServerfrommcp.server.mcpserver. Old tutorials fail withNo module named 'mcp.server.fastmcp'. - Test it in the terminal first by piping four JSON-RPC lines into
uv run server.py. You should get three JSON replies. - In a client config, always use
uv run --directory /full/path server.py. Clients don't start in your project folder, so plainuv run server.pyfails withFailed to spawn: server.py. - Logs go to stderr.
logging.basicConfig()already does that.print()doesn't.
Table of Contents
- Set up the project with uv
- Write a small MCP server
- "No module named mcp.server.fastmcp": the 2.x rename
- Test it from the terminal with raw JSON-RPC
- The client config that works
- Four errors you'll probably hit
- Test it with a UI and with AI models
- FAQ
I built and ran everything in this post with uv 0.13.7, Python 3.13 and the official MCP Python SDK 2.3.0. The outputs are copied from my terminal.
Set up the project with uv
uv handles Python, the virtual environment and dependencies in one tool. If you don't have it yet:
# macOS
brew install uv
# Windows
winget install astral-sh.uv
# macOS / Linux, official installer
curl -LsSf https://astral.sh/uv/install.sh | sh
Now create the project and add the SDK:
uv init --no-package unit-tools
cd unit-tools
uv add "mcp[cli]"
Why --no-package? With uv 0.13.7, a plain uv init created a src/unit_tools/ package with a build backend. That's fine for a published package, but for a local server you want a flat folder with one script. The flag gives you that.
uv creates main.py. Delete it, or rename it to server.py.
Write a small MCP server
Put this in server.py. Two tools with no network calls, so nothing outside the server can break the test:
import logging
from mcp.server.mcpserver import MCPServer
logging.basicConfig(level=logging.INFO) # stderr, never stdout
log = logging.getLogger("unit-tools")
mcp = MCPServer("unit-tools")
@mcp.tool()
def celsius_to_fahrenheit(celsius: float) -> float:
"""Convert a temperature from Celsius to Fahrenheit."""
log.info("converting %s C", celsius)
return celsius * 9 / 5 + 32
@mcp.tool()
def word_count(text: str) -> int:
"""Count the words in a piece of text."""
return len(text.split())
if __name__ == "__main__":
mcp.run() # stdio by default
The docstrings become the tool descriptions a model reads, and the type hints become the input schema. Write the docstring for the model, not for yourself.
"No module named mcp.server.fastmcp": the 2.x rename
Many Python MCP tutorials written for SDK 1.x start with from mcp.server.fastmcp import FastMCP. On SDK 2.x that line fails. This is the exact error I got:
ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x, where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import MCPServer) and other APIs changed; see the migration guide at https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver or pin 'mcp<2' to keep running v1 code.
The error tells you both fixes:
- New code: use
from mcp.server.mcpserver import MCPServer, as above. The@mcp.tool()decorator andmcp.run()work the same way. - Old code you don't want to touch: pin the SDK with
uv add "mcp[cli]<2".
Other APIs changed in 2.x too, so read the migration guide before you upgrade a bigger server. The standalone fastmcp package from my FastMCP tutorial is a separate project, imported as from fastmcp import FastMCP.
Test it from the terminal with raw JSON-RPC
Before any client is involved, check the server answers. A stdio MCP server reads one JSON-RPC message per line from stdin. Save these four lines as session.jsonl:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"check","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"celsius_to_fahrenheit","arguments":{"celsius":100}}}
Then pipe them in. The sleep keeps stdin open long enough for the tool call to finish:
(cat session.jsonl; sleep 2) | uv run server.py
Here's what I got back (the tool list is trimmed):
{"jsonrpc":"2.0","id":1,"result":{"capabilities":{...,"tools":{"listChanged":false}},"protocolVersion":"2025-11-25","serverInfo":{"name":"unit-tools","version":""}}}
INFO:unit-tools:converting 100.0 C
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"description":"Convert a temperature from Celsius to Fahrenheit.","inputSchema":{...},"name":"celsius_to_fahrenheit",...},{...,"name":"word_count",...}]}}
{"jsonrpc":"2.0","id":3,"result":{"content":[{"text":"212.0","type":"text"}],"isError":false,"structuredContent":{"result":212.0}}}
Three things to check:
- One reply per request, with ids 1, 2 and 3. The notification gets no reply, which is correct.
- The
INFO:unit-toolsline is stderr. Your terminal shows both streams. Add2>/dev/nulland it disappears, which proves stdout is clean. - 212.0 for 100 Β°C. The tool really ran.
If you see any other line on stdout, such as output from a print(), the client will choke on it. My post on stdout breaking stdio MCP servers explains the errors that causes.
The client config that works
Here's the entry for Claude Desktop, Cursor or Windsurf. Use the full path to your project folder:
{
"mcpServers": {
"unit-tools": {
"command": "uv",
"args": ["run", "--directory", "/Users/me/projects/unit-tools", "server.py"]
}
}
}
On Windows, escape the backslashes in the path: "C:\\Users\\me\\projects\\unit-tools", or use forward slashes.
--directory is the important part. It tells uv where the project and its .venv are, and runs server.py from there. Without it, uv starts in whatever folder the client starts in, which is usually not your project.
Four errors you'll probably hit
1. error: Failed to spawn: server.py
I ran uv run server.py from my home folder instead of the project:
error: Failed to spawn: server.py
cause: No such file or directory (os error 2)
That's what happens when a client config says "args": ["run", "server.py"]. Fix: add --directory /full/path/to/project.
2. ModuleNotFoundError: No module named 'mcp'
I got this running python3 server.py with my system Python. The SDK is installed in the project's .venv, not globally. Run the server through uv run, or point the config at the venv's Python: /full/path/unit-tools/.venv/bin/python (.venv\Scripts\python.exe on Windows).
3. spawn uv ENOENT
The client can't find uv at all. On a Mac this is common: apps opened from the Dock don't get your Terminal PATH, and the uv installer puts uv in ~/.local/bin.
Fix: run which uv (where uv on Windows) and put that full path in "command". My guide to fixing spawn npx ENOENT covers this for every runtime.
4. No module named 'mcp.server.fastmcp'
Covered above: SDK 2.x renamed FastMCP to MCPServer. Switch the import or pin mcp<2.
Test it with a UI and with AI models
Raw JSON-RPC proves the server starts. For calling tools with different inputs, a UI is quicker. MCP Playground Desktop runs local stdio servers like this one:
- Add a server and choose Custom, with transport stdio.
- Command:
uv. Arguments:run --directory /Users/me/projects/unit-tools server.py. - Click Connect, approve the command once, then pick a tool and click Run tool.
Already have the server in your Claude Desktop config? Import it instead of retyping it.
A few things make this easier than the terminal:
- Your
log.infolines show up in the Server stderr tab, next to every JSON-RPC message. - On a Mac it uses your login-shell PATH, so
uvin~/.local/binor Homebrew is found. - If uv is missing, you get "Couldn't find "uv" on this computer." with the install command, not
spawn uv ENOENT. - Agent Studio lets AI models call your tools, so you can see whether they pick
word_countfrom your docstring.
Get MCP Playground Desktop for Mac and Windows β
Planning to deploy the server over HTTP later? Change mcp.run() to mcp.run(transport="streamable-http"), and once it's on a public URL you can test it free in your browser.
FAQ
How do I test a Python MCP server without Claude Desktop?+
Why do I get "No module named 'mcp.server.fastmcp'"?+
How do I run a uv MCP server from Claude Desktop?+
What does "error: Failed to spawn: server.py" mean?+
Can I use print() in a Python MCP server?+
Conclusion
A Python MCP server with uv takes five minutes: uv init --no-package, uv add "mcp[cli]", one script with MCPServer. Test it with raw JSON-RPC first, then use --directory in every client config.
When you want to call tools without typing JSON, or watch AI models use them, get MCP Playground Desktop for Mac and Windows β
Written by Nikhil Tiwari
15+ years in product development. AI enthusiast building developer tools that make complex technologies accessible to everyone.
Free MCP Tools (no install)
Build, compare & ship MCP agents
Connect any MCP server, run evals on it, compare 80+ models side-by-side, deploy hosted servers, and save reusable agents you can export as an API β all in your browser.