Launching on Product Hunt today
Blog

Guide5 min read

How to use MCP servers in CI and headless agents

Agents in CI can’t sign in through a browser. Give them a bearer token from your secrets, a config file written by the job, and an allowlist of tools.

Eldad FuxFounder and CEO, Appwrite

CI job

  • checkout
  • install
  • claude --bare -p "triage"

MCP1_API_KEY

CI job

  • checkout
  • install
  • claude --bare -p "triage"

MCP1_API_KEY

To use MCP servers in CI or any headless agent, authenticate with a bearer token from your secret store instead of OAuth, load the servers from a config file the job writes, and pre-approve only the tools the job needs. A runner has no browser and no human to approve a tool call, so both decisions have to be made before the job starts.

Below is how that looks in Claude Code and Codex, the mistakes that make a server silently not load, and how to keep a CI credential from doing more than the job.

Why doesn’t OAuth work in a pipeline?

Remote MCP servers mostly sign people in with OAuth 2.1: the client opens a browser, a person approves it, and the client stores a token. In a runner nobody is there to approve it, and a token copied from a laptop ties the job to one person’s session. You have two options:

MethodWhen it fits
Bearer token
Unattended jobs. The server has to accept a personal access token or API key, sent as an Authorization header.
Device flow
A terminal with a person nearby. The tool shows a code, someone enters it in a browser (RFC 8628). Not for scheduled jobs.

For most pipelines that means a token. Check the server’s docs for which kind it accepts, and give it its own token rather than reusing one from a person’s laptop.

How do I add an MCP server to Claude Code in GitHub Actions?

Write an MCP config in the job, pass it with --mcp-config, and run Claude Code with -p. This step does that for a server that takes a bearer token:

.github/workflows/triage.yml
- name: Triage the failing test
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
    MCP_TOKEN: ${{ secrets.MCP_TOKEN }}
  run: |
    npm install -g @anthropic-ai/claude-code
    cat > "$RUNNER_TEMP/mcp.json" <<EOF
    {
      "mcpServers": {
        "tools": {
          "type": "http",
          "url": "https://mcp.example.com/mcp",
          "headers": { "Authorization": "Bearer ${MCP_TOKEN}" }
        }
      }
    }
    EOF
    claude --bare -p "Find the error behind the failing test and summarize it" \
      --mcp-config "$RUNNER_TEMP/mcp.json" \
      --allowedTools "mcp__tools__*"

Each flag is there for a reason:

  • --bare skips everything Claude Code would otherwise discover, including the repository’s .mcp.json. Without it, a -p run connects the servers in that file without asking, which matters when the job checks out a pull request from someone else.
  • "type": "http" is required for a URL entry. Leave it out and Claude Code skips the server and still exits successfully.
  • --allowedTools pre-approves tools. A -p run has nobody to ask, so a call that needs approval is denied. mcp__tools__* allows every tool on the tools server. To allow one tool, write its full name, like mcp__tools__get_issue.

The unquoted EOF makes the shell put the token into the file, which lives in the runner’s temp directory and goes away with the job. Don’t name the variable ANTHROPIC_API_KEY or another credential Claude Code guards: if you let Claude Code expand ${VAR} in a header itself, those names read as empty and the server answers 401.

To fail the job when a server didn’t load, run with --output-format stream-json --verbose and check the system/init event. Its mcp_servers field lists each server’s status, and mcp_server_errors lists entries that were skipped. When a CI runner captures stderr, that field is the only place a skipped server shows up.

How do I use an MCP server with Codex in CI?

Codex reads servers from config.toml and can take the bearer token from an environment variable, so the file holds the variable’s name and never the token:

config.toml
[mcp_servers.tools]
url = "https://mcp.example.com/mcp"
bearer_token_env_var = "MCP_TOKEN"

Write the file in the job, export MCP_TOKEN from your secrets, and run the task with codex exec. For headers other than Authorization, env_http_headers maps a header name to a variable in the same way.

What should a CI agent be allowed to call?

Less than you would give a person, because its input is less trusted. A CI agent reads issue bodies, commit messages and test logs, and any of them can carry text that asks it to do something else. The prompt can’t be the boundary. The credential has to be.

  1. 1Give each pipeline its own token, named after the job, so you can revoke one without breaking the others.
  2. 2Use read-only scopes when the job only reads. A triage or review job rarely needs to write.
  3. 3Set an expiry and put the rotation in your calendar.
  4. 4Allow only the servers the job needs, and the tools on them, with --allowedTools.
  5. 5Keep a record of what the job called, so a strange run can be explained afterwards.

Most servers make the first four steps a token setting each. The fifth needs logging on the server side, which most servers don’t give you per token. For more on scoping a token down to reads, see how to make an MCP server read-only.

Using an MCP1 key in CI

With MCP1, the job holds one key for one gateway instead of a token per server. Create it under Keys in your workspace, store it as a CI secret, and point the config at the gateway:

mcp.json, the server entry
"url": "https://mcp1.com/mcp/<workspace>/<gateway>",
"headers": { "Authorization": "Bearer ${MCP1_API_KEY}" }
  • A key belongs to one gateway and can be limited to some of its servers. It expires in 7, 30, 90 or 365 days, or never.
  • It acts as the person who made it, so their groups, role and tool rules apply. On a read-only grant, calls go out with the server’s read-only secret where it has one.
  • Every call is in the audit log, with the client shown as API key · <name>.
  • MCP1 keeps only a hash, and revoking a key under Keys stops it at once. Admins can stop members from making keys, or turn keys off for the workspace.
  • On Enterprise, an IP allowlist can apply to API keys only, so keys work from your runners and nowhere else.

The tool names follow the same pattern through the gateway: a gateway named mcp1 in the config exposes mcp__mcp1__github__get_file_contents and so on, and mcp__mcp1__* allows the lot. Since the key already limits the job to one gateway and its servers, that broad allow is less risky than it looks. The docs cover keys in full.

Common questions

Can I use OAuth for an MCP server in CI?

Not unattended. OAuth for MCP needs a person to approve the client in a browser. The device flow works when someone is at the terminal, but scheduled jobs need a bearer token or API key.

Does claude -p load my project’s .mcp.json?

Yes, unless you pass --bare. Without it, a -p run connects the servers in the working directory’s .mcp.json without asking, so pass --bare and --mcp-config in CI.

Why does my MCP server not load in Claude Code, with no error?

A URL entry without "type": "http" fails validation and is skipped, and the run still exits successfully. Check mcp_server_errors in the system/init event of stream-json output.

How do I rotate an MCP token used in CI?

Create a new token, update the CI secret, run the job once, then revoke the old token. Tokens with an expiry date force the rotation to happen.

Give your team one URL

Create a workspace, install your servers and share a gateway with a group. Free for individuals.