Claude Desktop + MCP: Complete Setup Guide
I spent two hours staring at Claude Desktop wondering why my MCP server wouldn't connect. The config looked perfect. The server ran fine standalone. But Claude showed no tools. The problem? A single trailing comma in my JSON config that Claude silently ignored.
This is everything I wish the docs had told me upfront.
What This Guide Covers
You've built an MCP server (or grabbed one from GitHub). Now you need Claude Desktop to actually use it. This involves:
- Finding the right config file (it's in different places on Mac/Windows/Linux)
- Writing valid JSON config (one syntax error breaks everything silently)
- Restarting Claude Desktop the right way (closing the window isn't enough)
- Debugging when tools don't appear (they won't, the first time)
- Testing tool calls and reading logs
This assumes you have a working MCP server. If you don't, start with Building Your First MCP Server.
Find Your Config File
Claude Desktop stores MCP server configuration in a JSON file. The location depends on your OS:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
Windows:
%APPDATA%\Claude\claude_desktop_config.json
Linux:
~/.config/Claude/claude_desktop_config.json
If the file doesn't exist, create it with this starter:
{
"mcpServers": {}
}
Pro tip: Open this file in VS Code or another editor with JSON validation. Claude Desktop doesn't show config errors, so you'll waste time chasing phantom issues if your JSON is malformed.
Add Your Server Configuration
Here's what a working config looks like:
{
"mcpServers": {
"my-notes": {
"command": "python",
"args": ["/Users/tony/projects/notes-mcp/server.py"],
"env": {
"LOG_LEVEL": "info"
}
}
}
}
Breaking this down:
"my-notes"
: A label for your server. Use lowercase with hyphens. This shows up in Claude's UI."command"
: The executable—python
,node
,dotnet
, etc."args"
: Array of arguments. Use absolute paths, not relative ones."env"
: Optional environment variables your server needs.
Common mistakes I made:
Trailing commas:
{
"mcpServers": {
"my-notes": {
"command": "python",
"args": ["/path/to/server.py"],
} // ← This comma breaks everything
}
}
Relative paths:
"args": ["./server.py"] // ✗ Breaks - Claude doesn't know where "./" is
"args": ["/Users/tony/projects/server.py"] // ✓ Works
Wrong quotes:
"command": "python" // ✗ Smart quotes from copying docs
"command": "python" // ✓ Regular double quotes
Multiple servers:
{
"mcpServers": {
"notes": {
"command": "python",
"args": ["/path/to/notes-server.py"]
},
"tasks": {
"command": "node",
"args": ["/path/to/tasks-server/dist/index.js"]
}
}
}
Notice the comma between servers but not after the last one.
Restart Claude Desktop (Really Restart It)
Save your config, then restart Claude Desktop. Not close-the-window restart—actually quit the application.
macOS: Cmd+Q
or Claude menu → Quit Claude
Windows: Right-click the taskbar icon → Quit (don't just X the window)
Linux: Use your window manager's quit command
Claude caches MCP connections on startup. If you just close the window, your changes won't load.
Test Discovery
Open Claude Desktop and start a new conversation. Ask:
"What MCP tools do you have available?"
Claude should list your tools with descriptions. If you see your tools, success! Skip to the next section.
If you don't see tools:
-
Check Claude Desktop logs
Settings → Advanced → View Logs
Look for MCP connection errors or your server name -
Test your server independently
Run the command from your config manually:python /path/to/server.py
You should see startup logs. If it crashes, fix that first.
-
Verify the config file saved
Open it again and make sure your changes are there (easy to edit the wrong file) -
Check for JSON syntax errors
Usejq
or an online JSON validator:cat ~/Library/Application\ Support/Claude/claude_desktop_config.json | jq .
-
Look for process launch errors
On macOS, check Console.app for errors from Claude Desktop
Make Your First Tool Call
Once tools appear, test them. Start simple:
"Search my notes for MCP"
Watch what happens:
- Claude decides to use your
search_notes
tool - Claude shows you the tool call with parameters
- Your server executes and returns results
- Claude incorporates the results into its response
If the tool call fails:
- Check your server logs (they should be going to stderr)
- Look at the parameters Claude sent—does your schema match?
- Test the exact same parameters by running your server directly
Common Tool Call Problems
Claude invents fields that don't exist:
Your JSON Schema isn't strict enough. Add "required"
arrays and enums:
@mcp.tool(description="Search notes")
async def search_notes(
query: Annotated[str, Field(description="Search term")],
limit: Annotated[int, Field(description="Max results", ge=1, le=50)] = 10,
sort: Annotated[
Literal["relevance", "date"],
Field(description="Sort order")
] = "relevance"
) -> dict:
...
Now Claude can't invent a sort="best"
value.
Tool returns too much data:
Claude gets overwhelmed by large JSON responses. Return IDs and summaries, not full objects:
# ✗ Bad - too much data
return {"results": [full_note_object for note in results]}
# ✓ Good - just IDs
return {
"count": len(results),
"note_ids": [note.id for note in results]
}
Let Claude call another tool like get_note_by_id
if it needs details.
Claude never uses your tool:
Your description isn't clear enough. Compare these:
# ✗ Vague
description="Search notes"
# ✓ Specific
description="Search through your personal notes by keyword or phrase. Use this when the user asks about their notes, ideas, or past entries."
The second one tells Claude exactly when to use it.
Environment Variables and Secrets
Don't hardcode API keys in your server. Use environment variables:
{
"mcpServers": {
"github": {
"command": "python",
"args": ["/path/to/github-mcp/server.py"],
"env": {
"GITHUB_TOKEN": "ghp_your_token_here",
"LOG_LEVEL": "debug"
}
}
}
}
Your server reads them:
import os
GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN")
if not GITHUB_TOKEN:
raise ValueError("GITHUB_TOKEN environment variable required")
Better: Store tokens in a .env
file and reference them. But for local development, putting them directly in the config works.
Debugging Tips That Actually Help
Add verbose logging:
import logging
logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()])
logger = logging.getLogger(__name__)
@mcp.tool(description="Search notes")
async def search_notes(query: str) -> dict:
logger.info(f"Received search query: {query}")
results = do_search(query)
logger.info(f"Returning {len(results)} results")
return {"results": results}
Watch your server logs while testing:
Run your server in a terminal window while using Claude Desktop. You'll see every tool call in real time.
Test tools manually first:
Before blaming Claude, call your tools directly:
import asyncio
result = asyncio.run(search_notes("MCP"))
print(result)
If this doesn't work, your tool has bugs independent of Claude.
Use correlation IDs:
Generate a unique ID for each tool call:
import uuid
@mcp.tool(description="Search notes")
async def search_notes(query: str) -> dict:
request_id = str(uuid.uuid4())
logger.info(f"[{request_id}] Search query: {query}")
# ... your code ...
logger.info(f"[{request_id}] Completed in {elapsed}s")
return results
Now you can trace specific calls through your logs.
Why This Setup Works
Claude Desktop launches your server as a subprocess and communicates over stdin/stdout using JSON-RPC. This means:
- Your server must be non-interactive (no
input()
prompts) - Logs must go to stderr, not stdout
- The process must stay running and responsive
- Config changes require a full restart
Once you understand this model, debugging gets much easier. Most issues are:
- Invalid JSON config
- Wrong paths
- Server crashes on startup
- Print statements breaking stdout
Fix these, and everything works.
Key Takeaways
- Use absolute paths everywhere in your config—relative paths don't work
- Watch for trailing commas in JSON—they break everything silently
- Actually quit Claude Desktop (Cmd+Q), don't just close the window
- Test your server standalone first before blaming the integration
- Add verbose logging to stderr so you can see what's happening
- Start with one server and get it working before adding more
- JSON Schema descriptions matter—the better they are, the more accurately Claude uses your tools
Resources
- Anthropic MCP Docs – Official Claude Desktop MCP setup
- FastMCP GitHub – Best Python MCP library
- MCP Specification – Full protocol details
- Building Your First MCP Server – Start here if you don't have a server yet
Still stuck? Drop a question—we're all figuring this out together.