Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
e82aa67
feat: add cloud gateway account management
jeremymcs Jun 2, 2026
f064ea6
test(cloud-gateway): use scanner-safe auth fixtures
jeremymcs Jun 2, 2026
beae6a3
fix(mcp-server): avoid leaking HTTP errors
jeremymcs Jun 2, 2026
1ad3628
fix(cloud-gateway): declare clerk backend root dependency
jeremymcs Jun 2, 2026
d5b4d2a
ci: rerun swift codeql
jeremymcs Jun 2, 2026
4f2fe9e
fix(security): timing-safe auth token comparison and readLimit fracti…
jeremymcs Jun 2, 2026
29f6ba7
fix: resolve cloud gateway rebase test conflict
jeremymcs Jul 2, 2026
da539f6
Apply PatchDeck fixes for PR #402
jeremymcs Jul 7, 2026
84ebe9a
fix(cloud-gateway): harden shutdown, tool results, and usage persistence
jeremymcs Jul 25, 2026
2756b27
fix(cloud-gateway): harden token add, /mcp routing, auth trimming, us…
jeremymcs Jul 25, 2026
8da04c5
fix(cloud-gateway): resolve sharp types build and harden gateway inte…
jeremymcs Jul 25, 2026
8780556
fix(cloud-gateway): escape U+2028/U+2029 in account UI config script
jeremymcs Jul 25, 2026
b24766a
fix(cloud-gateway): close per-request MCP server on abort and guard s…
jeremymcs Jul 25, 2026
675fdbe
fix(cloud-gateway): return copies from auth/usage store snapshots
jeremymcs Jul 25, 2026
a4d12f6
fix(daemon): keep unknown-tool executor test from spawning a real MCP…
jeremymcs Jul 25, 2026
0f213a0
Merge origin/main; repair duplicated postcss@8.5.23 key in pnpm-lock.…
jeremymcs Jul 25, 2026
afe31c0
fix(daemon): guard parseArgsValue against malformed JSON array env va…
jeremymcs Jul 25, 2026
e3117aa
fix(cloud-gateway): return 400 on malformed path encoding and swallow…
jeremymcs Jul 25, 2026
8b35728
fix(cloud-gateway): reserve billable quota, rate-limit unknown tools,…
jeremymcs Jul 25, 2026
2a9bfb4
fix(cloud-gateway): index token auth lookups and accept trailing slas…
jeremymcs Jul 25, 2026
6bd270b
fix(cloud-gateway): report day/month usage and reject non-object JSON…
jeremymcs Jul 25, 2026
4251970
fix(cloud-gateway): reject server startup on listen errors
jeremymcs Jul 25, 2026
b5760e2
fix(cloud-gateway): surface minute reset time on accepted quota status
jeremymcs Jul 25, 2026
db09626
fix(cloud-gateway): broaden loopback detection and use sessionStorage…
jeremymcs Jul 25, 2026
fb5fc0e
fix(cloud-gateway): read abort signal optionally in tool handler
jeremymcs Jul 25, 2026
d381aea
fix(cloud-gateway): clamp fractional quota overrides to at least 1
jeremymcs Jul 25, 2026
c503c7e
Merge remote-tracking branch 'origin/main' into codex/cloud-mcp-gatew…
jeremymcs Jul 26, 2026
8cd0cf2
fix(build): drop obsolete sharp paths override so build:core passes
jeremymcs Jul 26, 2026
f01803a
fix(cloud-gateway): make debounced store persists non-fatal
jeremymcs Jul 26, 2026
c5b2830
fix: guard usage reservation release across window rollover and wire …
jeremymcs Jul 26, 2026
3988666
fix(mcp-server): bracket IPv6 hosts in the HTTP server URL
jeremymcs Jul 26, 2026
09d5f9c
chore(security): allowlist dummy HTTP CLI test tokens in secret scan
jeremymcs Jul 26, 2026
68d02f6
fix: skip runtime tool lookup for built-ins and validate CLI --port i…
jeremymcs Jul 26, 2026
30ffc96
fix(cloud-gateway): throttle all calls but reserve day/month quota on…
jeremymcs Jul 26, 2026
02cdc23
fix(daemon): merge external MCP server env over default environment t…
jeremymcs Jul 26, 2026
a876839
fix(cloud-gateway): keep admin token in memory unless remember opt-in
jeremymcs Jul 26, 2026
0246b58
fix(daemon): always close external MCP transport to avoid leaking std…
jeremymcs Jul 26, 2026
f028f59
fix(cloud-gateway): trim bearer tokens and handle CLI parse errors cl…
jeremymcs Jul 26, 2026
42289bf
fix(cloud-gateway): require exact segment count on parameterized admi…
jeremymcs Jul 26, 2026
6b37003
fix(cloud-gateway): boot account UI after DOMContentLoaded so deferre…
jeremymcs Jul 26, 2026
761b370
fix(mcp-server): reject a following flag as a CLI value instead of co…
jeremymcs Jul 26, 2026
d4125e0
fix(cloud-gateway): reject tokens for disabled users and clarify Cler…
jeremymcs Jul 26, 2026
51d9da4
fix(daemon): rate-limit external MCP route refresh on unknown tool names
jeremymcs Jul 26, 2026
819bb75
fix(cloud-gateway): skip minute tracking when no minute limit applies
jeremymcs Jul 26, 2026
db13f15
fix(cloud-gateway): catch boot rejection and normalize parseBoolean i…
jeremymcs Jul 26, 2026
9dd1ac6
fix: strip brackets from IPv6 listen host and deep-copy quotaOverride…
jeremymcs Jul 26, 2026
8930bef
fix(cloud-gateway): harden normalizeInputSchema against malformed pro…
jeremymcs Jul 26, 2026
5dc62a7
fix(daemon): trim explicit MCP command and close in-flight connection…
jeremymcs Jul 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 179 additions & 19 deletions docs/user-docs/cloud-mcp-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,118 @@

The Cloud MCP Gateway lets a hosted MCP client call GSD workflow tools through a local runtime. Use it when the MCP client cannot reach your workstation directly, but your workstation can open an outbound WebSocket connection to a gateway.

The reader for this guide is an operator setting up a gateway and a local runtime. After reading it, they should be able to start the gateway, pair one runtime, connect it, and confirm that MCP tool calls can reach local projects.
The reader for this guide is an operator setting up a gateway, accounts, usage limits, and one or more local runtimes. After reading it, they should be able to start the gateway, issue MCP tokens, pair a runtime, connect it, and confirm that remote MCP tool calls can reach local projects and runtime-advertised MCP tools.

## Architecture

The gateway exposes two HTTP surfaces:
The gateway exposes these HTTP surfaces:

- An authenticated MCP endpoint for remote MCP clients.
- A pairing endpoint that issues one-time runtime device tokens.
- `/mcp`: authenticated Streamable HTTP MCP endpoint for remote MCP clients.
- `/runtime/connect`: outbound WebSocket target for paired local runtimes.
- `/pairing-codes` and `/pairing/exchange`: pairing-code issuance and exchange for local runtime device tokens.
- `/admin`: operator UI for user management, pairing codes, connected runtimes, and usage.
- `/account`: optional Clerk-backed self-service account UI for end users.
- `/register`: optional public self-registration endpoint when explicitly enabled.

The local runtime runs under `gsd-daemon`. After pairing, it stores the gateway URL, runtime ID, and device token in the daemon config. When `gsd-daemon cloud connect` starts, it connects back to the gateway with the device token, advertises local projects, and forwards tool calls to the local GSD runtime.
The gateway is a routing layer. It does not host workspaces, clone source code, store `.gsd` artifacts, or run GSD workflows itself. The local runtime runs under `gsd-daemon cloud` or the `gsd-mcp-runtime` alias. After pairing, it stores the gateway URL, runtime ID, and encrypted device token in the daemon config. When it connects, it advertises local projects and optional local MCP tools, then forwards tool calls to the local GSD runtime.

## Gateway Requirements

Run the gateway with Node 22 or newer. The gateway listens on port `8787` by default.

Set `GSD_CLOUD_USER_TOKEN` before starting the gateway. Remote MCP clients and pairing-code requests use this value as a bearer token.
`GSD_CLOUD_USER_TOKEN` is required at startup. It seeds the initial gateway user as an `admin` user with the `unlimited` plan. Use a long random value and treat it as a secret.

For production, also configure persistent auth and usage stores. Without these paths, users, tokens, pairing codes, and usage counters are in memory only.

```bash
export GSD_CLOUD_USER_TOKEN="$(openssl rand -hex 32)"
export GSD_CLOUD_ADMIN_TOKEN="$(openssl rand -hex 32)"

gsd-cloud-mcp-gateway \
--port 8787 \
--auth-store /secure/path/gsd-cloud-auth.json \
--usage-store /secure/path/gsd-cloud-usage.json
```

The process prints the listen URL and admin UI URL on startup. In local development, the default URL is `http://localhost:8787`. In production, put TLS and any public routing in front of the gateway, then give clients the public HTTPS URL.

Equivalent environment variables are available for persistent stores:

```bash
export GSD_CLOUD_AUTH_STORE_PATH=/secure/path/gsd-cloud-auth.json
export GSD_CLOUD_USAGE_STORE_PATH=/secure/path/gsd-cloud-usage.json
```

The auth store persists users, user tokens, device tokens, and pairing codes as salted scrypt-derived hashes. Raw bearer tokens and device tokens are not written to disk.

## Admin Access

Open `/admin` on the gateway and enter a bearer token.

When `GSD_CLOUD_ADMIN_TOKEN` is set, `/admin/api/*` accepts only that dedicated operator token. When it is not set, `/admin/api/*` accepts bearer tokens for gateway users with the `admin` role. The startup seed user created from `GSD_CLOUD_USER_TOKEN` is an admin.

The admin UI lets an operator:

- create `member` and `admin` users
- assign `free`, `paid`, or `unlimited` plans
- set per-user quota overrides
- issue user bearer tokens and pairing codes
- revoke user tokens
- disable or re-enable users
- view connected runtimes and their advertised projects/tools
- inspect aggregate MCP usage and recent tool calls

Use admin-issued user bearer tokens for `/mcp` clients and pairing-code creation. Do not expose `GSD_CLOUD_ADMIN_TOKEN` to MCP clients.

## Register Users

Public self-registration is disabled by default. To allow anonymous `POST /register` calls that create `member` users on the `free` plan and return a bearer token that is shown once, start the gateway with:

```bash
gsd-cloud-mcp-gateway --allow-registration
# or
GSD_CLOUD_ALLOW_REGISTRATION=1 gsd-cloud-mcp-gateway
```

Registration requires an email in the request body:

```bash
curl -sS -X POST "https://gateway.example.com/register" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","name":"Example User"}'
```

Use the returned `userToken` as the bearer token for `/mcp` and `/pairing-codes`.

## Clerk Account UI

For public sign-up and sign-in, enable Clerk and send users to `/account`. Clerk authenticates the human user. The gateway still creates local gateway users and manages MCP bearer tokens, pairing codes, plans, quota overrides, and usage locally.

```bash
export GSD_CLOUD_USER_TOKEN="replace-with-a-long-random-token"
gsd-cloud-mcp-gateway --port 8787
export CLERK_SECRET_KEY=sk_live_...
export CLERK_PUBLISHABLE_KEY=pk_live_...
# Optional: networkless JWT verification.
export CLERK_JWT_KEY='-----BEGIN PUBLIC KEY-----...'

gsd-cloud-mcp-gateway \
--auth-store /secure/path/gsd-cloud-auth.json \
--usage-store /secure/path/gsd-cloud-usage.json
```

The process prints the listen URL on startup. In local development, the default URL is `http://localhost:8787`. In production, put TLS and any public routing in front of the gateway, then give clients the public HTTPS URL.
If `CLERK_FRONTEND_API_URL` is not set, the gateway derives the ClerkJS script origin from `CLERK_PUBLISHABLE_KEY`.

The `/account` page loads ClerkJS. Signed-in users can:

- create MCP bearer tokens
- revoke their own MCP bearer tokens
- create local runtime pairing codes
- view their plan, billable usage, throttled attempts, and quota status

On first authenticated Clerk access, the gateway creates a local `free` user linked by `clerkUserId`. MCP token verification remains local, so normal tool calls do not require a Clerk round trip.

## Pair a Local Runtime

First create a pairing code with the user token:
Create a pairing code with a gateway user token. This can be the seeded `GSD_CLOUD_USER_TOKEN`, an admin-created user token, a self-registration token, or a token created from `/account`.

```bash
curl -sS -X POST "https://gateway.example.com/pairing-codes" \
Expand All @@ -44,7 +129,16 @@ gsd-daemon cloud pair \
--runtime-name "Laptop"
```

Pairing saves the cloud runtime fields in the daemon config and enables cloud runtime mode. The stored device token is secret. Use the status command when you need to inspect the config safely:
You can use the standalone alias for the same runtime flow:

```bash
gsd-mcp-runtime pair \
--gateway "https://gateway.example.com" \
--code "PAIRING_CODE" \
--runtime-name "Laptop"
```

Pairing saves the cloud runtime fields in the daemon config and enables cloud runtime mode. The stored device token is secret and is written encrypted in current configs. Use the status command when you need to inspect the config safely:

```bash
gsd-daemon cloud status
Expand All @@ -66,25 +160,91 @@ gsd-daemon cloud connect --verbose

The runtime connects to `/runtime/connect` on the gateway with the saved device token. HTTPS gateway URLs become secure WebSocket URLs automatically. If the connection drops, the runtime retries periodically.

The runtime advertises projects discovered by the daemon. Remote MCP callers can list the advertised projects with `gsd_cloud_projects`, then pass `projectAlias` or `runtimeId` when calling a forwarded GSD tool.
The runtime advertises projects discovered by the daemon. Remote MCP callers can list the advertised projects with `gsd_cloud_projects`, then pass `projectAlias` or `runtimeId` when calling a forwarded GSD tool. When more than one runtime is connected for the same user, callers must provide `runtimeId` or an unambiguous `projectAlias`.

## Runtime-Advertised MCP Tools

The gateway always lists `gsd_cloud_projects`, GSD session tools, and GSD workflow tools on `/mcp`. It also includes MCP tools advertised by connected local runtimes.

By default, the local runtime tries to advertise `gsd-browser mcp` when `gsd-browser` is available on `PATH`:

```bash
npm install -g @opengsd/gsd-browser
gsd-daemon cloud connect --verbose
```

Configure the browser MCP command explicitly:

```bash
export GSD_CLOUD_BROWSER_MCP_COMMAND=gsd-browser
export GSD_CLOUD_BROWSER_MCP_ARGS=mcp
gsd-daemon cloud connect --verbose
```

Disable browser MCP advertisement:

```bash
export GSD_CLOUD_BROWSER_MCP=0
gsd-daemon cloud connect
```

Advertise additional stdio MCP servers from the same runtime with `GSD_CLOUD_MCP_SERVERS`:

```bash
export GSD_CLOUD_MCP_SERVERS='[
{ "id": "gsd-browser", "command": "gsd-browser", "args": ["mcp"] }
]'
gsd-daemon cloud connect --verbose
```

Runtime-advertised tools are merged into the `/mcp` `tools/list` response. The gateway adds routing fields where needed and forwards calls to the connected runtime that advertised the project or matches the requested `runtimeId`.

## Configure a Remote MCP Client

Point the client at the gateway MCP endpoint and pass the user token as a bearer token:
Point the client at the gateway MCP endpoint and pass a gateway user token as a bearer token:

```text
URL: https://gateway.example.com/mcp
Authorization: Bearer <GSD_CLOUD_USER_TOKEN>
Authorization: Bearer <GATEWAY_USER_TOKEN>
```

The gateway forwards GSD session tools, GSD workflow tools, and runtime-advertised MCP tools to an online local runtime owned by the authenticated user.

## Usage Store and Quotas

The gateway records every `/mcp` `tools/call`, including forwarded GSD tools and runtime-advertised MCP tools. Usage records include user ID, tool name, optional runtime/project routing fields, status, duration, billable status, throttle status, and timestamp.

Accepted MCP tool calls are billable and count toward user quotas. Throttled attempts are recorded as non-billable so a retry loop cannot keep increasing a user's quota counter after enforcement starts.

Default plan limits are:

- `free`: 12 calls/minute, 100 billable calls/day, 1,000 billable calls/month
- `paid`: 60 calls/minute, 2,000 billable calls/day, 50,000 billable calls/month
- `unlimited`: no quota checks

Override plan defaults with environment variables. Set a value to `0` to make that dimension unlimited.

```bash
export GSD_CLOUD_FREE_CALLS_PER_MINUTE=12
export GSD_CLOUD_FREE_CALLS_PER_DAY=100
export GSD_CLOUD_FREE_CALLS_PER_MONTH=1000

export GSD_CLOUD_PAID_CALLS_PER_MINUTE=60
export GSD_CLOUD_PAID_CALLS_PER_DAY=2000
export GSD_CLOUD_PAID_CALLS_PER_MONTH=50000
```

The gateway forwards GSD session tools and workflow tools to an online local runtime. When multiple runtimes are connected, provide `runtimeId` or `projectAlias` so the gateway can route the call.
When a user exceeds quota, `/mcp` returns a tool error such as `Usage limit exceeded`, the runtime tool call is not forwarded, and the denied attempt appears in the admin usage view as `Throttled`.

## Failure Expectations

- `401 Unauthorized`: the user token or device token is missing, invalid, or revoked.
- `400 Pairing code is invalid or expired`: the code was mistyped, already used, or expired.
- `No Local GSD Runtime is connected`: the gateway is running, but no paired runtime is online.
- `401 Unauthorized`: the user token, admin token, or device token is missing, invalid, revoked, or disabled.
- `403 Registration is disabled`: anonymous `POST /register` was attempted without registration enabled.
- `503 Clerk authentication is not configured`: `/account/api/*` was called without Clerk environment variables.
- `400 Pairing code is invalid or expired`: the code was mistyped, already used, superseded, or expired.
- `No Local GSD Runtime is connected`: the gateway is running, but no paired runtime is online for the authenticated user.
- `runtimeId or projectAlias is required`: more than one runtime is online and the call did not identify a target.
- `Usage limit exceeded`: the user's minute, daily, or monthly quota denied the tool call before forwarding.
- Tool call timeout: the runtime accepted the call but did not answer before the gateway timeout.

Treat user tokens and device tokens like passwords. Do not commit them to project files or paste them into issue trackers.
Treat user tokens, admin tokens, and device tokens like passwords. Do not commit them to project files or paste them into issue trackers.
12 changes: 9 additions & 3 deletions docs/user-docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -549,17 +549,23 @@ For an auto-mode run, call `gsd_execute` first with an absolute `projectDir`. It

## Cloud MCP Gateway Runtime

`gsd-cloud-mcp-gateway` starts an HTTP gateway for remote MCP clients. `gsd-daemon cloud` pairs and connects a local runtime to that gateway.
`gsd-cloud-mcp-gateway` starts an HTTP gateway for remote MCP clients. `gsd-daemon cloud` pairs and connects a local runtime to that gateway. The gateway exposes `/mcp` for MCP clients, `/admin` for operators, and optional `/account` self-service accounts when Clerk is configured.

```bash
GSD_CLOUD_USER_TOKEN="replace-with-a-long-random-token" gsd-cloud-mcp-gateway --port 8787
GSD_CLOUD_USER_TOKEN="$(openssl rand -hex 32)" \
GSD_CLOUD_ADMIN_TOKEN="$(openssl rand -hex 32)" \
gsd-cloud-mcp-gateway \
--port 8787 \
--auth-store /secure/path/gsd-cloud-auth.json \
--usage-store /secure/path/gsd-cloud-usage.json

gsd-daemon cloud status
gsd-daemon cloud pair --gateway "https://gateway.example.com" --code "PAIRING_CODE" --runtime-name "Laptop"
gsd-daemon cloud connect --verbose
gsd-daemon cloud disconnect
```

See [Cloud MCP Gateway](./cloud-mcp-gateway.md) for the full operator setup flow, token model, ports, and failure modes.
See [Cloud MCP Gateway](./cloud-mcp-gateway.md) for the full operator setup flow, token model, account registration, usage quotas, runtime-advertised MCP tools, ports, and failure modes.

## In-Session Update

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@
"@anthropic-ai/vertex-sdk": "^0.14.4",
"@aws-sdk/client-bedrock-runtime": "^3.983.0",
"@clack/prompts": "^1.1.0",
"@clerk/backend": "^3.4.14",
"@google/genai": "^1.40.0",
"@mariozechner/jiti": "^2.6.2",
"@mistralai/mistralai": "2.2.1",
Expand Down
Loading
Loading