Skip to content

feat(environments): expose Project Environments on the API, SDK, and CLI - #3425

Merged
chelojimenez merged 2 commits into
mainfrom
feat/environments-public-surface
Jul 25, 2026
Merged

chelojimenez merged 2 commits into
mainfrom
feat/environments-public-surface

Conversation

@chelojimenez

@chelojimenez chelojimenez commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Project Environments have existed in the backend and the hosted UI since #3405, but had zero public surface — and the v1 eval-run route actively rejected environmentId. This adds the whole surface and makes environment runs work end to end.

Pairs with MCPJam/mcpjam-backend#783. Both repos keep a byte-identical copy of the v1 error contract, guarded by golden fixtures. Merge that one too — each repo's fixtures are self-consistent, so neither breaks alone, but they'll drift until both land.

What a Project Environment is

A named, project-scoped, live-editable execution bundle that eval suites and journeys run against: one host + optional standalone server group + optional pinned skill selection + optional pinned plugin versions. Revisioned for optimistic concurrency.

Not a Computer sandbox image (/images, renamed in #3418) and not the eval-suite environment servers bag. Nothing named images / PlatformImage* / computerEnvironments:* was touched.

Changes

v1 routes — new server/routes/v1/environments.ts:

Route Notes
GET /projects/:p/environments ?includeArchived=true — needed to find something to restore
POST /projects/:p/environments 201
GET/PATCH …/:environmentId PATCH requires expectedRevision
POST …/:environmentId/archive | /restore explicit sub-actions, not DELETE
GET …/:environmentId/resolve launch preview

Archive/restore aren't a DELETE because the backend keeps the row and restore has real semantics — it re-checks the name and drops irrecoverably-dead plugin pins. Routes stay off the guest allowlist. Reads need project membership; every write needs project admin, mapped to a real 403 (while non-membership stays a 404, so project existence never leaks).

Eval runs now accept environmentId. This was the gap that mattered most — without it the API could create environments it couldn't use. The route resolves the environment before building the manager, connects its closed server set (including servers contributed by pinned plugin versions), and passes that same resolution to prepareEvalRun so the run is pinned to the revision whose tool snapshot was captured.

SDK — PlatformEnvironment* types, PlatformApiClient methods, and seven MCP operations (list_project_environments, get_, resolve_, create_, update_, archive_, restore_). Not added to PLATFORM_CATALOG_OPERATIONS (the guest worker surface), matching hosts and images.

CLI — mcpjam environments {list,get,resolve,create,update,archive,restore}.

OpenAPI + docs — full spec entries (the openapi-drift test requires them), CLI reference, overview, and public-api.mdx.

Two contracts surfaced rather than smoothed over

expectedRevision is required on every mutation, not re-read server-side. Silently supplying the current revision would convert the backend's safe concurrent-edit rejection into a last-write-wins clobber.

PATCH keeps the tri-state: omitted = unchanged, explicit null = clear, value = set. Empty arrays stay rejected — they are not a way to clear. Both directions are covered by tests.

Error mapping

Errors branch on the structured ConvexError.data.code rather than sniffing messages. Note CONFLICT is overloaded backend-side — stale revision, duplicate live name, edit-archived, already-archived, not-archived — so all map to 409 and the backend's own message is surfaced instead of us asserting a cause. The /resolve endpoint maps the ENV_* family, keeping the specific code in details.code.

This required adding CONFLICT (409) to the v1 public contract, which had no code for "well-formed request, wrong resource state." That also fixes a live bug: ENVIRONMENT_REVISION_CONFLICT was missing from INTERNAL_TO_V1_CODE, so run-start revision conflicts were already reaching API callers as a 500.

Verification

  • Full suite: 9,734 passed, 0 failed (2 files skipped).
  • Server typecheck: 85 errors vs. the 88 pre-existing baseline — diffed, zero new. The three that vanished were missing-bundle artifacts generated as a test prerequisite, unrelated to this change.
  • typecheck:client and tsc -p cli/tsconfig.json: clean.
  • openapi-drift: green.
  • New environments.test.ts: 29 tests — guest denial, DTO shape (no environmentId leak), includeArchived forwarding, tri-state PATCH, revision 409, admin-403 vs member-404, archive/restore preconditions, and ENV_* resolve mapping.

Pre-existing Prettier violations in sdk/src/platform/errors.ts, show-servers.ts, and one block of v1/evals.ts were left alone; only my own lines are formatted.

Not in this PR

Journeys still have no public run path — the swarm runner is web-only and takes no environmentId. Worth a follow-up.

🤖 Generated with Claude Code


Note

Low Risk
CLI and documentation only; delegates to existing SDK platform operations with no server or auth logic changes in this diff.

Overview
Adds mcpjam environments so hosted projects can be managed from the terminal: list, get, resolve, create, update, archive, and restore project environments (host + optional servers, skills, and plugin pins — distinct from Computer images).

The new command module follows the same platform CLI pattern as hosts: --api-key / --api-url, SDK operation execution with timeouts, Zod validation, and --file / --json bodies where flags do not cover structured fields. Mutations require --expected-revision from get, with local validation so bad revision strings fail before the API. Create/update merge JSON with CLI flags (flags win); update documents clearing pins via explicit null in JSON.

cli/src/index.ts registers the command group. Docs add the environments row to the CLI overview and a full reference section (revision workflow, admin-gated writes, pinning rules).

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


Summary by cubic

Expose Project Environments on the public surface (v1 API, @mcpjam/sdk, and @mcpjam/cli) and allow eval runs to target an environmentId. Also fixes name-based selection across live vs archived environments and updates OpenAPI so environment-backed runs validate correctly.

  • New Features

    • v1 API routes: list/get/create/update/archive/restore plus /resolve; supports ?includeArchived; archive/restore are sub-actions.
    • Eval runs: accept environmentId, resolve environment and connect its closed server set, pin run to the resolved revision.
    • SDK (@mcpjam/sdk): PlatformEnvironment* types and operations (list/get/resolve/create/update/archive/restore).
    • CLI (@mcpjam/cli): mcpjam environments {list,get,resolve,create,update,archive,restore}.
    • Access: not on the guest allowlist; reads require project membership, writes require project admin.
  • Bug Fixes

    • Add CONFLICT (409) to the v1 error contract; map ENVIRONMENT_REVISION_CONFLICT to 409 instead of 500. Enforce optimistic concurrency: all mutations require expectedRevision; PATCH keeps omit/null/set tri-state.
    • Environment selector: prefer live names for all ops (restore prefers archived), exact ID wins, and ambiguity is reported correctly.
    • Eval runs with environmentId: ignore caller serverIds, connect the environment’s closed set (plugin-contributed servers included), pin the resolved revision, map launch-resolution failures to 409 with ENV_* in details, and return 404 for unreadable environments.
    • OpenAPI: allow new-suite bodies with suiteName + tests + environmentId (no serverIds), and refresh request descriptions.

Written for commit b62b4fb. Summary will update on new commits.

Review in cubic

Project Environments existed in the backend and the hosted UI but had no
public surface, and the v1 eval-run route actively rejected `environmentId`.
This adds the whole surface and makes environment runs work end to end.

- v1 routes (`server/routes/v1/environments.ts`): list (with
  `?includeArchived`), get, create, patch, archive, restore, and a `/resolve`
  launch preview. Archive/restore are explicit sub-actions rather than a
  DELETE because the row is kept and restore has real semantics. Off the
  guest allowlist; reads need project membership, writes need project admin.
- Eval runs: lifted the `environmentId` rejection. The route now resolves the
  environment before building the manager, connects its closed server set
  (including servers contributed by pinned plugin versions), and hands the
  resolution to `prepareEvalRun` so the run is pinned to the revision whose
  tools were captured.
- SDK: `PlatformEnvironment*` types, `PlatformApiClient` methods, and seven
  MCP operations.
- CLI: `mcpjam environments {list,get,resolve,create,update,archive,restore}`.
- OpenAPI + docs, including the revision workflow and the pin constraints.

Two contracts are surfaced rather than smoothed over:

`expectedRevision` is required on every mutation instead of being re-read
server-side. Hiding it would convert the backend's safe concurrent-edit
rejection into a last-write-wins clobber.

PATCH keeps the backend's tri-state: an omitted field is unchanged, an
explicit `null` clears. Empty arrays stay rejected — they are not a clear.

Adds `CONFLICT` (409) to the v1 public error contract, which had no code for
"well-formed request, wrong resource state". This also fixes an existing bug:
`ENVIRONMENT_REVISION_CONFLICT` was missing from `INTERNAL_TO_V1_CODE`, so a
run-start revision conflict reached API callers as a 500. The byte-identical
backend copy of the contract is updated in a paired mcpjam-backend commit;
both repos' golden fixtures move together.

Verified: 9,734 tests pass; server typecheck adds zero errors over the
pre-existing baseline; client and CLI typechecks clean; openapi-drift green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. enhancement New feature or request labels Jul 25, 2026
@chelojimenez

chelojimenez commented Jul 25, 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 Jul 25, 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 Jul 25, 2026, 7:32 PM

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

@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: 2d5a06e769

ℹ️ 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 +294 to +300
.refine(
(body) =>
body.suiteId || body.environmentId || (body.serverIds?.length ?? 0) > 0,
{
message:
"serverIds are required when creating a new suite without an environment",
}

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 Add tests for environment-backed eval runs

This new branch changes server selection, manager priming, resolution-error mapping, and revision handoff, but the added tests cover only the environment CRUD/resolve routes; a repo-wide search finds no eval-route test that submits environmentId. Add happy-path and resolution/conflict coverage so regressions such as reconnecting the suite servers or dropping resolvedEnvironment are detected.

AGENTS.md reference: mcpjam-inspector/AGENTS.md:L53-L55

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b62b4fb — the gap was real, write-routes.test.ts had no eval-run test that sent environmentId. Added an environment-backed runs block covering exactly the four things that could silently regress:

  • the manager connects the resolved closed set including plugin-contributed servers (servers[].serverId, live-healed), caller-supplied serverIds are ignored, and testSuites:getSuiteRunServerSelection is never consulted;
  • the resolution object is handed to prepareEvalRun as resolvedEnvironment, so the run pins the revision whose servers were just connected rather than resolving a second time;
  • a launch-resolution failure maps to 409 CONFLICT with the ENV_* reason in details.code (and the manager is never built);
  • ENV_NOT_FOUND maps to 404, not a conflict.

Plus a new-suite-with-environmentId-and-no-serverIds case for the lifted schema refinement.

Comment on lines +4283 to +4285
"environmentId": {
"type": "string",
"description": "Run against a project environment. The environment supplies the closed server set (so `serverIds` is not required and is ignored if sent), and the run is pinned to the revision resolved at launch \u2014 if the environment changes in between, the run is rejected with 409 rather than executing against a different configuration."

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 Allow environmentId in the new-suite OpenAPI branch

When creating a new suite with suiteName, inline tests, and environmentId but no serverIds, the route now explicitly accepts the request because the environment supplies its servers. However, EvalRunCreateRequest.anyOf still requires serverIds for every new-suite shape, so OpenAPI-generated clients and request validators reject this newly supported request despite the added environmentId property. Add an environment-backed new-suite alternative and update the stale request/server descriptions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b62b4fb. Added a third anyOf alternative (suiteName + tests + environmentId) so the environment-backed new-suite body validates, and un-staled the descriptions: the request-level one now names all three shapes, and serverIds says it is not required when environmentId is given (and is ignored if sent). The public-api.mdx endpoint table row got the same note.

Comment on lines +2772 to +2774
const page = await client.listEnvironments(
{ projectId: project.id, includeArchived: true },
{ signal }

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 Resolve live environments without archived-name collisions

Because archiving explicitly frees a name for reuse, a project can contain an archived Staging and a new live Staging. Listing both here makes resolveByIdOrName report that name as ambiguous, so name-based get, resolve, update, and archive operations all fail even though live environment names are unique. Restrict normal operations to live rows and include archived rows only when resolving a restore target, or otherwise prefer the live match.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in b62b4fb. You are right — assertNameAvailable skips archived rows, so a live and an archived Staging can coexist and the flat lookup called that name ambiguous.

resolveEnvironmentSelector now takes a prefer side: an exact ID still wins outright, then a name is resolved against the live rows for every operation except restore_project_environment (whose target is archived by definition), falling back to the full listing when the preferred side has no name match — so "get an archived env by name" still works and the not-found message still enumerates everything. A name that is ambiguous within the preferred side (two archived Stagings for a restore) still reports as ambiguous, because there it genuinely is. Covered by sdk/tests/platform/operations-environments.test.ts.

@github-actions

github-actions Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL: https://mcp-inspector-pr-3425.up.railway.app
Deployed commit: 4d33919
PR head commit: b62b4fb
Backend target: staging fallback.
Health: ✅ Convex reachable
Access is employee-only in non-production environments.

@cubic-dev-ai cubic-dev-ai 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.

7 issues found across 20 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="cli/src/commands/environments.ts">

<violation number="1" location="cli/src/commands/environments.ts:47">
P3: Platform command timeout/error handling is now duplicated across four command modules, so future changes can make command behavior drift. A shared CLI platform-command helper would keep all surfaces on one implementation.</violation>

<violation number="2" location="cli/src/commands/environments.ts:410">
P3: Restore help currently suggests an invalid command: `environments get` has no `--include-archived` option. Point users to `environments get` (which already finds archived environments) or to `environments list --include-archived`.</violation>
</file>

<file name="mcpjam-inspector/server/routes/v1/environments.ts">

<violation number="1" location="mcpjam-inspector/server/routes/v1/environments.ts:228">
P2: Non-member create/update/archive/restore requests return `404 NOT_FOUND` whenever Convex emits its normal `FORBIDDEN` message without “admin”, contrary to the documented 403 write contract. Preserve 404 for `NOT_FOUND`, but map structured `FORBIDDEN` writes to `403 FORBIDDEN` instead of inferring authorization state from message text.</violation>

<violation number="2" location="mcpjam-inspector/server/routes/v1/environments.ts:343">
P2: The `description` field in `updateEnvironmentSchema` uses only `.optional()` (no `.nullable()`), so callers cannot clear it with explicit `null` — they'd get a 400. All other clearable fields (`serverAttachmentId`, `skillSelection`, `pluginVersionIds`) use `.nullable().optional()` and correctly support `null`-clearing per the contract. If `description` is meant to be clearable (it's optional at creation, so clearing it to "no description" is a natural use case), add `.nullable()`. If it's intentionally set-only, the inconsistency with the rest of the schema should be documented so callers know they must send an empty string `""` instead of `null`.

If clearing via empty string is the intended design, `description` schema needs no change. But the discrepancy means a caller following the contract statement "explicit `null` = clear" will get a confusing 400 instead of having their description cleared — the mismatch should at minimum be documented so callers know `description` uses `""` instead of `null` for clearing.</violation>
</file>

<file name="mcpjam-inspector/server/routes/v1/evals.ts">

<violation number="1" location="mcpjam-inspector/server/routes/v1/evals.ts:296">
P2: The runtime validation now accepts a new-suite request with `environmentId` and no `serverIds` (the environment supplies its servers), but the OpenAPI `EvalRunCreateRequest.anyOf` definition still requires `serverIds` for every new-suite branch. OpenAPI-generated clients and request validators will reject this newly supported request shape. Consider adding an environment-backed new-suite alternative to the `anyOf` and updating the affected request/server descriptions.</violation>

<violation number="2" location="mcpjam-inspector/server/routes/v1/evals.ts:556">
P2: The `translateEnvironmentResolveError` function in evals.ts rebuilds the `details` object as only `{ code }` when returning a 409, while the equivalent `translateResolveError` in environments.ts spreads the full `data?.details` object. This means additional structured fields that the Convex backend includes (like `{ reason: "disabled" }`) are surfaced to callers of the environment resolve route but silently dropped when the same error occurs on an eval-run. If both routes serve the same public surface, the error detail should be preserved consistently across both.</violation>

<violation number="3" location="mcpjam-inspector/server/routes/v1/evals.ts:1427">
P2: The environment-backed eval run path (resolving the launch, overriding server selection, error mapping, and passing `resolvedEnvironment` to pin the revision) is untested. The new `environments.test.ts` covers CRUD and resolve routes, but no test submits `environmentId` to the eval-run endpoint. Adding at least a happy-path and a resolution-conflict test would catch regressions in server reconnection logic or a dropped `resolvedEnvironment`.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread sdk/src/platform/operations.ts
if (code === "NOT_FOUND") {
return new WebRouteError(404, ErrorCode.NOT_FOUND, "Environment not found");
}
if (code === "FORBIDDEN") {

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: Non-member create/update/archive/restore requests return 404 NOT_FOUND whenever Convex emits its normal FORBIDDEN message without “admin”, contrary to the documented 403 write contract. Preserve 404 for NOT_FOUND, but map structured FORBIDDEN writes to 403 FORBIDDEN instead of inferring authorization state from message text.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/routes/v1/environments.ts, line 228:

<comment>Non-member create/update/archive/restore requests return `404 NOT_FOUND` whenever Convex emits its normal `FORBIDDEN` message without “admin”, contrary to the documented 403 write contract. Preserve 404 for `NOT_FOUND`, but map structured `FORBIDDEN` writes to `403 FORBIDDEN` instead of inferring authorization state from message text.</comment>

<file context>
@@ -0,0 +1,579 @@
+  if (code === "NOT_FOUND") {
+    return new WebRouteError(404, ErrorCode.NOT_FOUND, "Environment not found");
+  }
+  if (code === "FORBIDDEN") {
+    if (structuredMessage && /admin/i.test(structuredMessage)) {
+      return new WebRouteError(403, ErrorCode.FORBIDDEN, structuredMessage);
</file context>


const createEnvironmentSchema = z.strictObject({
name: z.string().trim().min(1),
description: z.string().optional(),

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: The description field in updateEnvironmentSchema uses only .optional() (no .nullable()), so callers cannot clear it with explicit null — they'd get a 400. All other clearable fields (serverAttachmentId, skillSelection, pluginVersionIds) use .nullable().optional() and correctly support null-clearing per the contract. If description is meant to be clearable (it's optional at creation, so clearing it to "no description" is a natural use case), add .nullable(). If it's intentionally set-only, the inconsistency with the rest of the schema should be documented so callers know they must send an empty string "" instead of null.

If clearing via empty string is the intended design, description schema needs no change. But the discrepancy means a caller following the contract statement "explicit null = clear" will get a confusing 400 instead of having their description cleared — the mismatch should at minimum be documented so callers know description uses "" instead of null for clearing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/routes/v1/environments.ts, line 343:

<comment>The `description` field in `updateEnvironmentSchema` uses only `.optional()` (no `.nullable()`), so callers cannot clear it with explicit `null` — they'd get a 400. All other clearable fields (`serverAttachmentId`, `skillSelection`, `pluginVersionIds`) use `.nullable().optional()` and correctly support `null`-clearing per the contract. If `description` is meant to be clearable (it's optional at creation, so clearing it to "no description" is a natural use case), add `.nullable()`. If it's intentionally set-only, the inconsistency with the rest of the schema should be documented so callers know they must send an empty string `""` instead of `null`.

If clearing via empty string is the intended design, `description` schema needs no change. But the discrepancy means a caller following the contract statement "explicit `null` = clear" will get a confusing 400 instead of having their description cleared — the mismatch should at minimum be documented so callers know `description` uses `""` instead of `null` for clearing.</comment>

<file context>
@@ -0,0 +1,579 @@
+
+const createEnvironmentSchema = z.strictObject({
+  name: z.string().trim().min(1),
+  description: z.string().optional(),
+  hostId: z.string().trim().min(1),
+  serverAttachmentId: z.string().trim().min(1).optional(),
</file context>
Suggested change
description: z.string().optional(),
description: z.string().nullable().optional(),

const message = (data as { message?: unknown }).message;
if (typeof code === "string" && code.startsWith("ENV_")) {
if (code === "ENV_NOT_FOUND" || code === "ENV_CROSS_PROJECT") {
return new WebRouteError(

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: The translateEnvironmentResolveError function in evals.ts rebuilds the details object as only { code } when returning a 409, while the equivalent translateResolveError in environments.ts spreads the full data?.details object. This means additional structured fields that the Convex backend includes (like { reason: "disabled" }) are surfaced to callers of the environment resolve route but silently dropped when the same error occurs on an eval-run. If both routes serve the same public surface, the error detail should be preserved consistently across both.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/routes/v1/evals.ts, line 556:

<comment>The `translateEnvironmentResolveError` function in evals.ts rebuilds the `details` object as only `{ code }` when returning a 409, while the equivalent `translateResolveError` in environments.ts spreads the full `data?.details` object. This means additional structured fields that the Convex backend includes (like `{ reason: "disabled" }`) are surfaced to callers of the environment resolve route but silently dropped when the same error occurs on an eval-run. If both routes serve the same public surface, the error detail should be preserved consistently across both.</comment>

<file context>
@@ -533,6 +537,41 @@ function isConvexNotVisibleError(error: unknown): boolean {
+    const message = (data as { message?: unknown }).message;
+    if (typeof code === "string" && code.startsWith("ENV_")) {
+      if (code === "ENV_NOT_FOUND" || code === "ENV_CROSS_PROJECT") {
+        return new WebRouteError(
+          404,
+          ErrorCode.NOT_FOUND,
</file context>

//
// Any caller-supplied `serverIds` are deliberately ignored for an
// environment run: the closed set is the point.
let environmentLaunch: ResolvedEnvironmentForLaunch | undefined;

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: The environment-backed eval run path (resolving the launch, overriding server selection, error mapping, and passing resolvedEnvironment to pin the revision) is untested. The new environments.test.ts covers CRUD and resolve routes, but no test submits environmentId to the eval-run endpoint. Adding at least a happy-path and a resolution-conflict test would catch regressions in server reconnection logic or a dropped resolvedEnvironment.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/routes/v1/evals.ts, line 1427:

<comment>The environment-backed eval run path (resolving the launch, overriding server selection, error mapping, and passing `resolvedEnvironment` to pin the revision) is untested. The new `environments.test.ts` covers CRUD and resolve routes, but no test submits `environmentId` to the eval-run endpoint. Adding at least a happy-path and a resolution-conflict test would catch regressions in server reconnection logic or a dropped `resolvedEnvironment`.</comment>

<file context>
@@ -1375,12 +1414,35 @@ evals.post("/projects/:projectId/eval-runs", async (c) => {
+  //
+  // Any caller-supplied `serverIds` are deliberately ignored for an
+  // environment run: the closed set is the point.
+  let environmentLaunch: ResolvedEnvironmentForLaunch | undefined;
   let serverIds = body.serverIds ?? [];
   let serverNames = body.serverNames;
</file context>

// run legitimately sends zero serverIds.
.refine(
(body) =>
body.suiteId || body.environmentId || (body.serverIds?.length ?? 0) > 0,

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: The runtime validation now accepts a new-suite request with environmentId and no serverIds (the environment supplies its servers), but the OpenAPI EvalRunCreateRequest.anyOf definition still requires serverIds for every new-suite branch. OpenAPI-generated clients and request validators will reject this newly supported request shape. Consider adding an environment-backed new-suite alternative to the anyOf and updating the affected request/server descriptions.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/routes/v1/evals.ts, line 296:

<comment>The runtime validation now accepts a new-suite request with `environmentId` and no `serverIds` (the environment supplies its servers), but the OpenAPI `EvalRunCreateRequest.anyOf` definition still requires `serverIds` for every new-suite branch. OpenAPI-generated clients and request validators will reject this newly supported request shape. Consider adding an environment-backed new-suite alternative to the `anyOf` and updating the affected request/server descriptions.</comment>

<file context>
@@ -282,19 +288,17 @@ const createEvalRunSchema = RunEvalsRequestSchema.omit({
+  // run legitimately sends zero serverIds.
+  .refine(
+    (body) =>
+      body.suiteId || body.environmentId || (body.serverIds?.length ?? 0) > 0,
+    {
+      message:
</file context>

);
}

async function runPlatformCommand<TOutput>(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Platform command timeout/error handling is now duplicated across four command modules, so future changes can make command behavior drift. A shared CLI platform-command helper would keep all surfaces on one implementation.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/commands/environments.ts, line 47:

<comment>Platform command timeout/error handling is now duplicated across four command modules, so future changes can make command behavior drift. A shared CLI platform-command helper would keep all surfaces on one implementation.</comment>

<file context>
@@ -0,0 +1,439 @@
+    );
+}
+
+async function runPlatformCommand<TOutput>(
+  options: PlatformOptions,
+  timeoutMs: number,
</file context>

.requiredOption("--environment <id-or-name>", "Environment name or ID")
.requiredOption(
"--expected-revision <n>",
"The revision you last read (from `environments get --include-archived` via list)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Restore help currently suggests an invalid command: environments get has no --include-archived option. Point users to environments get (which already finds archived environments) or to environments list --include-archived.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At cli/src/commands/environments.ts, line 410:

<comment>Restore help currently suggests an invalid command: `environments get` has no `--include-archived` option. Point users to `environments get` (which already finds archived environments) or to `environments list --include-archived`.</comment>

<file context>
@@ -0,0 +1,439 @@
+      .requiredOption("--environment <id-or-name>", "Environment name or ID")
+      .requiredOption(
+        "--expected-revision <n>",
+        "The revision you last read (from `environments get --include-archived` via list)"
+      )
+      .option("--project <id-or-name>", "Project name or ID")
</file context>

@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@chelojimenez, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 8 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8de6fedf-90e6-4df0-ab6f-827c0d03369d

📥 Commits

Reviewing files that changed from the base of the PR and between 2d5a06e and b62b4fb.

📒 Files selected for processing (5)
  • docs/reference/openapi.json
  • docs/reference/public-api.mdx
  • mcpjam-inspector/server/routes/v1/__tests__/write-routes.test.ts
  • sdk/src/platform/operations.ts
  • sdk/tests/platform/operations-environments.test.ts

Walkthrough

Adds project environment management across the v1 API, SDK, CLI, and documentation. The API supports listing, reading, resolving, creating, updating, archiving, and restoring environments with strict validation, project scoping, authorization, and revision conflicts. Eval runs can resolve and use an environment configuration. The CLI exposes matching commands and input modes, while OpenAPI, public API, error mappings, and route tests are updated.


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.

@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: 5

🧹 Nitpick comments (4)
docs/reference/openapi.json (1)

2186-2188: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Let the cancel-run 409 speak the same language as the new Conflict response.

This PR introduces #/components/responses/Conflict with a schema and example; leaving this sibling 409 as a bare description means generated clients get no error body type for it. A $ref costs nothing and makes the contract uniform.

♻️ Suggested alignment
           "409": {
-            "description": "The run already finished and cannot be cancelled."
+            "$ref": "`#/components/responses/Conflict`"
           },
🤖 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 `@docs/reference/openapi.json` around lines 2186 - 2188, Update the cancel-run
409 response definition to reference the existing
`#/components/responses/Conflict` response instead of retaining only an inline
description, ensuring it uses the shared schema and example consistently.
mcpjam-inspector/server/routes/v1/__tests__/environments.test.ts (1)

158-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reset the Convex mock behaviours, not just their call records.

vi.clearAllMocks() wipes call history but leaves implementations intact, so a mockRejectedValue set in one test (e.g. Line 278, Line 543) survives into every later test until something overwrites it. Today that is harmless only because each subsequent test happens to re-stub the mock it exercises; a future test that reads without stubbing would inherit a stale rejection. Seeding a neutral default per test makes the suite order-independent.

♻️ Suggested hardening of the per-test baseline
   beforeEach(() => {
     vi.clearAllMocks();
+    convexQueryMock.mockReset();
+    convexMutationMock.mockReset();
+    convexQueryMock.mockResolvedValue(null);
     process.env.CONVEX_URL = "https://convex.example.com";
     validateGuestTokenMock.mockResolvedValue({ valid: false });
   });
🤖 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/routes/v1/__tests__/environments.test.ts` around
lines 158 - 167, Update the beforeEach setup in the environments test suite to
reset Convex mock implementations, not only call history. Seed
validateGuestTokenMock and any other Convex mocks with neutral default behavior
for each test so prior mockRejectedValue or other overrides cannot persist
across tests, while preserving the existing environment-variable reset logic.
cli/src/commands/environments.ts (1)

154-439: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Seven subcommands repeat the same dispatch boilerplate.

Each of list/get/resolve/create/update/archive/restore repeats the identical getGlobalOptions → runPlatformCommand → writeResult shape, differing only in the operation and its input object. Worth folding into a small generic dispatcher to cut ~250 lines of repetition and reduce the risk that a future subcommand forgets a step (e.g., signal passthrough or writeResult(result, format)).

♻️ Sketch of a shared dispatch helper
async function dispatch<TInput, TOutput>(
  op: PlatformOperation<TInput, TOutput>,
  options: PlatformOptions,
  command: Command,
  buildInput: () => TInput
): Promise<void> {
  const globalOptions = getGlobalOptions(command);
  const result = await runPlatformCommand(
    options,
    globalOptions.timeout,
    ({ client, signal }) => op.execute(buildInput(), { client, signal })
  );
  writeResult(result, globalOptions.format);
}
🤖 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 `@cli/src/commands/environments.ts` around lines 154 - 439, Extract the
repeated getGlobalOptions → runPlatformCommand → writeResult flow in
registerEnvironmentsCommands into a generic dispatch helper that accepts the
operation, PlatformOptions, command, and an input builder, while preserving
client/signal passthrough, timeout handling, and output formatting. Replace all
seven subcommand action handlers’ duplicated dispatch blocks with this helper,
keeping each command’s existing input construction and validation unchanged.
mcpjam-inspector/server/routes/v1/environments.ts (1)

169-189: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated ENV_* → HTTP status mapping across two files. Both translateResolveError and translateEnvironmentResolveError independently classify the same Convex ENV_* codes into 404 (not-found codes) vs 409 CONFLICT (everything else); evals.ts's docstring even states it mirrors the environments.ts route, but the mapping logic itself is copy-pasted rather than shared. A future ENV_* code added to only one side would make these documented "mirrored" behaviors quietly diverge.

  • mcpjam-inspector/server/routes/v1/environments.ts#L169-L189: extract this ENV_* classification (the RESOLVE_NOT_FOUND_CODES set + status/message mapping) into a small shared helper (e.g. in web/errors.ts or services/evals/environment-launch.ts).
  • mcpjam-inspector/server/routes/v1/evals.ts#L548-L573: replace the inlined duplicate logic in translateEnvironmentResolveError with a call to the extracted shared helper.
🤖 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/routes/v1/environments.ts` around lines 169 - 189,
Extract the shared ENV_* error classification, including RESOLVE_NOT_FOUND_CODES
and the 404/409 status and message mapping, into a reusable helper; update
translateResolveError in
mcpjam-inspector/server/routes/v1/environments.ts:169-189 to use it. Replace the
duplicated logic in translateEnvironmentResolveError in
mcpjam-inspector/server/routes/v1/evals.ts:548-573 with the same helper,
preserving existing WebRouteError handling and fallback translation.
🤖 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/commands/environments.ts`:
- Around line 108-119: The --file "-" path in the environment command currently
uses synchronous fd 0 access, which can fail for piped or delayed stdin. Update
the options.file handling to read stdin asynchronously through process.stdin,
while retaining synchronous file reads for regular paths; validate the collected
stdin payload before command execution and convert read failures into the
existing usageError format.

In `@docs/reference/openapi.json`:
- Around line 6097-6119: Update serverAttachmentId, skillSelection, and
pluginVersionIds to express nullability as OpenAPI 3.1 type unions including
{"type":"null"}, removing nullable: true. For skillSelection, replace the allOf
wrapper around EnvironmentSkillSelection with an anyOf/schema union containing
the $ref and null schema, while preserving each field’s existing descriptions
and constraints.

In `@mcpjam-inspector/server/routes/v1/environments.ts`:
- Around line 68-83: The resolve DTO currently requires hostName and
hostConfigId and unconditionally serializes them. Update ResolvedEnvironmentRow
and toResolvedDto so both fields are optional and only included when the backend
provides values, matching the ResolvedEnvironmentForLaunch contract and
preserving their absence otherwise.
- Around line 191-237: Update translateConvexError to classify FORBIDDEN
failures using structured backend sub-codes, such as FORBIDDEN_NOT_ADMIN for the
403 response and FORBIDDEN_NOT_MEMBER for the 404 response, instead of testing
structuredMessage with /admin/i. Preserve the existing fallback behavior for
unrecognized forbidden sub-codes.

In `@mcpjam-inspector/server/routes/v1/evals.ts`:
- Around line 540-573: Update translateEnvironmentResolveError so its fallback
delegates non-WebRouteError, non-ENV_* failures to mapRuntimeError instead of
returning the raw error. Import mapRuntimeError from the existing web errors
module and preserve the current ENV_* mappings and WebRouteError passthrough.

---

Nitpick comments:
In `@cli/src/commands/environments.ts`:
- Around line 154-439: Extract the repeated getGlobalOptions →
runPlatformCommand → writeResult flow in registerEnvironmentsCommands into a
generic dispatch helper that accepts the operation, PlatformOptions, command,
and an input builder, while preserving client/signal passthrough, timeout
handling, and output formatting. Replace all seven subcommand action handlers’
duplicated dispatch blocks with this helper, keeping each command’s existing
input construction and validation unchanged.

In `@docs/reference/openapi.json`:
- Around line 2186-2188: Update the cancel-run 409 response definition to
reference the existing `#/components/responses/Conflict` response instead of
retaining only an inline description, ensuring it uses the shared schema and
example consistently.

In `@mcpjam-inspector/server/routes/v1/__tests__/environments.test.ts`:
- Around line 158-167: Update the beforeEach setup in the environments test
suite to reset Convex mock implementations, not only call history. Seed
validateGuestTokenMock and any other Convex mocks with neutral default behavior
for each test so prior mockRejectedValue or other overrides cannot persist
across tests, while preserving the existing environment-variable reset logic.

In `@mcpjam-inspector/server/routes/v1/environments.ts`:
- Around line 169-189: Extract the shared ENV_* error classification, including
RESOLVE_NOT_FOUND_CODES and the 404/409 status and message mapping, into a
reusable helper; update translateResolveError in
mcpjam-inspector/server/routes/v1/environments.ts:169-189 to use it. Replace the
duplicated logic in translateEnvironmentResolveError in
mcpjam-inspector/server/routes/v1/evals.ts:548-573 with the same helper,
preserving existing WebRouteError handling and fallback translation.
🪄 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 Plus

Run ID: 0aaeb9b6-8c7d-4880-94dd-2849dd42f3fa

📥 Commits

Reviewing files that changed from the base of the PR and between 251b532 and 2d5a06e.

📒 Files selected for processing (20)
  • cli/src/commands/environments.ts
  • cli/src/index.ts
  • docs/cli/overview.mdx
  • docs/cli/reference.mdx
  • docs/reference/openapi.json
  • docs/reference/public-api.mdx
  • mcpjam-inspector/server/routes/v1/__fixtures__/error-codes.json
  • mcpjam-inspector/server/routes/v1/__fixtures__/error-status.json
  • mcpjam-inspector/server/routes/v1/__fixtures__/internal-to-v1-code.json
  • mcpjam-inspector/server/routes/v1/__tests__/environments.test.ts
  • mcpjam-inspector/server/routes/v1/environments.ts
  • mcpjam-inspector/server/routes/v1/evals.ts
  • mcpjam-inspector/server/routes/v1/index.ts
  • mcpjam-inspector/server/routes/web/errors.ts
  • sdk/src/platform/client.ts
  • sdk/src/platform/errors.ts
  • sdk/src/platform/index.ts
  • sdk/src/platform/operations.ts
  • sdk/src/platform/types.ts
  • sdk/src/public-api/index.ts

Comment on lines +108 to +119
if (options.file !== undefined) {
let text: string;
try {
text =
options.file === "-"
? readFileSync(0, "utf8")
: readFileSync(options.file, "utf8");
} catch (error) {
throw usageError(`Failed to read --file "${options.file}".`, {
source: error instanceof Error ? error.message : String(error),
});
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Node.js fs.readFileSync(0) reliably read piped stdin on macOS, Linux, and Windows in current LTS versions, or does it still throw EAGAIN/EOF errors?

💡 Result:

Using fs.readFileSync(0) to read piped stdin in Node.js is not universally reliable across platforms and execution contexts, even in current LTS versions [1][2]. While it often works for simple piping, it remains susceptible to EAGAIN and other runtime errors depending on the state of the stdin stream [1][3][2]. Key considerations: EAGAIN Errors: On macOS and Linux, fs.readFileSync(0) can throw an EAGAIN error if the stdin file descriptor has been set to non-blocking mode [1][2]. This frequently occurs in environments where hooks, plugins, or third-party wrappers manage stdin or when multiple piped commands interact with Node.js [1][4][2]. Node.js synchronous file system APIs do not automatically retry operations when the kernel returns EAGAIN, causing the application to crash if not specifically handled [1][3]. Windows Compatibility: While historical issues causing EISDIR or other exceptions when reading from empty stdin on Windows have been largely addressed in modern LTS versions (e.g., v20.x and later) [5][6], the use of raw file descriptors remains less idiomatic and potentially fragile compared to using asynchronous stream-based APIs [7][6]. Reliability: Because of these platform-specific behaviors and the potential for non-blocking stream states, relying on fs.readFileSync(0) is generally discouraged for robust applications [1][3]. Developers encountering EAGAIN errors often implement custom retry logic (typically involving a loop with a small delay) [1][3] or prefer using asynchronous Readable streams (e.g., process.stdin) to avoid blocking and state-related errors [7][8]. For production-grade applications that must handle stdin reliably across all environments, using the asynchronous stream interface (process.stdin) or a dedicated library for synchronous input reading is recommended [7][8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- environments.ts relevant section ---\n'
sed -n '85,140p' cli/src/commands/environments.ts

printf '\n--- usages of loadJsonObject/options.json/json ---\n'
rg -n "loadJsonObject|options\.json|--json|stdin|readFile" cli/src/commands/environments.ts

printf '\n--- command declarations for create/update relevant flags ---\n'
rg -n "program\.command|create\(|update\(|json|stdin|file" cli/src/commands/environments.ts

Repository: MCPJam/inspector

Length of output: 245


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- environments.ts relevant section ---'
sed -n '85,140p' cli/src/commands/environments.ts

printf '%s\n' ''
printf '%s\n' '--- usages of loadJsonObject/options.json/json stdin read ---'
rg -n "loadJsonObject|options\.json|--json|stdin|readFile" cli/src/commands/environments.ts

printf '%s\n' ''
printf '%s\n' '--- command declarations for relevant flags ---'
rg -n "program\.command|create\(|update\(|json|stdin|file" cli/src/commands/environments.ts

Repository: MCPJam/inspector

Length of output: 5130


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
const { readFileSync, openSync, readSync, closeSync, F_OK } = require('node:fs');
const { constants } = require('node:constants');
const { spawnSync } = require('node:child_process');

console.log('process.stdin.isTTY:', process.stdin.isTTY);
console.log('readFileSync(0, utf-8) directly:', typeof readFileSync(0, 'utf8') === 'string' ? 'readable' : '!readable');

// Probe stdin FD flags on non-Windows without reading a payload.
const stdout = spawnSync('sh', ['-c', 'echo ${E2BIG:-x}' && '[ -r /dev/stdin ] && echo readable || echo unreadable'], { encoding: 'utf8' });
console.log('/dev/stdin probe:', stdout.stdout.trim(), 'stderr:', stdout.stderr.trim());

console.log('readFileSync(0, utf-8) after direct probe:', typeof readFileSync(0, 'utf8') === 'string' ? 'readable' : '!readable');
JS

Repository: MCPJam/inspector

Length of output: 484


Avoid using readFileSync(0) for --file - stdin input.

This CLI path advertises -, --file -, and --json -, but reading fd 0 synchronously is platform-fragile: non-blocking stdin such as delayed or chained pipes can surface EAGAIN/other Node fs exceptions instead of the intended clean usage error. Consider reading through process.stdin asynchronously and validating the resulting payload before executing the command.

🤖 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 `@cli/src/commands/environments.ts` around lines 108 - 119, The --file "-" path
in the environment command currently uses synchronous fd 0 access, which can
fail for piped or delayed stdin. Update the options.file handling to read stdin
asynchronously through process.stdin, while retaining synchronous file reads for
regular paths; validate the collected stdin payload before command execution and
convert read failures into the existing usageError format.

Comment on lines +6097 to +6119
"serverAttachmentId": {
"type": "string",
"nullable": true,
"description": "`null` clears the pin and falls back to the host config's servers."
},
"skillSelection": {
"allOf": [
{
"$ref": "#/components/schemas/EnvironmentSkillSelection"
}
],
"nullable": true,
"description": "`null` clears the pinned skill selection."
},
"pluginVersionIds": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
},
"nullable": true,
"description": "Pinned plugin VERSION IDs. Narrow by design: the plugin must be installed and enabled, the version must be `ready`, at most one version per plugin may be pinned, and none of its skills may carry supporting files. Not a general-purpose plugin list. `null` clears all pins."
}

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the declared OpenAPI version and inventory the mixed nullability idioms.
fd -t f 'openapi.json' | while read -r f; do
  echo "== $f"
  jq -r '.openapi // "NO openapi FIELD"' "$f"
  echo "-- occurrences of 3.0-only \"nullable\":"
  jq '[paths as $p | getpath($p) | select(type=="object" and has("nullable"))] | length' "$f"
  echo "-- occurrences of 3.1-only \"const\":"
  jq '[paths as $p | getpath($p) | select(type=="object" and has("const"))] | length' "$f"
  echo "-- occurrences of union type arrays (3.1-only):"
  jq '[paths as $p | getpath($p) | select(type=="object" and (.type|type=="array"))] | length' "$f"
done

# Locate any spec-drift/lint check that would have caught this.
rg -n --iglob '*.{json,yml,yaml,ts,js,mjs,sh}' -C2 'openapi.*(drift|lint|validate)|redocly|spectral' | head -50

Repository: MCPJam/inspector

Length of output: 321


🌐 Web query:

Is the nullable keyword valid in OpenAPI 3.1 or replaced by type null

💡 Result:

Yes, the nullable keyword has been removed in OpenAPI 3.1 [1][2][3]. In its place, OpenAPI 3.1 utilizes standard JSON Schema functionality, which allows for type arrays to define nullable values [4][3]. To specify that a field can be a particular type or null, you use the type keyword with an array containing the desired type and the null type [1][4]. For example, a property that accepts either a string or null should be defined as follows [4]: type: - "string" - "null" This approach is part of the deeper integration of JSON Schema into OpenAPI 3.1, which avoids adding custom, redundant keywords like nullable [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="docs/reference/openapi.json"

echo "== declared version =="
jq -r '.openapi' "$file"

echo "== surrounding pins (context lines) =="
sed -n '6088,6125p' "$file" | cat -n

echo "== referenced components =="
jq '{"EnvironmentSkillSelection": .components.schemas.EnvironmentSkillSelection}' "$file"

echo "== exact occurrences and line numbers for nullable fields =="
python3 - <<'PY'
import json
from pathlib import Path
file=Path("docs/reference/openapi.json")
data=json.loads(file.read_text())
for i, p in enumerate(data.get("paths", {}).items(), 1):
    pass
matches=[]
def walk(node, path=()):
    if isinstance(node, dict):
        if "nullable" in node:
            matches.append((*path, node))
        for k,v in node.items():
            walk(v, path+(k,))
    elif isinstance(node, list):
        for idx,v in enumerate(node):
            walk(v, path+(idx,))
walk(data)
ctx_paths=sorted(set("/".join(str(p) for p in m[:-1]) for m in matches))
for cp in ctx_paths:
    print(f"\n# {cp}")
PY

echo "== tooling references around openapi =="
rg -n --iglob '*.{json,yml,yaml,ts,js,mjs,sh}' -C2 'openapi|redocly|spectral|drift|lint|validate' | head -80 || true

Repository: MCPJam/inspector

Length of output: 2996


Express the documented null clears as OpenAPI 3.1 unions

This spec declares 3.1.0, so nullable should not be used. The documented null clears are invisible to validators/clients, while the same document already uses const and type unions for nullable semantics. For skillSelection, also avoid wrapping the optional $ref in allOf; use an anyOf/schema union with {"type": "null"}.

🤖 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 `@docs/reference/openapi.json` around lines 6097 - 6119, Update
serverAttachmentId, skillSelection, and pluginVersionIds to express nullability
as OpenAPI 3.1 type unions including {"type":"null"}, removing nullable: true.
For skillSelection, replace the allOf wrapper around EnvironmentSkillSelection
with an anyOf/schema union containing the $ref and null schema, while preserving
each field’s existing descriptions and constraints.

Comment on lines +68 to +83
type ResolvedEnvironmentRow = {
environmentRef: { environmentId: string; name: string; revision: number };
hostId: string;
hostName: string;
hostConfigId: string;
serverAttachmentId?: string;
selectedServerIds: string[];
effectiveServerIds: string[];
pluginVersions: Array<{
pluginId: string;
pluginVersionId: string;
name: string;
bundleHash: string;
}>;
servers: Array<{ serverId: string; name: string }>;
};

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for any shared or canonical definition of the resolveEnvironmentForLaunch return shape.
rg -n "resolveEnvironmentForLaunch" --type=ts -C3
rg -n "hostConfigId|hostName" mcpjam-inspector/server/services/evals/environment-launch.ts mcpjam-inspector/server/routes/v1/environments.ts

Repository: MCPJam/inspector

Length of output: 154


🏁 Script executed:

#!/bin/bash
set -u

echo "Files:"
git ls-files | rg 'mcpjam-inspector/server/services/evals/environment-launch\.ts|mcpjam-inspector/server/routes/v1/environments\.ts|convex|api|schema|query' || true

echo
echo "Search identifiers broadly:"
rg -n "resolveEnvironmentForLaunch|ResolvedEnvironmentForLaunch|ResolvedEnvironmentRow|hostConfigId|hostName|ResolvedEnviron" . || true

Repository: MCPJam/inspector

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -u

echo "Exact tracked files:"
for f in \
  "mcpjam-inspector/server/services/evals/environment-launch.ts" \
  "mcpjam-inspector/server/routes/v1/environments.ts"
do
  if git ls-files --error-unmatch "$f" >/dev/null 2>&1; then
    echo "OK $f"
  else
    echo "MISSING $f"
  fi
done

echo
echo "Target symbols in tracked files only:"
rg -n --glob '!node_modules/**' 'resolveEnvironmentForLaunch|ResolvedEnvironmentForLaunch|ResolvedEnvironmentRow|hostConfigId|hostName|toResolvedDto' \
  mcpjam-inspector/server || true

echo
echo "Show relevant slices if present:"
for f in \
  "mcpjam-inspector/server/services/evals/environment-launch.ts" \
  "mcpjam-inspector/server/routes/v1/environments.ts"
do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    wc -l "$f"
    sed -n '1,180p' "$f"
  fi
done

Repository: MCPJam/inspector

Length of output: 20698


🏁 Script executed:

#!/bin/bash
set -u

echo "Locate Convex environment function definitions:"
rg -n --glob '!node_modules/**' 'resolveEnvironmentForLaunch|projectEnvironments' . | rg '\.[tj]s$|\.ts$' | sed -n '1,220p'

echo
echo "Candidate files from rg (tracked):"
rg -l --glob '!node_modules/**' 'resolveEnvironmentForLaunch|projectEnvironments' . | sed -n '1,200p'

Repository: MCPJam/inspector

Length of output: 1346


🏁 Script executed:

#!/bin/bash
set -u

echo "Locate convex/projectEnvironments:"
fd -a 'projectEnvironments|environments' . | sed -n '1,160p'

echo
echo "Search backend-style convex references broadly:"
rg -n --glob '!node_modules/**' 'projectEnvironments:|projectEnvironments\.|import .*environment-launch|ResolvedEnvironmentRow|ResolvedEnvironmentForLaunch|resolveEnvironmentForLaunch' . | sed -n '1,240p'

echo
echo "If candidate convex files exist, show relevant slices:"
for f in $(fd 'projectEnvironments|environment' . | sed -n '1,40p'); do
  echo "--- $f ---"
  wc -l "$f" 2>/dev/null || true
  rg -n -C 8 'resolveEnvironmentForLaunch|ResolvedEnvironmentRow|ResolvedEnvironmentForLaunch|hostName|hostConfigId' "$f" 2>/dev/null | sed -n '1,220p' || true
done

Repository: MCPJam/inspector

Length of output: 25499


Align the resolve DTO with the launch resolver contract.

ResolvedEnvironmentRow treats hostName and hostConfigId as required, but the shared ResolvedEnvironmentForLaunch type — and its test — allow them to be omitted. Since toResolvedDto writes both fields unconditionally, an omitted backend value can be serialized as undefined or null instead of remaining absent. Mark those fields optional and conditionally emit them to match the launch resolver contract.

🤖 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/routes/v1/environments.ts` around lines 68 - 83, The
resolve DTO currently requires hostName and hostConfigId and unconditionally
serializes them. Update ResolvedEnvironmentRow and toResolvedDto so both fields
are optional and only included when the backend provides values, matching the
ResolvedEnvironmentForLaunch contract and preserving their absence otherwise.

Comment on lines +191 to +237
/**
* CRUD failures. The backend throws structured `ConvexError` data with a
* `code`, so branch on that first and fall back to message sniffing only for
* errors raised outside the module's own `fail()` helper.
*
* FORBIDDEN is deliberately split. "Not a project member" becomes 404 so the
* surface never confirms that a project id exists to someone outside it; the
* admin gate becomes a true 403, because the caller demonstrably CAN see the
* project and needs to know the block is their role, not a bad id.
*/
function translateConvexError(
error: unknown,
fallbackMessage = "Environment write rejected by the platform"
): WebRouteError {
if (error instanceof WebRouteError) return error;
const data = convexErrorData(error);
const code = typeof data?.code === "string" ? data.code : undefined;
const structuredMessage =
typeof data?.message === "string" ? data.message : undefined;

if (code === "CONFLICT") {
return new WebRouteError(
409,
ErrorCode.CONFLICT,
structuredMessage ?? "Environment changed since you loaded it."
);
}
if (code === "VALIDATION") {
return new WebRouteError(
400,
ErrorCode.VALIDATION_ERROR,
structuredMessage ?? fallbackMessage
);
}
if (code === "NOT_FOUND") {
return new WebRouteError(404, ErrorCode.NOT_FOUND, "Environment not found");
}
if (code === "FORBIDDEN") {
if (structuredMessage && /admin/i.test(structuredMessage)) {
return new WebRouteError(403, ErrorCode.FORBIDDEN, structuredMessage);
}
return new WebRouteError(
404,
ErrorCode.NOT_FOUND,
"Environment or project not found, or you do not have access to it."
);
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate environments.ts =="
fd -a 'environments\.ts$' . | sed 's#^\./##'

echo
echo "== relevant file slice =="
file="$(fd 'environments\.ts$' . | head -n1)"
wc -l "$file"
sed -n '1,280p' "$file"

echo
echo "== convex-error helper definitions/usages =="
rg -n "convexErrorData|failure|fail\(|ConvexError|FORBIDDEN|NOT_MEMBER|NOT_ADMIN|admin" -S .

Repository: MCPJam/inspector

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="mcpjam-inspector/server/routes/v1/environments.ts"
echo "== environments.ts translateConvexError slice =="
sed -n '146,260p' "$file"

echo
echo "== ConvexError construction in mcpjam workspace =="
rg -n "ConvexError|fail\\(|failures\\(\\)|\\.failures\\(" -g '*.{ts,tsx,js}' mcpjam-inspector/server mcpjam-inspector/src | head -n 120

echo
echo "== relevant tests for environments ConvexError mapping =="
test_file="mcpjam-inspector/server/routes/v1/__tests__/environments.test.ts"
if [ -f "$test_file" ]; then
  sed -n '120,350p' "$test_file"
fi

echo
echo "== focused backend files likely referencing environments fail/admin =="
rg -n "environments|environment|admin|member|owner|FORBIDDEN|failures" -g '*.{ts,tsx,js}' mcpjam-inspector/server | rg -n "environment|environments|FORBIDDEN|failures|admin|member|owner" | head -n 200

Repository: MCPJam/inspector

Length of output: 42070


Use structured FORBIDDEN sub-codes instead of message matching.

The current split between 404 and 403 relies on /admin/i.test(message), so a backend wording change can turn an admin-rejection into a misleading 404 even though the caller has project access. Emit backend sub-codes such as FORBIDDEN_NOT_MEMBER and FORBIDDEN_NOT_ADMIN to keep the route classification stable.

🤖 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/routes/v1/environments.ts` around lines 191 - 237,
Update translateConvexError to classify FORBIDDEN failures using structured
backend sub-codes, such as FORBIDDEN_NOT_ADMIN for the 403 response and
FORBIDDEN_NOT_MEMBER for the 404 response, instead of testing structuredMessage
with /admin/i. Preserve the existing fallback behavior for unrecognized
forbidden sub-codes.

Comment on lines +540 to +573
/**
* Map a launch-resolution failure onto the public envelope. The environment
* exists and is readable, but cannot currently produce a runnable
* configuration (a pinned plugin was disabled, the host was deleted, the
* closed server set came out empty) — that is a 409 conflict, not bad input,
* and the machine-readable `ENV_*` code rides along in `details` so callers can
* branch on the reason. Mirrors `/v1/projects/:p/environments/:e/resolve`.
*/
function translateEnvironmentResolveError(error: unknown): unknown {
if (error instanceof WebRouteError) return error;
const data = (error as { data?: unknown } | null)?.data;
if (data && typeof data === "object" && !Array.isArray(data)) {
const code = (data as { code?: unknown }).code;
const message = (data as { message?: unknown }).message;
if (typeof code === "string" && code.startsWith("ENV_")) {
if (code === "ENV_NOT_FOUND" || code === "ENV_CROSS_PROJECT") {
return new WebRouteError(
404,
ErrorCode.NOT_FOUND,
"Environment not found"
);
}
return new WebRouteError(
409,
ErrorCode.CONFLICT,
typeof message === "string"
? message
: "Environment cannot be launched right now.",
{ code }
);
}
}
return error;
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Fallback path skips runtime-error classification, unlike the route it mirrors.

The docstring says this "Mirrors /v1/projects/:p/environments/:e/resolve", but the fallback differs: environments.ts's translateResolveError delegates non-ENV_* errors to translateConvexError, which applies mapRuntimeError for timeout/network patterns (yielding 504/502). Here, Line 572 (return error;) returns the raw error unchanged for any non-WebRouteError, non-ENV_* failure — e.g. a transient Convex network error during resolution would propagate unclassified instead of surfacing as a proper 5xx/502/504.

🔧 Proposed fix to restore parity with the mirrored route
   return error;
 }
+
+// Fallback for callers of this function's `return error` path — see call site.

Simplest fix: replace the final return error; with return mapRuntimeError(error); (importing mapRuntimeError from ../web/errors.js) so an unclassified failure gets the same 502/504/500 treatment as elsewhere in the file.

🤖 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/routes/v1/evals.ts` around lines 540 - 573, Update
translateEnvironmentResolveError so its fallback delegates non-WebRouteError,
non-ENV_* failures to mapRuntimeError instead of returning the raw error. Import
mapRuntimeError from the existing web errors module and preserve the current
ENV_* mappings and WebRouteError passthrough.

…, OpenAPI shape

Review follow-ups on the Project Environments public surface.

Archiving frees the name, so a project can hold an archived "Staging" and a
live "Staging" at once. `resolveEnvironmentSelector` listed both and handed the
flat list to `resolveByIdOrName`, which called the name ambiguous — breaking
name-based get/resolve/update/archive even though live names are unique. It now
resolves a name against the side the operation is for (live for everything,
archived for restore, which targets an archived row by definition), falling
back to the whole listing so an archived-only name still resolves and the
not-found message still enumerates everything. An exact ID still wins, and a
name that is ambiguous within the preferred side still reports as ambiguous.

`EvalRunCreateRequest.anyOf` still required `serverIds` on every new-suite
shape, so generated clients and request validators rejected the newly supported
`suiteName` + `tests` + `environmentId` body. Added that alternative and
un-staled the `serverIds` / request descriptions.

Added the missing eval-route coverage for environment runs: the closed set is
connected (plugin-contributed servers included) and caller `serverIds` ignored,
the suite's saved selection is never consulted, the resolution is handed to
`prepareEvalRun` so the run pins that revision, a launch-resolution failure maps
to 409 with its `ENV_*` reason, and an unreadable environment maps to 404. Plus
SDK tests for the selector behavior above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai 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.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="mcpjam-inspector/server/routes/v1/__tests__/write-routes.test.ts">

<violation number="1" location="mcpjam-inspector/server/routes/v1/__tests__/write-routes.test.ts:461">
P2: `convexQueryMock.mockResolvedValue(RESOLVED_ENVIRONMENT)` in "accepts a new suite with an environment and no serverIds" makes ALL Convex queries resolve to the environment object. If the route handler or a service it calls makes additional queries (e.g., inside `resolveEnvironmentForLaunch`), they silently get `RESOLVED_ENVIRONMENT` too, masking misrouted or missing queries. The other test in this block uses `mockImplementation` with a discriminator, which is the safer pattern.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


it("accepts a new suite with an environment and no serverIds", async () => {
mockHappyCreate();
convexQueryMock.mockResolvedValue(RESOLVED_ENVIRONMENT);

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: convexQueryMock.mockResolvedValue(RESOLVED_ENVIRONMENT) in "accepts a new suite with an environment and no serverIds" makes ALL Convex queries resolve to the environment object. If the route handler or a service it calls makes additional queries (e.g., inside resolveEnvironmentForLaunch), they silently get RESOLVED_ENVIRONMENT too, masking misrouted or missing queries. The other test in this block uses mockImplementation with a discriminator, which is the safer pattern.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At mcpjam-inspector/server/routes/v1/__tests__/write-routes.test.ts, line 461:

<comment>`convexQueryMock.mockResolvedValue(RESOLVED_ENVIRONMENT)` in "accepts a new suite with an environment and no serverIds" makes ALL Convex queries resolve to the environment object. If the route handler or a service it calls makes additional queries (e.g., inside `resolveEnvironmentForLaunch`), they silently get `RESOLVED_ENVIRONMENT` too, masking misrouted or missing queries. The other test in this block uses `mockImplementation` with a discriminator, which is the safer pattern.</comment>

<file context>
@@ -371,6 +371,163 @@ describe("v1 write routes", () => {
+
+      it("accepts a new suite with an environment and no serverIds", async () => {
+        mockHappyCreate();
+        convexQueryMock.mockResolvedValue(RESOLVED_ENVIRONMENT);
+
+        const res = await request(
</file context>

@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: b62b4fba66

ℹ️ 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".

.strictObject({
expectedRevision: z.number().int().nonnegative(),
name: z.string().trim().min(1).optional(),
description: z.string().optional(),

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 Accept null when clearing descriptions

When an existing description needs to be removed, this schema rejects description: null with a 400, even though the established backend-facing editor sends exactly that clear value (ProjectEnvironmentEditor.tsx lines 189-190) and its mutation type explicitly permits it (useProjectEnvironments.ts line 106). The documented empty string is forwarded unchanged rather than translated to the backend's clear sentinel, so API and SDK callers cannot reliably clear this field; make description nullable and cover this null case alongside the other clearable fields.

AGENTS.md reference: mcpjam-inspector/AGENTS.md:L72-L74

Useful? React with 👍 / 👎.

@chelojimenez
chelojimenez merged commit 09dac8d into main Jul 25, 2026
15 checks passed
@chelojimenez
chelojimenez deleted the feat/environments-public-surface branch July 25, 2026 20:11

This branch was successfully deployed

2 active deployments
staging - docs — b62b4fba Deployed Jul 25, 2026 by mintlify[bot]
preview-pr-3425 — b62b4fba Deployed Jul 25, 2026 by chelojimenez via upsert-preview #8792
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.

1 participant