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

> Build a small Python MCP server with uv and the MCP SDK 2.x, test it from the terminal with real JSON-RPC, then wire it into Claude Desktop without the usual errors. Includes the FastMCP rename in SDK 2.x, the --directory fix and the exact error text for each mistake.

**Source:** https://mcpplaygroundonline.com/blog/test-python-mcp-server-locally-uv  
**Author:** Nikhil Tiwari  
**Published:** 2026-11-03  
**Category:** Tutorials  
**Reading time:** 9 min read  
**Topics:** test python mcp server locally, uv mcp server, uv run mcp server claude desktop, no module named mcp.server.fastmcp, mcp python sdk 2 mcpserver, uv run --directory mcp, python mcp server stdio test, failed to spawn server.py uv, fastmcp renamed mcpserver, test mcp server without claude desktop

---

📖 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

-   [Set up the project with uv](#setup)
-   [Write a small MCP server](#server)
-   ["No module named mcp.server.fastmcp": the 2.x rename](#fastmcp)
-   [Test it from the terminal with raw JSON-RPC](#terminal)
-   [The client config that works](#config)
-   [Four errors you'll probably hit](#errors)
-   [Test it with a UI and with AI models](#desktop)
-   [FAQ](#faq)

I built and ran everything in this post with **uv 0.13.7**, **Python 3.13** and the official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) **2.3.0**. The outputs are copied from my terminal.

## Set up the project with uv

[uv](https://docs.astral.sh/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](https://py.sdk.modelcontextprotocol.io/v2/migration/) before you upgrade a bigger server. The standalone `fastmcp` package from my [FastMCP tutorial](/blog/build-mcp-server-python-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](/blog/mcp-stdio-unexpected-token-json) 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](/blog/fix-spawn-npx-enoent-mcp) covers this for every runtime.

### 4\. No module named 'mcp.server.fastmcp'

Covered [above](#fastmcp): 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](/blog/mcp-playground-desktop-guide) 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](/blog/import-claude-desktop-cursor-mcp-config).

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 →](/local-mcp-server-testing)

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](/mcp-test-server).

## 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 →](/local-mcp-server-testing)

## Frequently asked questions

### 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.


---

_Canonical page: https://mcpplaygroundonline.com/blog/test-python-mcp-server-locally-uv — MCP Playground (mcpplaygroundonline.com), the free browser-based tool for testing MCP servers and building AI agents._
