Skip to content

mcpjam tunnel: expose local MCP servers through the relay from the CLI#2638

Merged
chelojimenez merged 14 commits into
mainfrom
claude/optimistic-hamilton-u7s4de
Jun 12, 2026
Merged

mcpjam tunnel: expose local MCP servers through the relay from the CLI#2638
chelojimenez merged 14 commits into
mainfrom
claude/optimistic-hamilton-u7s4de

Conversation

@chelojimenez

@chelojimenez chelojimenez commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

MCPJam Tunnel — CLI MVP

Adds mcpjam tunnel so a developer can expose a local MCP server (HTTP URL or stdio command — parity with the web UI) and get a public relay URL, with the tunnel registered as a first-class servers record in their MCPJam project so evals and chatboxes can target it.

mcpjam tunnel http://localhost:9090/mcp --id my-server --project acme
mcpjam tunnel --id everything --project acme -- npx -y @modelcontextprotocol/server-everything

Built entirely on the existing Railway relay: zero tunnel-edge changes, zero mcpjam-backend changes (contracts verified against the backend repo before implementation). When the CLI exits, the server record stays and calls fail fast at the edge; re-running revives the same slug with a fresh secret.

What's in here

Hosted inspector v1 surface

  • POST /v1/projects/:projectId/tunnels — creates the named server record if missing, mints a relay grant (every mint rotates the secret and revokes the previous grant at the edge), and persists the bearer URL onto the record — converting name-colliding stdio records to transportType: "http" so the platform actually uses the URL. existed/previousUrl/previousTransportType are derived from a pre-read so the CLI can warn before clobbering hand-configured servers (same-tunnel re-runs where only the ?k= secret rotated don't warn). The response whitelists fields — the upstream grant's secret/secretHash never pass through.
  • POST /v1/projects/:projectId/tunnels/:serverId/close — revokes the grant; never touches the server record.
  • Grant plumbing extracted from routes/mcp/tunnels.ts into services/tunnel-grants.ts, shared by both surfaces (web route behavior unchanged).

SDKcreateTunnel/closeTunnel client methods plus create_tunnel/close_tunnel operations (non-read, shared project resolution). Exported and packaging-asserted, but deliberately not added to the MCP tool surfaces.

CLI — three pieces per session:

  • lib/tunnel/relay-client.ts: CLI-local port of the hosted relay client (third hand-mirrored copy of tunnel-edge/src/protocol.ts; constants/close codes byte-aligned, banner names all copies).
  • lib/tunnel/local-bridge.ts: 127.0.0.1 bridge serving only the tunnel's path scope. HTTP targets get a streaming reverse proxy (prefix stripped, only ?k= dropped, SSE unbuffered). stdio targets get one persistent MCPClientManager child behind a stateless streamable-HTTP facade — initialize answered from the real handshake, notifications → 202 (required by TS-SDK clients), everything else passed through verbatim, GET → 405.
  • lib/tunnel/tunnel-session.ts: close-code policy. 4000 re-creates the grant (the create route is the rotation path) with a rapid-loop cap; 4001/4002 exit 1 (takeover usually surfaces as 4002 since mints disconnect at the edge before the new socket registers); first Ctrl-C closes the bridge and best-effort revokes the grant, second force-quits.

⚠️ Changed invariant (deliberate MVP trade-off)

The tunnel bearer URL — plaintext ?k= secret included — is persisted to servers.url. Until now the backend stored only the secret hash and the hosted inspector's memory was the single holder of the plaintext URL. This is what lets evals/chatboxes target the tunnel, and it's the same trust level as user-pasted auth headers, but it is a changed invariant. Mitigations: every create rotates the secret and revokes the old grant; close revokes on demand. Documented in the OpenAPI description; follow-up should move this to the secrets path.

Testing

  • New: 12 v1 route tests (call order, response whitelist, existed/previous derivation, close never mutates the record), SDK client/operation tests, 30 CLI tests (target parsing, bridge path-scope/k-stripping/SSE-streaming/502, stdio facade incl. notifications→202, session close-code policy/remint cap/Ctrl-C).
  • Suites: SDK 1456 ✅, CLI 273 ✅, packaging + runtime-safety guards ✅, root aggregate typecheck ✅. Full inspector suite: 5788 pass; the 22 failures are pre-existing/environmental (browser_unavailable in computer-use/harness tests; useLocation Router errors in App.hosted-oauth.test.tsx from the Home-landing change at base) — no client code is touched here.

Needs live credentials (not runnable in the dev container)

  • Tunnel an HTTP server and a stdio server against app.mcpjam.com; curl initialize + tools/list through the public URL
  • Both credential kinds: sk_ API key (delegated JWT) and mcpjam login (OAuth)
  • Server appears in the project UI; eval + chatbox can target it
  • Ctrl-C mid-flight → next eval fails fast, record persists; re-run → same slug, new secret, record URL updated
  • Bonus: paste the tunnel URL into a ChatGPT/Claude connector

Non-goals (MVP)

SSE/notification passthrough for stdio targets; legacy-SSE HTTP targets (edge path-scopes to /api/mcp/adapter-http/<serverId>, and endpoint events inside response bodies can't be rewritten); status webhooks; secret-less records; multi-tunnel invocations; stdio child auto-restart; exposing tunnel ops on MCP tool surfaces.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF


Generated by Claude Code


Note

High Risk
Persists plaintext tunnel secrets on server records, overwrites/conflicts with existing server configs, and introduces long-lived relay/WebSocket infrastructure with complex failure/remint semantics—security- and data-handling sensitive despite mitigations (rotation, revoke on close).

Overview
Adds end-to-end MCPJam relay tunnels so developers can expose a local MCP server (HTTP URL or stdio command) through a public URL and have it registered as a project server for evals and chatboxes.

The SDK gains PlatformApiClient.createTunnel / closeTunnel and create_tunnel / close_tunnel platform operations. The CLI adds mcpjam tunnel with a local bridge (HTTP reverse proxy or stdio streamable-HTTP facade), a WebSocket relay client, and session orchestration (grant mint/revoke, remint on expired tokens, graceful Ctrl-C). The hosted inspector adds v1 POST .../tunnels and POST .../tunnels/:serverId/close, refactors grant HTTP into shared tunnel-grants.ts, and persists the tunnel bearer URL (including plaintext ?k=) on servers.url—a deliberate MVP trade-off so the platform can call the tunnel like any remote server. OpenAPI and CLI docs are updated; extensive tests cover v1 routes, bridge behavior, and session policy.

Reviewed by Cursor Bugbot for commit 49868fa. Bugbot is set up for automated code reviews on this repo. Configure here.

claude added 5 commits June 12, 2026 08:43
…servers

POST /v1/projects/:projectId/tunnels creates the named server record if
missing, mints a relay grant (every mint rotates the secret and revokes
the previous grant at the edge), and persists the bearer URL onto the
record — converting stdio records to http — so evals/chatboxes can
target the tunnel. existed/previousUrl/previousTransportType are derived
from a pre-read so callers can warn before clobbering hand-configured
servers. The close route revokes the grant and never touches the record.

Grant plumbing (validateGrant/fetchRelayGrant/close) is extracted from
routes/mcp/tunnels.ts into services/tunnel-grants.ts, shared by both
surfaces; web route behavior is unchanged.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
PlatformTunnelGrant carries the whole connection credential (bearer URL
with ?k=, connectToken, relayWsUrl) plus existed/previous* so surfaces
can warn on overwrites. create_tunnel/close_tunnel are non-read
operations reusing the shared project resolution; they are exported (and
packaging-asserted) but intentionally NOT added to the MCP tool surfaces.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
…m tunnel

mcpjam tunnel http://localhost:9090/mcp --id my-server [--project ref]
mcpjam tunnel --id everything [--project ref] -- npx -y @modelcontextprotocol/server-everything

Three pieces per session:
- relay-client.ts: CLI-local port of the hosted relay client (third
  hand-mirrored copy of tunnel-edge/src/protocol.ts; constants and close
  codes byte-aligned, logger injected).
- local-bridge.ts: 127.0.0.1 bridge serving only the tunnel's path scope.
  HTTP targets get a streaming reverse proxy (prefix stripped, only ?k=
  dropped, SSE unbuffered); stdio targets get one persistent
  MCPClientManager child behind a stateless streamable-HTTP facade
  (initialize answered from the real handshake, notifications -> 202,
  everything else passed through verbatim, GET -> 405).
- tunnel-session.ts: close-code policy. 4000 re-creates the grant (the
  create route is the rotation path) with a rapid-loop cap; 4001/4002
  exit 1 (takeover usually lands as 4002 since mints disconnect at the
  edge); first Ctrl-C closes the bridge and best-effort revokes the
  grant, second force-quits. The server record intentionally outlives
  the session: callers fail fast at the edge until the tunnel is re-run.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
Including the deliberate MVP trade-off: the tunnel bearer URL (plaintext
?k= secret) is persisted on the server record, mitigated by rotation on
every create and revocation on close.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
Re-running a tunnel reuses the slug, so the record's previous URL
differs only by the rotated ?k= secret — the normal rotation path, not
an overwrite the CLI should warn about. Compare endpoints ignoring the
query before including previousUrl.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
@chelojimenez

chelojimenez commented Jun 12, 2026

Copy link
Copy Markdown
Contributor Author

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@mintlify

mintlify Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
mcpjam 🟢 Ready View Preview Jun 12, 2026, 8:54 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@github-actions

github-actions Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL: https://mcp-inspector-pr-2638.up.railway.app
Deployed commit: 5644ae2
PR head commit: 49868fa
Backend target: staging fallback.
Health: ✅ Convex reachable
Access is employee-only in non-production environments.

@chelojimenez
chelojimenez marked this pull request as ready for review June 12, 2026 09:08
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request labels Jun 12, 2026
Comment thread cli/src/lib/tunnel/tunnel-session.ts
Comment thread cli/src/lib/tunnel/tunnel-session.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 52787a696f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +2 to +5
import {
closeTunnelOperation,
createTunnelOperation,
type CreateTunnelResult,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin the SDK version that contains the tunnel exports

This new runtime import depends on createTunnelOperation/closeTunnelOperation, but the CLI package still declares @mcpjam/sdk as ^1.16.0 while the pre-change 1.16.0 SDK did not export these symbols. In a published/non-workspace install, npm can satisfy the range with the already-published SDK and the CLI will fail during module initialization before any command runs. Please bump/pin the SDK dependency to a version that includes these exports, or bundle the dependency.

Useful? React with 👍 / 👎.

Comment thread cli/src/lib/tunnel/tunnel-session.ts Outdated
Comment on lines +148 to +150
await withTimeout(
this.deps.closeGrant(this.grantResult),
CLOSE_GRANT_TIMEOUT_MS,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Abort the revoke request when the grace period expires

When the close API is slow or unreachable, this races closeGrant against a 5s timer but leaves the underlying PlatformApiClient fetch and its timeout running. After logging that revocation timed out, the pending request/timer can still keep the Node process alive until the global API timeout (30s by default), so the first Ctrl-C path does not actually finish after the advertised grace period. Pass an abort signal into the close operation or otherwise cancel the request when this timeout wins.

Useful? React with 👍 / 👎.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR adds end-to-end tunnel support: a relay WebSocket client and binary framing, a local bridge (HTTP proxy + stdio JSON‑RPC facade), a TunnelSession orchestrator (grant mint/rotation, remint policy, shutdown), CLI command wiring and output modes, inspector backend grant services and v1 routes, SDK platform client and operations, OpenAPI updates, and comprehensive tests covering protocol, proxying, session lifecycle, races, and routes.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/src/lib/tunnel/local-bridge.ts`:
- Around line 191-200: In handleFacadeJsonRpc, don't treat missing or non-string
method values as notifications; validate that method is a string and only
short-circuit to 202 for explicit notification methods that start with
"notifications/". Replace the current if (!method ||
method.startsWith("notifications/")) branch with a check that: if typeof method
!== "string" then return an invalid-request JSON-RPC error envelope (code
-32600) tied to the incoming id (or null if id is absent), otherwise if
method.startsWith("notifications/") return null (no response). Use the existing
id, method, params variables and produce the proper error structure instead of
silently accepting malformed envelopes.

In `@cli/src/lib/tunnel/tunnel-session.ts`:
- Around line 123-133: On startup failure paths where a grant has already been
created (the block that calls this.deps.createGrant and assigns
this.grantResult), perform a best-effort revoke of that minted grant before
rethrowing: if this.grantResult exists, call and await a revoke method on the
deps (e.g. this.deps.revokeGrant(this.grantResult) or
this.deps.revokeGrant?.(this.grantResult)) inside a try/catch that swallows/logs
any revoke error, then proceed to cleanup and rethrow the original error; apply
the same change to the similar failure block around the second occurrence (lines
216-231) so both paths revoke the latest grant on failure.

In `@cli/tests/tunnel-bridge.test.ts`:
- Around line 100-102: The finally blocks currently call server.close() and
return immediately, causing races; change each to await the server shutdown by
replacing plain server.close() with an awaited close promise—e.g. await new
Promise(resolve => server.close(resolve)) or await events.once(server,
'close')—so the test waits for the 'close' event (apply this change for the
server.close() calls in the finally blocks and mirror the pattern used in the
unreachable-upstream case).

In `@cli/tests/tunnel-session.test.ts`:
- Around line 31-40: The test helper's connect() currently just throws when
behavior.failOnConnect is set, so it never invokes the handshake-race callback;
update connect() (or immediately after throwing) to call the onPermanentFailure
callback with the same reason and a close code (e.g. 1002) to simulate the
permanent-close race path; specifically modify the Mock/Stub class's
constructor/ connect() logic (referencing connect(), onPermanentFailure,
behavior.failOnConnect, and permanentFailure) to invoke onPermanentFailure
(synchronously or via setImmediate/nextTick to model a race) when failOnConnect
is configured so the test actually covers the handshake-race branch.

In `@docs/reference/openapi.json`:
- Around line 2088-2138: The TunnelGrant schema currently omits
connectTokenExpiresAt and secretVersion from its required list; update the
OpenAPI schema so both "connectTokenExpiresAt" and "secretVersion" are included
in the "required" array for the TunnelGrant object (leave their property
definitions unchanged) so generated clients and validations treat them as
mandatory fields.

In `@mcpjam-inspector/server/services/tunnel-grants.ts`:
- Around line 75-82: The error-handling path currently calls response.json()
unguarded which will throw on non-JSON bodies; wrap the JSON parsing in a
try/catch and if parsing fails fall back to response.text() (or a default
message) and include response.status/response.statusText in the thrown Error so
callers see a meaningful tunnel failure; apply the same robust parsing to both
places around validateGrant (the response.json() before validateGrant) and the
similar block (lines 102-109) — mirror the safer approach used in
closeTunnelGrant to avoid SyntaxError leaks.
- Around line 65-73: Multiple Convex HTTP calls (fetchRelayGrant,
fetchRotationGrant, reportTunnelClosure, closeTunnelGrant, fetchProjectServers)
can hang forever; add a shared timeout wrapper function (e.g.,
convexFetchWithTimeout or withTimeoutFetch) in tunnel-grants.ts that uses
AbortController and a configurable TIMEOUT_MS constant, starts a timer that
aborts the fetch and clears the timer on completion, and throws a clear timeout
error; replace direct fetch(...) calls in the listed functions to call this
wrapper (preserving URL, options, method, headers and returning the same
response semantics) and ensure the timer is cleaned up to avoid leaks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: acedf726-c383-417e-b737-bc04043a4b19

📥 Commits

Reviewing files that changed from the base of the PR and between 3e37cd6 and 52787a6.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (25)
  • cli/package.json
  • cli/src/commands/tunnel.ts
  • cli/src/index.ts
  • cli/src/lib/tunnel/local-bridge.ts
  • cli/src/lib/tunnel/relay-client.ts
  • cli/src/lib/tunnel/tunnel-session.ts
  • cli/tests/tunnel-bridge.test.ts
  • cli/tests/tunnel-command.test.ts
  • cli/tests/tunnel-session.test.ts
  • docs/cli/overview.mdx
  • docs/cli/reference.mdx
  • docs/reference/openapi.json
  • mcpjam-inspector/server/routes/mcp/tunnels.ts
  • mcpjam-inspector/server/routes/v1/__tests__/tunnels.test.ts
  • mcpjam-inspector/server/routes/v1/index.ts
  • mcpjam-inspector/server/routes/v1/tunnels.ts
  • mcpjam-inspector/server/services/relay-client.ts
  • mcpjam-inspector/server/services/tunnel-grants.ts
  • sdk/package.json
  • sdk/src/platform/client.ts
  • sdk/src/platform/index.ts
  • sdk/src/platform/operations.ts
  • sdk/src/platform/types.ts
  • sdk/tests/platform/client.test.ts
  • sdk/tests/platform/operations.test.ts

Comment thread cli/src/lib/tunnel/local-bridge.ts Outdated
Comment thread cli/src/lib/tunnel/tunnel-session.ts
Comment thread cli/tests/tunnel-bridge.test.ts
Comment thread cli/tests/tunnel-session.test.ts
Comment thread docs/reference/openapi.json
Comment thread mcpjam-inspector/server/services/tunnel-grants.ts Outdated
Comment thread mcpjam-inspector/server/services/tunnel-grants.ts
claude added 2 commits June 12, 2026 09:25
- Permanent closes that race a dial are stashed and consumed after the
  handshake (or flushed once live) instead of being dropped, so a dead
  relay can never be reported as up regardless of event ordering.
- Remints are single-flight: a second 4000 while one rotation is in
  progress is dropped rather than minting a competing grant, and a
  stop/fail landing mid-remint can no longer leak a fresh connection.
- Ctrl-C grant revocation aborts its in-flight request when the 5s grace
  period wins (a hung backend kept the process alive until the API's own
  30s timeout), and the deadline timer is no longer unref'd so it fires
  even with no other event-loop handles.
- A grant minted for a session that fails during startup or remint is
  now best-effort revoked instead of leaving its fresh secret live.
- The stdio facade answers envelopes without a string method with a
  JSON-RPC -32600 instead of a silent 202 the caller would hang on.
- Add a changeset releasing @mcpjam/sdk and @mcpjam/cli together so the
  published CLI's dependency range can't resolve to an SDK without the
  tunnel exports.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
All Convex round-trips in the tunnel lifecycle (token mint, rotate,
close, project-servers pre-read) now share a 15s abort deadline instead
of hanging routes on a wedged backend, and upstream failure bodies that
aren't JSON (proxy HTML, empty 5xx) surface as tunnel errors with the
status code instead of SyntaxErrors.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
Comment thread cli/src/lib/tunnel/tunnel-session.ts
Comment thread mcpjam-inspector/server/routes/v1/tunnels.ts Outdated
Two grant-lifecycle holes from review:

- CLI: a stop() that completes while a remint's mint is still in flight
  can have its revocation race AHEAD of the mint at the backend; the
  session now revokes the fresh grant explicitly once the mint resolves
  (deterministic ordering), on both the guard and dial-failure paths.
- v1 route: when servers:updateServer fails after a successful mint, the
  grant the caller never received is now best-effort revoked before the
  error surfaces, instead of leaving a live secret with a stale record
  URL.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
mcpjam-inspector/server/services/tunnel-grants.ts (1)

36-57: ⚡ Quick win

Guard against caller-provided signals being silently overwritten.

Line 46 spreads init then overwrites signal with the timeout controller's signal. If a future caller passes init.signal, it will be silently ignored. Consider validating init.signal is absent or merging both signals to honor both the timeout and the caller's cancellation intent.

One approach: validate init.signal is absent
 export async function convexFetch(
   url: string,
   init: RequestInit = {}
 ): Promise<Response> {
+  if (init.signal) {
+    throw new Error("convexFetch does not support caller-provided signals; use the built-in timeout.");
+  }
   const controller = new AbortController();
   const timer = setTimeout(
     () => controller.abort(),
     CONVEX_FETCH_TIMEOUT_MS
   );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@mcpjam-inspector/server/services/tunnel-grants.ts` around lines 36 - 57, The
convexFetch function currently overwrites any caller-provided init.signal;
update it to preserve caller cancellation by detecting if init.signal exists and
wiring it into the timeout controller instead of silently replacing it: create a
composite AbortController (e.g., timeoutController) and if init.signal is
present, add a listener so when init.signal aborts it calls
timeoutController.abort(), then use timeoutController.signal in the fetch call;
ensure you also forward timeoutController.abort() to respect the timeout and
remove any added event listeners in the finally block to avoid leaks (refer to
convexFetch, controller/timer, and init.signal).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@mcpjam-inspector/server/services/tunnel-grants.ts`:
- Around line 36-57: The convexFetch function currently overwrites any
caller-provided init.signal; update it to preserve caller cancellation by
detecting if init.signal exists and wiring it into the timeout controller
instead of silently replacing it: create a composite AbortController (e.g.,
timeoutController) and if init.signal is present, add a listener so when
init.signal aborts it calls timeoutController.abort(), then use
timeoutController.signal in the fetch call; ensure you also forward
timeoutController.abort() to respect the timeout and remove any added event
listeners in the finally block to avoid leaks (refer to convexFetch,
controller/timer, and init.signal).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ef6c9eb2-b280-415b-89ad-f145b00b3d29

📥 Commits

Reviewing files that changed from the base of the PR and between 52787a6 and e70dd65.

📒 Files selected for processing (8)
  • .changeset/cli-tunnel-command.md
  • cli/src/commands/tunnel.ts
  • cli/src/lib/tunnel/local-bridge.ts
  • cli/src/lib/tunnel/tunnel-session.ts
  • cli/tests/tunnel-bridge.test.ts
  • cli/tests/tunnel-session.test.ts
  • mcpjam-inspector/server/routes/v1/tunnels.ts
  • mcpjam-inspector/server/services/tunnel-grants.ts
✅ Files skipped from review due to trivial changes (1)
  • .changeset/cli-tunnel-command.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • mcpjam-inspector/server/routes/v1/tunnels.ts
  • cli/src/commands/tunnel.ts
  • cli/tests/tunnel-bridge.test.ts
  • cli/src/lib/tunnel/local-bridge.ts
  • cli/src/lib/tunnel/tunnel-session.ts

The shared deadline owns the signal slot; a caller-provided one was
silently clobbered. Fail loudly until a caller actually needs both
(then merge with AbortSignal.any).

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
Comment thread cli/src/commands/tunnel.ts
Signal handlers were attached only after session.start() resolved, so an
interrupt during grant mint / bridge bind / relay connect took the
default kill path — no graceful stop, and a possibly-minted grant left
live on the server record. Handlers now attach before start; a stop()
landing mid-startup aborts the bring-up (revoking a grant whose mint
resolved after stop's own revocation ran empty), and the command races
startup against the stop outcome so the interrupt exits promptly instead
of waiting out a relay connect timeout.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 208f2fbfcf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread cli/src/commands/tunnel.ts Outdated
Comment on lines +191 to +195
createGrant: () =>
createTunnelOperation.execute(
{ project: options.project, name: options.id },
{ client },
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Abort in-flight grant creation on shutdown

When the user hits Ctrl-C while the initial grant creation (or a remint) is still inside createTunnelOperation.execute, session.stop() can settle the command but this callback has no abort signal to cancel the SDK request. The pending fetch/timer keeps Node alive until the platform timeout, and if it eventually succeeds it can still mint a live tunnel grant after the user asked to stop before the later best-effort revoke runs. Please thread an AbortSignal through createGrant the same way closeGrant is wired so startup/remint requests are canceled on shutdown.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/tests/tunnel-session.test.ts`:
- Around line 407-415: The test must assert that startBridge() was never
invoked: update makeHarness() to record bridge start attempts (e.g., increment a
bridgeStarts counter or push to harness.bridgeStarts when startBridge() is
called) and then add an assertion after resolveGrant!/await assert.rejects that
harness.bridgeStarts === 0 (or assert.equal(harness.bridgeStarts.length, 0) if
using an array) so the test verifies the local bridge was never bound; reference
the existing makeHarness() factory and the startBridge() call-site to add the
counter and assert it here.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9873942-124d-46f9-9aa9-5d75551f4810

📥 Commits

Reviewing files that changed from the base of the PR and between 280a255 and 208f2fb.

📒 Files selected for processing (3)
  • cli/src/commands/tunnel.ts
  • cli/src/lib/tunnel/tunnel-session.ts
  • cli/tests/tunnel-session.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • cli/src/commands/tunnel.ts
  • cli/src/lib/tunnel/tunnel-session.ts

Comment thread cli/tests/tunnel-session.test.ts
stop() now cancels the createTunnel request via an AbortSignal threaded
through the session deps into the SDK operation: the pending fetch (and
its 30s timeout) no longer outlives the session, and usually no grant
gets minted at all. A mint whose response wins the race is still revoked
by the existing stopped-guards. Also assert in tests that an interrupted
startup never binds the local bridge.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 847cfa5a9c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +192 to +195
await convex.mutation("servers:updateServer" as any, {
serverId,
url: grant.url,
transportType: "http",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Serialize per-server tunnel mint/update

When two POST /v1/projects/:projectId/tunnels calls for the same server overlap, the later fetchRelayGrant revokes the earlier grant at the edge, but the earlier request can still reach this updateServer afterward and persist its now-revoked grant.url. That leaves the project server pointing evals/chatboxes at a dead bearer URL even though another CLI session may have connected successfully; the existing web tunnel route serializes this lifecycle with withTunnelLock because grant minting and URL persistence are a multi-step critical section.

Useful? React with 👍 / 👎.

Two overlapping creates for the same server could interleave so the
losing request's updateServer landed last, persisting a URL whose secret
the winning mint had already revoked at the edge — pointing evals and
chatboxes at a dead bearer URL while the winner's session was live. Wrap
mint + persist (and close) in the same per-server withTunnelLock the web
tunnel routes use, with a test pinning the no-interleave ordering.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2750062. Configure here.

Comment thread cli/src/lib/tunnel/local-bridge.ts
connectToServer can throw after spawning the child (e.g. a handshake
timeout); disconnect the manager before propagating, matching the
existing listen-failure cleanup.

https://claude.ai/code/session_0188QJZL15MSGhjRU69523cF
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@chelojimenez
chelojimenez merged commit cd25d0d into main Jun 12, 2026
14 checks passed
@chelojimenez
chelojimenez deleted the claude/optimistic-hamilton-u7s4de branch June 12, 2026 18:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:XXL This PR changes 1000+ lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants