fix(mcp): let MCP tokens call preview run tools (jobs:run scope) — Fixes GIT-920 - #10107
Conversation
The MCP proxy mints an internal JWT scoped to exactly `scope_for_route` for the endpoint it forwards to. For preview run routes (`run/preview`, `run/preview_bundle`, `run/preview_flow`, `run_wait_result/preview`, `run_wait_result/preview_flow`), `determine_kind_from_route` matched the `SCRIPT_JOBS` prefix `jobs/run_wait_result/p` (because "preview" starts with "p") and derived `jobs:run:scripts`. But the preview handlers run arbitrary request-supplied code with no deployed path and require the broad `jobs:run` scope, so `jobs:run:scripts` was rejected with 403 "Required scope: jobs:run". Preview/bundle routes now carry no runnable kind, so the derived scope is the broad `jobs:run` the handlers expect. This also aligns the route-level access check with the handler check for these routes. Fixes GIT-920 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
I'll analyze this and get back to you. |
|
Claude finished @rubenfiszel's task in 2m 33s —— View job PR Review — fix(mcp): let MCP tokens call preview run toolsMergeable, but should ideally address a nit: the
The fix is correct for the reported bug and the root-cause analysis is accurate: FindingsP2 —
Impact is low, not a functional break: the by-path handler check passes either way (
A prefix-anchored check avoids the edge case and is exactly as readable: if route_path.starts_with("jobs/run/preview")
|| route_path.starts_with("jobs/run_wait_result/preview")
{
return None;
}This still covers all five routes ( AGENTS.md complianceNo violations. The added comment records the non-obvious constraint (why preview routes must carry no kind) rather than narrating the code — consistent with the "comments record constraints" rule. Test coverageBackend-only change. The new No integration test needed here — the PR notes an end-to-end MCP verification against a local |
Deploying windmill with
|
| Latest commit: |
5c2812a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://872ba7f7.windmill.pages.dev |
| Branch Preview URL: | https://ruben-git-920-bug-mcp-token.windmill.pages.dev |
Pi Reviewcc @rubenfiszel Mergeable, but should ideally address nits: comment exceeds AGENTS.md 4-line limit in P2 — Comment exceeds AGENTS.md length limit and includes drafting historyFile: The new comment before
Two issues:
The essential invariant — "preview routes execute arbitrary code and require (Similarly, the test comment at ~line 1311 is 5 lines including the GIT-920 parenthetical; trimming it to ≤4 lines would align with the same guideline, though test code is less critical.) What looks good
Test coverageBackend (Rust): Adequate. The PR adds a unit test covering all five preview paths in Manual verification: No in-app UI surface to exercise. The fix is purely a backend MCP-proxy scope-derivation change. The e2e test described in the PR body (drive a real MCP streamable-HTTP connection with an |
Codex Reviewcc @rubenfiszel Found 1 issue:
Test coverageThe added unit cases cover the intended preview endpoints but need regression cases for by-path script and flow routes containing No frontend surface changed. Manually run such a deployed path with a kind-scoped token and confirm it still succeeds, alongside the MCP preview-run scenario. |
Address CI review: `route_path.contains("preview")` also matched by-path
runs of a deployed runnable whose path contains "preview" (e.g.
`jobs/run_wait_result/p/f/team/preview_report`). Since determine_kind_from_route
also feeds check_route_access, such a route would derive the broad `jobs:run`
and reject a legitimately kind-scoped `jobs:run:scripts:*`/`jobs:run:flows:*`
token with 403.
Anchor the exception to the actual preview endpoints
(`jobs/run/preview*`, `jobs/run_wait_result/preview*`) so by-path runs keep
their kind. Add regression tests for preview-named by-path paths, and trim
the comments per AGENTS.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pi ReviewGood to merge. The combined two-commit diff resolves the prior reviews' substring-matching concern cleanly. Findings: none. The only remaining delta from AGENTS.md is the 6-line comment at What looks good
Test coverageBackend (Rust): Well-covered. The new assertions in
The PR body also documents an end-to-end MCP streamable-HTTP test against a local backend confirming the 403→200 fix. No additional integration tests are needed. Manual verification: No in-app UI surface. The e2e scenario described in the PR body (MCP streamable-HTTP with |
Codex ReviewGood to merge No issues found. The latest commit resolves the prior substring-matching concern, and no new public surfaces were introduced. Test coverageBackend unit coverage includes all five preview endpoints and by-path regressions containing No UI surface changed. The documented MCP end-to-end check confirms the preview request succeeds with the expected result. |

Problem
An MCP token (e.g.
mcp:all) could not call the built-inrunScriptPreviewAndWaitResulttool. Every call returned:Read tools and deployed-script tools over the same token worked fine; only ad-hoc preview runs failed. Reported as fallout from the scope-enforcement work (#9712).
Root cause
The MCP proxy (
create_http_requestinmcp/utils.rs) mints an internal JWT scoped to exactlyscope_for_route(method, path)for the endpoint it forwards to, so a scope-restricted MCP token can't be widened into a blank check.For preview run routes,
determine_kind_from_route("jobs/run_wait_result/preview")matched theSCRIPT_JOBSprefix"jobs/run_wait_result/p"— because"preview"starts with"p"— and derived the runnable kindscripts, producing the minted scopejobs:run:scripts.But preview handlers (
run_preview_script/run_preview_flow_job) run arbitrary, request-supplied code with no deployed path, and deliberately require the broadjobs:runscope so a narrowly-scoped token can't escape its scope.jobs:run:scriptsdoes not includejobs:run, socheck_scopesrejected it with 403.The route-derivation (
scope_for_route) and the handler requirement (check_scopes(jobs:run)) had silently diverged for preview routes.Fix
determine_kind_from_routenow returns no kind for preview/bundle routes, soscope_for_routederives the broadjobs:runthat the handlers require. The match is anchored to the endpoint segment (jobs/run/preview*orjobs/run_wait_result/preview*) rather than acontains("preview")substring test.This matters because
determine_kind_from_routealso feeds the direct-APIcheck_route_accessmiddleware. A substring test would also match a by-path run of a deployed runnable whose own path containspreview(e.g.jobs/run_wait_result/p/f/team/preview_report): that route would then derive the broadjobs:run, and a legitimately kind-scopedjobs:run:scripts:*/jobs:run:flows:*token would be rejected with 403. Anchoring to the endpoint segment keeps those by-path runs on their correct kind.scope_for_routeis only consumed by the MCP proxy; the by-path run routes (run/p/*,run/f/*, etc.) keep their kind.Validation
Unit tests (
test_scope_for_route):jobs:run.preview-named path still derive their kind:run/p/u/alice/preview_reportandrun_wait_result/p/f/team/preview_report→jobs:run:scripts;run/f/f/team/preview_report→jobs:run:flows.End-to-end, against a local backend built with
--features mcp,quickjs, before/after each change:MCP preview path — driving the real MCP streamable-HTTP protocol with an
mcp:alltoken callingrunScriptPreviewAndWaitResult:403 Required scope: jobs:run.200, result"hello-git920".By-path regression — a deployed bash script at
u/admin/preview_report, run viaPOST /w/admins/jobs/run_wait_result/p/u/admin/preview_reportwith a token scoped only tojobs:run:scripts:u/admin/preview_report:contains("preview")(rebuilt):403 Access denied. Required scope: jobs:run— confirms the substring approach was a real functional break.200, result"bypath-git920", while the MCP preview path still returns200.Fixes GIT-920
🤖 Generated with Claude Code