ClaudeMap

·MCP Servers

A 2026 guide to the MCP server config format — where Claude Desktop and Claude Code read their config, layered precedence (MDM / project / user), stdio vs HTTP shapes, working examples for filesystem / GitHub / Postgres servers, OAuth scope drift debugging, and a 7-step troubleshooting checklist.

Configuring MCP Servers in Claude Desktop and Claude Code (2026)

MCP only earns its keep when you wire servers into the host you actually use. This guide, current as of the MCP 2025-11-25 spec, walks through the unified JSON config format read by Claude Desktop / Claude Code / Cursor / Zed, gives three working reference servers (filesystem / GitHub / Postgres), covers the dual-host differences (stdio vs stdio+OAuth), and ships a 7-step troubleshooting checklist for when a server refuses to load.

TL;DR

  • Every MCP host reads the same JSON config, structured as {mcpServers: {name: {command, args, env}}}
  • stdio servers use command + args + env; HTTP servers use url + headers
  • Claude Desktop config: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
  • Claude Code config: project-level .mcp.json + user-level ~/.claude.jsonone extra precedence layer vs Desktop
  • Troubleshooting order: Inspector runs OK → check Host log → verify env var propagation → permissions and sandbox
  • 2026 pitfalls: claude_desktop_config.json and claude.json use different field names; OAuth scope drift on remote servers

Where each host reads its config

Before writing anything, know where the file lives.

Claude Desktop reads a single config file:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

Claude Desktop also exposes this file through its UI: open Settings → Developer → Edit Config and it opens the file in your editor. You can enable, disable, and inspect servers there too.

Claude Code reads MCP servers from several layers, all merged:

  • .mcp.json in your project root (checked into the repo, shared with the team)
  • ~/.claude.json or your user settings (personal servers)
  • Plugin-provided servers

The project .mcp.json is the one you commit. It is the standard way to share a set of MCP servers with everyone working in a repository.

Both hosts use the same mcpServers shape, so a server entry you write once can move between them with minor path changes.

The config shape

Every config is an object with one key, mcpServers, whose value is a map of server name to server definition. The name is just a label you pick — it shows up in the host UI and in tool names. Each definition needs a command and args, and optionally env:

{
  "mcpServers": {
    "my-server": {
      "command": "node",
      "args": ["/absolute/path/to/index.js"],
      "env": {
        "API_KEY": "sk-..."
      }
    }
  }
}

A few rules worth internalizing:

  • Use absolute paths. Relative paths are resolved against the host's working directory, which is not always where you expect. Hardcoding the absolute path to your server's entry point removes a whole class of "it works on my machine" bugs.
  • command is an executable, not a shell string. If you need a shell, run bash or cmd and pass -c plus the script in args.
  • env is additive. The spawned process inherits the host's environment, and entries here are merged on top. This is where API tokens and config flags go.
  • The server name must be unique within one config file. Duplicate keys silently overwrite.

A filesystem server

The official @modelcontextprotocol/server-filesystem server exposes a set of directories to the model as readable and writable resources. It is the simplest real server to configure and a good first smoke test.

Install it once so the host can find it:

npm install -g @modelcontextprotocol/server-filesystem

Then add it to claude_desktop_config.json:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/me/projects",
        "/Users/me/notes"
      ]
    }
  }
}

The trailing path arguments are the directories the server is allowed to see. Anything outside that list is invisible to the model — that boundary is the security model, so list exactly the roots you want to expose and nothing more. After saving the file and restarting Claude Desktop, you can ask "what files are in my projects directory?" and the model will call the filesystem server to answer.

For Claude Code, the same entry goes into .mcp.json in the project root. Claude Code also lets you add it interactively:

claude mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem /Users/me/projects

The CLI writes the entry into the right config layer for you and validates the JSON on the way in.

A GitHub server

The official GitHub MCP server (@modelcontextprotocol/server-github or the newer @modelcontextprotocol/server-github) lets the model read issues, pull requests, and repository metadata through the GitHub REST API. It needs a personal access token, which is exactly what the env block is for.

{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here"
      }
    }
  }
}

Create the token at github.com/settings/tokens with the scopes you actually need (repo for private repos, read:org for org data). Treat the token like a password — never commit the config file with a real token in it. If you are sharing a project config, use a placeholder and document where to put the real value, or load it from a separate local file that is gitignored.

Once loaded, you can ask the model things like "list the open issues in my repo labeled bug" or "summarize the comments on PR #1234" and it will call the GitHub server to fetch the data. Tools show up as mcp__github__<tool_name> in Claude Code, which makes it obvious which server a given tool came from.

A Postgres server

The official Postgres server (@modelcontextprotocol/server-postgres) exposes a read-only (by default) view of a database: tables, schemas, and the ability to run SELECT queries. You pass a connection string:

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-postgres",
        "postgresql://user:password@localhost:5432/mydb"
      ]
    }
  }
}

A few notes specific to databases:

  • Prefer a read-only role. The Postgres server is read-only by default, but connecting with a role that only has SELECT privileges is belt-and-suspenders. Never hand the model a connection string for a production write user.
  • Schema is everything. The model reads the schema to decide what queries to run, so a well-named schema produces far better queries than a cryptic one.
  • Connection pooling. Each MCP server process holds its own connection. For a local dev database that is fine; for a shared database, point it at a connection pooler like PgBouncer.

Claude Desktop vs Claude Code

The two hosts overlap heavily but differ in a few practical ways.

Claude Desktop is a desktop app with a graphical settings panel. You edit claude_desktop_config.json, restart the app, and the servers appear under the hammer icon in the composer. There is no per-project config — every chat sees the same set of servers.

Claude Code is a terminal tool that lives alongside your code. Its .mcp.json is per-project and checked into git, so a repository can declare the servers it depends on. It also layers user-level and plugin-provided servers on top. You can scope servers to a project, inspect them with claude mcp list, and remove them with claude mcp remove <name>. The tool-prefix convention (mcp__<server>__<tool>) makes the source of every tool call explicit.

In practice: use Claude Desktop config for personal, cross-project servers (your notes, your calendar). Use a project .mcp.json for servers that are part of how a specific codebase works (its database, its issue tracker, its internal APIs).

Making changes take effect

MCP servers are spawned by the host, so config changes only apply after the host picks them up:

  • Claude Desktop — fully quit and relaunch the app. Closing the window is not enough; the server processes keep running until the app exits.
  • Claude Code — start a new session, or run /mcp to reconnect within a running session.

If a server still does not appear after a restart, the first thing to check is whether the JSON parses. A trailing comma or a missing brace will make the host silently ignore the whole config, and no servers will load at all. Paste the file through a JSON validator if you are unsure.

Field precedence: Claude Code's layered override model

Claude Desktop reads a single config (claude_desktop_config.json), but Claude Code uses three stacked layers — the most common trip-hazard for new users:

| Layer | Path | Purpose | Priority | |---|---|---|---| | Enterprise managed | MDM push / managed settings | Company-wide distribution | Highest (cannot be overridden) | | Project-level | <project>/.mcp.json | Team-shared, checked into git | High | | User-level | ~/.claude.json mcpServers field | Personal tools, cross-project | Low |

Merge rule: for the same slug, enterprise layer overrides project layer overrides user layer. Concretely: (1) a senior engineer's local tool doesn't leak into the project (project layer wins), (2) a company-pushed audit server can't be bypassed by the user (enterprise layer wins).

Key differences between .mcp.json (Claude Code project) and claude_desktop_config.json (Desktop):

  • .mcp.json supports a trust field — true skips the Host's startup confirmation prompt.
  • ~/.claude.json (Claude Code user-level) has an enableMcpServers allowlist — servers not on the list are blocked even if configured (2026 safety feature).
  • Shared field names across Desktop and Code: command / args / env / url / headers are all the same, but Desktop's claude_desktop_config.json puts mcpServers at the top level, while Code's ~/.claude.json nests it inside other user-level config (see code.claude.com/docs/en/mcp).

Anti-pattern: putting a server with sensitive tokens in ~/.claude.json and pushing it to git. User-level config tokens should be machine-local only — anything the team needs must go in .mcp.json with tokens injected from a secret store at deploy time.

stdio vs HTTP: when to switch config shapes

{command, args, env} suits local stdio servers (the MCP 2025-11-25 transports spec) — the Host spawns the server as a child process, and the three fields map directly to child_process.spawn. Pros: zero network, zero auth, zero ops. Cons: server must live on the Host's machine; no team sharing.

{url, headers} suits remote Streamable HTTP servers — the Host communicates with the server on a separate machine via HTTP POST + SSE. OAuth 2.1 + Bearer Token flows through headers.Authorization (authorization spec). Pros: cross-machine, team-shareable, callable from Claude Desktop / Code / Cursor concurrently. Cons: adds an OAuth ops layer.

Config example: stdio → HTTP switch:

// stdio: server on the same machine
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_..." }
    }
  }
}

// HTTP: server remote, OAuth required
{
  "mcpServers": {
    "github": {
      "url": "https://mcp.example.com/github",
      "headers": {
        "Authorization": "Bearer ${GITHUB_MCP_TOKEN}"
      }
    }
  }
}

Decision rule:

  • Personal local tools → stdio is always enough
  • Team-shared server → HTTP, the only option
  • One server wanted by multiple Hosts (Desktop + Code + Cursor) → HTTP; under stdio each Host forks its own copy and configs can't sync
  • SaaS / public catalog → HTTP + MCP Registry (modelcontextprotocol/registry)

Migration steps: (1) decouple the server's business logic from its transport adapter; (2) re-wrap with the Streamable HTTP transport from an official SDK; (3) configure OAuth 2.1 and headers.Authorization; (4) implement session isolation on the server (Mcp-Session-Id); (5) walk the security best practices checklist (Origin checking + DNS rebinding defence).

OAuth scope drift: the most common "worked yesterday, fails today"

Once you switch to HTTP, the strangest bug is: Inspector calls the server fine, but the Host gets 401 with no body. 90% of the time it's OAuth scope drift — the token the Host receives doesn't include the resource scope the server actually needs.

OAuth 2.1 + RFC 8707 Resource Indicators in the MCP context looks like:

| Symptom | Root cause | Fix | |---|---|---| | Inspector OK, Host 401 | Host's token scope lacks this resource | Use a scope-correct token in headers.Authorization, or trigger OAuth refresh | | Works but tools/list is empty | server didn't declare capabilities.tools | Add the capabilities field in the server's SDK | | Intermittent 401 | token expired, not auto-refreshed | Configure OAuth client's refresh_token rotation | | Every call 401s | token audience wrong (resource binding failed) | Re-run OAuth flow; token must match server URL's audience |

Debugging steps:

  1. Hit the server directly via Inspector (bypass the Host) — if OK, the problem is in the Host's token flow; if not, the server is the issue.
    1. Capture the Authorization header in Host logs, check token scope (usually needs server-side log help).
  2. Have the server print the token scopes it received during initialize (debug mode).

Prevention: in CI, run an automated OAuth flow → token validation → real tools/list call test, every time the server SDK upgrades.

Troubleshooting checklist

When a server refuses to load or a tool does not show up, work through these in order:

  1. Does the JSON parse? One syntax error disables every server in the file.
  2. Is the path absolute and correct? Print the full command from your config and run it in a terminal. If it errors there, it will error in the host too.
  3. Are the dependencies installed? npx -y <package> downloads on first run, which needs network access. In air-gapped environments install the package globally first.
  4. Are the environment variables set? A server that needs GITHUB_PERSONAL_ACCESS_TOKEN and does not get it will start, then fail on the first tool call. Check the token has the right scopes.
  5. Does the server print anything to stderr? Claude Desktop writes server logs to ~/Library/Logs/Claude/mcp-server-<name>.log on macOS; Claude Code surfaces them through the /mcp panel. Read them before guessing.
  6. Is there a version mismatch? MCP is still maturing. If a server was written against an older protocol version, pin the version in args (e.g. @modelcontextprotocol/server-filesystem@0.6.0) until the host catches up.

The fastest debugging loop is usually: run the server command directly in a terminal, watch it print its startup banner, then kill it and let the host spawn it. If the standalone run works but the host run does not, the problem is in the config or environment, not the server.

Frequently asked questions

Do I need to restart Claude Desktop after editing the config file?

Yes. Claude Desktop spawns MCP servers when the app starts and keeps them alive for the session. Editing claude_desktop_config.json changes nothing until you fully quit and relaunch the app; closing the window is not a restart.

What is the difference between claude_desktop_config.json and .mcp.json?

claude_desktop_config.json is Claude Desktop's single global config file and applies to every chat. .mcp.json is Claude Code's per-project config file, checked into the repo so the whole team gets the same servers. Both use the same mcpServers shape.

Can a server access my whole filesystem?

Only what you explicitly allow. The filesystem server takes the allowed directories as command-line arguments, and anything outside that list is invisible to the model. Treat that allowlist as the security boundary and list exactly the roots you want to expose.

How do I pass an API token to a server safely?

Put it in the server's env block. The host forwards these variables to the spawned process. Never commit real tokens into a shared config — use placeholders, load from a gitignored local file, or inject from a secrets manager at deploy time.

What wins when Claude Code's project .mcp.json and user ~/.claude.json conflict?

By layer, from highest to lowest: enterprise managed (MDM) overrides project-level overrides user-level. When the same slug appears in multiple layers, the Host uses the highest-priority version. If you want to force a user-level override over the project-level one, set the project's server to disabled: true; the enterprise managed layer cannot be overridden by anything below.

A remote HTTP server suddenly returns 401 with no body — what now?

Four-step debug: (1) connect to the server directly via Inspector to see if it's OK — if yes, the Host's token-acquisition path is the problem; if no, it's the server. (2) Capture the Authorization header in the Host logs, verify the token scope is sufficient. (3) Trigger an OAuth refresh — the token may have expired. (4) Verify the token's audience matches the server URL (RFC 8707 requires binding). 90% of "worked yesterday, fails today" is step 3 or step 4 — automatic token rotation, or the client switched audiences.

Official references

This article reflects publicly available information as of the MCP 2025-11-25 spec (August 2026). Spec evolves; check the version number every six months.