Skip to content

For agents (API and MCP)

Prodpeek has two doors for automation, and they do the same things. Use whichever your caller speaks.

  • /api/v1 — JSON over HTTP. For CI, scripts, the CLI.
  • /mcp-admin — the same operations as MCP tools. For an agent.

Both need an admin key. Both refuse a client key with wrong_door, audited.

The one rule

An agent never holds a credential. Call add_service without one, get a one-time URL back, and hand that to a human. Never ask someone to paste a secret into a chat — once it is in a transcript it is in a log, a vector store and whatever the provider retains.

Mint an admin key

prodpeek admin-key create onboarding-agent --ttl 24h

Default lifetime is 24 hours. An admin key can be scoped to one project, and a scoped key can only ever mint another key inside its own scope — widening by minting is the oldest escalation trick there is.

The whole flow

// 1. What can this instance govern?
{"jsonrpc":"2.0","id":1,"method":"tools/call",
 "params":{"name":"list_profiles","arguments":{}}}

// 2. Connect a service. Note: NO credential.
{"jsonrpc":"2.0","id":2,"method":"tools/call",
 "params":{"name":"add_service","arguments":{
    "profile":"coolify/read-only",
    "url":"https://coolify.ejd.dk/mcp",
    "name":"ejd-coolify"}}}
// → returns credential_drop.path — hand it to a human

// Some upstreams need a header as well as a token. You MAY set these —
// they are routing, not secrets. You may still not set the credential.
{"jsonrpc":"2.0","id":3,"method":"tools/call",
 "params":{"name":"add_service","arguments":{
    "profile":"grafana/read-only",
    "url":"http://mcp-grafana:8000/mcp",
    "name":"ejd-grafana",
    "headers":{"X-Tenant":"ejd-production"}}}}

// 3. Once they have pasted it
{"jsonrpc":"2.0","id":3,"method":"tools/call",
 "params":{"name":"test_connection","arguments":{"service":"ejd-coolify"}}}

// 4. A key for a developer. Short TTL.
{"jsonrpc":"2.0","id":4,"method":"tools/call",
 "params":{"name":"mint_token","arguments":{
    "label":"anders","services":["ejd-coolify"],"ttl":"8h"}}}

// 5. Make the instance prove itself
{"jsonrpc":"2.0","id":5,"method":"tools/call",
 "params":{"name":"run_proof","arguments":{}}}
H="Authorization: Bearer $PRODPEEK_ADMIN_KEY"
B=https://prodpeek.example.dk

curl -sS -H "$H" $B/api/v1/profiles

curl -sS -X POST -H "$H" -H 'Content-Type: application/json' \
  -d '{"profile":"coolify/read-only","url":"https://coolify.ejd.dk/mcp","name":"ejd-coolify"}' \
  $B/api/v1/services
# → {"state":"awaiting_credential","credential_drop":{"path":"/drop/..."}}

curl -sS -X POST -H "$H" $B/api/v1/services/ejd-coolify/test

curl -sS -X POST -H "$H" -H 'Content-Type: application/json' \
  -d '{"label":"anders","services":["ejd-coolify"],"ttl":"8h"}' \
  $B/api/v1/tokens

curl -sS -X POST -H "$H" $B/api/v1/proofs

Every endpoint

Method Path
GET /api/v1/status version, licence, counts. One call for "is this healthy"
GET POST DELETE /api/v1/projects customers and environments
GET POST DELETE /api/v1/services what the gate reaches
POST /api/v1/services/{name}/test connect and classify what it advertises
GET POST DELETE /api/v1/tokens client keys
GET POST DELETE /api/v1/admin-keys admin keys
GET /api/v1/profiles the policies, with provenance
GET POST /api/v1/proofs read or run a proof
GET POST DELETE /api/v1/monitors uptime checks
GET /api/v1/incidents incidents with their evidence
GET /api/v1/audit the log, filtered
GET PUT /api/v1/settings non-secret settings only

An OpenAPI document is at /api/docs on your instance.

Refusals

Every refusal carries a reason from one small vocabulary, shared with the audit log — so a caller that can read a denial from /mcp can read one from here without learning a second language.

reason Means
wrong_door A client key at an admin door, or the reverse
no_such_profile The policy name does not exist on this instance
no_credential The service is still awaiting its drop link
upstream_unreachable Prodpeek could not reach the system
invalid_ttl A lifetime it would not guess at — 8hours is not 8h
secret_not_accepted You tried to PUT a secret through the API. Use the console
invalid_header An extra header this will not send — Authorization, Host, or a value with a newline in it
token_expired The key ran out. Mint another

What the API will not do

It will not take a secret. PUT /api/v1/settings refuses any key ending .api_key or _secret with secret_not_accepted. That is principle 11 applied to Prodpeek's own configuration: an agent with an admin key should not be a courier for the operator's provider key either.

It will not read production. Nothing here reaches a customer's system except test_connection, which returns the classification of what a service advertises and never a response body. Reading production is /mcp, with a client key.

If you find a way to write

Through any service, by any route: stop and tell the human, with the exact call and the exact response. That is the most important thing you could find, and it is worth more than whatever task you were doing.