Agent Tokens
Agent tokens provide scoped, revocable credentials for AI agents connecting to MCPProxy. Instead of sharing the admin API key with every agent, each agent gets its own token with restricted access to specific servers and permission tiers.
Why Agent Tokens?
MCPProxy sits between AI agents and upstream MCP servers. Without agent tokens, every connection gets full admin access — any agent can call any tool on any server with no restrictions.
This creates real problems:
- A CI/CD bot that only needs to read GitHub issues can also delete repositories
- A monitoring agent that checks server status can also modify configurations
- A compromised agent has unlimited access to all upstream servers
- No audit trail — you can't tell which agent performed which action
Agent tokens solve this with defense-in-depth scoping:
┌─────────────────────────────────────────┐
│ AI Agent (e.g., deploy-bot) │
│ Token: mcp_agt_a1b2c3... │
│ Servers: github, gitlab │
│ Permissions: read, write │
└──────────────┬──────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ MCPProxy │
│ │
│ 1. retrieve_tools → filters results │
│ to github + gitlab only │
│ │
│ 2. call_tool_write → allowed │
│ 3. call_tool_destructive → BLOCKED │
│ 4. call_tool_read(slack:...) → BLOCKED │
└─────────────────────────────────────────┘
Token Format
Agent tokens use the mcp_agt_ prefix followed by 64 hex characters:
mcp_agt_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
Tokens are hashed with HMAC-SHA256 before storage — the raw token is shown once at creation and cannot be retrieved again.
Quick Start
Create a Token
mcpproxy token create \
--name deploy-bot \
--servers github,gitlab \
--permissions read,write \
--expires 30d
Output:
Agent token created successfully.
Token: mcp_agt_a1b2c3d4...
IMPORTANT: Save this token now. It cannot be retrieved again.
Name: deploy-bot
Servers: github, gitlab
Permissions: read, write
Expires: 2026-04-05 14:30
Use the Token
Agents authenticate by passing the token via any standard method:
# X-API-Key header
curl -H "X-API-Key: mcp_agt_a1b2c3d4..." http://localhost:8080/mcp
# Authorization: Bearer header
curl -H "Authorization: Bearer mcp_agt_a1b2c3d4..." http://localhost:8080/mcp
# Query parameter
curl "http://localhost:8080/mcp?apikey=mcp_agt_a1b2c3d4..."
In MCP client configurations:
{
"mcpServers": {
"mcpproxy": {
"url": "http://localhost:8080/mcp",
"headers": {
"X-API-Key": "mcp_agt_a1b2c3d4..."
}
}
}
}
Enforcing Authentication on /mcp
By default, the /mcp endpoint allows unauthenticated access for backward compatibility with existing MCP clients. This means agent tokens are optional — agents that don't provide a token get full admin access.
To make agent tokens mandatory, enable require_mcp_auth:
{
"require_mcp_auth": true
}
Or via CLI flag:
mcpproxy serve --require-mcp-auth
With this enabled:
- Requests without a token → 401 Unauthorized
- Requests with an invalid token → 401 Unauthorized
- Requests with a valid agent token → scoped access
- Requests with the admin API key → full admin access
- Tray/socket connections → always trusted (OS-level auth)
Recommended setup: Enable require_mcp_auth when deploying MCPProxy in environments where multiple agents connect, or when you want to enforce least-privilege access.
Permission Tiers
Each token specifies which permission tiers the agent can use:
| Permission | Tool Variants Allowed | Use Case |
|---|---|---|
read | call_tool_read | Monitoring, querying, status checks |
write | call_tool_read, call_tool_write | Creating issues, updating records |
destructive | All variants | Deleting resources, admin operations |
Permissions are cumulative — write implies read, and destructive implies both. The read permission is always required.
# Read-only monitoring agent
mcpproxy token create --name monitor --servers "*" --permissions read
# CI/CD agent that creates and updates
mcpproxy token create --name ci-agent --servers github --permissions read,write
# Full-access admin agent
mcpproxy token create --name admin-bot --servers "*" --permissions read,write,destructive
Server Scoping
Tokens restrict which upstream servers an agent can access:
# Only GitHub and GitLab
mcpproxy token create --name deploy-bot --servers github,gitlab --permissions read,write
# All servers (wildcard)
mcpproxy token create --name all-access --servers "*" --permissions read
Server scoping is enforced at three levels:
-
Tool discovery (
retrieve_tools) — only returns tools from allowed servers -
Tool execution (
call_tool_*) — blocks calls to out-of-scope servers -
Enumeration — since issue #1166,
allowed_serversalso scopes what the REST surface will list, not only what the token may call. A scoped token sees only its own servers onGET /api/v1/servers(array and thestatscounters),GET /api/v1/status(upstream_stats), the/eventsSSE stream,GET /api/v1/tools,GET /api/v1/index/search,GET /api/v1/diagnostics/doctor,GET /api/v1/profiles,GET /api/v1/annotations/coverageandGET /api/v1/security/scans.The whole
/api/v1/servers/{id}subtree answers404 Server not foundfor a server outside the scope —tools,logs,tool-calls,diagnostics,scan/status,scan/report,scan/files,integrity,tools/export,tools/{tool}/diffand every sub-resource added later, since the gate is a middleware on the subtree. It is the same404a server that does not exist returns — byte for byte, once the echoed name is normalised — so the response cannot be used to probe for hidden servers.logsmatters most: upstream stderr routinely echoes the argv and env the server process was launched with.The activity, tool-call and usage doors are scoped to records attributable to an allowed server:
GET /api/v1/activity,/activity/summary,/activity/usage,/activity/export,/activity/{id},GET /api/v1/tool-callsand/tool-calls/{id}(plus its/replay). The entitlement is applied inside the query, sototaland the page always describe the same record set, and a?server=filter narrows within the scope rather than escaping it. Records with no server attribution (system_start,config_change, …) are operator-plane events and are not shown. On/activity/usage, aggregates that cannot be re-derived per server — the tokens-saved headline and the global timeline — are omitted rather than reported fleet-wide.Denied outright (
403) to agent tokens, because there is nothing per-server to project:GET /api/v1/config— an admin document, and it carries the admin API key.GET /api/v1/stats/tokens—per_server_tool_list_sizesis keyed by every configured server, and the scalars beside it are fleet-wide.GET /api/v1/sessions,GET /api/v1/sessions/{id}— an MCP session describes a client and the user's workspace, with no server attribution.GET /api/v1/security/overview,GET /api/v1/security/queue— fleet-wide scan and finding counts, and a queue that names every server waiting to be scanned. A scoped caller reads its own server's verdict fromGET /api/v1/servers/{id}/scan/status, which the subtree gate scopes.GET /api/v1/telemetry/payload— the heartbeat carriesserver_count,connected_server_count,tool_countandserver_docker_isolated_count: precisely the count oracle removed from/status.GET /api/v1/onboarding/state,POST /api/v1/onboarding/mark(which echoes the same document) —configured_server_countis an inventory size andconnected_client_idsis the operator's MCP-client inventory.GET /api/v1/secrets/refs,GET /api/v1/secrets/config— values are masked, so this is a credential inventory rather than a disclosure, but it names the secrets of servers the caller may not enumerate. A strictly narrower view of the documentGET /api/v1/configalready denies.
Withheld rather than denied.
GET /api/v1/statusstays open — agents legitimately poll it for liveness — but itsactivationblock is omitted for a scoped caller.mcp_clients_seen_everis the operator's MCP-client inventory andretrieve_tools_calls_24his an exact deployment-wide counter, neither of which has a per-server part to project. The key is already absent when telemetry is unwired, so clients tolerate its absence.PUT /api/v1/profiles/activeanswers403: the active profile is server-level shared state that decides what the Web UI and tray render, so a read-scoped credential must not be able to change it. It is gated by the sameconfig_writepolicy as the other config-level writes.On the
/eventsstream, scoping applies per event, not only to theservers.changedserver list:- An event that names a server the token may not enumerate — through
server_name,server,target_serveroraffected_entity, which is every activity, OAuth and security event — is not delivered to that subscriber at all. It is dropped rather than blanked, because a frame with the name removed still discloses the mutation, its timing and the number of servers being hidden. servers.changedis the exception and is always delivered, because it is coalesced last-write-wins and carries the state a client renders. Its server list is narrowed, itsstatsrecomputed, and any coalescer extra that names an out-of-scope server ("server": "beta") is removed.config.reloaded,config.savedandsecrets.changedannounce mutations of the admin config document and are dropped, matching the403onGET /api/v1/config.
Admin subscribers — the API key, the Web UI, the tray over the unix socket — receive every event unchanged; the stream is rendered per connection.
Administrative Operations Are Admin-Only
Agent tokens can discover and call tools (within their scope and permission tier) but can never administer servers. Server-mutating operations require the admin API key (or a local tray/socket connection, which is admin by OS-level auth) on every surface — the MCP tools and the REST API share one policy (internal/auth), so an agent cannot do over HTTP what it is blocked from doing over MCP.
Denied to agent tokens on both surfaces:
- Lifecycle: add, remove, update/patch, enable, disable, restart, reconnect, refresh/discover-tools, add-from-registry, login/logout, move-config-value-to-secret
- Security state: quarantine, unquarantine, tool approve/block, and the security scanner (scan start/cancel, security approve/reject)
- Config & registries: applying/patching configuration (which can add/remove/enable/disable servers) and mutating registry sources — an agent must not bypass the per-server gate by rewriting config or a registry wholesale
On the MCP surface (upstream_servers, quarantine_security) these return a tool error; on the REST surface (mutating /api/v1/servers/..., /api/v1/config/..., and /api/v1/registries/... routes) they return 403 Forbidden (operation requires admin access). Read-only operations stay available to scoped tokens: upstream_servers list/tail_log, GET /api/v1/servers, per-server diagnostics, registry reads, and GET /api/v1/index/search (which honors quarantine — a quarantined server's tools are withheld from search on every surface). Those reads are scope-filtered as described above. GET /api/v1/config is the exception: it is an admin document (it carries the global api_key, every server's credentials, and a second enumeration of server names under profiles[].servers), so it returns 403 for an agent token rather than a filtered view.
Profile Pinning
A profile scopes tool discovery and calls to a named subset of upstream servers. With --profile-pin, you can bind a token to a single profile so it can never operate outside it — regardless of the URL it connects to or any set_profile call it makes.
# This token can ONLY ever see/use the "research" profile
mcpproxy token create \
--name research-agent \
--servers "*" \
--permissions read \
--profile-pin research
Server-side enforcement (no client cooperation required):
set_profile("other")is rejected — a pinned token cannot switch its session to a different profile (switching to its own pinned profile, or clearing, is allowed)./mcp/p/<other>returns403— connecting to any profile URL other than the pinned one is forbidden; the pinned profile's own URL works.- The pin is the highest-precedence resolver source, above an explicit
/mcp/p/<slug>URL scope and above a sessionset_profileselection. - Every dispatch surface resolves it —
retrieve_tools,describe_tool,call_tool_*, thecode_executionsandbox, direct-routing mode (server__tool) and preflight all bound themselves by the pin, so no routing mode is a way around it.
Resolution precedence (highest wins):
1. agent-token profile_pin (server-enforced; this section)
2. /mcp/p/<slug> URL scope (per-request override)
3. set_profile session state (base /mcp endpoint default for the session)
4. none (no profile filtering — all allowed servers)
Validation & config changes: the pinned slug must name a configured profile at creation time (creation is rejected otherwise). If the profile is later removed from the configuration, the pin resolves to a deny-all scope: the token sees no upstream servers and no tools, on the MCP session path and in preflight alike. The request is logged with a warning naming the removed profile, not hard-failed at the transport. The pin is a restriction the operator applied, so losing the profile it names must never hand the token a wider view than it had the day before — re-create the profile, or re-mint the token against a live one, to restore it. Pinning composes with server scoping and permission tiers: a request must satisfy all of them.
The pin is shown by token list (PROFILE PIN column) and token show (Profile Pin field), and is preserved across token regenerate.
Managing Tokens
List All Tokens
mcpproxy token list
NAME PREFIX SERVERS PERMISSIONS REVOKED EXPIRES
deploy-bot mcp_agt_a1b2 github,gitlab read,write no 2026-04-05 14:30
monitor mcp_agt_c3d4 * read no 2026-04-05 14:30
old-bot mcp_agt_e5f6 github read yes 2026-03-01 10:00
Show Token Details
mcpproxy token show deploy-bot
Revoke a Token
Immediately invalidates the token. Revoke is a soft delete: the record is kept (so the token name stays reserved) and any further use is rejected:
mcpproxy token revoke deploy-bot
Delete a Token
Permanently removes the token, freeing its name for reuse. Unlike revoke, delete removes the record entirely — after deleting, you can create a new token with the same name:
mcpproxy token delete deploy-bot # aliases: rm, remove
Regenerate a Token
Invalidates the old secret and generates a new one, keeping the same name and settings:
mcpproxy token regenerate deploy-bot
The new token is displayed once — save it immediately.
JSON Output
All commands support JSON output for scripting:
mcpproxy token list -o json
mcpproxy token create --name bot --servers github --permissions read -o json
Activity Logging
Agent token usage is tracked in the activity log. Each tool call records the agent identity:
# Filter activity by agent
mcpproxy activity list --agent deploy-bot
# Filter by auth type
mcpproxy activity list --auth-type agent
mcpproxy activity list --auth-type admin
Activity records include _auth_type, _auth_agent, and _auth_token_prefix metadata fields for audit trails.
REST API
Agent tokens can also be managed via the REST API (requires admin API key):
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v1/tokens | Create a new agent token |
GET | /api/v1/tokens | List all tokens |
GET | /api/v1/tokens/{name} | Get token details |
DELETE | /api/v1/tokens/{name} | Revoke a token (soft delete; name stays reserved) |
DELETE | /api/v1/tokens/{name}/permanent | Permanently delete a token (frees the name for reuse) |
POST | /api/v1/tokens/{name}/regenerate | Regenerate token secret |
Create Token via API
curl -X POST http://localhost:8080/api/v1/tokens \
-H "X-API-Key: your-admin-key" \
-H "Content-Type: application/json" \
-d '{
"name": "deploy-bot",
"allowed_servers": ["github", "gitlab"],
"permissions": ["read", "write"],
"expires_in": "30d"
}'
Security Model
- HMAC-SHA256 hashing — raw tokens are never stored; only HMAC hashes are persisted
- Constant-time comparison — prevents timing attacks during token validation
- Automatic expiry — tokens expire after a configurable duration (default: 30 days)
- Revocation — tokens can be immediately invalidated
- Prefix identification — the
mcp_agt_prefix distinguishes agent tokens from admin API keys without database lookups - Tray bypass — local tray/socket connections always get admin access (authenticated by OS-level socket permissions)
Configuration Reference
Config File
{
"require_mcp_auth": false,
"api_key": "your-admin-key"
}
| Field | Type | Default | Description |
|---|---|---|---|
require_mcp_auth | bool | false | Require authentication on /mcp endpoint |
api_key | string | auto-generated | Admin API key for full access |
CLI Flags
mcpproxy serve --require-mcp-auth # Enforce /mcp authentication
Token Create Flags
| Flag | Required | Default | Description |
|---|---|---|---|
--name | Yes | — | Unique token name |
--servers | Yes | — | Comma-separated server names or "*" |
--permissions | Yes | — | Comma-separated: read, write, destructive |
--expires | No | 30d | Expiry duration (e.g., 7d, 90d, 365d) |
--profile-pin | No | — | Pin the token to a single profile (see Profile Pinning) |