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¶
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.