Skip to content

MCP in Production ​

What you'll learn

  • .mcp.json at the project root defines team-shared MCP configuration; commit it and reference environment variables for secrets
  • CI/CD runs MCP with --mcp-config <file> and --strict-mcp-config to load only the servers the pipeline needs
  • Security model: credentials never touch config files; read-only database users for shared servers; local scope for personal experiments
  • The production validation pattern runs a Postgres MCP query in CI, gates deployment on the result, and keeps the same config that developers use locally

The problem: your dev machine setup does not survive your team ​

The previous lessons connected MCP servers on your personal machine using claude mcp add. Your .claude/settings.local.json has a Postgres connection string. Your ~/.claude.json has a GitHub token. Everything works when you type claude in your terminal.

Then your teammate asks: "Which MCP servers should I connect to review your PR?" And CI asks: "How do I run the same database validation the developer runs before merging?" And you realize: your personal config is the only source of truth, it contains secrets that should not be shared, and it does not work outside your machine.

The gap between "works on my machine" and "works on the team" is where most MCP setups stall. This lesson covers the three things that bridge that gap: project-scoped configuration that is safe to commit, CI/CD integration that uses the same config, and security boundaries that keep credentials where they belong.

Options & when to use each ​

OptionGood forCostsWhen to pick
.mcp.json at the project rootTeam-shared MCP configuration. Defines which servers the project needs without embedding credentials. Committed to git.Everyone on the team sees the server list. Credentials must live in environment variables or a secrets manager, not the file.You work on a team and want every developer to connect the same MCP servers without a manual setup document.
CI/CD with --mcp-config + --strict-mcp-configAutomated validation in GitHub Actions, GitLab CI, or any pipeline. Claude Code runs in print mode with exactly the servers the pipeline needs.Claude Code must be installed in the CI environment. MCP servers need runtime access (npx, Postgres connectivity from CI runners).You want database validation, schema checks, or automated reviews to run in CI using the same MCP servers developers use locally.
Per-environment .mcp.json filesDifferent server configurations for dev, staging, and production. mcp.staging.json points to staging databases; mcp.prod.json points to production (read-only).More files to maintain. Risk of a developer accidentally loading the production config during development.You have multiple environments with different database instances and want Claude Code to connect to the right one without manual switching.
Personal ~/.claude.json only (what previous lessons teach)Quick experiments, personal projects, solo work. One command, no file to manage.Not shareable. Not reproducible. Dies when you switch machines.You are the only person using the setup and you do not need CI. Fine for learning; not for team workflows.

Build it ​

Part 1: Create a project-scoped .mcp.json ​

A .mcp.json file at the root of your project defines which MCP servers Claude Code should load when working in that directory. It replaces the manual claude mcp add commands with a declarative config that you can commit.

Step 1: Write the file.

Create .mcp.json in your project root:

json
{
  "mcpServers": {
    "postgres": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "DATABASE_URL": "${DATABASE_URL}"
      }
    },
    "filesystem": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
    },
    "github": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {
        "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
      }
    }
  }
}

Key details:

  • "type": "stdio" tells Claude Code to spawn the server as a child process on stdin/stdout.
  • "${DATABASE_URL}" uses shell variable expansion. The value comes from the environment where Claude Code runs, not from the file.
  • "." in the Filesystem args scopes the server to the project directory. In .mcp.json, relative paths resolve against the file's location (the project root).
  • No secrets in the file. Tokens and connection strings are environment variable references.

Step 2: Set the environment variables.

Each developer (and the CI runner) needs the referenced variables:

bash
export DATABASE_URL="postgresql://claude_readonly:password@localhost:5432/myapp_dev"
export GITHUB_PERSONAL_ACCESS_TOKEN="ghp_your_token"

Use a .env file for local development if your team prefers:

bash
# .env (gitignored)
DATABASE_URL=postgresql://claude_readonly:password@localhost:5432/myapp_dev
GITHUB_PERSONAL_ACCESS_TOKEN=ghp_your_token

Then source it:

bash
set -a && source .env && set +a
claude

Step 3: Verify project config loads.

bash
claude mcp list

You should see postgres, filesystem, and github all listed as "connected," with their source showing the .mcp.json path rather than a personal config file. If you previously added any of these via claude mcp add, remove the duplicates first:

bash
claude mcp remove postgres   # removes the personal entry
claude mcp remove filesystem
claude mcp remove github

Step 4: Commit and share.

.mcp.json is safe to commit. It contains no secrets. Add it to version control:

bash
git add .mcp.json
git commit -m "Add project MCP configuration for Postgres, Filesystem, and GitHub"

Every developer who clones the repo and sets the required environment variables gets the same MCP servers. No setup document, no manual claude mcp add commands.

Part 2: CI/CD integration ​

The goal: a CI job that runs Claude Code in print mode, connects to the same MCP servers, and validates something that gates deployment.

Step 1: Create a CI-specific MCP config (optional).

If your CI environment needs different servers or stricter configuration, create a separate file:

json
// .mcp.ci.json
{
  "mcpServers": {
    "postgres": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "DATABASE_URL": "${CI_DATABASE_URL}"
      }
    }
  }
}

--strict-mcp-config ensures only the servers in this file load. It ignores .mcp.json, ~/.claude.json, and any other config source.

Step 2: Write the GitHub Actions workflow.

yaml
# .github/workflows/mcp-validation.yml
name: MCP Data Validation

on:
  pull_request:
    paths:
      - 'db/migrations/**'
      - 'src/models/**'

jobs:
  validate-schema:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install Claude Code
        run: npm install -g @anthropic-ai/claude-code

      - name: Validate database schema with MCP
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          CI_DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
        run: |
          claude --bare -p \
            "Connect to the Postgres database via MCP. List all tables and verify that every table has a primary key. Report any tables missing primary keys. If all tables have primary keys, output 'VALIDATION PASSED'." \
            --mcp-config .mcp.ci.json \
            --strict-mcp-config

What this does:

  • Triggers on PRs that touch database-related files.
  • Installs Claude Code and Node.js on the runner.
  • Loads only the Postgres MCP server from .mcp.ci.json.
  • Runs a validation prompt in print mode (-p) with bare output (--bare).
  • Uses --strict-mcp-config so no other MCP configs interfere.

Step 3: Set up CI secrets.

In GitHub repository settings, add:

  • ANTHROPIC_API_KEY: your Anthropic API key (Claude Code needs it for API access in CI).
  • CI_DATABASE_URL: a read-only connection string for the CI database.

Never use your personal development database credentials for CI. Create a dedicated CI database user with minimal permissions.

Step 4: Test the workflow locally.

Before pushing, test that your CI config works from the command line:

bash
export CI_DATABASE_URL="postgresql://ci_readonly:password@localhost:5432/myapp_test"
export ANTHROPIC_API_KEY="sk-ant-your-key"

claude --bare -p \
  "Connect to the Postgres database via MCP. List all tables." \
  --mcp-config .mcp.ci.json \
  --strict-mcp-config

If you see a table list, the CI config works. Push and let the workflow run on the next matching PR.

Part 3: Security boundaries ​

MCP servers run with whatever access their configuration grants. A misconfigured server is a direct path to data exposure or modification. These rules prevent the common failure modes:

1. Credentials never touch config files.

Environment variables are the only allowed place for tokens, passwords, and connection strings. JSON config files contain ${VAR_NAME} references, never the value itself. This applies to .mcp.json (committed), personal config files (not committed but still on disk), and CI config files.

Wrong:

json
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://admin:hunter2@prod-db:5432/production"]
    }
  }
}

Right:

json
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-postgres"],
      "env": {
        "DATABASE_URL": "${DATABASE_URL}"
      }
    }
  }
}

2. Read-only database users for shared servers.

If multiple people connect to the same database through an MCP server, create a read-only user. Even if your team trusts each other, a prompt that includes "UPDATE" (intended as a typing instruction) can become a real UPDATE if the database user has write permissions.

sql
CREATE USER mcp_readonly WITH PASSWORD 'generate-a-strong-password';
GRANT CONNECT ON DATABASE myapp TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;

The last line ensures future tables are also readable.

3. Local scope for personal experiments.

When you want to try a new MCP server or a different configuration, use local scope (-s local). Entries in .claude/settings.local.json are gitignored by default. You can break things in local scope without affecting your team. If the experiment works, move it to .mcp.json.

4. Production databases get read-only MCP, always.

If you connect an MCP server to a production database, the connection user must be read-only. No exceptions. Even if the server advertises write tools, they will fail at the database level. This is a belt-and-suspenders approach: the database permission is the safety net; the read-only user is the belt.

5. Audit your MCP servers before sharing config.

Before committing a .mcp.json or sharing a server list with your team, run:

bash
claude mcp list

Review every server. For each one, ask: does this expose data I would not want every team member to see? If yes, move it to local scope. The project config is public to the team.

What goes wrong ​

MistakeHow you notice itThe fix
.mcp.json committed with a hardcoded connection stringSomeone on the team finds credentials in the git history. A security scanner flags the commit.Immediately rotate the exposed credential. Remove the hardcoded value and replace with an environment variable reference. Use git filter-branch or BFG Repo-Cleaner to scrub the secret from history, then force push.
CI runner cannot reach the databaseThe CI job fails with a connection timeout or "could not connect to server" errorCI runners are ephemeral and may not have network access to your database. Use a CI-hosted test database, a Docker service container in the workflow, or a database with public access restricted by IP whitelist.
Developer loads production config by accidentProduction data appears in a development Claude Code session. Queries return unexpected (production-scale) results.Use different environment variable names for different environments: DEV_DATABASE_URL vs PROD_DATABASE_URL. Never use the same variable name for both. Keep production config in a separate .mcp.prod.json file and do not source it by default.
--strict-mcp-config ignores a server the job needsClaude reports "no tool available" for a task that needs a specific MCP serverThe server is missing from the config file passed to --mcp-config. Add it. Remember that --strict-mcp-config loads only the servers in that file, nothing else.
Team adds a server to .mcp.json that requires a paid API keyNew team members get errors because they do not have the keyDocument required environment variables in the project README or a CONTRIBUTING.md. Add a check script that validates the variables exist at session start: [ -z "$REQUIRED_API_KEY" ] && echo "Set REQUIRED_API_KEY" && exit 1.

Confirm it worked ​

Run this sequence to validate the full production setup:

bash
# 1. Verify .mcp.json loads without environment variables set (should show errors)
unset DATABASE_URL
claude mcp list
# Expected: postgres shows an error or is not listed, because DATABASE_URL is empty

# 2. Set the variables and verify they load
export DATABASE_URL="postgresql://user:pass@localhost:5432/testdb"
claude mcp list
# Expected: postgres: connected (stdio)

# 3. Test CI mode with strict config
claude --bare -p "List all tables in the Postgres database" \
  --mcp-config .mcp.json \
  --strict-mcp-config
# Expected: a list of table names printed to stdout

# 4. Verify git will not commit secrets
git diff --cached .mcp.json
# Expected: the file contains ${VAR_NAME} references, not actual values

If step 1 shows errors (as expected), step 2 shows success, step 3 returns table names, and step 4 confirms no plaintext secrets, your MCP setup is production-ready.

Previous: Multi-Tool MCP Workflows