paperclip-mcp
by FournyP
README.md
# Paperclip MCP Railway Template
Deploys [paperclip-mcp](https://github.com/wizarck/paperclip-mcp) β an operator-side MCP server for the [Paperclip](https://github.com/paperclipai/paperclip) AI agent orchestration platform, giving you 95 tools over issues, agents, goals, projects, approvals, costs, routines, decisions and pipelines β behind an NGINX bearer-token auth gateway.
[](https://railway.com/deploy/paperclip-mcp?referralCode=C3Uv6n&utm_medium=integration&utm_source=template&utm_campaign=generic)
## ποΈ Architecture
```
client ββAuthorization: Bearer <key>βββΊ paperclip-mcp-gateway (nginx, public)
β
βΌ private network
paperclip-mcp (private) βββΊ Paperclip
```
Two Railway services:
- **`paperclip-mcp-gateway`** β `nginx:1.29.8-alpine`, exposes a public domain, validates the `Authorization: Bearer <key>` header against `API_KEYS`, and forwards streamable-HTTP traffic to the mcp service via Railway's private network.
- **`paperclip-mcp`** β built from the upstream source at a pinned commit (see `mcp/Dockerfile`), run with `--transport=streamable-http`. **Do not give this service a public domain**; it is only reachable at `paperclip-mcp.railway.internal:8000`.
The gateway talks to Paperclip through nothing but the mcp service, and the mcp service reaches Paperclip over `PAPERCLIP_API_URL` β point that at your Paperclip's private endpoint so Paperclip itself never needs to be public either.
## β¨ Features
- Bearer-token auth with a comma-separated allowlist of keys
- Streamable HTTP passthrough (`/mcp`)
- Unauthenticated `/health` (and `/healthz`) on the gateway for Railway healthchecks
- Board or agent API key β the Paperclip-side boundary on top of the network-level bearer auth
- Optional keyed-path entrypoint for MCP clients that cannot send an `Authorization` header
- Zero custom code β gateway is plain nginx, mcp is the upstream package with a pinned dependency set
## πββοΈ How to use
1. Click the Railway button π
2. Fill in the variables (see below)
3. Deploy! π
4. Let Paperclip accept the private hostname (see [Reaching Paperclip](#-reaching-paperclip-over-the-private-network))
5. Point your MCP client at `https://<gateway-domain>/mcp` (streamable-HTTP, `"type": "http"`) with header `Authorization: Bearer <your-key>`. Quick check:
```bash
curl -sS -X POST https://<gateway-domain>/mcp \
-H "Authorization: Bearer <your-key>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'
```
With Claude Code:
```bash
claude mcp add paperclip --transport http https://<gateway-domain>/mcp \
--header "Authorization: Bearer <your-key>"
```
## π§± Infrastructure as Code
`.railway/railway.ts` defines the whole project β both services and every variable.
```bash
railway link
npm install
# First apply only; later runs omit these and preserve() keeps the values.
export API_KEYS=$(openssl rand -hex 32)
export PAPERCLIP_API_URL=http://paperclip.railway.internal:3100
export PAPERCLIP_API_KEY=pcp_board_...
export PAPERCLIP_COMPANY_ID=<uuid>
npm run plan # read the diff before applying
npm run apply
railway domain --service paperclip-mcp-gateway
```
Give the domain to the gateway only. `paperclip-mcp` holds your Paperclip key and has no
authentication of its own.
Paperclip is external to this template. Point `PAPERCLIP_API_URL` at whichever instance
the operator should run, and allow the hostname on that instance (see below).
Needs the Railway CLI 5.42.1 or newer: the IaC engine ships in the CLI, not in the npm
package. If you forked this repo, change `REPO` in `railway.ts` to your own before applying.
Link it to a project dedicated to this template. An apply deletes every resource **and
every variable** the file does not declare, so from then on variables live in `railway.ts`,
not the dashboard. Do not point it at a project created from the deploy button β the
service names differ, and a mismatch is a delete and recreate, not a rename.
## β¬οΈ Upgrading
Railway template updates are opt-in β an existing deployment keeps running until you apply the update. See the [changelog](CHANGELOG.md) for what each update contains.
## π§ Variables
### Gateway service
| Variable | Required | Description |
| --------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `API_KEYS` | yes | Comma-separated list of allowed bearer tokens. Allowed chars per key: `A-Z a-z 0-9 . _ ~ + / = -` |
| `MCP_HOST` | no | Defaults to `paperclip-mcp.railway.internal`. Only override if you rename the mcp service. |
| `MCP_PORT` | no | Defaults to `8000`. |
| `PATH_KEY_AUTH` | no | `true` enables the keyed-path entrypoint (see below). Default `false`. |
### MCP service
| Variable | Required | Description |
| ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PAPERCLIP_API_URL` | yes | Base URL of your Paperclip, e.g. `http://paperclip.railway.internal:3100`. `/api` is appended when missing. |
| `PAPERCLIP_API_KEY` | no | A **board** API key (`pcp_board_...`) for full operator access, or an **agent** API key scoped to that agent. Leave empty only for a `local_trusted` Paperclip, which then treats every request as the local board operator. |
| `PAPERCLIP_COMPANY_ID` | yes | Company UUID every company-scoped tool targets β the UUID in the Paperclip UI URL, `/companies/<uuid>`. |
| `PAPERCLIP_AGENT_ID` | no | Default agent UUID for `checkout_issue` when using a board key. |
| `PAPERCLIP_RUN_ID` | no | Forwarded as `X-Paperclip-Run-Id` on writes. Only ever a **real** heartbeat run id; a made-up UUID breaks a foreign key server-side. |
| `PORT` | no | Defaults to `8000`. Railway injects this. |
Mint a board key with `paperclipai login`, or `POST /api/board-api-keys` from a logged-in board session. Run the `whoami` tool to confirm which credential is active β the mcp service also logs it at startup.
## π Keyed-path entrypoint (opt-in)
Some MCP clients enumerate a server's tools before they have anywhere to store a
credential, so their discovery request arrives with no `Authorization` header and
takes a `401`. Setting `PATH_KEY_AUTH=true` on the gateway adds a second way in:
```
https://<gateway-domain>/k/<your-key>/mcp
```
The key is validated against the same `API_KEYS` allowlist. An absent or wrong
key is still `401`, and a valid key unlocks nothing but `/mcp` β the key segment
is stripped before proxying, so the mcp service only ever sees `/mcp`.
**The key travels in the URL**, where it can be recorded by edge and proxy logs
outside your control (the gateway itself logs nothing for this path). So:
- Issue a **separate key** in `API_KEYS` for each client that uses this path, so
it can be rotated without touching the others.
- Leave `PATH_KEY_AUTH` off and use the header form everywhere else.
- Keys used on this path may not contain `/` (the header form allows it), since
a slash would split the path segment.
## π Reaching Paperclip over the private network
Paperclip's private-hostname guard answers `403` to any request whose `Host` is not on its
allowlist, and by default that list holds only the public hostname derived from
`BETTER_AUTH_BASE_URL`. A request from the mcp service arrives as
`paperclip.railway.internal`, so every tool fails with:
```
This hostname is not allowed for this Paperclip instance. If you want to allow a hostname, run npx paperclipai allowed-hostname <host>.
```
Set this on the **Paperclip** service (not on this template) and redeploy it:
```
PAPERCLIP_ALLOWED_HOSTNAMES=paperclip.railway.internal
```
It is a comma-separated list; the public hostname stays allowed. Use your Paperclip
service's own `RAILWAY_PRIVATE_DOMAIN` if it is not named `paperclip`. The alternative is
pointing `PAPERCLIP_API_URL` at the public URL, which works but sends every call out through
Railway's edge instead of the private network.
## π Two layers of protection
- **Bearer auth at the gateway** is the network boundary β nothing reaches the MCP without a valid key.
- **The Paperclip API key** is the Paperclip-side boundary. A board key can approve hires, pause and terminate agents, set budgets and mint API keys, so a leaked gateway key hands all of that over. An agent key limits the blast radius to what that one agent may do (board-only tools return `403`), at the cost of the operator features this server exists for.
paperclip-mcp has **no client authentication of its own** β it binds to loopback upstream precisely because it is meant to sit next to your MCP client. The gateway is therefore mandatory if the service is reachable from anything outside Railway's private network.
## π Notes
- **Generate strong keys:** `openssl rand -hex 32`
- **Rotating a key:** update `API_KEYS` on the gateway service and redeploy it. The mcp service is untouched.
- **`/health` and `/healthz` are unauthenticated** so Railway (and any uptime monitor) can probe without a token. Everything else requires `Authorization: Bearer <key>`.
- **Invalid / missing token:** the gateway returns `401` with a `WWW-Authenticate: Bearer realm="paperclip-mcp"` header.
- **Do not expose the mcp service publicly.** All traffic should enter through the gateway.
- **Gateway port:** nginx listens on `PORT`, which the IaC file pins to `80`. Railway injects a random `PORT` when the variable is unset, so if you create the gateway by hand and give its domain an explicit target port, set `PORT` to match or the edge gets `connection refused`.
- **Startup is best-effort:** the mcp service probes Paperclip's `/health` and classifies the key when it boots, but an unreachable Paperclip is logged, not fatal. Check the service logs if every tool errors.
- **The PyPI `paperclip-mcp` package is a different project** (older, another author). This template builds the `wizarck/paperclip-mcp` source at a pinned SHA via `ARG PAPERCLIP_MCP_SHA` in `mcp/Dockerfile`, with its dependency tree frozen in `mcp/requirements.txt`. Bump the SHA and regenerate the requirements to pick up upstream changes.
## βοΈ License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues