Skip to main content
Claude Code Tutorial: From First Command to Custom Workflows

Configuring MCP Servers: `.mcp.json` Schema and Scopes

Exact configuration: server types (stdio, HTTP, SSE), all required/optional fields, environment variables, per-scope settings, and managed MCP for organizations.

Advanced20 minBy ToolDix Editorial

Learning objectives

  • Understand the complete `.mcp.json` schema for all server types
  • Configure stdio (local) and HTTP (remote) servers with exact field names
  • Use environment variables and dynamic headers for authentication
  • Apply settings scopes: local, project, user, and managed
  • Control MCP server availability with allowlists and denylists

ToolDix original visual

Claude Code Tutorial practice loop
1

Frame

Name the outcome and constraints.

2

Build

Try one bounded workflow.

3

Review

Keep evidence, revise, and share.

MCP servers are configured in .mcp.json (project scope) or ~/.claude.json (user scope), with optional enforcement via managed settings. The configuration schema differs slightly by server type (stdio vs. HTTP), and scopes determine who can see and use each server.

ToolDix original diagram
MCP: connecting tools via servers
Claude Code
Asks for tools via MCP
MCP Servers
Playwright (browser)
GitHub (git ops)
Slack (messaging)
Your custom APIs
Tools appear as: mcp__server_name__action

The complete .mcp.json schema

A .mcp.json file in your project root defines servers shared with your team:

{
  "mcpServers": {
    "server_name_1": { ... server config ... },
    "server_name_2": { ... server config ... }
  }
}

The mcpServers object is a map where each key is a server name you choose, and each value is a server configuration object.

Server type 1: Stdio (local process)

Stdio servers run as local processes on your machine. Claude Code starts the process, connects via stdin/stdout with JSON-RPC messages.

Schema

{
  "type": "stdio",
  "command": "/absolute/path/to/executable",
  "args": ["arg1", "arg2"],
  "env": {
    "VAR_NAME": "value"
  }
}

Required fields:

  • type (string): Always "stdio" for local servers.
  • command (string): Absolute path to the executable. Relative paths are not supported. Use $HOME or construct full path.

Optional fields:

  • args (array of strings): Command-line arguments passed to the executable. Claude Code runs: command arg1 arg2 ...
  • env (object): Environment variables passed to the subprocess. Keys are variable names, values are strings. These supplement (not replace) the parent process's environment.

Example: Playwright MCP server

{
  "type": "stdio",
  "command": "npx",
  "args": ["-y", "@playwright/mcp@latest"]
}

This tells Claude Code to run: npx -y @playwright/mcp@latest

The server responds with tools like browser_navigate, browser_click, browser_screenshot.

Example: Custom server with environment

{
  "type": "stdio",
  "command": "/usr/local/bin/my-server",
  "args": ["--mode", "production"],
  "env": {
    "API_KEY": "${API_KEY}",
    "DEBUG": "false",
    "LOG_LEVEL": "info"
  }
}

Claude Code:

  1. Expands ${API_KEY} by reading from the process environment (see below)
  2. Starts: /usr/local/bin/my-server --mode production
  3. Sets env vars: API_KEY=<from process>, DEBUG=false, LOG_LEVEL=info

Server type 2: HTTP (remote service)

HTTP servers are hosted at a URL. Claude Code POSTs tool calls and requests to the server endpoint.

Schema

{
  "type": "http",
  "url": "https://mcp.example.com/api",
  "headers": {
    "Authorization": "Bearer token_string",
    "X-Custom-Header": "value"
  }
}

Required fields:

  • type (string): Always "http" for remote servers.
  • url (string): HTTPS endpoint URL where Claude Code sends tool calls. Must be reachable from your machine.

Optional fields:

  • headers (object): Custom HTTP headers sent with every request. Used for authentication, API keys, custom metadata.

Example: GitHub MCP server

{
  "type": "http",
  "url": "https://api.github.com/mcp",
  "headers": {
    "Authorization": "Bearer ghp_..."
  }
}

Claude Code POSTs to https://api.github.com/mcp with the token in the Authorization header.

Example: Sentry MCP server (OAuth)

{
  "type": "http",
  "url": "https://mcp.sentry.dev/mcp"
}

For OAuth servers, headers are not needed at setup. Claude Code prompts you to authenticate in your browser on first use.

Server type 3: SSE (Server-Sent Events, rare)

SSE servers send events from server to client. Uncommon; most servers use HTTP POST.

Schema

{
  "type": "sse",
  "url": "https://mcp.example.com/events"
}

Less documented and rarely used. Stick with stdio and HTTP for most cases.

Authentication: static tokens vs. OAuth vs. dynamic headers

Static token in headers

For services with API keys or bearer tokens that don't expire:

{
  "type": "http",
  "url": "https://api.example.com/mcp",
  "headers": {
    "Authorization": "Bearer sk_live_abcdef123456"
  }
}

Risk: Token is stored in plain text in .mcp.json. For shared projects, never commit real tokens.

Environment variable expansion

Use ${VAR_NAME} or ${VAR_NAME:-default} to read from environment variables:

{
  "type": "http",
  "url": "https://api.example.com/mcp",
  "headers": {
    "Authorization": "Bearer ${API_TOKEN}"
  }
}

At runtime, Claude Code reads $API_TOKEN from your shell environment. Commit the file with ${API_TOKEN}; the actual token is only in your environment.

# Set the token before running Claude Code
export API_TOKEN="sk_live_..."
claude

OAuth (browser sign-in)

Some servers (Sentry, Linear, GitHub) support OAuth. No token needed in config; Claude Code prompts you to sign in on first use:

{
  "type": "http",
  "url": "https://mcp.sentry.dev/mcp"
}

First time Claude tries to use it:

/mcp
Select: sentry
> Authenticate
[Browser opens, you sign in]
Status: Connected

Configuration scopes: where servers live

MCP servers can be defined at different scopes, each with different visibility and sharing:

ScopeFile LocationShared?Use CasePrecedence
Managed/Library/Application Support/ClaudeCode/managed-mcp.json (macOS)
/etc/claude-code/managed-mcp.json (Linux)
C:\Program Files\ClaudeCode\managed-mcp.json (Windows)
Yes (IT deployed)Organization-wide policy; users cannot override1 (highest)
Local~/.claude.json (per-project state, in your home directory)No (not shared)Personal servers used in all projects; per-project OAuth state2
Project.mcp.json in repo rootYes (git committed)Team-shared servers; part of reproducible setup3
User~/.claude.json (global)No (personal only)Personal servers used everywhere4

Precedence: If the same server name is defined in multiple scopes, the highest-precedence one wins (Managed > Local > Project > User).

Which scope to use

Use Managed (enterprise policy file): Deploy identical servers to all employees; users cannot add their own MCP servers.

Use Project (.mcp.json): Team wants a shared GitHub server, a Slack server, and maybe a custom internal tool. Commit .mcp.json to git so teammates get the same servers when they clone.

Use User (~/.claude.json): You personally use a Playwright server across all your projects, or you have a personal API key to an internal tool. Not shared; stays on your machine.

Use Local (~/.claude.json per-project state): Claude Code stores per-project OAuth state here automatically (e.g., Sentry session tokens). You don't write to this directly.

Example: project-level server in .mcp.json

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.github.com/mcp",
      "headers": {
        "Authorization": "Bearer ${GITHUB_TOKEN}"
      }
    },
    "playwright": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@playwright/mcp@latest"]
    },
    "internal-api": {
      "type": "http",
      "url": "https://internal.example.com/mcp",
      "headers": {
        "Authorization": "Bearer ${INTERNAL_TOKEN}"
      }
    }
  }
}

Team members clone the repo and run:

export GITHUB_TOKEN="ghp_..."
export INTERNAL_TOKEN="..."
claude

First time, they approve each server. Then servers are cached and ready.

Managing server access with settings

You can control which servers Claude Code allows using settings in any .claude/settings.json:

{
  "allowedMcpServers": [
    { "serverUrl": "https://api.github.com/*" },
    { "serverUrl": "https://mcp.sentry.dev/*" }
  ],
  "deniedMcpServers": [
    { "serverUrl": "https://untrusted.example.com/*" }
  ],
  "enabledMcpjsonServers": ["github", "playwright"],
  "disabledMcpjsonServers": ["internal-api"]
}

Settings fields:

  • allowedMcpServers (array): List of allowed servers by URL or command. If set, only matching servers load.
  • deniedMcpServers (array): List of blocked servers. Always applied; overrides allowlist.
  • enabledMcpjsonServers (array): Names of servers from .mcp.json to enable.
  • disabledMcpjsonServers (array): Names of servers from .mcp.json to disable.
  • allowManagedMcpServersOnly (boolean, managed only): If true, only servers in managed-mcp.json are allowed.

Allowlist and denylist matching

Entries match by URL pattern (with * wildcards), exact command string, or server name:

{
  "allowedMcpServers": [
    { "serverUrl": "https://api.github.com/*" },
    { "serverUrl": "https://*.internal.example.com/*" },
    { "serverCommand": ["npx", "-y", "@playwright/mcp@latest"] }
  ],
  "deniedMcpServers": [
    { "serverUrl": "https://untrusted.example.com/*" }
  ]
}

URL matching rules:

  • https://api.github.com/* matches https://api.github.com/mcp
  • https://*.example.com/* matches https://api.example.com/mcp, https://data.example.com/mcp, etc.
  • *://example.com/* matches both HTTP and HTTPS to that domain
  • http://localhost:*/* matches any port on localhost

Command matching: Must match exactly, including all arguments in order.

Managed MCP for organizations

If you're an IT administrator, deploy managed-mcp.json at the system level to enforce which servers run in your organization:

Exclusive control: all users get the same servers

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.github.com/mcp"
    },
    "sentry": {
      "type": "http",
      "url": "https://mcp.sentry.dev/mcp"
    },
    "internal-tools": {
      "type": "stdio",
      "command": "/usr/local/bin/company-mcp-server",
      "env": {
        "COMPANY_API_URL": "https://internal.example.com"
      }
    }
  }
}

Users cannot add, modify, or use any other MCP servers. claude mcp add fails with: Cannot add MCP server: enterprise MCP configuration is active and has exclusive control over MCP servers.

Disable MCP entirely

{
  "mcpServers": {}
}

All MCP functionality is disabled; users see no MCP servers in /mcp and cannot add any.

Allowlist with policy

For an approved-catalog approach, combine managed-mcp.json with allowlists in user/project settings:

In managed-mcp.json:

{
  "allowedMcpServers": [
    { "serverUrl": "https://api.github.com/*" },
    { "serverUrl": "https://mcp.sentry.dev/*" }
  ],
  "allowManagedMcpServersOnly": true
}

Only servers in the allowlist can be added. Users cannot add servers outside the list, even if they know the URL.

Troubleshooting configuration

Server shows as "Connected" but no tools appear

The server started but didn't return tools. Check the server's logs:

# For stdio servers, the command runs and outputs to stdout
# Test manually:
npx @playwright/mcp@latest

For HTTP servers, the endpoint is unreachable or returns wrong format.

"Failed to connect"

Stdio server: The command doesn't exist or isn't executable. Check the path:

# Test if command exists
/usr/local/bin/my-server --version

HTTP server: The URL is unreachable. Test with curl:

curl -I https://api.example.com/mcp

Settings don't take effect

Claude Code reads settings at session start. If you edit .claude/settings.json while a session is active, exit and restart:

# Exit Claude Code
# Edit .claude/settings.json
# Start a new session
claude

Example: complete team MCP setup

Here's a realistic .mcp.json for a team:

{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.github.com/mcp",
      "headers": {
        "Authorization": "Bearer ${GITHUB_TOKEN}"
      }
    },
    "slack": {
      "type": "http",
      "url": "https://mcp.slack.com/api/mcp",
      "headers": {
        "Authorization": "Bearer ${SLACK_BOT_TOKEN}"
      }
    },
    "postgres": {
      "type": "stdio",
      "command": "/usr/local/bin/mcp-postgres",
      "args": ["--host", "db.internal.example.com", "--port", "5432"],
      "env": {
        "PGUSER": "${DB_USER}",
        "PGPASSWORD": "${DB_PASSWORD}"
      }
    },
    "playwright": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@playwright/mcp@latest"]
    }
  }
}

Add .mcp.json to git. Team members:

git clone <repo>
export GITHUB_TOKEN="ghp_..."
export SLACK_BOT_TOKEN="xoxb-..."
export DB_USER="claude_code"
export DB_PASSWORD="..."
claude

On first run, Claude Code prompts to approve each server. After that, all four servers are available for every session.

Common mistake

Storing plaintext secrets in .mcp.json and committing to git. Always use environment variable expansion (${VAR_NAME}) for tokens and passwords, and set them in your shell before running Claude Code. Keep real secrets out of version control. If a secret is already in git history, rotate it immediately.

Sources and license context

These references informed the lesson. ToolDix adds its own explanation, workflow, and practice rather than reproducing source material. Every link below leaves ToolDix and opens the publisher's own site in a new tab.

Keep going

Read these next on ToolDix.

Original lessons that build on what you just read.