proxycept-mcp
by MoathOthman
README.md
# `@proxycept/mcp` — stdio bridge to a Proxycept MCP server
A **thin stdio↔HTTP bridge**. It runs a [Model Context Protocol](https://modelcontextprotocol.io)
server on stdio and forwards every JSON-RPC message, unchanged, to a Proxycept MCP endpoint
(`https://proxycept.com/api/mcp` by default) with your API token as a bearer header.
**Most people do not need this package.** The Proxycept MCP server is a hosted HTTP server —
Claude Desktop and Cursor can connect to it with a URL and an `Authorization` header, no install.
This package exists for MCP clients that only speak **stdio**, and because `npx` is the install
idiom directory listings expect.
For the no-install path, the tool catalog and the security model, see
**[proxycept.com/docs#mcp-server](https://proxycept.com/docs#mcp-server) → "Connect Claude or
Cursor"**.
Settings → API tokens has the same configuration ready to copy, addressed to that deployment and
naming the token you picked — the token *value* is shown once, on create, so the block carries a
placeholder you substitute.
## What it is not
It contains **no tool list, no tool schemas, and no Proxycept-specific logic**. `tools/list` is
answered by the server, not by this package. That is deliberate: the server is the single source
of truth for the tool catalog, so a tool added or changed server-side needs **no release here**
and there is no version drift to support. If you are reading this because you want to add a
special case for a tool name, that is the design going wrong.
## Requirements
- Node.js **20 or newer** (`node --version`).
- A Proxycept API token (`prx_live_…`) — Settings → API tokens at
[proxycept.com/settings](https://proxycept.com/settings).
## Use a read-only token
**Create the token as read-only. This is the default and it is the security boundary, not a
suggestion.**
Proxycept's read tools return *captured HTTP traffic* — content your account did not author. A
malicious response body from some upstream can read to a model as if it were an instruction
("prompt injection"). Put plainly:
> **A `read_write` MCP token means anything your proxy captures can write to your Proxycept
> configuration.**
A response body from someone else's server, written to look like an instruction, becomes a mock
route, an intercept pattern, or a stopped profile in *your* account. A read-only token makes that
chain un-completable inside Proxycept: there is nothing for an injected instruction to call.
Treat anything a Proxycept tool returns as untrusted data, not instructions — especially if you
have other MCP servers connected that can fetch URLs, run shells, or write files.
### If you really do want read-write
Only for a client you are deliberately letting change your setup. Then, all of these:
1. Create a **separate** token (do not upgrade the one you read traffic with).
2. Never enable "always allow" for Proxycept tool calls in your client.
3. Do not use it in the same session where you are reading captured traffic — that is the exact
combination that makes an injected instruction actionable.
One thing a read-write token still cannot do: **replay a captured request.** `replay_flow` re-sends
a real request to a third party with the captured credentials, so it requires a human to approve it
in the Proxycept web console (Settings → Connect your AI client). Your AI client cannot approve its
own replay, whatever token it holds.
## Configure it — put the token in the environment, not in the config file
Your MCP client's config is **plaintext JSON on disk**, often inside a directory synced to cloud
storage. A `prx_live_…` token pasted there is a long-lived credential in a backup you did not
think about. Prefer the environment.
Claude Desktop (`claude_desktop_config.json`), Cursor (`~/.cursor/mcp.json`) and most other
clients use the same shape:
```jsonc
{
"mcpServers": {
"proxycept": {
"command": "npx",
"args": ["-y", "@proxycept/mcp"],
"env": {
// Read from your shell environment; the secret itself stays out of this file.
"PROXYCEPT_API_TOKEN": "${PROXYCEPT_API_TOKEN}"
}
}
}
}
```
Not every client expands `${VAR}` in its config. If yours does not, the next-best option is to
export the variable where the client will inherit it (a login shell profile, `launchctl setenv`
on macOS, the systemd unit on Linux) and drop the `env` block entirely — the bridge reads
`PROXYCEPT_API_TOKEN` from its inherited environment. Pasting the literal token into the config
file works, and is the option to reach for last.
`--token` on the command line is supported but **discouraged**: an argument is visible to every
process on the machine via the process list.
### Environment variables
| Variable | Required | Default | Meaning |
| --- | --- | --- | --- |
| `PROXYCEPT_API_TOKEN` | yes | — | Your API token. `PROXYCEPT_TOKEN` is accepted as an alias. |
| `PROXYCEPT_BASE_URL` | no | `https://proxycept.com` | Origin of your Proxycept. `/api/mcp` is appended. |
| `PROXYCEPT_URL` | no | — | Full endpoint URL. Overrides `PROXYCEPT_BASE_URL`. |
| `PROXYCEPT_TIMEOUT_MS` | no | `60000` | How long to wait for response headers. Streaming replies are not cut off by it. |
| `PROXYCEPT_VERBOSE` | no | — | `1` logs method, HTTP status and timing to stderr. Never the token. |
Every variable has a matching flag (`--base-url`, `--url`, `--token`, `--timeout`, `--verbose`).
### Self-hosted
```bash
PROXYCEPT_BASE_URL=https://proxy.internal.example PROXYCEPT_API_TOKEN=… npx -y @proxycept/mcp
```
Plaintext `http://` is refused for anything except loopback, so the token can never cross a
cleartext hop. This package provides **no flag to disable TLS verification** and will not.
Node itself has one — `NODE_TLS_REJECT_UNAUTHORIZED=0` turns off certificate verification for the
whole process — and the bridge **refuses to start** if it finds that set for a non-loopback
endpoint, rather than silently inheriting it. If you hit that error, unset the variable (check
your shell profile and your MCP client's `env` block); it is almost always a leftover from
debugging something else, and with it set your token would go out over a connection anyone on the
path can read.
## Check your setup
```bash
PROXYCEPT_API_TOKEN=prx_live_… npx -y @proxycept/mcp --check
```
It performs a real MCP handshake and a real `tools/list`, then prints what the **server** said:
```
endpoint: https://proxycept.com/api/mcp
server: proxycept 1.0.0 (MCP 2025-06-18)
tools: 20 — list_workspaces, list_profiles, …
ok
```
The tool count comes from the server, not from this package, so it changes without a release here.
Write tools are listed to every client and refused at the API layer unless the token is
`read_write` — see below.
Exit codes: `0` ok · `1` could not reach or authenticate to the server · `2` bad configuration.
### When something is wrong
| Symptom | Meaning |
| --- | --- |
| `no API token` (exit 2) | `PROXYCEPT_API_TOKEN` is not set, or your client did not pass it through. |
| `unauthenticated: …` | The token is invalid, expired or revoked. Create a new one in Settings. |
| `forbidden: …` | Read-only token attempting a write, or no access to that resource. |
| `not_found: no MCP endpoint at this URL` | Wrong `PROXYCEPT_BASE_URL`, or MCP is not enabled on that server. |
| `session_expired: …` | The server ended the MCP session (it restarted, or the session timed out). Reconnect the server in your client; the bridge has already dropped the stale session id. |
| `rate_limited: …` | Per-token rate limit. The message carries the retry delay. |
| `cannot reach …` | Network/DNS/firewall. The bridge never hangs on this — it errors. |
## Security properties
- The token is sent as `Authorization: Bearer …` **to the configured origin and nowhere else**.
A redirect to a different origin is refused, not followed — the credential is never handed to
a host you did not configure. Same-origin `307`/`308` is followed (up to 3 hops), since
preserving both origin and method means re-sending the credential changes nothing about who
receives it, and reverse proxies do emit them; `301`/`302`/`303` is refused, because replaying
a POST as a GET would silently drop your request. (The hosted server never redirects.)
- **The token is never logged, and never returned to your client.** Every line written to
stderr, and every error message this bridge *synthesizes* for stdout, goes through a redactor
that removes the configured secret, anything shaped like a `prx_…` token, and any `Bearer …`
string — including in `--verbose` mode and including a credential a **server** echoes back in
a 4xx/5xx body. That last case matters because stdout is your MCP client: it lands in the
model's context and in the client's logs. Stack traces are never printed. There is a test for
each of those.
- **The protocol stream is never rewritten.** Redaction applies to messages this bridge builds,
not to the server's own JSON-RPC payloads — mangling those would corrupt the traffic you are
debugging. Redaction of captured traffic is the **server's** job (it applies its masking
denylists on the read path).
- **Nothing is written to disk.** No cache, no config file, no credential store.
- stdout carries protocol messages only; all diagnostics go to stderr.
- Zero runtime dependencies. `npm ls --omit=dev` on this package is empty — the only supply
chain is Node itself.
## Development
```bash
cd clients/mcp
npm ci
npm test # builds, then runs the suite against a fake JSON-RPC server
npm run typecheck
```
```bash
npm run check-package # asserts the tarball ships every module, the bin, README and LICENSE
```
Layout: `src/config.ts` (resolve + validate config), `src/redact.ts` (redaction + the only
logger), `src/http.ts` (one POST per message; SSE, redirect policy, HTTP→JSON-RPC error
mapping), `src/bridge.ts` (the generic forwarding loop), `src/stdio.ts` (newline-delimited JSON
framing), `src/cli.ts` (arguments, `--check`, wiring).
### Verified against a fake server
The suite runs against a fake JSON-RPC HTTP server in `test/helpers.js`: verbatim forwarding
(including methods this package has never heard of), header shape, `Mcp-Session-Id` and
protocol-version echo, SSE streaming, `202` notifications, HTTP→MCP error mapping, batch
handling, the redirect policy, session expiry, NDJSON framing across chunk boundaries, and the
token's absence from all output.
### Verified against a real Proxycept
`--check` has been run end to end against a real Control API with the MCP server enabled (V4-T66):
real `initialize` capability negotiation, a real `tools/list` (20 tools), with a read-only
`prx_live_` token. The remaining unknowns are production-specific — whether the hosted deployment
issues an `Mcp-Session-Id`, whether a long tool call comes back as JSON or SSE, and the exact
wording of its 401/403/429 bodies. The bridge is generic, so any of those can differ without a code
change here; the live pass against `https://proxycept.com` is step 5 of `PUBLISHING.md`.
## Publishing (maintainers)
Releases are manual on purpose — there is deliberately **no CI job that publishes**, so a
compromised workflow token cannot ship a package that handles customer credentials. The full
runbook (org creation, preflight, the exact `npm publish --access public` sequence, verification
and the deprecate path) is **`PUBLISHING.md`**, which lives in the maintainers' source tree and is
deliberately shipped nowhere — not in this tarball, not in the public mirror. There is no link here
on purpose: it would resolve to a 404 on both.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues