Skip to content

feat(api): headless agent turn endpoint (POST /api/v1/projects/:projectId/agent) - #3629

Merged
chelojimenez merged 3 commits into
mainfrom
worktree-slack-agent-endpoint
Aug 2, 2026
Merged

feat(api): headless agent turn endpoint (POST /api/v1/projects/:projectId/agent)#3629
chelojimenez merged 3 commits into
mainfrom
worktree-slack-agent-endpoint

Conversation

@chelojimenez

@chelojimenez chelojimenez commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

What

A new public-API terminal for the shared agent engine: POST /api/v1/projects/:projectId/agent runs ONE assistant turn over a caller-supplied message history and returns the reply, invoked operations, created-resource references (with app deep links), and token usage. First consumer: the MCPJam Slack app (separate repo/PR) — Slack collects the thread, calls this endpoint, posts the reply.

Design decisions (from the reviewed plan)

  • One engine, another terminal: composes prepareChatV2resolveTurnRuntimerunUnifiedAssistantTurn (streamSink: "none", persistMode: "caller", approvalMode: "auto-deny"), modeled on drainAssistantTurn. No forked engine logic.
  • Spend ops excluded: the tool surface is platform-op reads + atomic create_eval_suite. run_*/cancel_*/generate_eval_cases are deliberately absent — auto-deny has no interactive fallback, so an unattended turn must not spend eval quota/credits. Runs stay an explicit caller action (POST /eval-runs).
  • Hard project clamp: every op input is clamped to the route's projectId; an explicit foreign selector is rejected. The in-app WORKSPACE_OPERATIONS gate (isMcpjamToolId) is untouched — asserted by test.
  • Auth: the delegated org-scoped Convex JWT (getConvexBearerForRequest) backs both the engine's /stream calls (JWT-only) and the self-dispatched platform-op calls (keeps agent tool calls out of the caller's per-key sk_ rate bucket). Billing rides the path projectId on the hosted rail.
  • Static system prompt (cacheable prefix), pinned hosted model (anthropic/claude-sonnet-5), docs MCP server with preflight-degrade, skillsSource: none, no tasks seam, no chat persistence.
  • Caps: 4 concurrent turns/org → 429; 12 steps; 90 s wall clock → 504; 50 messages × 8 KB input limits. Engine cap/quota errors map to RATE_LIMITED; missing turnTrace → INTERNAL_ERROR; missing backend wiring (OSS installs) → FEATURE_NOT_SUPPORTED.
  • createdResources are collected via an execution wrapper before the 24 KB model-facing cap can truncate ids.
  • New server-authoritative telemetry event api_agent_turn_completed (names/counts/durations only — never args/outputs).

Security notes

  • Guest-denied by default (no GUEST_ALLOWED_V1_RULES entry).
  • Project authorization enforced by Convex through the delegated token (no gateway membership scan); org isolation unchanged.
  • The clamp means prompt injection in a Slack thread cannot roam the model to another project; worst case within-project is authoring (free) — never running — evals.

Tests

13 new tests (agent.test.ts): auth/guest gating, schema limits, deployment guard, delegated-JWT plumbing, docs-degrade, spend-cap → 429 mapping, concurrency cap, project clamp, pre-truncation collector, op-list/gate assertions. Full v1 suite + built-in tools: 247 passed. openapi-drift green (new Agent tag + schemas).

🤖 Generated with Claude Code


Note

High Risk
New public surface spends hosted model credits, can create eval suites on non-idempotent turns, and relies on project clamping plus delegated JWT for authorization—mistakes in tool scope or retry guidance could cause duplicate resources or cross-tenant issues.

Overview
Adds POST /api/v1/projects/:projectId/agent, a synchronous public API for one headless assistant turn over caller-owned messages, wired through the existing prepareChatV2resolveTurnRuntimerunUnifiedAssistantTurn path (streamSink: "none", persistMode: "caller", approvalMode: "auto-deny").

The route exposes a curated platform-op tool set (reads plus create_eval_suite only—no run/cancel/generate), hard-clamps every op to the path projectId, strips cross-project metadata, and returns reply, toolCalls, createdResources (with deep links), and usage. Auth uses a delegated Convex JWT for the engine and in-process v1 op calls; guests are denied; hosted-only with caps (4 concurrent turns/org, ~90s, message byte limits) and partial-state details.createdResources on failures.

Also documents the Agent tag in OpenAPI/public-api.mdx, exports capForModel/toToolError for reuse, adds api_agent_turn_completed analytics, and ships agent.test.ts coverage for gating, limits, concurrency, and the tool adapter.

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


Summary by cubic

Adds a new headless agent endpoint POST /api/v1/projects/:projectId/agent that runs one assistant turn on a pinned hosted model and returns the reply, invoked operations, created-resource links, and token usage. First consumer is the MCPJam Slack app; preflight, limits, and retry guidance were tightened.

  • New Features
    • One-turn agent over caller-supplied messages. Responds with reply, toolCalls, createdResources, and usage. Not idempotent — dedupe on trigger identity and check details.createdResources before retrying.
    • Tool surface: read-only platform ops plus create_eval_suite. Run/cancel and generation ops are excluded; approvalMode: "auto-deny".
    • Project clamp: all op inputs are forced to the route projectId; foreign selectors are rejected. Tool schemas advertise project as optional; otherProjects metadata is stripped from results.
    • Auth: uses a delegated org-scoped Convex JWT for both engine and platform-op calls. Billing is per path projectId.
    • Model and prompt: pinned hosted model anthropic/claude-sonnet-5, static system prompt, docs MCP server with a 5s preflight raced against the turn’s abort signal (bounded, timer disarmed on settle). Authored suites default to anthropic/claude-haiku-4.5.
    • Limits and errors: 4 concurrent turns/org → 429 (JWT callers fall back to a project-scoped bucket), 12 steps, ~90s wall clock → 504. Input caps: 50 messages × 8 KB, 8192 bytes per message, and 96 KB total history. Engine cap/quota map to RATE_LIMITED; missing turnTraceINTERNAL_ERROR; missing hosted wiring → FEATURE_NOT_SUPPORTED. Caller disconnects abort the turn. Failed/timed-out turns include details.createdResources.
    • Pre-truncation collector for createdResources (e.g., eval suites with app deep links).
    • New telemetry event api_agent_turn_completed. OpenAPI and public-api.mdx updated (Agent tag and schemas; documented 500 response; byte/concurrency caps). Tests: 21 cases for route, limits, clamp, preflight, disconnect abort, and created-on-failure behavior.

Written for commit 9d1ff5d. Summary will update on new commits.

Review in cubic

…rn for the Slack app

One assistant turn over a caller-supplied message history, run through the
shared engine facade (runUnifiedAssistantTurn) on the hosted /stream rail
with a pinned hosted model, billed to the path project.

- Tools: platform-op reads + atomic create_eval_suite only. Run/cancel and
  generate_eval_cases (spend ops) are excluded — runs stay human-gated
  caller-side via POST /eval-runs. Every op input is hard-clamped to the
  route projectId; the in-app WORKSPACE_OPERATIONS gate is untouched.
- Auth: the delegated org-scoped Convex JWT backs both the engine's
  /stream calls and the self-dispatched platform-op calls (keeps the
  agent's own tool calls out of the caller's per-key rate bucket).
- Docs MCP server mounted with preflight-degrade; skillsSource none;
  no tasks seam; no chat-session persistence (caller owns transcript).
- Caps: 4 concurrent turns/org (429), 12 steps, 90s wall clock (504);
  strict input limits (50 msgs × 8KB). Engine cap/quota failures map to
  RATE_LIMITED; missing turnTrace maps to INTERNAL_ERROR.
- createdResources collected via an execution wrapper BEFORE the 24KB
  model-facing result cap, with app deep links.
- Server-authoritative telemetry event api_agent_turn_completed
  (names/counts/durations only).
- OpenAPI + public-api.mdx entries; 13 route/tool-adapter tests.

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

mintlify Bot commented Aug 2, 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 Aug 2, 2026, 10:02 PM

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

@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. enhancement New feature or request labels Aug 2, 2026
@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0f689021-b97c-47c6-8387-dfaf9406d3ba)

@dosubot

dosubot Bot commented Aug 2, 2026

Copy link
Copy Markdown

📄 Knowledge review

Dosu skipped reviewing this PR because your organization has used its 200 included credits for the month. Your usage will reset on 2026-09-01. To have Dosu review this PR before then, ask your organization admin to upgrade to a pro account.


Leave Feedback Ask Dosu about inspector Add Dosu to your team

@chelojimenez

chelojimenez commented Aug 2, 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.

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Internal preview

Preview URL will appear in Railway after the deploy finishes.
Deployed commit: 2a29eec
PR head commit: 9d1ff5d
Backend target: staging fallback.
Access is employee-only in non-production environments.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds an authenticated POST /projects/{projectId}/agent endpoint for synchronous, project-scoped assistant turns. The route validates bounded conversation history, uses hosted runtime execution, exposes read operations and atomic eval-suite creation, and returns replies, tool calls, created resources, and token usage. It adds documentation, route mounting, server telemetry, exported tool helpers, and tests for authorization, validation, execution, limits, scoping, and resource collection.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Unexpected top-level property "__esModule".

    at ConfigValidator.validateConfigSchema (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/soundcheck/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/soundcheck/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/soundcheck/node_modules/eslint/lib/cli.js:410:24)
    at async main (/soundcheck/node_modules/eslint/bin/eslint.js:152:22)

mcpjam-inspector/server/routes/v1/agent.ts

Oops! Something went wrong! :(

ESLint: 8.57.1

Error: ESLint configuration in --config is invalid:

  • Unexpected top-level property "__esModule".

    at ConfigValidator.validateConfigSchema (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2177:19)
    at ConfigArrayFactory._normalizeConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3019:19)
    at ConfigArrayFactory._loadConfigData (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2984:21)
    at ConfigArrayFactory.loadFile (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2850:40)
    at createCLIConfigArray (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3660:35)
    at new CascadingConfigArrayFactory (/soundcheck/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3735:29)
    at new CLIEngine (/soundcheck/node_modules/eslint/lib/cli-engine/cli-engine.js:617:36)
    at new ESLint (/soundcheck/node_modules/eslint/lib/eslint/eslint.js:430:27)
    at Object.execute (/soundcheck/node_modules/eslint/lib/cli.js:410:24)
    at async main (/soundcheck/node_modules/eslint/bin/eslint.js:152:22)


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: 3

🧹 Nitpick comments (4)
mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts (2)

292-353: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore the operation spies unconditionally.

Both tests call vi.spyOn on listProjectServersOperation and createEvalSuiteOperation, which are shared singletons imported from @mcpjam/sdk/platform. mockRestore() runs on the last line of each test, so a failing assertion above it leaves the spy attached for every subsequent test in the worker. The afterEach(vi.clearAllMocks) at Line 163 is scoped to the first describe and does not cover this block. An afterEach with vi.restoreAllMocks() makes the teardown unconditional.

Based on learnings, the 30_000 fixture is scenario data for the truncation path, not a canonical cap, so I make no claim about MODEL_OUTPUT_CAP from it.

🧪 Proposed fix
 describe("agent tool surface", () => {
+  afterEach(() => {
+    vi.restoreAllMocks();
+  });
+
   it("keeps spend ops out of the op list and the in-app gate unchanged", () => {
🤖 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__/agent.test.ts` around lines 292 -
353, Add an afterEach teardown for these tests that calls vi.restoreAllMocks(),
ensuring spies on listProjectServersOperation.execute and
createEvalSuiteOperation.execute are restored even when assertions fail. Remove
the per-test mockRestore calls if the shared teardown makes them redundant.

Source: Learnings


251-278: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the 504 TIMEOUT path.

The suite exercises every documented failure mapping except the wall-clock one. agent.ts Lines 439-450 return TIMEOUT when abortController.signal.aborted is true after the turn resolves, and both docs/reference/openapi.json (Line 3446) and the public guide publish that 504. A mock that aborts the supplied abortSignal before resolving would pin the branch down without fake timers.

🧪 Sketch
+  it("maps a wall-clock abort to TIMEOUT", async () => {
+    runUnifiedAssistantTurnMock.mockImplementation(async (opts: any) => {
+      (opts.abortSignal as AbortSignal).dispatchEvent?.(new Event("abort"));
+      // Simpler: have the route's controller abort by stubbing the timer,
+      // or expose the signal and abort it here before resolving.
+      return okTurnResult({ aborted: true });
+    });
+    const res = await turnRequest(makeApp(), OK_BODY);
+    expect(res.status).toBe(504);
+    const body = (await res.json()) as { code: string };
+    expect(body.code).toBe("TIMEOUT");
+  });
🤖 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__/agent.test.ts` around lines 251 -
278, Add a test covering the timeout mapping in the agent request suite by
making runUnifiedAssistantTurnMock abort the supplied abortSignal before
resolving successfully, then assert turnRequest returns HTTP 504 with the
TIMEOUT response. Keep the mock scoped to this test and preserve existing
concurrency and failure-mapping tests.
mcpjam-inspector/server/routes/v1/agent.ts (2)

230-243: 🩺 Stability & Availability | 🔵 Trivial

The concurrency cap is per process, not per organization.

activeTurnsByOrg is module state. If the inspector runs more than one instance behind a load balancer, the effective ceiling becomes 4 × instances, while docs/reference/openapi.json (Line 3376) and docs/reference/public-api.mdx (Line 282) both promise a flat 4 per organization. The code comment acknowledges the in-process scope; the public contract does not. Either soften the documented wording to "approximately" or move the counter to shared state when the deployment scales horizontally.

🤖 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/agent.ts` around lines 230 - 243, Align the
concurrency limit with the documented per-organization contract by replacing
module-local activeTurnsByOrg tracking with shared state that coordinates
acquireTurnSlot and releaseTurnSlot across all inspector instances; otherwise
update the referenced OpenAPI and public API documentation to explicitly
describe the limit as approximate and process-local.

211-228: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider a total-history character budget alongside the per-message cap.

MAX_MESSAGES and MAX_MESSAGE_CHARS bound each message, but nothing bounds their product. A caller may send 50 messages of 8,000 characters each, so one request can carry roughly 400,000 characters of prompt into a hosted turn billed to the project. The concurrency cap limits parallel turns, not the size of any single one. A summed budget check after parsing would close that gap cheaply.

♻️ Sketch
 const agentTurnSchema = z.object({
   messages: z
     .array(
       z.object({
         role: z.enum(["user", "assistant"]),
         content: z.string().min(1).max(MAX_MESSAGE_CHARS),
       })
     )
     .min(1)
-    .max(MAX_MESSAGES),
+    .max(MAX_MESSAGES)
+    .refine(
+      (messages) =>
+        messages.reduce((sum, m) => sum + m.content.length, 0) <=
+        MAX_TOTAL_HISTORY_CHARS,
+      { error: "Conversation history is too large for one turn." }
+    ),
 });
🤖 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/agent.ts` around lines 211 - 228, Update
agentTurnSchema validation to enforce a total character budget across all
message content in addition to the existing per-message and message-count
limits. Add a post-parse/refinement check over the messages array, using a named
aggregate-history limit, and reject requests whose summed content exceeds that
limit before the hosted turn is started.
🤖 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 `@docs/reference/openapi.json`:
- Around line 3400-3449: Add a 500 response entry to the operation’s responses
map, referencing the existing internal-error response component used for
INTERNAL_ERROR outcomes. Keep the current 400, 401, 422, 429, and 504 response
definitions unchanged.

In `@mcpjam-inspector/server/routes/v1/agent.ts`:
- Around line 349-373: Update the docs preflight around manager.listTools in the
wall-clock-controlled turn flow so it observes the existing abortController
signal and cannot outlive TURN_WALL_CLOCK_MS. Race the preflight against that
signal or allocate only the remaining turn budget, preserving the current
selected-server and warning behavior while ensuring an expired budget propagates
to the existing 504 handling.
- Around line 494-500: Harden the cleanup in the finally block around
manager.disconnectAllServers(): normalize both synchronous throws and promise
rejections so cleanup cannot override the successful response, and log any
disconnect failure at debug level instead of silently swallowing it. Preserve
the existing timeout clearing and turn-slot release behavior.

---

Nitpick comments:
In `@mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts`:
- Around line 292-353: Add an afterEach teardown for these tests that calls
vi.restoreAllMocks(), ensuring spies on listProjectServersOperation.execute and
createEvalSuiteOperation.execute are restored even when assertions fail. Remove
the per-test mockRestore calls if the shared teardown makes them redundant.
- Around line 251-278: Add a test covering the timeout mapping in the agent
request suite by making runUnifiedAssistantTurnMock abort the supplied
abortSignal before resolving successfully, then assert turnRequest returns HTTP
504 with the TIMEOUT response. Keep the mock scoped to this test and preserve
existing concurrency and failure-mapping tests.

In `@mcpjam-inspector/server/routes/v1/agent.ts`:
- Around line 230-243: Align the concurrency limit with the documented
per-organization contract by replacing module-local activeTurnsByOrg tracking
with shared state that coordinates acquireTurnSlot and releaseTurnSlot across
all inspector instances; otherwise update the referenced OpenAPI and public API
documentation to explicitly describe the limit as approximate and process-local.
- Around line 211-228: Update agentTurnSchema validation to enforce a total
character budget across all message content in addition to the existing
per-message and message-count limits. Add a post-parse/refinement check over the
messages array, using a named aggregate-history limit, and reject requests whose
summed content exceeds that limit before the hosted turn is started.
🪄 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: 3f4d474a-852f-40c1-8695-d258c0a980bb

📥 Commits

Reviewing files that changed from the base of the PR and between c2a459a and 644a366.

📒 Files selected for processing (7)
  • docs/reference/openapi.json
  • docs/reference/public-api.mdx
  • mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts
  • mcpjam-inspector/server/routes/v1/agent.ts
  • mcpjam-inspector/server/routes/v1/index.ts
  • mcpjam-inspector/server/utils/built-in-tools/mcpjam.ts
  • mcpjam-inspector/shared/analytics-events.ts

Comment thread docs/reference/openapi.json
Comment thread mcpjam-inspector/server/routes/v1/agent.ts Outdated
Comment thread mcpjam-inspector/server/routes/v1/agent.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: 644a366a6c

ℹ️ 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 +445 to +449
return v1Error(
c,
"TIMEOUT",
`Agent turn exceeded the ${TURN_WALL_CLOCK_MS / 1000}s limit.`
);

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 Preserve resources created before a failed turn

If create_eval_suite completes and the subsequent model step times out, created already contains the persisted suite, but this error response discards it; the missing-turnTrace branch below does the same after a post-tool engine failure. A caller retrying the apparently failed request can therefore create duplicate suites without ever receiving the first suite's ID. Return the collected resource references in these error responses or otherwise make the mutation idempotent.

Useful? React with 👍 / 👎.

for (const operation of AGENT_API_OPERATIONS) {
tools[operation.name] = tool({
description: `${operation.description} (Scoped to the current project automatically.)`,
inputSchema: operation.inputSchema,

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 Hide the required project selector from get_eval_run

getEvalRunOperation is the one included operation whose input schema requires project, so reusing its schema tells the model that this field is mandatory while the system prompt tells it to omit the field and never exposes the route's project ID. For a user who supplies only a run ID, the model must either violate the advertised schema or invent a selector that this wrapper rejects, making the promised run lookup unreliable unless an earlier tool happened to reveal the exact project ID. Adapt this schema so project is optional or absent, then inject the path project as the wrapper already does.

Useful? React with 👍 / 👎.

@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 7 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="mcpjam-inspector/server/routes/v1/agent.ts">

<violation number="1" location="mcpjam-inspector/server/routes/v1/agent.ts:306">
P1: Concurrent JWT-authenticated turns across every organization share the four-slot `anonymous` bucket, so one tenant can exhaust this endpoint globally. Derive an authenticated organization key for JWT callers or use a project-scoped fallback instead of a shared anonymous key.</violation>
</file>

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

Re-trigger cubic

);

const orgKey =
c.get("mcpjamOrganizationId") ?? c.get("workosUserId") ?? "anonymous";

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: Concurrent JWT-authenticated turns across every organization share the four-slot anonymous bucket, so one tenant can exhaust this endpoint globally. Derive an authenticated organization key for JWT callers or use a project-scoped fallback instead of a shared anonymous key.

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/agent.ts, line 306:

<comment>Concurrent JWT-authenticated turns across every organization share the four-slot `anonymous` bucket, so one tenant can exhaust this endpoint globally. Derive an authenticated organization key for JWT callers or use a project-scoped fallback instead of a shared anonymous key.</comment>

<file context>
@@ -0,0 +1,538 @@
+  );
+
+  const orgKey =
+    c.get("mcpjamOrganizationId") ?? c.get("workosUserId") ?? "anonymous";
+  if (!acquireTurnSlot(orgKey)) {
+    return v1Error(
</file context>

Comment thread mcpjam-inspector/server/routes/v1/agent.ts
Comment thread mcpjam-inspector/server/routes/v1/agent.ts Outdated
Comment thread mcpjam-inspector/server/routes/v1/agent.ts Outdated
Comment thread mcpjam-inspector/server/routes/v1/agent.ts
Comment thread mcpjam-inspector/server/routes/v1/agent.ts Outdated
Comment thread mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts
Comment thread docs/reference/openapi.json
- Docs preflight bounded: raced against a 5s deadline + the turn's abort
  signal, so a slow docs server can no longer stack its 30s connect
  timeout on top of the 90s turn budget (coderabbit major, cubic P2).
- JWT callers no longer share one global 'anonymous' concurrency bucket —
  fall back to a project-scoped key (cubic P1).
- Failed/timed-out turns surface already-persisted suites in the error
  body's details.createdResources, so retries don't double-create
  (codex P1, cubic P2); documented in the OpenAPI description.
- Advertised tool schemas mark project optional even where the op
  requires it (get_eval_run), removing the schema-vs-prompt contradiction
  (codex/cubic P2).
- otherProjects switching metadata stripped from op results — a clamped
  surface must not disclose the org's other projects (cubic P2).
- Message quota enforced in BYTES (8192) as well as chars — the char-only
  cap was 4x bypassable with multibyte text (cubic P2).
- finally-cleanup guarded against synchronous throws (coderabbit minor).
- OpenAPI documents the 500 response (cubic/coderabbit P3).
- Tests: +6 (docs-available path, hung-preflight degrade under fake
  timers, byte cap, created-on-failure details, optional-project schema,
  otherProjects strip) — 19 in agent.test.ts, full v1 suite 253 green.

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

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_211f1ae5-54e1-4e4a-8c52-bfd5381b51b4)

@chelojimenez

Copy link
Copy Markdown
Contributor Author

Pushed 24c2fa6 addressing all bot review findings:

  • Docs preflight bounded (coderabbit major / cubic P2): raced against a 5 s deadline + the turn's abort signal — a slow docs server can't stack its 30 s connect timeout on the 90 s budget anymore.
  • cubic P1 — shared anonymous concurrency bucket: JWT callers now fall back to a project-scoped bucket key.
  • codex P1 / cubic P2 — created resources lost on failure: failed/timed-out turns now carry already-persisted suites in details.createdResources (documented in the OpenAPI description) so retries don't double-create.
  • codex/cubic P2 — get_eval_run schema contradiction: advertised schemas mark project optional wherever the op requires it; the clamp fills it in.
  • cubic P2 — otherProjects disclosure: switching metadata is stripped from op results before they reach the model.
  • cubic P2 — multibyte quota bypass: message quota now enforced in bytes (8192) as well as chars.
  • coderabbit minor — finally hardening: teardown guarded against synchronous throws.
  • P3s: OpenAPI documents the 500 response; tests now cover the docs-available path (plus hung-preflight degrade under fake timers).

agent.test.ts is up to 19 tests; full v1 + built-in-tools suites: 253 passed. openapi-drift green.

@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: 24c2fa6b72

ℹ️ 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 +435 to +438
const deadline = setTimeout(() => {
logger.warn("[v1/agent] docs MCP preflight timed out; continuing");
resolve(false);
}, DOCS_PREFLIGHT_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 Cancel the losing docs preflight deadline

When manager.listTools() succeeds or rejects before five seconds, Promise.race settles but this deadline remains armed, so it later emits a false “preflight timed out” warning for every normally completed preflight (and duplicates the warning on fast failures). Because server warnings are forwarded to Sentry, this creates misleading production alerts; retain the timer handle and clear it when either preflight branch wins.

AGENTS.md reference: mcpjam-inspector/AGENTS.md:L22-L22

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/reference/openapi.json (1)

6508-6617: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the content byte cap in AgentTurnRequest.

AgentTurnRequest only advertises maxLength: 8000, but v1/agent.ts also enforces Buffer.byteLength(..., "utf8") <= 8192, so a schema-valid 8,000-character UTF-8 message can still return 400. Add the byte limit to the OpenAPI property contract so callers know both constraints.

🤖 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 6508 - 6617, Update the
AgentTurnRequest.messages item content schema to document the enforced UTF-8
byte limit alongside maxLength: 8000. Add the appropriate OpenAPI byte-cap
constraint to the content property while preserving its existing
character-length validation.
🧹 Nitpick comments (1)
mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts (1)

323-341: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cover project-scoped concurrency isolation.

The test at Line 323 sends every request to p1. An organization-wide bucket would pass this test, even though it violates the required project-scoped behavior. Hold four p1 turns, then start a p2 turn with the same delegated caller and assert that it reaches the engine.

As per PR objectives, use project-scoped concurrency buckets for JWT callers.

🤖 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__/agent.test.ts` around lines 323 -
341, Extend the “caps concurrent turns per organization” test to verify
project-scoped isolation for JWT callers: keep four held turns targeting project
p1, then issue a turn for project p2 using the same delegated caller and assert
it reaches the engine successfully rather than receiving 429. Ensure the test
releases all held gates and awaits the added request while preserving the
existing p1 saturation assertion.
🤖 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 `@mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts`:
- Around line 397-416: The test mock for listProjectServersOperation.execute
uses the wrong result property. Update the mock in “strips otherProjects
switching metadata from results” to return items: [] per the operation contract,
and assert result.items equals [] while retaining the otherProjects omission
assertion.

---

Outside diff comments:
In `@docs/reference/openapi.json`:
- Around line 6508-6617: Update the AgentTurnRequest.messages item content
schema to document the enforced UTF-8 byte limit alongside maxLength: 8000. Add
the appropriate OpenAPI byte-cap constraint to the content property while
preserving its existing character-length validation.

---

Nitpick comments:
In `@mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts`:
- Around line 323-341: Extend the “caps concurrent turns per organization” test
to verify project-scoped isolation for JWT callers: keep four held turns
targeting project p1, then issue a turn for project p2 using the same delegated
caller and assert it reaches the engine successfully rather than receiving 429.
Ensure the test releases all held gates and awaits the added request while
preserving the existing p1 saturation assertion.
🪄 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: e789af58-20f9-4e15-858e-83dd99f0f4a9

📥 Commits

Reviewing files that changed from the base of the PR and between 644a366 and 24c2fa6.

📒 Files selected for processing (3)
  • docs/reference/openapi.json
  • mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts
  • mcpjam-inspector/server/routes/v1/agent.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • mcpjam-inspector/server/routes/v1/agent.ts

Comment thread mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts

@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 3 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/agent.ts">

<violation number="1" location="mcpjam-inspector/server/routes/v1/agent.ts:365">
P1: Unauthenticated or JWT callers can bypass the four-turn gate by varying `projectId`; a member can also multiply capacity across projects in one org. Authenticate/resolve an organization before acquiring the slot, then use that organization as the key rather than an untrusted path parameter.</violation>
</file>

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

Re-trigger cubic

const orgKey =
c.get("mcpjamOrganizationId") ??
c.get("workosUserId") ??
`project:${projectId}`;

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: Unauthenticated or JWT callers can bypass the four-turn gate by varying projectId; a member can also multiply capacity across projects in one org. Authenticate/resolve an organization before acquiring the slot, then use that organization as the key rather than an untrusted path parameter.

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/agent.ts, line 365:

<comment>Unauthenticated or JWT callers can bypass the four-turn gate by varying `projectId`; a member can also multiply capacity across projects in one org. Authenticate/resolve an organization before acquiring the slot, then use that organization as the key rather than an untrusted path parameter.</comment>

<file context>
@@ -302,8 +355,14 @@ agent.post("/projects/:projectId/agent", async (c) => {
-    c.get("mcpjamOrganizationId") ?? c.get("workosUserId") ?? "anonymous";
+    c.get("mcpjamOrganizationId") ??
+    c.get("workosUserId") ??
+    `project:${projectId}`;
   if (!acquireTurnSlot(orgKey)) {
     return v1Error(
</file context>

Comment thread mcpjam-inspector/server/routes/v1/agent.ts Outdated
Comment thread mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts
…sconnect fixes

- Retry contract made explicit: the operation is documented (route
  docblock, OpenAPI, public-api.mdx) as NOT idempotent with a
  no-blind-retry policy — dedupe on trigger identity, check
  details.createdResources before retrying. Durable Idempotency-Key
  support through the mutation path is called out as the prerequisite
  for going beyond dogfood.
- Aggregate history budget: 96 KB total UTF-8 cap on the message array
  (per-message caps alone admitted ~400 KB per request).
- Docs preflight timer disarmed when the race settles — a successful
  preflight no longer leaves a live 5s timer emitting a false timeout
  warning.
- Caller disconnects propagate: c.req.raw.signal aborts the turn, so an
  abandoned request stops consuming model capacity.
- Teardown failures are logged (observably detached, not silently
  swallowed).
- Suite default model decided: authored suites default to
  anthropic/claude-haiku-4.5 (the cheap eval workhorse), not the agent's
  own Sonnet 5 — suites run every case × iteration on schedules.
- OpenAPI: byte caps + process-local concurrency documented.
- Test fixtures corrected to list_project_servers' real `items` field;
  +2 tests (aggregate budget, disconnect abort) — 21 in agent.test.ts,
  full v1 suite 255 green.

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

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_1cf3e607-26c2-429c-80b7-1bd812a31579)

@chelojimenez

Copy link
Copy Markdown
Contributor Author

Pushed 9d1ff5d for the round-2 review:

  1. Idempotency: took the documented-policy path for v1 — route docblock, OpenAPI, and public-api.mdx now state the operation is not idempotent with an explicit no-blind-retry contract (dedupe on trigger identity; check details.createdResources before any retry). The Slack client makes exactly one attempt per call and says so. Durable Idempotency-Key through the mutation path is called out as the gate for going beyond dogfood (it needs backend-side dedupe — separate PR).
  2. Aggregate history cap: 96 KB total UTF-8 budget on the message array, on top of the per-message caps (tested).
  3. Preflight timer: the losing 5 s timer is disarmed in a .finally when the race settles — no more false timeout warnings after a successful preflight.
  4. Caller disconnects: c.req.raw.signal now aborts the turn (tested — the engine sees the abort when the request is dropped mid-turn).

Cleanup: teardown failures are logged instead of silently swallowed; test fixtures use list_project_servers' real items field; byte caps + process-local concurrency documented in OpenAPI; and authored suites now default to anthropic/claude-haiku-4.5 rather than the agent's own Sonnet 5 (suites run every case × iteration on schedules — the cheap workhorse is the right default, users can name a bigger model).

21 agent tests / 255 full v1 suite green, tsc clean. The preview redeploy for this head should follow from the push — the stale status comment was pointing at the pre-round-1 commit.

@chelojimenez
chelojimenez merged commit e1d5afa into main Aug 2, 2026
14 of 15 checks passed
@chelojimenez
chelojimenez deleted the worktree-slack-agent-endpoint branch August 2, 2026 23:04

@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: 9d1ff5dff0

ℹ️ 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 +584 to +590
if (!result.turnTrace) {
const message =
lastEngineError?.message ??
"Agent turn failed: the engine returned no turn trace.";
const rateLimited =
lastEngineError?.httpStatus === 429 ||
rt.classifyFailure(message) === "rate_limited";

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 Handle engine errors even when a trace is present

When the hosted /stream request returns a non-OK response such as 429 or 500, runChatEngineLoop invokes onEngineError but returns normally; its epilogue marks the run successful and builds a turnTrace (mcpjam-stream-handler.ts:2250-2260, 3273-3279, 3343-3345). Consequently this condition is false despite lastEngineError being populated, and the route responds 200—often with an empty reply—instead of the documented RATE_LIMITED or INTERNAL_ERROR. Check lastEngineError independently of the missing-trace fallback.

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.

🧹 Nitpick comments (3)
mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts (3)

168-187: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add a delegated-authentication failure test.

The suite covers a missing bearer token and a valid guest token. It does not make getConvexBearerMock return no delegated token or reject. A regression in delegated JWT validation could therefore pass all authentication tests. Add a case that expects 401 with UNAUTHORIZED and verifies runUnifiedAssistantTurnMock is not called.

🤖 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__/agent.test.ts` around lines 168 -
187, Add a test in the agent authentication suite that configures
getConvexBearerMock to return no delegated token or reject, then sends a request
through the existing turnRequest/makeApp flow. Assert a 401 response with code
UNAUTHORIZED and verify runUnifiedAssistantTurnMock was not called.

246-260: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Assert documentation preflight teardown.

This test verifies the fallback response but not cleanup. managerDisconnectMock is configured but never asserted. A regression could return 200 while leaving the documentation manager active. Assert that teardown runs after the timeout.

🤖 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__/agent.test.ts` around lines 246 -
260, Extend the hanging documentation preflight test around turnRequest to
assert that managerDisconnectMock is called after the 5-second timeout fallback
completes. Keep the existing response and selectedServers assertions, and verify
teardown occurs before restoring real timers.

383-392: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert the complete operation allowlist.

This test excludes only three known operations. It can remain green if another write or spend operation is added to AGENT_API_OPERATIONS. Assert the complete approved set, or verify that every operation is read-only except createEvalSuiteOperation.

🤖 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__/agent.test.ts` around lines 383 -
392, Update the “agent tool surface” test to validate the complete
AGENT_API_OPERATIONS allowlist rather than excluding only the currently known
operations. Assert the exact approved operation names, or verify that every
listed operation is read-only with createEvalSuiteOperation as the sole
exception, while preserving the existing in-app gate assertion.
🤖 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/routes/v1/__tests__/agent.test.ts`:
- Around line 168-187: Add a test in the agent authentication suite that
configures getConvexBearerMock to return no delegated token or reject, then
sends a request through the existing turnRequest/makeApp flow. Assert a 401
response with code UNAUTHORIZED and verify runUnifiedAssistantTurnMock was not
called.
- Around line 246-260: Extend the hanging documentation preflight test around
turnRequest to assert that managerDisconnectMock is called after the 5-second
timeout fallback completes. Keep the existing response and selectedServers
assertions, and verify teardown occurs before restoring real timers.
- Around line 383-392: Update the “agent tool surface” test to validate the
complete AGENT_API_OPERATIONS allowlist rather than excluding only the currently
known operations. Assert the exact approved operation names, or verify that
every listed operation is read-only with createEvalSuiteOperation as the sole
exception, while preserving the existing in-app gate assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 26ab0c4e-5251-4153-8896-e660a84e4e3c

📥 Commits

Reviewing files that changed from the base of the PR and between 24c2fa6 and 9d1ff5d.

📒 Files selected for processing (4)
  • docs/reference/openapi.json
  • docs/reference/public-api.mdx
  • mcpjam-inspector/server/routes/v1/__tests__/agent.test.ts
  • mcpjam-inspector/server/routes/v1/agent.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • docs/reference/public-api.mdx
  • mcpjam-inspector/server/routes/v1/agent.ts
  • docs/reference/openapi.json

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:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant