30·Day·Pivot
MCP Server Ops · Spoke 2 of 10 · Test Your Server

Test before you trust it.

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.

5 Testing methods covered
6274 Inspector's default port
1hr Log retention on Vercel Hobby
0 Guessing — Inspector shows raw JSON-RPC
What's in this guide
  1. The Testing Stack: Five Methods, When to Use Each
  2. MCP Inspector: Your First Stop
  3. Connecting Inspector to Vercel
  4. Claude Desktop Wiring
  5. Python End-to-End Test Script
  6. Reading Vercel Logs
  7. The 5 Errors You'll Hit
  8. Step-by-Step Process
  9. FAQ

Five methods. One sequence.

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.

Method 1

MCP Inspector (local stdio)

Browser UI at localhost:6274. Lists tools, calls them, shows raw JSON-RPC. Use first, before any deployment.

Method 2

MCP Inspector (remote HTTP)

Same tool, pointed at your Vercel URL. Confirms deployed server is reachable and advertises correct tools.

Method 3

Python SDK test script

Programmatic assertions on response shape and error handling. Catches serialization bugs the Inspector UI masks.

Method 4

Claude Desktop (local)

Real end-to-end with an LLM. Use after Inspector and test script pass. Tests tool selection and prompt rendering.

Method 5

Vercel Logs dashboard

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

See the raw JSON-RPC.

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.

Requirement

MCP Inspector v0.21.2 requires Node.js ≥ 22.7.5. Older Node versions fail silently or refuse to run. Check: node --version

Connect to a local stdio server

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.

Critical Mistake

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.

What a passing result looks like

What a failing result looks like

SymptomCause
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

Test the deployed server.

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
Two-Step Check

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.

The config file, both options.

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.

Config file locations

OSPath
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
LinuxClaude 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.

Option A: Local stdio — direct Python subprocess

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"
      }
    }
  }
}
macOS note

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.

Option B: Remote HTTP via mcp-remote

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.

claude_desktop_config.json — remote server without auth
{
  "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"
      }
    }
  }
}
The Fully-Quit Requirement

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.

Checking Claude Desktop MCP logs

If tools don't appear, check the logs that Claude Desktop writes for every MCP subprocess:

macOS — follow MCP logs in real time
tail -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.

Deterministic assertions, no LLM guessing.

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 dependencies
pip 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
Why the SDK vs. Inspector

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.

Production errors, decoded.

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.

Dashboard navigation

  1. Open your project in the Vercel dashboard
  2. Click Logs in the left sidebar (under the project name)
  3. Filter by Status Code → 5xx to isolate all errors immediately
  4. Click any log row to open the detail panel: function metadata, log messages, and event timeline
  5. Toggle Live mode to stream logs in real time during manual testing

Log retention by plan

PlanRetention
Hobby1 hour
Pro1 day
Pro + Observability Plus30 days
Enterprise3 days

CLI tailing

Stream live logs until interrupted
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]"

Three log patterns you will see

Successful invocation

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

Cold-start crash (500, no app logs)

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

Runtime bug (500, app logs present)

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

Every error, exact fix.

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.

Error 01

Tools not appearing in Claude Desktop

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.

Fix: On macOS: Cmd+Q to fully quit, then verify with 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.
Error 02

405 Method Not Allowed on POST /mcp

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

Fix A: Dashboard → Settings → General → Deployment Protection → disable. Or add x-vercel-protection-bypass header to your requests.

Fix B: FastMCP 2.x streamable-HTTP endpoint is /mcp (POST). SSE clients connect to /sse (GET). Match your client transport to your server transport.
Error 03

Connection refused when testing locally

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.

Fix:
Wrong: python server.py & npx @modelcontextprotocol/inspector --cli localhost:8000
Correct: npx @modelcontextprotocol/inspector uv run server.py

For HTTP transport (server already running): npx @modelcontextprotocol/inspector --cli http://localhost:8000/mcp --transport http
Error 04

500 on first call (cold start), 200 on second

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.

Fix: Defer resource initialization to FastMCP's 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)
Error 05

mcp-remote auth failure / Token exchange failed: HTTP 400

Cause: Stale OAuth credentials cached in ~/.mcp-auth from a previous auth attempt or changed server configuration.

Fix — Step 1: Clear cached state: rm -rf ~/.mcp-auth

Fix — Step 2: Test the auth flow directly: npx -p mcp-remote@latest mcp-remote-client https://your-project.vercel.app/mcp

Fix — Node version: mcp-remote requires Node 18+. Claude Desktop uses the system Node, not your nvm-managed version. Verify: node --version

The complete testing sequence.

Follow these steps in order. Each one must pass before you proceed to the next — skipping ahead is how bugs get deployed to production.

  1. Run MCP Inspector against your local server

    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.

  2. Run Inspector against your deployed Vercel server

    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.

  3. Run the Python test script against the deployed URL

    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.

  4. Wire Claude Desktop and verify tools appear

    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.

  5. Execute a real tool call through Claude and check Vercel logs

    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.

Questions from the real install.

Why use MCP Inspector instead of going straight to Claude Desktop?

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.

My server is on Vercel but MCP Inspector shows no tools. Where do I start?

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.

How do I tell if a Vercel invocation was a cold start?

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.

Why does search_products work in Inspector but fail in Claude Desktop?

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.

Can I use Claude Desktop with a Vercel server without mcp-remote?

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.

How do I pass API keys to my server when using MCP Inspector?

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.

My Vercel logs show a 500 but no error message. How do I get the stack trace?

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.

Ready to add a real database?

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 →
Free Guide

Ship faster. Break less.

Get the MCP Server Ops checklist — testing sequence, error decoder, and Vercel config template — delivered to your inbox.