@priventai/mcp-gateway
v0.2.0
Published
Privent MCP Gateway — wrap any MCP server (stdio or Streamable HTTP) and enforce tokenization on tool-call arguments and results (ADR 0001 enforcement path)
Downloads
183
Readme
@priventai/mcp-gateway (PoC)
Enforcement path for Privent MCP — monorepo package under privent-mcp (ADR 0001 / PRI-169).
MCP Client (Claude Desktop / Cursor / Inspector)
│ Streamable HTTP or stdio
▼
@priventai/mcp-gateway ← this package
│ proxies tools/list + tools/call
│ protects tool-call ARGUMENTS before forwarding (default on, MCP-002)
│ protects tool RESULT text → risk + vault tokenize
▼
Upstream MCP server (any) — Streamable HTTP URL or stdio subprocessAssist @priventai/mcp-server (repo root) remains optional tools. This gateway is where traffic must pass for E1.
Wrap a stdio server (the config-swap path)
Most local MCP servers are stdio subprocesses ("command": "npx", "args": [...] in a client
config). The gateway spawns and fronts one itself via PRIVENT_UPSTREAM_MCP_CMD:
export PRIVENT_API_KEY=sk-...
export PRIVENT_UPSTREAM_MCP_CMD='npx -y @modelcontextprotocol/server-filesystem "/Users/you/Documents"'
pnpm gateway:start # stdio downstream — for Claude Desktop / Cursor entriesRules, each load-bearing:
- One of URL or CMD, never both. Both set is a boot refusal, not a silent priority — a priority order would hide which upstream the gateway actually fronts.
- CMD is one quoted string. Quote arguments containing spaces (
"..."or'...'). No shell ever re-parses it — the child is spawned withshell: false— the quoting exists only to express argv items with spaces. - The child inherits your environment minus the
PRIVENT_namespace. Upstream servers keep getting their own credentials (GITHUB_TOKEN, …) exactly as if the client had spawned them directly; the Privent credential is never handed to a third-party process. Consequence: an upstream that itself wantsPRIVENT_API_KEY(a middleware-wrapped server) cannot get it through the gateway. - If the upstream dies, the gateway dies — one structured stderr record
(
"event":"upstream_closed"), then exit 1. A dead upstream has no traffic to fail open with (ADR K3 governs protection failures, not upstream death); the client sees exactly what a direct child death looks like. No reconnect loop, deliberately. - Windows:
npxshims cannot be spawned undershell: false(same constraintscripts/verify-published-install.mjsdocuments) — point CMD atnode <absolute path to the server's entry>instead.
Copy-paste client entries: examples/claude_desktop_config.gateway.json and
examples/cursor.gateway.mcp.json (local-path form — the package is not on npm yet, and an
npx instruction that cannot succeed is MCP-013's defect).
Local PoC (K2)
- Run an upstream MCP over Streamable HTTP (example: another process on
:3001/mcp) — or usePRIVENT_UPSTREAM_MCP_CMDabove and skip the separate process. - Configure env and start gateway on
:8090:
# from repo root
pnpm install
pnpm gateway:build
export PRIVENT_UPSTREAM_MCP_URL=http://127.0.0.1:3001/mcp
export PRIVENT_BASE_URL=https://api.privent.ai
export PRIVENT_API_KEY=sk-...
export MCP_PORT=8090
# optional: PRIVENT_GATEWAY_PROTECT_MODE=fail_closed
# optional: PRIVENT_GATEWAY_PROTECT=0 # list/proxy only, no tokenize
# optional: PRIVENT_GATEWAY_PROTECT_INPUT=0 # stop protecting tool-call arguments (default on)
# optional: PRIVENT_GATEWAY_AUDIT_INGEST=0 # stderr only, no /v1/audit/events
# OAuth edge (PRI-170 / ADR K4) — HTTP only; stdio still uses PRIVENT_API_KEY
# export PRIVENT_AUTH_MODE=oauth
# export PRIVENT_OAUTH_JWKS_URL=https://auth.example.com/.well-known/jwks.json
# export PRIVENT_OAUTH_ISSUER=https://auth.example.com/
# export PRIVENT_OAUTH_AUDIENCE=privent-mcp-gateway
# export PRIVENT_OAUTH_RESOURCE=http://127.0.0.1:8090/mcp
# export PRIVENT_OAUTH_AUTHORIZATION_SERVERS=https://auth.example.com
pnpm gateway:start:http- Point the client at
http://127.0.0.1:8090/mcp(not the upstream). - Discovery:
GET /.well-known/oauth-protected-resource(resource + AS list). Health:GET /healthzincludesauth_mode.
Cursor sketch
{
"mcpServers": {
"upstream-via-privent": {
"url": "http://127.0.0.1:8090/mcp"
}
}
}(Exact Cursor remote MCP config may vary by version. In oauth mode send Authorization: Bearer <access_token> on initialize.)
Behavior (ADR)
| Topic | PoC default |
|-------|-------------|
| Protect | Tool-call arguments (every string leaf, any depth — default on since MCP-002) AND tool result text blocks via /v1/risk/score + vault find-or-create |
| Upstream | PRIVENT_UPSTREAM_MCP_URL (Streamable HTTP) or PRIVENT_UPSTREAM_MCP_CMD (stdio subprocess) — exactly one |
| Fail mode | fail-open + stderr protection.* + best-effort POST /v1/audit/events |
| Non-text blocks | PRIVENT_GATEWAY_NONTEXT_BLOCKS = pass (default: forward + protection.skipped) / strip (placeholder text block) / refuse (error result). structuredContent strings are protected like text and survive all three; _meta is audited on pass, dropped on strip/refuse (MCP-003 / MCP-027, ADR 0001 amendment) |
| Audit | tool_call, protection.applied / skipped / unavailable (PRI-164 gateway half) |
| Identity | HTTP: api_key (default) or oauth + required JWKS claim map (K4); stdio: API key only |
| Boot | Refuses with no PRIVENT_API_KEY and no OAuth JWKS, both fail modes (MCP-006 — "nothing set" is never an intentional production config). HTTP-only escape hatch for per-request-Bearer topologies: PRIVENT_GATEWAY_ALLOW_NO_CREDENTIALS=1, announced as credentials=none_allowed on every boot line |
| Hosted GA | Same package; deploy as service (follow-up) |
Spike notes (PRI-167 / PRI-169 / PRI-170)
- SDK-level proxy (this PoC):
Serverhandlers forwardtools/list+tools/callto upstreamClient— cleaner than raw JSON-RPC re-encode for tool surface. - Streamable HTTP both sides: client→gateway and gateway→upstream.
- stdio downstream supported (
pnpm gateway:start) for local IDE; stdio upstream viaPRIVENT_UPSTREAM_MCP_CMD(this section's earlier "upstream still HTTP" limitation is closed). - Resources/prompts not proxied yet (tools-only PoC).
- OAuth edge on HTTP initialize (JWKS required in oauth mode; 401 on missing/invalid bearer or missing JWKS).
Scripts
| Script | Meaning |
|--------|---------|
| pnpm gateway:build | Compile this package |
| pnpm gateway:start:http | Listen /mcp + /healthz |
| pnpm gateway:test | Unit tests |
