Skip to content

Architecture

One process, one port, one data directory. Everything below runs inside the single Prodpeek container; the only things outside it are your agents and the services you connected.

flowchart LR
    agent["Agent<br/>Claude Code, Cursor, CI"] -->|client key| mcp["/mcp"]
    human["You, in a browser"] -->|session| console["/admin console"]
    automation["Script or admin agent"] -->|admin key| api["/api/v1 and /mcp-admin"]

    subgraph prodpeek["Prodpeek container"]
        mcp --> listgate["policy on tools/list<br/>(shapes what is shown)"]
        mcp --> callgate["policy on tools/call<br/>(the wall)"]
        callgate --> adapters["in-process adapters<br/>SSH, Postgres, Git, Grafana,<br/>Prometheus, DigitalOcean, Cloudflare,<br/>OpenAPI (your own spec)"]
        callgate --> remote["remote MCP upstreams<br/>Coolify, GitHub, Confluence"]
        console --> services["services/<br/>one operation, three transports"]
        api --> services
        data[("data/<br/>SQLite, encrypted credentials,<br/>audit chain, git mirrors,<br/>local profiles")]
        services --- data
        callgate --- data
    end

    adapters --> upstreams["Your production services"]
    remote --> upstreams

The agent door: /mcp

The one endpoint an agent connects to, with a client key. It speaks Streamable HTTP MCP, statelessly: every request carries the bearer key, including initialize. A key minted for one developer or one agent is tied to a project and to the services it may reach.

The policy, twice

Every decision is made by one pure function, decide(), with no I/O, clock or network in it. It runs in two places that do not depend on each other.

On tools/list it shapes what the agent is shown: tools the profile does not allow for this key are left out. That is a courtesy, and nothing relies on it.

On tools/call it runs again on every single call, including calls naming a tool that was never advertised. A refusal returns a JSON-RPC error with a stable reason and is written to the audit log. It never touches the network. This is the wall; see How a decision is made.

Adapters, in-process

Adapters are upstreams Prodpeek implements itself, rather than talks to. From the runtime's own source (adapters/__init__.py):

Each adapter is two halves that never depend on each other:

  • menu — pure, standard library only. The whole reviewable surface: what may run, and with which arguments. It is the file a security reader reads.
  • executor — the I/O. Opens the SSH connection or the database connection, or runs git against the local mirror, doing exactly what the menu produced.

They are reached through LocalUpstream (in-process, the default) or through server.create_app (the same adapter behind HTTP, for running it on a different host than the gateway). Both go through one run_tool per adapter, so the two deployment shapes cannot drift in what they permit.

The native API adapters (Grafana, Prometheus, DigitalOcean, Cloudflare) follow the same rule and speak the vendor's own REST API with the stored token. No sidecar.

The OpenAPI adapter is the one whose menu is not written by us: it is generated from your own API's spec, one per connection, and only GET and HEAD can ever be allowed on it. Its generated profile and the spec it came from live in data/profiles/local/openapi/.

Remote MCP upstreams

Where a vendor ships its own MCP server (Coolify, GitHub, Confluence), Prodpeek connects to it over HTTP and injects the stored credential on the way out. The agent never sees the credential and never talks to the upstream directly. Every call still passes the wall first.

The console and the admin API

/admin is the console, for a person with a session. /api/v1 (JSON) and /mcp-admin (MCP) are the same operations for an admin key: add a service, mint a key, run a proof. Every operation lives once, in the runtime's services/ package; the three are transports over it, and a test keeps them in step.

A key presented at the wrong door is refused with wrong_door and audited. See Endpoints and ports.

data/

Everything the instance knows, in one directory you back up and treat as sensitive:

  • prodpeek.db: SQLite. Projects, services, client and admin keys (hashed), monitors, incidents, settings. Upstream credentials are encrypted at rest with PRODPEEK_SECRET_KEY.
  • audit.jsonl: the append-only, hash-chained audit log. One line per decision; prodpeek audit-verify checks the chain.
  • instance.json: instance id, install date, the first agent call, and the last signed feed state.
  • git/: bare mirrors for Git services. A copy of your source.
  • profiles/: local profiles, which may only narrow the bundled ones.

What leaves the box

The upstream calls your policy allows, and nothing else unless you configure it. See What leaves your network.