Back to Blog
TutorialsNov 3, 20269 min read

Test a Python MCP Server Locally With uv, Step by Step

NT

Nikhil Tiwari

MCP Playground

πŸ“– TL;DR

  • Create the project with uv init --no-package, then uv add "mcp[cli]". Current uv makes a src/ package layout without the flag.
  • The Python SDK is now 2.x. FastMCP was renamed: import MCPServer from mcp.server.mcpserver. Old tutorials fail with No 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 plain uv run server.py fails with Failed to spawn: server.py.
  • Logs go to stderr. logging.basicConfig() already does that. print() doesn't.

Table of Contents

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 and mcp.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-tools line is stderr. Your terminal shows both streams. Add 2>/dev/null and 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:

  1. Add a server and choose Custom, with transport stdio.
  2. Command: uv. Arguments: run --directory /Users/me/projects/unit-tools server.py.
  3. 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.info lines show up in the Server stderr tab, next to every JSON-RPC message.
  • On a Mac it uses your login-shell PATH, so uv in ~/.local/bin or 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_count from 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?+
Pipe JSON-RPC messages into it from the terminal: an initialize request, the initialized notification, then tools/list and tools/call. For a UI, connect it as a stdio server in an MCP tester such as MCP Playground Desktop.
Why do I get "No module named 'mcp.server.fastmcp'"?+
You have version 2.x of the MCP Python SDK, where FastMCP was renamed to MCPServer. Import it with from mcp.server.mcpserver import MCPServer, or pin the SDK below version 2 to keep old code running.
How do I run a uv MCP server from Claude Desktop?+
Set command to uv and args to run, --directory, the full path to your project, and server.py. The --directory flag matters because Claude Desktop doesn't start the server in your project folder.
What does "error: Failed to spawn: server.py" mean?+
uv couldn't find server.py in the folder it started in. Add --directory with the full path to your project to the uv run arguments.
Can I use print() in a Python MCP server?+
Not with the stdio transport, because print writes to stdout, which is reserved for JSON-RPC messages. Use the logging module, which writes to stderr by default, or print with file=sys.stderr.

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 β†’

NT

Written by Nikhil Tiwari

15+ years in product development. AI enthusiast building developer tools that make complex technologies accessible to everyone.

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.

Try for Free β†’