MCP Scoped API Keys
Per-key MCP scopes with proper JSON-RPC insufficient_scope errors — opt-in, backward compatible
Give different MCP clients different API keys, each with different powers — reader can list tools, agent-bot can call them, admin can do everything.
Quick Start
- Create a keys file
Give each client a key and a list of scopes.
*means “all scopes”.keys.json { "keys": [ { "key": "reader-abc", "scopes": ["tools:read", "resources:read"] }, { "key": "agent-def", "scopes": ["tools:call", "tools:read"] }, { "key": "admin-ghi", "scopes": ["*"] } ] } - Serve with the keys file
bash praisonai mcp serve --transport http-stream --port 8080 --keys-file ./keys.jsonEach request sends its key as a Bearer token. The server looks up the key, resolves its scopes, and enforces them per method.
- Single key (unchanged)
A single scalar key still works and gets wildcard (
*) scope — every method allowed.bash praisonai mcp serve --transport http-stream --api-key MY_SECRET
How It Works
Each method has a required scope. The transport resolves the caller’s key to granted_scopes and the server checks it before running the handler.
| Step | What happens |
|---|---|
| Lookup | Transport matches the Bearer key to an APIKeyAuth entry |
| Resolve | The key’s scopes become granted_scopes |
| Check | The method’s required scope is compared against granted_scopes |
| Allow / Deny | Match → run handler; miss → -32001 insufficient_scope |
Two Ways to Configure
Pick the shortest path for your setup.
A) Single key (wildcard)
praisonai mcp serve --transport http-stream --api-key MY_SECRETBehaviour is unchanged. The scalar --api-key is wrapped as a single key with wildcard (*) scope, so it satisfies every method — the scalar path and the multi-key path share the same enforcement.
B) Multiple scoped keys
praisonai mcp serve --transport http-stream --keys-file ./keys.json{
"keys": [
{ "key": "reader-abc", "scopes": ["tools:read", "resources:read"] },
{ "key": "agent-def", "scopes": ["tools:call", "tools:read"] },
{ "key": "admin-ghi", "scopes": ["*"] }
]
}| Field | Type | Description |
|---|---|---|
key | string | The Bearer token the client sends |
scopes | string[] | Scopes this key grants. "*" satisfies every requirement |
Scopes and Methods
Each MCP method maps to a required scope in OPERATION_SCOPES.
| Method | Required scope |
|---|---|
tools/list | tools:read |
tools/call | tools:call |
resources/list | resources:read |
resources/read | resources:read |
resources/subscribe | resources:subscribe |
prompts/list | prompts:read |
prompts/get | prompts:read |
sampling/createMessage | sampling:create |
tasks/create | tasks:write |
tasks/get | tasks:read |
tasks/list | tasks:read |
tasks/cancel | tasks:write |
logging/setLevel | admin |
Runtime log-level changes are administrative, so logging/setLevel sits behind the admin scope — a client cannot quiet or verbose the server’s logging without an admin key.
Available scopes:
| Scope | Grants |
|---|---|
tools:read | Read tool definitions |
tools:call | Execute tools |
resources:read | Read resources |
resources:subscribe | Subscribe to resource changes |
prompts:read | Read prompts |
prompts:execute | Execute prompts |
sampling:create | Create sampling requests |
tasks:read | Read tasks |
tasks:write | Create and manage tasks |
admin | Administrative access (implies all) |
* | Wildcard — satisfies every requirement |
Error Response
A request without the required scope returns a JSON-RPC error with code -32001.
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32001,
"message": "insufficient_scope",
"data": { "required": "tools:call" }
}
}Over HTTP the response also carries a challenge header naming the missing scope:
WWW-Authenticate: Bearer realm="mcp", scope="tools:call", error="insufficient_scope"The client can read scope="…" to know exactly which grant to request next.
Common Patterns
Match a key’s scopes to what each client actually needs.
Read-only monitor
A monitoring bot that only lists tools and reads status — no execution.
{ "key": "monitor-key", "scopes": ["tools:read", "resources:read"] }Agent caller
An agent that runs tools but should not touch resources.
{ "key": "agent-key", "scopes": ["tools:call"] }tools:call implies tools:read, so listing tools works too.
Admin
A maintenance key with full access.
{ "key": "admin-key", "scopes": ["*"] }Rotate a key
Rotation is a pure JSON edit — add the new key, drain traffic to it, then remove the old one.
{
"keys": [
{ "key": "agent-old", "scopes": ["tools:call"] },
{ "key": "agent-new", "scopes": ["tools:call"] }
]
}Best Practices
Grant the narrowest scopes that work
Start from tools:read and add only what a client needs. Reserve * and admin for maintenance keys, not day-to-day clients.
One key per client role
Give each bot or service its own key so you can rotate or revoke it without affecting others. Name keys by role (reader, agent-bot, admin).
Rotate without downtime
Add the replacement key alongside the old one, point clients at the new key, then delete the old entry. No restart of clients is forced mid-rotation.
Keep the keys file out of version control
Treat keys.json like any secret — store it outside the repo and restrict file permissions. Never commit real keys.