Your server runs. Your tools are registered. But does a real MCP client actually see them? This guide walks through five testing methods — from MCP Inspector's raw JSON-RPC view to Vercel's production log dashboard — so you can catch every bug before Claude does.
Don't start with Claude Desktop. Start with MCP Inspector, which removes the LLM from the loop entirely. Work through the stack in order — each layer catches a different class of bug.
Browser UI at localhost:6274. Lists tools, calls them, shows raw JSON-RPC. Use first, before any deployment.
Same tool, pointed at your Vercel URL. Confirms deployed server is reachable and advertises correct tools.
Programmatic assertions on response shape and error handling. Catches serialization bugs the Inspector UI masks.
Real end-to-end with an LLM. Use after Inspector and test script pass. Tests tool selection and prompt rendering.
Production runtime: cold-start timing, missing env vars, import errors, 5xx rates. Use after every deploy.
| Method | What It Tests | When to Use | Command |
|---|---|---|---|
| Inspector (stdio) | Full local server: tools, resources, schema, raw JSON-RPC | First test during development | npx @modelcontextprotocol/inspector uv run server.py |
| Inspector (HTTP) | Deployed server over Streamable HTTP | After Vercel deploy | npx @modelcontextprotocol/inspector https://your.vercel.app/mcp |
| Python test script | Programmatic assertions on response shape | CI/CD, regression testing | python test_server.py |
| Claude Desktop (stdio) | End-to-end with a real AI client, local server | After Inspector passes | Edit config, fully quit, relaunch |
| Claude Desktop (mcp-remote) | End-to-end with real AI client, deployed server | Final production validation | Add mcp-remote entry, quit, relaunch |
| Vercel Logs | Live function execution, cold starts, 5xx errors | After any of the above hit errors | vercel logs --follow |
MCP Inspector is the official visual testing tool — maintained by Anthropic at github.com/modelcontextprotocol/inspector. No permanent install required. Two ports: 6274 for the browser UI, 6277 for the local proxy.
MCP Inspector v0.21.2 requires Node.js ≥ 22.7.5. Older Node versions fail silently or refuse to run. Check: node --version
Pass your server launch command directly to the Inspector. It launches the server as a child process and connects via stdio.
FastMCP server via uv (recommended)npx @modelcontextprotocol/inspector uv run server.py
# With environment variables injected
npx @modelcontextprotocol/inspector \
-e DATABASE_URL=postgresql://localhost/mydb \
-e API_KEY=sk-test-placeholder \
uv run server.py
FastMCP server via Python directly
npx @modelcontextprotocol/inspector python server.py
The browser opens automatically at http://localhost:6274 with a session token pre-filled in the URL. Go to the Tools tab and confirm your tools appear with their names, descriptions, and input schemas.
Don't pre-start the server, then try to connect. stdio transport cannot attach to an already-running process — Inspector must launch the server as a child process. For HTTP transport (a running Vercel or uvicorn server), pre-starting is correct.
| Symptom | Cause |
|---|---|
| Tools tab is empty | Server initialized but tools not registered — decorator missing, or import failed silently |
ERR_CONNECTION_REFUSED on launch |
Port 6274 or 6277 already in use — check with lsof -i :6274 |
| 401 in proxy console | Auth header missing or invalid for an authenticated server |
| Stderr in Notifications pane | Server process crashed — full stack trace is captured here |
| Timeout in Ping tab | Server unreachable or hung after init — check the remote URL |
Once your server is on Vercel, point Inspector at the live URL. No local server running. No guessing whether the deployment is healthy.
Connect to a Vercel-deployed FastMCP server (streamable HTTP)npx @modelcontextprotocol/inspector https://your-project.vercel.app/mcp
Same, but with an API key header
npx @modelcontextprotocol/inspector https://your-project.vercel.app/mcp \
--header "X-API-Key: your-key-here"
CLI (headless) mode — good for CI pipelines
# List tools from the remote server
npx @modelcontextprotocol/inspector \
--cli https://your-project.vercel.app/mcp \
--transport http \
--method tools/list
# Call a specific tool on the remote server
npx @modelcontextprotocol/inspector \
--cli https://your-project.vercel.app/mcp \
--transport http \
--method tools/call \
--tool-name search_products \
--tool-arg query=laptop
Browser UI for remote HTTP — navigate to this URL manually
http://localhost:6274/?transport=streamable-http&serverUrl=https://your-project.vercel.app/mcp
Use the CLI tools/list command first. A clean {"tools": [...]} response confirms the server is reachable and the protocol is working. Only then wire it into Claude Desktop.
Claude Desktop reads claude_desktop_config.json only at startup. After any edit, fully quit — not just close the window — and relaunch. Success is confirmed by the MCP slider icon appearing in the chat input area.
| OS | Path |
|---|---|
| macOS | ~/Library/Application Support/Claude/claude_desktop_config.json |
| Windows | %APPDATA%\Claude\claude_desktop_config.json |
| Linux | Claude Desktop not officially available — use MCP Inspector instead |
If the file doesn't exist, enable it via Settings → Developer → Edit Config in the Claude Desktop app.
Use when your FastMCP server runs locally. Claude Desktop spawns the process and communicates over stdio. Always use absolute paths — relative paths fail because Claude Desktop does not run from your project directory.
claude_desktop_config.json — local server via uv{
"mcpServers": {
"my-shop-server": {
"command": "uv",
"args": [
"run",
"--project", "/absolute/path/to/your/project",
"python",
"server.py"
],
"env": {
"DATABASE_URL": "postgresql://user:pass@host/db",
"API_KEY": "sk-your-actual-key-here",
"PYTHONUNBUFFERED": "1"
}
}
}
}
Install uv via Homebrew (brew install uv) so Claude Desktop's sandboxed environment can locate it. Installing via curl | sh may place it in a path Claude Desktop's launch environment doesn't inherit. Also set PYTHONUNBUFFERED=1 to prevent stdio buffering from stalling the JSON-RPC message stream.
For a Vercel-deployed server. mcp-remote runs as a local stdio subprocess that Claude Desktop spawns, then proxies to your remote HTTP endpoint. The package name is mcp-remote on npm — there is no @anthropic-ai/mcp-remote.
{
"mcpServers": {
"my-shop-server-remote": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"https://your-project.vercel.app/mcp"
]
}
}
}
claude_desktop_config.json — remote server with API key
{
"mcpServers": {
"my-shop-server-remote": {
"command": "npx",
"args": [
"-y",
"mcp-remote@latest",
"https://your-project.vercel.app/mcp",
"--header",
"X-API-Key:${API_KEY}"
],
"env": {
"API_KEY": "your-secret-key"
}
}
}
}
Claude Desktop is an Electron app. Closing the window hides it but leaves the main process running — MCP servers are only launched at startup. macOS: use Cmd+Q, not the red close button. Windows: right-click the system tray icon → Quit. After relaunching, the MCP slider icon in the bottom-right of the chat input confirms a successful connection.
If tools don't appear, check the logs that Claude Desktop writes for every MCP subprocess:
macOS — follow MCP logs in real timetail -n 50 -f ~/Library/Logs/Claude/mcp.log
tail -n 50 -f ~/Library/Logs/Claude/mcp-server-my-shop-server.log
Windows PowerShell
Get-Content "$env:APPDATA\Claude\logs\mcp.log" -Wait -Tail 50
These logs show stdout/stderr of every MCP subprocess Claude Desktop spawned, including Python tracebacks, missing module errors, and mcp-remote OAuth output.
The Python MCP SDK's ClientSession lets you call tools programmatically and assert on the response shape. This catches serialization bugs that the Inspector UI masks because it shows raw JSON.
Install dependenciespip install "mcp[cli]"
# or with uv:
uv add "mcp[cli]"
test_server.py — complete runnable script
"""
Test script for your FastMCP server.
Usage:
# Against a local server:
python test_server.py
# Against your deployed Vercel server:
MCP_SERVER_URL=https://your-project.vercel.app/mcp python test_server.py
"""
import asyncio
import os
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamable_http_client
from mcp import types
SERVER_SCRIPT = "/absolute/path/to/your/server.py"
SERVER_URL = os.environ.get("MCP_SERVER_URL")
API_KEY = os.environ.get("API_KEY", "")
async def test_server():
if SERVER_URL:
# Test against deployed server
print(f"Testing remote server: {SERVER_URL}")
headers = {"X-API-Key": API_KEY} if API_KEY else {}
async with streamable_http_client(SERVER_URL, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
await run_tests(session)
else:
# Test against local stdio server
print(f"Testing local server: {SERVER_SCRIPT}")
server_params = StdioServerParameters(
command="python",
args=[SERVER_SCRIPT],
env={"PYTHONUNBUFFERED": "1"},
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
await run_tests(session)
async def run_tests(session: ClientSession):
# 1. Verify expected tools are registered
tools_response = await session.list_tools()
tool_names = [t.name for t in tools_response.tools]
assert "search_products" in tool_names, \
f"search_products not found. Available: {tool_names}"
assert "initiate_checkout" in tool_names, \
f"initiate_checkout not found. Available: {tool_names}"
print(f"✓ Tools registered: {tool_names}")
# 2. Call search_products and assert shape
search_result = await session.call_tool(
"search_products",
arguments={"query": "widget", "limit": 5},
)
assert len(search_result.content) > 0, "search_products returned no content"
result_text = search_result.content[0]
assert isinstance(result_text, types.TextContent), \
f"Expected TextContent, got {type(result_text)}"
print(f"✓ search_products returned content")
# 3. Call initiate_checkout
checkout_result = await session.call_tool(
"initiate_checkout",
arguments={"sku": "WIDGET-001", "quantity": 1},
)
assert len(checkout_result.content) > 0, "initiate_checkout returned no content"
print(f"✓ initiate_checkout returned content")
print("\nAll assertions passed.")
if __name__ == "__main__":
asyncio.run(test_server())
Run locally
python test_server.py
Run against deployed server
MCP_SERVER_URL=https://your-project.vercel.app/mcp \
API_KEY=your-secret-key \
python test_server.py
MCP Inspector shows you what the response looks like. The SDK test script asserts that it has the shape you expect. A tool can return valid JSON that still breaks downstream — the Python script catches that. Run both.
Every POST to /mcp generates a log entry. The Vercel Logs tab shows status code, response time, and the full stderr output from your Python function — including tracebacks.
| Plan | Retention |
|---|---|
| Hobby | 1 hour |
| Pro | 1 day |
| Pro + Observability Plus | 30 days |
| Enterprise | 3 days |
vercel logs --follow
# Filter by error status only
vercel logs --status-code 500 --follow
# JSON output for piping to jq
vercel logs --json --status-code 5xx | \
python3 -c "import sys,json; [print(json.loads(l).get('message','')) for l in sys.stdin]"
[INFO] POST /mcp 200 142ms
[INFO] Tool called: search_products {"query":"laptop","limit":10}
[INFO] Tool returned 8 results
Status 200, your print() output appears as INFO lines, response time in the tens to hundreds of milliseconds after warmup.
[ERROR] POST /mcp 500 3241ms
[ERROR] ModuleNotFoundError: No module named 'pydantic_settings'
Traceback (most recent call last):
File "/var/task/server.py", line 3, in <module>
from pydantic_settings import BaseSettings
Key signals: high response time (2–5 seconds), error fires before any of your handler code runs (no tool-name log lines), and the message references module resolution. The 500 is generated by the Vercel runtime before your request handler gains control. Fix: add the missing package to requirements.txt and redeploy.
[INFO] POST /mcp 200 4100ms ← first call, cold start
[INFO] POST /mcp 200 38ms ← warm, fast
[ERROR] POST /mcp 500 41ms
[INFO] Tool called: initiate_checkout
[ERROR] KeyError: 'sku'
Normal response time, your log output appears up to the point of the error. This is a bug in your tool handler, not a cold-start issue.
These are the most common failures when testing FastMCP servers — from local Inspector debugging through Vercel production. Each one has a specific cause and a one-step fix.
Cause: Claude Desktop was not fully restarted. Editing and saving claude_desktop_config.json while Claude is running has no effect — the app only reads config at startup.
Also check: JSON syntax error in the config file; relative path in args instead of absolute; required env var missing from the env block.
pgrep -la Claude. Validate the config: python3 -m json.tool ~/Library/Application\ Support/Claude/claude_desktop_config.json. Then relaunch and look for the MCP slider icon.
Cause A: Vercel Deployment Protection is enabled and blocking unauthenticated POST requests. The platform intercepts before your function runs.
Cause B: Transport mismatch — the client is posting to /mcp but the server is configured for SSE transport (which expects a GET to /sse).
x-vercel-protection-bypass header to your requests.
/mcp (POST). SSE clients connect to /sse (GET). Match your client transport to your server transport.
Cause: The stdio-based Inspector command was pointed at an already-running server process. The stdio transport cannot attach to an existing process — it must launch the server as a child process.
python server.py & npx @modelcontextprotocol/inspector --cli localhost:8000
npx @modelcontextprotocol/inspector uv run server.py
npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport http
Cause: A Python import at module level depends on a runtime resource — reads an env var and fails, connects to a database in global scope, or uses a C extension not available in the Vercel Python runtime.
lifespan context manager:
@asynccontextmanager
async def lifespan(server):
http_client = httpx.AsyncClient(timeout=10.0)
yield {"http_client": http_client}
await http_client.aclose()
mcp = FastMCP("MyServer", lifespan=lifespan)
Cause: Stale OAuth credentials cached in ~/.mcp-auth from a previous auth attempt or changed server configuration.
rm -rf ~/.mcp-auth
npx -p mcp-remote@latest mcp-remote-client https://your-project.vercel.app/mcp
mcp-remote requires Node 18+. Claude Desktop uses the system Node, not your nvm-managed version. Verify: node --version
Follow these steps in order. Each one must pass before you proceed to the next — skipping ahead is how bugs get deployed to production.
Run npx @modelcontextprotocol/inspector uv run server.py and open http://localhost:6274. In the Tools tab, confirm your expected tools appear with correct schemas. Click each tool, enter test inputs, and confirm they return non-error responses. Check the Notifications pane for any stderr output. Fix all issues before deploying.
Run npx @modelcontextprotocol/inspector https://your-project.vercel.app/mcp. The same tools that passed locally should appear here. If you see a 401, add --header "X-API-Key: your-key". A 404 means your server isn't handling /mcp — check your vercel.json rewrite rules. A 405 means a transport mismatch — see the errors section.
Execute MCP_SERVER_URL=https://your-project.vercel.app/mcp python test_server.py. Every assertion must pass. A failing tool list assertion means the server is up but not advertising tools correctly. A failing response-shape assertion means your tool handler has a serialization bug. Fix and redeploy until all assertions pass.
Edit claude_desktop_config.json with Option A (local) or Option B (mcp-remote) config. Fully quit Claude Desktop (Cmd+Q on macOS) and relaunch. Confirm the MCP slider icon appears in the chat input area. Click it to see your tool names listed.
Type a prompt that should trigger one of your tools. Watch for the tool-call approval dialog. After it executes, open your Vercel project → Logs and filter by your /mcp route. Confirm you see 200 responses with your application log output. If you see any 500s, click the row to see the full Python traceback in the Log Messages panel.
MCP Inspector removes the LLM from the loop entirely. With Inspector, you have deterministic control over every call — you choose the tool, the inputs, and can inspect the raw JSON-RPC request and response. Claude Desktop adds LLM-mediated tool selection, which makes it harder to isolate bugs: you can't tell whether Claude chose the wrong tool, passed the wrong arguments, or whether your server returned a malformed response. Inspector makes that distinction clear. Use it first, always.
Look at the Notifications pane in Inspector. If you see a 401, add --header "X-API-Key: your-key" to the command. If you see a 404, your server isn't handling requests at /mcp — check your FastMCP mount path and vercel.json rewrite rules. If you see a 405, you're sending a GET to an endpoint that only accepts POST (Streamable HTTP spec behavior) — the Inspector should auto-detect this, but verify you're using the correct transport type.
Click any log row in the Logs tab to open the detail panel. The Function section includes a start type field that distinguishes cold from warm boots. You can also infer cold starts from the duration column — cold invocations run 1–3+ seconds longer than warm ones on comparable requests. A cold-start crash produces a 500 with high response time and no application-level log lines — your print() output never appears because the process crashed before reaching your handler code.
The most common cause is missing environment variables. The stdio subprocess in Claude Desktop does not inherit your shell's environment. Any env var your tool needs — DATABASE_URL, API keys, etc. — must be explicitly listed in the env block of claude_desktop_config.json. Check Claude Desktop logs: tail -f ~/Library/Logs/Claude/mcp-server-your-server.log on macOS to see the exact Python traceback.
Not with the standard stdio-only config. Claude Desktop's mcpServers configuration historically only supports launching local subprocess commands. mcp-remote bridges that gap by acting as a local stdio subprocess that proxies HTTP. Some newer Claude Desktop builds support native HTTP/SSE transport — check your version — but mcp-remote works for all current versions and is the safe default.
Use the -e flag before the server command for stdio servers: npx @modelcontextprotocol/inspector -e API_KEY=sk-test-placeholder uv run server.py. For a remote HTTP server, use --header "X-API-Key: your-key". You can also set environment variables in the Connection Setup panel on the left side of the browser UI. Do not hard-code secrets in the Inspector URL query params — they appear in browser history.
Filter by Level → Error in the Logs sidebar — this surfaces stderr output separately. If the Log Messages panel is still empty, add explicit print() statements or Python logging calls at the very top of your server.py before the import that's failing. Vercel only captures output after the function process starts, so an error during module import may not produce a log line — check the Build Logs instead (project → deployment tile → Build Logs) for build-time issues.
Your server is tested and verified. The next step is replacing the static product list with a live Postgres connection — asyncpg, Supabase, or Neon. The next guide covers all three options with complete, runnable code.
Connect a Database →Get the MCP Server Ops checklist — testing sequence, error decoder, and Vercel config template — delivered to your inbox.