|
| 1 | +# Token sprawl in enterprise teams |
| 2 | + |
| 3 | +## The problem |
| 4 | + |
| 5 | +A company with 50 engineers using AI tools (Claude Code, Cursor, Windsurf) needs access to Sentry, Slack, GitHub, Grafana, and a few internal services. Each service requires an API token. Each engineer needs their own tokens. |
| 6 | + |
| 7 | +The math gets ugly fast: |
| 8 | + |
| 9 | +| Engineers | Services | Tokens to manage | |
| 10 | +|-----------|----------|-----------------| |
| 11 | +| 10 | 5 | 50 | |
| 12 | +| 50 | 8 | 400 | |
| 13 | +| 200 | 12 | 2,400 | |
| 14 | + |
| 15 | +Every token is a secret. Every secret is a liability. Every engineer's laptop is an attack surface. |
| 16 | + |
| 17 | +Now multiply by the number of AI tools each engineer uses. Claude Code needs a `servers.json`. Cursor needs another. Windsurf needs a third. Same tokens, copied across machines, across config files, across tools. |
| 18 | + |
| 19 | +**What actually goes wrong:** |
| 20 | + |
| 21 | +- **Onboarding takes hours** — new engineer joins, needs tokens for 8 services. Opens 8 dashboards, generates 8 tokens, pastes them into 3 config files. One typo somewhere. Debug time. |
| 22 | +- **Offboarding is incomplete** — engineer leaves, someone revokes their GitHub token but forgets the Sentry one. The Grafana token lives on a laptop image that gets recycled. |
| 23 | +- **Token rotation is a myth** — security policy says rotate every 90 days. Nobody does it because it means updating tokens across every developer machine, every config file, every tool. |
| 24 | +- **Audit is impossible** — "who has access to what?" requires checking every engineer's local config. There's no central log. No visibility. |
| 25 | +- **Scope creep** — engineers generate broad-access tokens because it's easier than figuring out the minimum permissions. One leaked token exposes everything. |
| 26 | + |
| 27 | +## The fix: one proxy, zero tokens on developer machines |
| 28 | + |
| 29 | +Instead of distributing tokens to every engineer, run one MCP proxy on internal infrastructure. The proxy holds the service tokens. Engineers connect to the proxy. |
| 30 | + |
| 31 | +**Before** — every dev holds tokens for every service: |
| 32 | + |
| 33 | +```mermaid |
| 34 | +graph LR |
| 35 | + D1[Dev 1] -->|token| Sentry |
| 36 | + D1 -->|token| Slack |
| 37 | + D1 -->|token| GitHub |
| 38 | + D2[Dev 2] -->|token| Sentry |
| 39 | + D2 -->|token| Slack |
| 40 | + D2 -->|token| GitHub |
| 41 | + D3[Dev 3] -->|token| Sentry |
| 42 | + D3 -->|token| Slack |
| 43 | + D3 -->|token| GitHub |
| 44 | +
|
| 45 | + style Sentry fill:#f96 |
| 46 | + style Slack fill:#f96 |
| 47 | + style GitHub fill:#f96 |
| 48 | +``` |
| 49 | + |
| 50 | +> 50 devs × 8 services = **400 tokens** scattered across laptops. |
| 51 | +
|
| 52 | +**After** — one proxy holds the tokens, devs connect once: |
| 53 | + |
| 54 | +```mermaid |
| 55 | +graph LR |
| 56 | + D1[Dev 1] -->|auth| Proxy |
| 57 | + D2[Dev 2] -->|auth| Proxy |
| 58 | + D3[Dev 3] -->|auth| Proxy |
| 59 | +
|
| 60 | + Proxy["mcp serve<br/>(proxy)"] -->|token| Sentry |
| 61 | + Proxy -->|token| Slack |
| 62 | + Proxy -->|token| GitHub |
| 63 | +
|
| 64 | + style Proxy fill:#4a9,color:#fff |
| 65 | + style Sentry fill:#f96 |
| 66 | + style Slack fill:#f96 |
| 67 | + style GitHub fill:#f96 |
| 68 | +``` |
| 69 | + |
| 70 | +> 50 devs × 1 connection = **50 auth credentials**, centrally managed. |
| 71 | +
|
| 72 | +Service tokens live in one place. If you need to rotate the Sentry token, you update it once on the proxy. Zero touch on developer machines. |
| 73 | + |
| 74 | +## How to set it up |
| 75 | + |
| 76 | +### 1. Deploy the proxy |
| 77 | + |
| 78 | +On a shared server (VM, container, Kubernetes pod): |
| 79 | + |
| 80 | +```bash |
| 81 | +mcp serve --http 0.0.0.0:8080 --insecure |
| 82 | +``` |
| 83 | + |
| 84 | +> In production, put a reverse proxy (nginx, Caddy) in front for TLS. See the [proxy mode guide](proxy-mode.md#production-deployment) for details. |
| 85 | +
|
| 86 | +### 2. Configure backend tokens on the proxy |
| 87 | + |
| 88 | +The proxy's `servers.json` holds all service tokens: |
| 89 | + |
| 90 | +```json |
| 91 | +{ |
| 92 | + "mcpServers": { |
| 93 | + "sentry": { |
| 94 | + "url": "https://mcp.sentry.dev/sse", |
| 95 | + "headers": { "Authorization": "Bearer ${SENTRY_TOKEN}" } |
| 96 | + }, |
| 97 | + "slack": { |
| 98 | + "command": "npx", |
| 99 | + "args": ["-y", "@anthropic-ai/mcp-server-slack"], |
| 100 | + "env": { "SLACK_BOT_TOKEN": "${SLACK_TOKEN}" } |
| 101 | + }, |
| 102 | + "github": { |
| 103 | + "command": "npx", |
| 104 | + "args": ["-y", "@modelcontextprotocol/server-github"], |
| 105 | + "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" } |
| 106 | + } |
| 107 | + } |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +All secrets stay on the server. Engineers never see them. |
| 112 | + |
| 113 | +### 3. Add per-user authentication |
| 114 | + |
| 115 | +Use bearer tokens to identify each engineer and control access: |
| 116 | + |
| 117 | +```json |
| 118 | +{ |
| 119 | + "mcpServers": { ... }, |
| 120 | + "serverAuth": { |
| 121 | + "provider": "bearer", |
| 122 | + "bearer": { |
| 123 | + "tokens": { |
| 124 | + "eng-alice-a1b2c3": "alice", |
| 125 | + "eng-bob-d4e5f6": "bob", |
| 126 | + "eng-carol-g7h8i9": "carol" |
| 127 | + } |
| 128 | + }, |
| 129 | + "acl": { |
| 130 | + "default": "allow", |
| 131 | + "rules": [ |
| 132 | + { "subjects": ["carol"], "tools": ["sentry__*"], "policy": "deny" } |
| 133 | + ] |
| 134 | + } |
| 135 | + } |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +Or, if you already have an identity provider behind a reverse proxy: |
| 140 | + |
| 141 | +```json |
| 142 | +{ |
| 143 | + "serverAuth": { |
| 144 | + "provider": "forwarded", |
| 145 | + "forwarded": { "header": "x-forwarded-user" } |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +See the [proxy mode authentication docs](proxy-mode.md#authentication) for all provider options. |
| 151 | + |
| 152 | +### 4. Engineers connect — one line |
| 153 | + |
| 154 | +Each engineer adds one entry to their local config: |
| 155 | + |
| 156 | +```bash |
| 157 | +mcp add --url https://mcp.internal:8443/mcp team |
| 158 | +``` |
| 159 | + |
| 160 | +That's it. Every AI tool on their machine connects through this one endpoint. No service tokens on their laptop. No per-service config. |
| 161 | + |
| 162 | +```json |
| 163 | +{ |
| 164 | + "mcpServers": { |
| 165 | + "team": { |
| 166 | + "url": "https://mcp.internal:8443/mcp", |
| 167 | + "headers": { "Authorization": "Bearer eng-alice-a1b2c3" } |
| 168 | + } |
| 169 | + } |
| 170 | +} |
| 171 | +``` |
| 172 | + |
| 173 | +## What you gain |
| 174 | + |
| 175 | +**Onboarding in minutes** — new engineer gets one proxy token. Immediately has access to all approved tools. No 8-service token generation dance. |
| 176 | + |
| 177 | +**Offboarding in seconds** — remove the engineer's token from the proxy config. Access to everything is revoked instantly. No forgotten tokens on decommissioned laptops. |
| 178 | + |
| 179 | +**Token rotation without pain** — rotate a service token on the proxy, no engineer even notices. Zero coordination, zero downtime. |
| 180 | + |
| 181 | +**Audit in one place** — proxy logs show who called which tool, when. One log stream, one place to look. |
| 182 | + |
| 183 | +**Least privilege by default** — ACL rules control which engineers can use which tools. Carol from marketing can use Slack tools but not Sentry admin tools. |
| 184 | + |
| 185 | +**Tool-agnostic** — engineers can use Claude Code, Cursor, Windsurf, or any MCP-compatible client. All connect to the same proxy. Add or remove AI tools without touching service credentials. |
| 186 | + |
| 187 | +## Architecture |
| 188 | + |
| 189 | +```mermaid |
| 190 | +graph TB |
| 191 | + subgraph Developers["Developer machines (no service tokens)"] |
| 192 | + D1["Dev 1<br/>Claude Code"] |
| 193 | + D2["Dev 2<br/>Cursor"] |
| 194 | + D3["Dev 3<br/>Windsurf"] |
| 195 | + end |
| 196 | +
|
| 197 | + subgraph Infra["Internal infrastructure"] |
| 198 | + LB["nginx / Caddy<br/>(TLS termination)"] |
| 199 | + Proxy["mcp serve --http<br/>(proxy + auth + ACL)"] |
| 200 | +
|
| 201 | + subgraph Backends["Backend services (tokens stored here)"] |
| 202 | + Sentry |
| 203 | + Slack |
| 204 | + GitHub |
| 205 | + Grafana |
| 206 | + end |
| 207 | + end |
| 208 | +
|
| 209 | + D1 -->|"Bearer token<br/>(HTTPS)"| LB |
| 210 | + D2 -->|"Bearer token<br/>(HTTPS)"| LB |
| 211 | + D3 -->|"Bearer token<br/>(HTTPS)"| LB |
| 212 | + LB --> Proxy |
| 213 | + Proxy --> Sentry |
| 214 | + Proxy --> Slack |
| 215 | + Proxy --> GitHub |
| 216 | + Proxy --> Grafana |
| 217 | +
|
| 218 | + style Proxy fill:#4a9,color:#fff |
| 219 | + style LB fill:#69b,color:#fff |
| 220 | + style Sentry fill:#f96 |
| 221 | + style Slack fill:#f96 |
| 222 | + style GitHub fill:#f96 |
| 223 | + style Grafana fill:#f96 |
| 224 | +``` |
| 225 | + |
| 226 | +## Further reading |
| 227 | + |
| 228 | +- [Proxy mode](proxy-mode.md) — full technical guide on stdio/HTTP modes, endpoints, and configuration |
| 229 | +- [Proxy mode authentication](proxy-mode.md#authentication) — bearer tokens, forwarded user, and ACL rules |
| 230 | +- [Config file reference](../reference/config-file.md#server-authentication-serverauth) — serverAuth schema |
0 commit comments