---
title: "Create (or revive) a relay tunnel for a named project server"
method: POST
path: "/projects/{projectId}/tunnels"
tags: ["Tunnels"]
---

# Create (or revive) a relay tunnel for a named project server

`POST /projects/{projectId}/tunnels`

Registers a server record named `name` if missing, mints a relay tunnel grant for it, and **persists the tunnel bearer URL (including the plaintext `?k=` secret) onto the server record's `url`** so evals and chatboxes can target the tunnel like any remote server. The plaintext persistence is a deliberate trade-off of the current tunnel MVP — the backend otherwise stores only a hash of the secret — mitigated by rotation: **every call rotates the secret, revokes the previous grant at the edge (disconnecting any live tunnel session for the server), and updates the stored URL**, so re-calling this route is also the rotation/recovery path.

The caller hosts the tunnel itself: connect a WebSocket to `relayWsUrl` (subprotocol `mcpjam-tunnel.v1`, `Authorization: Bearer <connectToken>`) and serve the relayed requests — this is what `mcpjam tunnel` does. When the host disconnects, the server record stays and calls to the public URL fail fast at the edge.

## Path parameters

- `projectId` string, required

## Request body

- TunnelCreateRequest
  - `name` string, required — Server name to register the tunnel under. Reusing an existing server's name points that record at the tunnel: its `url` is overwritten and stdio records are converted to `transportType: "http"`.

## Response `201`

Tunnel grant minted and the server record updated.

- TunnelGrant — Everything the caller needs to host the tunnel connection. **Treat the whole object as a credential**: `url` embeds the plaintext `?k=` bearer secret and `connectToken` authenticates the relay WebSocket.
  - `serverId` string, required — The registered project server's ID.
  - `name` string, required
  - `existed` boolean, required — True when a server record with this name already existed (its config now points at the tunnel).
  - `previousUrl` string — The record's previous URL, present when it existed with a different one (it has been overwritten).
  - `previousTransportType` string — The record's previous transport, present when it existed (a `stdio` record has been converted to `http`).
  - `slug` string, required — Stable public subdomain slug; survives re-creates for the same server.
  - `url` string, required — Public tunnel URL including the `?k=` bearer secret. Also persisted on the server record's `url`.
  - `connectToken` string, required — Bearer for the relay edge WebSocket handshake.
  - `connectTokenExpiresAt` number — Epoch-ms expiry of `connectToken`.
  - `relayWsUrl` string, required — Relay edge WebSocket endpoint to connect to.
  - `secretVersion` number — Monotonic secret version; each create bumps it.

## Other responses

- `400` — Malformed body or parameters.
- `401` — Missing, invalid, revoked, or orphaned key (`UNAUTHORIZED`) — or the **target MCP server** needs an OAuth grant (`OAUTH_REQUIRED`), which is a property of the server, not your key.
- `403` — Key is valid but not allowed to do this.
- `404` — Unknown project, server, or resource.
- `429` — Per-key rate limit exceeded (60 requests/minute sustained, bursts up to 10). Honor `Retry-After` and back off with jitter.
- `500` — Something failed on MCPJam's side.
- `502` — Could not connect to the target MCP server.

---

[API](https://skmtc.net/mcpjam/apis/mcpjam-api.md) · [All operations](https://skmtc.net/mcpjam/apis/mcpjam-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/mcpjam/mcpjam-api/versions/b767df188d0c/schema)
