$ claude mcp add --transport http prorm https://mcp.prod-orm.io/mcp \
--header "Authorization: Bearer <your prorm token>"prorm mcp clients create does the same from a terminal.{
"mcpServers": {
"prorm": {
"command": "npx",
"args": ["-y", "mcp-remote@latest", "https://mcp.prod-orm.io/mcp",
"--header", "Authorization: Bearer <your prorm token>"]
}
}
}https://mcp.prod-orm.io/mcp/docs (the documentation tools, public) andhttps://mcp.prod-orm.io/mcp/account (the account tools).| Tool | Does |
|---|---|
prorm_search(query, limit?) | The passages matching a query, each with its page. |
prorm_ask(question) | A written answer, with the pages it used. |
prorm_vector_search(query, topK?, minScore?) | The vector index directly: nearest chunks and their scores. |
prorm_pages(filter?) | Every documentation page, with a one-line summary. |
prorm_page(slug) | One page in full. |
| Tool | Does |
|---|---|
prorm_dialects() | Every SQL dialect, grouped by compatibility. |
prorm_operators(filter?) | Every where operator, its SQL and an example. |
prorm_decorators(filter?, group?) | Every decorator and what it does. |
prorm_datatypes(filter?) | Every member of DataTypes. |
prorm_validators(filter?) | The validation rules and what each takes. |
prorm_hooks() | The lifecycle hooks and their handlers. |
prorm_errors(code?) | The error classes, for a catch block that names one, and the klock error codes; with code, the full entry of one klock error (see the klock error reference). |
prorm_cli(filter?, group?, detail?) | Every CLI command, by group, with flags. |
prorm_stores(filter?) | The non-SQL stores. |
prorm_diagrams() | The diagram kinds. |
prorm_vocabulary(name?) | Any of the library's enums and its members. |
prorm_version() | The current version and the install line. |
prorm_changelog(version?) | The release notes for one version. |
prorm_upgrade_notes(from) | Everything that changed since a given version. |
prorm_secure_methods() | How to declare a hashed column, and with what. |
| Tool | Does |
|---|---|
prorm_write_model(model, fields, track?) | A model from a field list. |
prorm_write_model_styles(model, fields) | The same model in all three styles. |
prorm_write_connection(dialect, url?) | The Prorm construction for a dialect. |
prorm_write_association(kind, source, target, …) | Both sides of a relationship. |
prorm_write_query(model, where?, order?, …) | A read as findAll options. |
prorm_write_migration(name, operation, table, …) | A migration whose down undoes its up. |
prorm_write_seeder(name, table, environments?) | A seeder safe to run twice. |
prorm_write_transaction(isolation?, retry?) | A managed transaction, with the retry. |
prorm_write_docker_compose(dialect) | A local database with a health check. |
prorm_review(code) | Findings on a snippet: raw SQL, deprecated calls, N+1. |
| Tool | Does |
|---|---|
prorm_list_databases(), prorm_get_database(id) | Your databases; one in full. |
prorm_create_database(name, engine, …), prorm_update_database(id, …), prorm_delete_database(id) | Provision, change, remove. |
prorm_list_stores(), prorm_get_store(id), prorm_create_store(…), prorm_delete_store(id) | The same for data stores. |
prorm_list_engines(), prorm_list_regions() | What can be provisioned, and where. |
prorm_connect_database(id), prorm_connect_store(id) | How to reach one from code. |
prorm_run_query(id, query) | Run a described query against a database. |
prorm_list_keys(id), prorm_issue_key(id, …), prorm_revoke_key(id, keyId) | API keys. |
prorm_list_logins(id), prorm_create_login(id, …), prorm_get_access(id) | Engine logins and access settings. |
prorm_backup_database(id), prorm_resize_database(id, …), prorm_pause_database(id), prorm_resume_database(id) | Maintenance; each returns an operation. |
prorm_list_operations(), prorm_get_operation(id) | Follow those operations. |
prorm_billing_estimate(), prorm_billing_upcoming() | What this month costs, and the next invoice. |
prorm_whoami() | Who the caller is, and what it may do. |
prorm_list_teams(), prorm_team_members(team), prorm_invite_member(team, …), prorm_team_projects(team), prorm_attach_resource(…) | Teams, their members and projects. |
prorm_list_sessions(), prorm_revoke_session(id), prorm_list_devices(), prorm_revoke_device(id), prorm_lockdown() | Your sessions and devices, and the emergency lockdown. |
prorm_list_access_rules(), prorm_add_access_rule(…) | Access rules; a new one always starts in report mode. |
prorm_list_passwordless_sessions(id), prorm_issue_passwordless_binding(id, …), prorm_revoke_passwordless(id, sessionId) | Passwordless database sessions; a binding returns an enrolment ticket, never a secret. |
prorm_admin_stats(), prorm_admin_find_user(…), prorm_admin_user_sessions(…), prorm_admin_operations() | Staff only, read-only, klock sessions with a step-up. |
tools/list and prompts/list show only what that principal may use. TheAuthorization headerworkers/mcp/src/auth.ts):| Principal | How it is recognised | What it is listed | Where its calls go |
|---|---|---|---|
| Klock session | Authorization: Klock <grant> plus an RFC 9421 signature by the device key (a v2 prorm login; prorm mcp sends these) | The grant's capabilities; a stepUp tool also when its capability is only dormant. Past the grant's checkInBy, only the public set. | Relayed to the API's forward |
| Delegated token | Authorization: Bearer prorm_dt_… | What GET /security/mcp/capabilities returns for the token: never a staff tool or a stepUp tool | POST /security/mcp/call |
| Bearer | Any other Authorization: Bearer …: a legacy CLI or console token | Your capabilities from GET /me/capabilities: never a staff tool | Forwarded to the API with your own token, as before |
| Guest | No Authorization header | The documentation, reference and scaffolding tools and every prompt; no account tools | – |
mcp), the request signaturetools/call must401 withWWW-Authenticate: Klock error="<code>" (or Bearer error="<code>"). Thegrant_invalid,grant_expired,signature_invalid,clock_skew,session_revoked, device_revoked,unauthenticated; and, as 503, capabilities_unavailable,no_keyset, keyset_invalid andfeed_invalid.| Route | Serves |
|---|---|
POST /mcp | Everything this caller may use |
POST /mcp/docs | The documentation, reference and scaffold tools only. Always public: a credential sent here is never looked at, so even a bad token does not cause a 401. |
POST /mcp/account | The account tools only, filtered as /mcp is |
GET /, GET /health | A description, and readiness. Both list every account tool with the capability it needs, and whether it confirms first or needs a step-up. |
src/mcp/registry.ts carries three fields beside itsbuild(args):needs: the capability the call needs. An array means any of them.prorm_run_query needs data:read or data:write, and the API then checksselect needs data:read, anything else needsdata:write.destructive: the call cannot be undone. The first call returnsconfirmation required: call again with confirm "<token>" instead ofconfirm set, so a person seesstepUp: the call needs a fresh step-up. A capability that is onlyprorm step-up) is not enough: theRun `prorm step-up`, then retry. Delegated tokens never see stepUp428 confirmation_required), and from the WorkerMCP_CONFIRM_KEY secret). Eitherconfirm string argument.needs decides visibility, not authority. The API re-checks every call,| Needs | Tools |
|---|---|
catalog:read (public) | prorm_list_engines, prorm_list_regions |
account:read | prorm_whoami |
resource:read | prorm_list_databases, prorm_get_database, prorm_list_stores, prorm_get_store, prorm_list_operations, prorm_get_operation |
resource:provision | prorm_create_database, prorm_create_store |
resource:operate | prorm_update_database, prorm_backup_database, prorm_resize_database, prorm_pause_database, prorm_resume_database |
resource:delete (destructive) | prorm_delete_database, prorm_delete_store |
key:read | prorm_connect_database, prorm_connect_store, prorm_list_keys |
key:write | prorm_issue_key, prorm_revoke_key (destructive) |
data:read or data:write | prorm_run_query |
access:read | prorm_list_logins, prorm_get_access, prorm_list_passwordless_sessions |
access:write | prorm_create_login, prorm_issue_passwordless_binding, prorm_revoke_passwordless (destructive) |
billing:read | prorm_billing_estimate, prorm_billing_upcoming |
team:read | prorm_list_teams, prorm_team_members |
team:manage-members | prorm_invite_member |
project:read | prorm_team_projects |
project:write | prorm_attach_resource |
session:read | prorm_list_sessions |
session:manage | prorm_revoke_session (destructive), prorm_lockdown (destructive, step-up) |
device:read | prorm_list_devices |
device:manage (destructive, step-up) | prorm_revoke_device |
access-rule:read | prorm_list_access_rules |
access-rule:write (step-up) | prorm_add_access_rule |
admin:dashboard (step-up) | prorm_admin_stats |
admin:users (step-up) | prorm_admin_find_user, prorm_admin_user_sessions |
admin:operations (step-up) | prorm_admin_operations |
prorm_add_access_rule always creates the rule in report mode. An agent canforce or enforceNow.prorm_issue_passwordless_binding callsPOST /provision/databases/:id/passwordless/enrol and returns an enrolmentsecret field. A hostprorm_admin_*) are read-only, need a step-up, and aredispatchTool inapi/security/controllers/mcpForward.ts):404 tool_not_found, and both the Worker and the local server turn-32602 "No such tool: <name>", so a client/provision/…, /billing/…, /teams…,/security/{sessions,devices,access-rules,behaviour,lockdown}… and/me/capabilities. /admin/… is reachable only by an admin tool, withGET, and an admin tool can reach nothing else.prorm mcp, not signed in, also lists the twoprorm_list_engines and prorm_list_regions.)$ prorm mcp clients create --name "Claude Desktop" --caps resource:read,data:read,billing:read --days 30
✓ Token for Claude Desktop (id 0b9f3c52-6d1e-4a7b-8c20-5e4f1a9d7c36): resource:read, data:read, billing:read, 30 days.
! Shown this once — put it in the client’s config now:
prorm_dt_…$ curl -s https://api.prod-orm.io/security/mcp/clients \
-H "Authorization: Bearer $PRORM_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{"clientName":"Claude Desktop","caps":["resource:read","data:read","billing:read"],"expiresInDays":30,"label":"laptop"}'201 { token, client }; client has id, clientId,clientName, label, caps, resource, teamId, createdAt,expiresAt, lastSeenAt, revokedAt and revokedReason. The API routeresource and teamUuid, which the CLI does not.src/cli/basic/commands/mcp.ts):$ prorm mcp clients create --name "Nightly agent" --caps resource:read --days 7
$ prorm mcp clients list # add --all to include revoked and expired tokens
$ prorm mcp clients revoke 0b9f3c52-6d1e-4a7b-8c20-5e4f1a9d7c36create takes --name (1–80 printable characters), --capsns:*) and --days (1–30, default 30). The tokenprorm_dt_…) is printed once. The API stores only its SHA-256 hash.list (alias mcp clients) shows the id, name, capabilities, expiry, lastrevoke takes the id list shows (the token row's uuid), asks for--yes skips it), takes effect on the next call, and themcp:clients capability, which a delegated tokendelegated maskssession:manage, device:manage, access-rule:*, behaviour:*,billing:manage, team:manage-members, mcp:clients,device:manage-members, session:manage-members and every admin:*KIND_MASK.delegated in src/capabilities.ts). The CLI refuses400 not_delegableadmin_not_delegable), and an id it does not know with 400
unknown_capability. You also cannot delegate a capability you do not hold403 capability_missing).resource: { kind: 'database' | 'store', id } and a teamUuid when a token isPOST /security/mcp/clients). A locked token sees only the tools that$ prorm mcp clients create --name "Claude Desktop" --caps resource:read,data:read,billing:readprorm_dt_… line; it is not shown again.$ curl -s https://api.prod-orm.io/security/mcp/capabilities \
-H "Authorization: Bearer prorm_dt_…"{
"tier": "user",
"caps": ["billing:read", "data:read", "resource:read"],
"dormant": [],
"session": { "kind": "delegated", "binding": "bearer", "expiresAt": "2026-10-26T09:14:02.000Z" },
"client": { "id": "5d0c2b8e-3f41-4a6c-9e7d-2b1a8f6c4e90", "name": "Claude Desktop" },
"resource": null,
"tools": [
"prorm_list_databases", "prorm_get_database", "prorm_list_stores", "prorm_get_store",
"prorm_run_query", "prorm_list_operations", "prorm_get_operation",
"prorm_billing_estimate", "prorm_billing_upcoming"
],
"capabilitiesVersion": 1
}tools is exactly what the client will be listed: prorm_run_query isdata:read satisfies it, but a write query through it is403 capability_missing. client.id here is the client's ownclientId), not the token id list and revoke use.{
"mcpServers": {
"prorm": {
"command": "prorm",
"args": ["mcp"],
"env": { "PRORM_TOKEN": "prorm_dt_…" }
}
}
}PRORM_TOKEN set to a delegated token, the local server lists what/security/mcp/capabilities returns and sends every call through/security/mcp/call. Nothing else is called with the token.{
"mcpServers": {
"prorm": {
"command": "npx",
"args": ["-y", "mcp-remote@latest", "https://mcp.prod-orm.io/mcp",
"--header", "Authorization: Bearer prorm_dt_…"]
}
}
}prorm is not installed. The Worker recognises theprorm_dt_ prefix and filters and relays exactly as the local server does.~/Library/Application Support/Claude/claude_desktop_config.json. Restart$ prorm mcp clients list
$ prorm mcp clients revoke <id>[!CAUTION]
Never put a CLI or console token (prorm login's) in a third-party
client's config: it acts as you, with every capability you hold,
including destructive ones. A delegated token holds only what you listed,
can never holdsession:manage,mcp:clientsor anyadmin:*
capability, and dies on its own.
tools/call, done with curl:$ curl -s https://api.prod-orm.io/security/mcp/call \
-H "Authorization: Bearer prorm_dt_…" -H "Content-Type: application/json" \
-d '{"tool":"prorm_run_query","arguments":{"id":"shop","query":{"select":{"from":"orders","where":{"status":"paid"},"limit":5}}}}'428, and runs only when theconfirm:{
"error": "confirmation_required",
"message": "confirmation required: call again with confirm \"q3VhZ0l5Yk5mTjB6d1lQeEpKbmVtcTRr\"",
"tool": "prorm_revoke_key",
"confirm": "q3VhZ0l5Yk5mTjB6d1lQeEpKbmVtcTRr",
"expiresIn": 120
}| Route | Credential | Does |
|---|---|---|
POST /security/mcp/clients | your session (mcp:clients) | { clientName, caps[], expiresInDays?, resource?, teamUuid?, label? } → 201 { token, client }. The token is shown once. |
GET /security/mcp/clients | your session (mcp:clients) | { clients }, never the tokens |
DELETE /security/mcp/clients/:id | your session (mcp:clients) | Revoke one token |
GET /security/mcp/capabilities | Bearer prorm_dt_… | What the token may do now: { caps, session, client, resource, tools, capabilitiesVersion } |
POST /security/mcp/call | Bearer prorm_dt_… | { tool, arguments } → the tool's answer |
/security/mcp/capabilities and /security/mcp/call, where each call| Status | error | Meaning |
|---|---|---|
| 401 | token_invalid, token_revoked, token_expired | The token is unknown, revoked, past its expiry, or idle for more than 7 days |
| 423 | session_frozen | The token was frozen |
| 404 | tool_not_found | No such tool, or one this token may not use |
| 403 | capability_missing | A visible tool used beyond its intent (a write query on a read-only token) |
| 428 | confirmation_required | A destructive tool; call again with the returned confirm |
| 401 | step_up_required | A stepUp tool; not available to a delegated token |
400 intent_mismatch and anwarning.POST, so its401 nonce_replayed./mcp or /mcp/account on an origin in MCP_ORIGINS (defaulthttps://mcp.prod-orm.io), with no query string.build(args) and runs it through the app from the top:subjectKind: mcp-client, client name mcp (klock forward), linked to itsPOST /internal/mcp/forwardkid triggers a refetch, at most once a minute. Aseq is refused as a rollback.seq seen. A Klock caller is refused (503 feed_invalid) if there has beensha256(token). A delegated token revoked on the feed is dropped at the nextworkers/mcp/wrangler.toml and the Worker's secrets:| Name | What it does |
|---|---|
API (service binding to prorm-control-plane) | How the Worker reaches the API. Without it, or with API_TRANSPORT=url, it uses plain fetch to API_URL. For wrangler dev without the API running: wrangler dev --var API_TRANSPORT:url. |
MCP_CONFIRM_KEY (optional secret) | The HMAC key for the confirmation tokens the Worker issues to legacy bearers |
PRORM_KLOCK_ROOTS | Replaces the pinned roots, for development and staging only. Never set it in production. |
KLOCK_ISSUER (optional) | The grant issuer to expect. Defaults to the origin of API_URL. |
signature, signature-input,content-digest, klock-client, idempotency-key, x-prorm-team) andwww-authenticate.prorm_upgrade(from) takes the version you are on and expands to theprorm_vector_search queries a vector index built from the same pages theprorm_ask'sprorm mcp runs the account tools as a local stdio server on yourprorm login, so no token goes into any agent's config:{ "mcpServers": { "prorm": { "command": "prorm", "args": ["mcp"] } } }prorm mcp --tools lists what it exposes without starting the server. This isprorm mcp runs onprorm mcp does not run on your whole session. When you are signed--caps does: the listing comes from/security/mcp/capabilities, every call goes through POST /security/mcp/call,delegated kindDESTRUCTIVE_CAPSsrc/mcp/stdio.ts). That set is every capability a destructive registry tool| Left out | Why |
|---|---|
resource:delete, key:write, session:manage, device:manage, access:write | A destructive tool needs it |
data:write | Writes rows |
migration:apply | Changes a schema |
billing:manage | Changes what you pay |
--caps … or --full for those.prorm mcp (<host>), labelprorm mcp (default: non-destructive) and a one-day lifetime. It isprorm mcp clients list while it lives and counts toward the liveprorm mcp: running on a narrowed session (non-destructive: <N> capabilities). --full for everything.{ "mcpServers": { "prorm": { "command": "prorm", "args": ["mcp"] } } }{ "mcpServers": { "prorm": { "command": "prorm", "args": ["mcp", "--caps", "resource:read,data:read"] } } }{ "mcpServers": { "prorm": { "command": "prorm", "args": ["mcp", "--full"] } } }--full is the old behaviour: the server runs on your whole session, and--full with --caps is a usage2)./security/mcp/clients, a 403 because the session may not create MCPprorm_dt_ token), or when/me/capabilities to say what the session holds, the server stillprorm mcp: this API has no narrowed MCP sessions (/security/mcp/clients). Running on your session narrowed here to its non-destructive capabilities (enforced by this process, not the API; destructive tools are not offered). Use --caps for a server-enforced session when the API supports it, or --full for everything.No such tool, and aprorm_run_query whose described query writes (insert, update, deleteupsert) is refused locally with "This session reads data only…".[!NOTE] Why fall back instead of refusing to start
An MCP host spawnsprorm mcpfrom a fixed config, so refusing to start
would lose every tool. Local narrowing gives the same ceiling against what
narrowing is for, a model steered into a destructive call. It is not
enforced by the server against the host process itself, but a host that can
run code as you can already read your credential from the same config and
keychain. It never silently runs with everything: that takes--full.--capsis stricter and still refuses to start without a server-side child.
PRORM_TOKEN (prorm_dt_…) runs as it is, since it isprocess.env.PRORM_TOKEN; your session stays configured only to--full), a destructive tool'sThis will delete a database (prorm_delete_database {"id":"1"}). confirmation required: call again with confirm "<32 chars>" — only once the user has agreed to this action. It is good for about 2 minutes, for exactly these arguments.confirm, and aconfirm is stripped before the call is built.inputSchema declares confirm, in every mode, so--caps), the confirmation is the428 confirmation_required, passed on in the same words.src/mcp/stdio.ts):tools/list by the local session, as the CLI's guard does:| Session | What is listed | How calls are made |
|---|---|---|
| Default (signed in) | Your non-destructive capabilities, from GET /security/mcp/capabilities on the child | Every call through POST /security/mcp/call on the child token |
klock v2, --full | The verified grant's capabilities | Signed by the device key |
Legacy token, --full | Your capabilities from /me/capabilities (cached an hour). If the API has no such route, every non-admin tool is listed and the API decides. | With the token |
Delegated token (PRORM_TOKEN=prorm_dt_…) | What GET /security/mcp/capabilities returns | Every call through POST /security/mcp/call; nothing else is called with it |
| Not signed in | The public catalogue tools plus the documentation set | – |
https://mcp.prod-orm.io/mcp/docssrc/mcp/docs-proxy.ts) rather than bundled. The listing is cached for 24~/.prorm/cache/mcp-docs.json, and no credential is ever sent toPRORM_MCP_DOCS_URL points it elsewhere (staging, tests).-32602 "No such tool: <name>",annotations.destructiveHint: true.prorm mcp --caps a,b [--hours N]. Use this when the{
"mcpServers": {
"prorm": {
"command": "prorm",
"args": ["mcp", "--caps", "resource:read,data:read", "--hours", "8"]
}
}
}POST /security/mcp/clients, one day, client name prorm mcp (<host>)).delegated mask first./security/mcp/capabilities, every call through /security/mcp/call. It1).--hours (default 12, 1–24) is enforced locally: after it, account toolsprorm mcp clients revoke <id> command to run.prorm mcp clients list while it lives. --caps needs your own session:PRORM_TOKEN=prorm_dt_… it refuses to start. The documentation set stays--caps leaves out docs:read, since it is public anyway.workers/mcp/ in the repository — the documentation tools in src/tools*.ts,src/tools-account.ts — with the shared tool registrysrc/mcp/registry.ts and the local stdio server in src/mcp/stdio.ts. Thesrc/hosts.ts, with every other prorm address.api/security/controllers/mcpForward.ts, with the routes inapi/basic/routes/mcpClients.js (mounted at /security/mcp and/internal/mcp).