PoryMCP
One key. Many shapes.
PoryMCP is an open-source MCP credential proxy that ships as one Docker container.
Register real MCP servers once, then mint per-agent virtual keys and endpoints. Agents never see the real credentials.
Website: porymcp.com
Repository: github.com/danjonesio/porymcp
What it does
Real upstream URLs and API keys stay hidden from agents. Each credential goes only to the upstream you registered it for, never to a host that upstream names in a redirect.
Every agent gets its own virtual key, which you can rotate, revoke or rate-limit on its own. One virtual key can cover a group of upstreams, each on its own endpoint.
You can ask a registered server what it offers, from the dashboard or the API, and read back the exact {slug}__{tool} name a rule has to be written in. The tool list is not stored. Only the result of the last test is.
Every tool call is written to a structured audit log: who called, when, what, and which upstream answered.
PoryMCP runs as a single Docker container with a REST API and a dashboard. Every action in the dashboard is available through the API.
Quick start
[ -f .env ] || cp .env.example .env
grep -q '^ADMIN_API_KEY=' .env || printf 'ADMIN_API_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
grep -q '^ENCRYPTION_KEY=' .env || printf 'ENCRYPTION_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
docker compose up --build
Compose refuses to start unless .env holds both ADMIN_API_KEY and ENCRYPTION_KEY. The guarded lines above create each one only when it is missing, so re-running the block never overwrites an existing value. Losing ENCRYPTION_KEY makes every stored credential unrecoverable, so back it up separately from the data volume. Open the dashboard at http://localhost:8080 and sign in with the admin key from .env:
grep '^ADMIN_API_KEY=' .env | cut -d= -f2-
Put the process behind TLS for anything other than localhost. See TLS and reverse-proxy deployment.
The same deployment runs from the published image:
# Generate each key once and keep it (0600): a second inline $(openssl …) would
# mint a different encryption key and lock the stored credentials away.
[ -f ~/porymcp.admin ] || (umask 077; openssl rand -hex 32 > ~/porymcp.admin)
[ -f ~/porymcp.key ] || (umask 077; openssl rand -hex 32 > ~/porymcp.key)
docker run -d \
-p 8080:8080 \
-e ADMIN_API_KEY="$(cat ~/porymcp.admin)" \
-e ENCRYPTION_KEY="$(cat ~/porymcp.key)" \
-v porymcp-data:/data \
ghcr.io/danjonesio/porymcp:latest
latest and vX.Y.Z are releases; edge and sha-<short sha> are the current main. edge and latest move, so a production deployment pins the digest the publish run prints in its summary: ghcr.io/danjonesio/porymcp@sha256:.... latest appears with the first release tag; until then, pull edge. See docs/08-docker.md for how images are built and tagged.
Back ENCRYPTION_KEY up separately from the data volume. Without it the
stored upstream credentials are unrecoverable, and restarting the same volume
under a different key puts the server into its degraded, fail-closed state
until you rotate or restore.
Local development
Go 1.26 or later. go.mod pins toolchain go1.26.7, which Go downloads automatically. Set GOTOOLCHAIN=local to insist on your installed release. The dashboard is compiled into the server binary. After go run ./cmd/server, open http://localhost:8080.
[ -f ~/porymcp.admin ] || (umask 077; openssl rand -hex 32 > ~/porymcp.admin)
export ADMIN_API_KEY=$(cat ~/porymcp.admin) # the same key in every shell: this is your dashboard sign-in
[ -f ~/porymcp.key ] || (umask 077; openssl rand -hex 32 > ~/porymcp.key)
export ENCRYPTION_KEY=$(cat ~/porymcp.key) # the same key in every shell: ./data/porymcp.db persists
go run ./cmd/server
Keep the same ENCRYPTION_KEY across restarts once an upstream has a stored
credential: the server refuses to start with none set against a database that
holds one.
To change the UI:
cd web && npm ci && npm run build
# then restart the Go server (or `npm run dev` on :3000 while the API stays on :8080)
make test covers auth, key hashing, store CRUD, management API, proxy credential injection and the tracked-file guards. Use the target rather than go test ./...: once npm ci has run, ./... also reaches a Go file shipped inside web/node_modules. make web-test (cd web && npm test) covers the dashboard's client-snippet generation. CONTRIBUTING.md lists every check CI runs and the order to run them in.
Core concepts
| Concept |
Description |
| Upstream |
A real MCP server (its final URL + credentials) |
| Group |
A collection of Upstreams that can be exposed together |
| Virtual key |
A key plus its endpoints: one per Upstream, or one per Group member plus an aggregate |
| Audit log |
Every MCP request made through a Virtual key is recorded |
How it works
- Register one or more real MCP servers as Upstreams
- (Optional) Bundle several Upstreams into a Group
- Create a Virtual Key. You receive the plaintext key and its endpoints
- Point your AI agent at those endpoints with
Authorization: Bearer <api_key>: one per group member (/{virtual_key_id}/{upstream_slug}/mcp), or the single proxy_url for an Upstream target
- PoryMCP injects the real credentials, forwards the request to that upstream's own URL (never to a host it names in a redirect) and logs the call
Each virtual key has its own endpoint. A group key has one per member, so your client sees the servers separately, with their original tool names. /{virtual_key_id}/mcp still serves the merged single-connection view, where every tool is named {upstream_slug}__{tool} so you can see which server a tool came from, and /mcp still works as a shared door. Install snippets for Claude Code, Cursor, Codex, OpenCode, and Gemini CLI: Connecting clients.
Features
- Streamable HTTP to upstreams:
POST carries every call and DELETE ends a
session; SSE-framed upstream responses are relayed, and a client's GET for
a server-initiated stream is answered 405 until PORM-5. The legacy HTTP+SSE
upstream transport is not implemented (PORM-5); sse is refused on write
since PORM-28.
- Encrypted storage of upstream secrets (AES-256-GCM)
- Virtual keys hashed with argon2id; plaintext shown only on create or rotate
- Virtual key rotation and revocation
- Optional per-key rate limiting
- Filterable audit logs
- Dashboard with an application shell (React + Tailwind CSS + Headless UI). Every component is PoryMCP's own
- REST management API.
openapi.yaml describes it; no route serves the file yet (PORM-22)
- SQLite by default. Postgres via
DATABASE_URL=postgres://... and docker compose --profile postgres up together (schema creation is verified, behaviour beyond that is not: PORM-18)
Documentation
Tech stack
- Backend: Go
- Database: SQLite (default) / Postgres
- Dashboard: React + Tailwind CSS + Headless UI
- Container: Single multi-stage Docker image
Status
Early development. The management API, proxy, audit log, Docker image and dashboard are in place; docs/05-mvp.md lists what ships and the known gaps. Releases are tagged vX.Y.Z, and the changes in each that affect a running deployment are recorded in CHANGELOG.md.
License
MIT (see LICENSE). Third-party notices for the bundled dashboard
dependencies are in NOTICE.