# MCP security: where MCP server credentials leak

URL: https://infro.io/blog/mcp-server-credential-leaks
Author: INFRO Editorial (https://infro.io/blog/editorial)
Publisher: INFRO
Published: 2026-10-21
Last modified: 2026-10-21
Last reviewed: 2026-10-21 (next review due 2026-11-20)
Topic: Agents and multimodal systems
Format: Guide

> MCP server credentials usually live as static API keys in plain-text JSON configuration (Claude Code's ~/.claude.json and .mcp.json, Cursor's mcp.json), passed to local servers that run with the user's privileges, or as OAuth tokens held by the client for remote servers. The MCP specification forbids token passthrough and warns about local server compromise; real malicious MCP packages have copied users' emails and harvested environment credentials. Prefer OAuth or narrowly scoped tokens, reference secrets from the environment, pin and review server packages, and put one gateway in front that holds the credentials.

Key takeaways:
- Most MCP credentials are static tokens in plain-text JSON files or environment variables handed to local server processes that run with the user's privileges.
- The MCP specification says servers MUST NOT accept tokens not explicitly issued for them, and treats local server compromise and over-broad scopes as named attack vectors.
- Real cases exist: the postmark-mcp npm package copied every email it sent to an attacker in 2025, and the May 2026 Mini Shai-Hulud campaign shipped credential-stealing versions of two MCP server packages.
- Prefer OAuth or the narrowest token a server accepts, reference secrets from the environment rather than writing them into config, and pin the server packages you run.
- A gateway that holds server credentials means agents and laptops never see them, and one place records every tool call.

**Where do MCP server credentials leak?** Mostly from where they are kept. A typical MCP setup stores each server's API key in a plain-text JSON file in the user's home directory or project, or passes it as an environment variable to a local server process that runs with the user's full privileges. Remote servers may use OAuth instead, with tokens held by the client. Credentials leak when those files are committed or copied, when a malicious or compromised server package reads them, and when a server forwards a token it should never have accepted. The fixes are narrow tokens (or OAuth), secrets referenced rather than written, pinned server packages, and one gateway in front that holds the credentials so agents never do.

[What is MCP](https://infro.io/blog/what-is-mcp) explains the protocol; [MCP gateways](https://infro.io/blog/mcp-gateway-govern-agent-tools) covers governing tool access in general. This guide is about the credentials.

## Where MCP tokens live

| Setup | Where the credential sits | Who can read it |
| --- | --- | --- |
| Local server (stdio), personal | `~/.claude.json` for Claude Code[1], `~/.cursor/mcp.json` for Cursor[2], as a literal or an environment variable | You, any process running as you, and the server itself |
| Local server, team-shared | `.mcp.json` (Claude Code) or `.cursor/mcp.json` (Cursor) in the repository[1][2] | Everyone with the repository, if a literal key is committed |
| Remote server, static token | An `Authorization` or API-key header in the same config files | Same as above, plus the server |
| Remote server, OAuth | Tokens held by the client; Claude Code says they are "stored securely and refreshed automatically", with a pre-configured client secret in the macOS keychain or a credentials file[1] | The client; the token is issued to that client |

Two details raise the stakes. First, these files are plaintext: Claude Code's `~/.claude.json` holds app state, OAuth sign-in and personal MCP servers[3], and old copies of it are kept in `~/.claude/backups/`[3]. Second, local servers are programs, not configuration: the specification warns that MCP servers run with the same privileges as the client[4]. A local server you give a GitHub token to can also read your SSH keys.

GitGuardian's 2026 report counts 24,008 unique secrets exposed in MCP configuration files in public GitHub commits[5]. Those are the files above, committed by mistake.

### Reference secrets, do not write them

Both major clients support references instead of literals. Claude Code expands `${VAR}` and `${VAR:-default}` in `.mcp.json` so teams can share configuration "while maintaining flexibility for … sensitive values like API keys", and in a remote server's URL and headers it reads some credential variables as empty so a project's file cannot send your credentials to a server it names[1]. Cursor supports `${env:NAME}` interpolation and an `envFile` for stdio servers, and recommends "restricted API keys with minimal required permissions" and environment variables for secrets[2].

```bash
{
  "mcpServers": {
    "issues": {
      "type": "http",
      "url": "https://mcp.example.com/mcp",
      "headers": { "Authorization": "Bearer ${ISSUES_MCP_TOKEN}" }
    }
  }
}
```

A reference keeps the key out of the repository; it does not keep it away from the server process, the agent's shell or anything else running as you. That is what the next sections address.

## What the specification says

The MCP specification's Security Best Practices page names several credential attacks. Three matter most for teams running servers.

**Token passthrough.** An MCP server that accepts a token not issued to it, and forwards it to a downstream API, is an anti-pattern the authorization specification explicitly forbids[4]. The risks it lists: security controls that depend on the token's audience are bypassed, the server cannot tell clients apart, downstream logs show the wrong identity, and a token accepted by several services lets an attacker who compromises one reach the others[4]. The rule is short: "MCP servers **MUST NOT** accept any tokens that were not explicitly issued for the MCP server"[4]. If you build an MCP server in front of an internal API, give it its own credential for that API, and validate the audience of tokens clients present.

**Local server compromise.** The specification describes three attacks on local servers: a malicious startup command in a client configuration, a malicious payload inside the server, and an insecure server left listening on localhost reached through DNS rebinding[4]. Its example of a malicious startup command installs a package and posts `~/.ssh/id_rsa` to a remote host[4]. Clients that offer one-click local server setup must show the exact command and require explicit approval, and should run servers sandboxed with minimal default privileges[4]. Claude Code, for example, asks for approval before using project-scoped servers from `.mcp.json` in interactive sessions[1].

**Scope minimization.** An access token carrying broad scopes such as `files:*` or `admin:*`, stolen from a log or memory, "enables lateral data access, privilege chaining, and difficult revocation"[4]. The specification recommends a minimal initial scope set with incremental elevation when a privileged operation is first attempted, and lists wildcard scopes and bundled privileges as common mistakes[4].

## What malicious MCP packages have done

Two reported cases show both ways a server package can turn on its user.

**postmark-mcp (2025): a server that abused the access it was given.** Koi Security found that the npm package `postmark-mcp`, impersonating Postmark's MCP server, which Postmark publishes on GitHub rather than npm, added a backdoor in version 1.0.16 after 15 clean versions; it silently copied outgoing emails to an attacker-controlled address, as reported by The Register[6]. The package had about 1,500 downloads in a week before removal; Postmark said it "had absolutely nothing to do with this package", and the advice was to remove it, review email logs and rotate credentials sent by email[6].

**Mini Shai-Hulud (May 2026): a server package that stole the environment.** The OSV advisory MAL-2026-4068 records malicious versions 0.2.8 and 0.3.8 of `@antv/mcp-server-antv` as part of a campaign in which a compromised npm maintainer account published 631 malicious versions across 314 packages in a 22-minute burst[7]. Each added a preinstall hook running an obfuscated script that, per the advisory, took AWS keys, GCP service accounts, Azure credentials, GitHub and npm tokens, Kubernetes tokens, SSH keys and database connection strings, and persisted through CI workflow injection and AI agent session hooks[7]. Nobody had to call a tool: installing the package was enough.

The first case is a reason to scope the token you give a server. The second is a reason to keep every other credential away from the machine that installs it. [AI supply chain attacks in 2026](https://infro.io/blog/ai-supply-chain-attacks-2026) puts both in context.

## Reducing the blast radius

- **Prefer OAuth where the server offers it.** A token issued to one client, with narrow scopes and an expiry, revocable per client, beats a long-lived personal access token pasted into JSON. It does not make a stolen token harmless; it makes it smaller and shorter-lived.
- **Use the narrowest token the server accepts.** Read-only tokens or read-only server modes when the agent does not need to write; one token per server; never an organization-admin token.
- **One token per person and per server.** Shared tokens make revocation break everyone and make audit trails useless.
- **Pin and verify server packages.** Install servers from the vendor's own publication, pin exact versions, read changelogs before upgrading, and treat a server like any other dependency in your [supply chain review](https://infro.io/blog/ai-supply-chain-attacks-2026).
- **Review startup commands.** `npx some-package@latest` in a config file runs whatever was published last.
- **Run local servers with less.** Containers or the client's sandbox, without access to `~/.ssh`, cloud credential files or unrelated environment variables. [Coding agents and secrets](https://infro.io/blog/ai-coding-agents-secrets-exposure) has the Claude Code sandbox settings.
- **Keep secrets out of config files** with references, and scan for them before push.
- **Rehearse revocation.** For each server, know where its token is issued and how fast you can revoke it.

## Put one gateway in front

The structural fix is to stop distributing server credentials to every laptop and agent. A gateway registers each MCP server once, with its credential, and agents reach the server through the gateway with their own identity. The credential lives in one sealed place; revoking an agent's access does not require rotating the server's token; and one log records which agent called which tool.

A gateway does not remove the risks inside the tool call. A server you trust can return content that steers the model, and a granted tool can still be misused by an agent following injected instructions. Least-privilege grants, read-only credentials and human review of writes remain necessary.

## How INFRO's MCP gateway handles server credentials

INFRO's [MCP gateway](https://infro.io/docs/mcp-gateway), on the Business plan, puts INFRO between your agents and the MCP servers they use. As documented in its [security model](https://infro.io/docs/mcp-gateway/security):

- **Server credentials are sealed.** An owner or admin registers a server with up to eight header values, such as a bearer token; they are encrypted at rest with AES-256-GCM, bound to the organization and the server, and never returned by any API, console screen, log, error or trace. Only header names are shown.
- **The agent never holds the server credential, and the server never sees the INFRO key.** Agents call with their INFRO gateway key; INFRO adds the server's headers to each call, and strips the agent's `Authorization` and `X-INFRO-*` headers — no passthrough in either direction.
- **Access is denied by default.** A gateway key with no grant sees no tools; a grant gives one key one server in a chosen mode, and only tools an admin approved are served. A tool whose definition changed on the server since approval is refused.
- **Guardrails apply to tool calls.** A `secret` rule with `block` on the input stage refuses tool calls whose arguments contain a credential, a common sign of an agent being steered into sending one out.
- **Every call is recorded** with metadata (server, tool, outcome, sizes, key, project, environment) on the same trace as the agent's model calls; contents only if you opt in.

Two limits, stated plainly. Servers that only accept OAuth sign-in, such as Notion, Vercel and Asana, cannot be registered yet; INFRO's gateway takes servers with no authentication or a static header credential. And INFRO governs calls that go through it: a server token that also sits on a laptop, or is stolen from anywhere, can be used directly against the server. Register with a dedicated, narrowly scoped token, keep it nowhere else, and keep revocation at the issuing service as the backstop.

## Summary

MCP credentials leak from where they are kept: plaintext JSON, inherited environments and local server processes with full user privileges. Follow the specification — no token passthrough, consent before running local servers, minimal scopes — and learn from the real cases: a server package can abuse the access you give it, or steal everything else on the machine. Use OAuth or the narrowest token, reference secrets instead of writing them, pin server packages, and centralize credentials in one gateway so agents never hold them.

## Questions

### Where are MCP server API keys stored?

Usually in plain-text JSON. Claude Code keeps personal MCP servers in ~/.claude.json and team servers in a project's .mcp.json; Cursor uses ~/.cursor/mcp.json and .cursor/mcp.json. Keys either sit in those files or in environment variables passed to the server process. Remote servers that use OAuth keep tokens with the client instead.

### What is token passthrough in MCP?

An anti-pattern where an MCP server accepts a token that was not issued for it and forwards it to a downstream API. The MCP specification forbids it: servers must not accept any tokens that were not explicitly issued for the MCP server, because passthrough bypasses controls, breaks audit trails and lets one stolen token reach several services.

### Is OAuth safer than an API key for MCP servers?

Usually. An OAuth token is issued to one client, can carry narrow scopes, expires and can be revoked per client, and it is not written into a config file by hand. It is not a cure: a stolen access token still works until it expires, and broad scopes widen the damage, which is why the specification recommends minimal initial scopes.

### Have malicious MCP servers actually been found?

Yes. Koi Security found that version 1.0.16 of the npm package postmark-mcp, which impersonated Postmark's server, copied every outgoing email to an attacker, as The Register reported in 2025. In May 2026 the OSV database recorded malicious versions of two @antv MCP server packages that harvested cloud keys, tokens and SSH keys during install.

## Sources

1. Connect Claude Code to tools via MCP — Claude Code Docs. https://code.claude.com/docs/en/mcp (accessed 2026-10-21)
2. Model Context Protocol (MCP) — Cursor Docs. https://cursor.com/docs/context/mcp (accessed 2026-10-21)
3. Explore the .claude directory — Claude Code Docs. https://code.claude.com/docs/en/claude-directory (accessed 2026-10-21)
4. Security Best Practices — Model Context Protocol. https://modelcontextprotocol.io/specification/latest/basic/security_best_practices (accessed 2026-10-21)
5. The State of Secrets Sprawl 2026 — GitGuardian. https://gitguardian.com/state-of-secrets-sprawl-report-2026 (accessed 2026-10-21)
6. Postmark MCP server code hijacked — The Register. https://www.theregister.com/2025/09/29/postmark_mcp_server_code_hijacked/ (accessed 2026-10-21)
7. MAL-2026-4068: Malicious code in @antv/mcp-server-antv (npm) — OSV. https://osv.dev/vulnerability/MAL-2026-4068 (accessed 2026-10-21)
