Skip to content

feat: server-side stored scripts for code execution - #990

Merged
github-actions[bot] merged 11 commits into
mainfrom
097-stored-scripts
Aug 14, 2026
Merged

feat: server-side stored scripts for code execution#990
github-actions[bot] merged 11 commits into
mainfrom
097-stored-scripts

Conversation

@Dumbris

@Dumbris Dumbris commented Aug 14, 2026

Copy link
Copy Markdown
Member

Implements spec 097 (specs/097-stored-scripts/): the daemon stores named scripts and callers invoke them by reference — script: "<name>" on the code_execution tool and REST, --script on the CLI. A 19KB workflow costs a name per run instead of ~4.8k tokens.

Related #986

Stacked PR: base is 096-batched-call-tools (PR #989), which stacks on #988. Merge in order; GitHub retargets automatically.

What it does

mkdir -p ~/.mcpproxy/scripts && $EDITOR ~/.mcpproxy/scripts/fetch-prs.js
mcpproxy code scripts list
mcpproxy code exec --script fetch-prs --input '{"owner":"acme","repo":"api"}'

MCP clients call code_execution with {"script": "fetch-prs", "input": {...}}. Editing by atomic replace (write + rename) takes effect on the next invocation — no restart. No write/upload API anywhere in v1: the filesystem is the only authoring surface.

Design (reviewed plan is binding — specs/097-stored-scripts/plan.md)

  • internal/codescripts is the single owner of script semantics. The confinement boundary is the pure name validator ([A-Za-z0-9_-]{1,64} — no separators, no dots, rejected before any filesystem access); on top sits the symlink/non-regular policy: atomic on Unix (O_NOFOLLOW|O_NONBLOCK — a symlink or FIFO in scripts/ is rejected, never followed or blocked on), checked best-effort on Windows. 256KB bound via LimitReader(max+1); one validated read per invocation; directory entries matched by exact byte comparison so case-insensitive filesystems cannot execute a script the listings don't show.
  • Handler-only resolution: the CLI sends the name in both daemon and standalone modes — XOR validation, resolution, records, and errors live in exactly one place, under an explicit config-file-path authority passed at construction (never derived from --data-dir; empty authority = no scripts, never CWD).
  • Discovery is error-driven: registrations stay static; a not-found invocation returns the first 20 available names + total count. mcpproxy code scripts list / GET /api/v1/code/scripts give the full listing with ok|ambiguous|invalid statuses.
  • Parity: stored scripts run under exactly the sandbox limits, scope enforcement, budgets, and activity logging of inline code; records keep the resolved source and add the script name.
  • Feature gate closed on every surface: enable_code_execution=false now refuses code_execution on REST and direct dispatch too (pre-existing gap for inline code — closed here because stored scripts widened it); the typed refusal maps to HTTP 403, and script-resolution failures map to 4xx instead of 500.

Review

Spec: 2 full review rounds (13 findings resolved). Plan: 2 rounds (12 findings — including the discovery that os.Root follows in-root symlinks and silently defeats caller O_NOFOLLOW, which reshaped the confinement design). Implementation: 15-agent adversarial review (3 lenses × verify-per-finding) + scoped opencode review → 9 distinct confirmed findings, all fixed (feature gate, FIFO hang, case-aliasing, CWD leak on empty authority, REST error mapping, CLI fallback notice, 3 test-hardening items). Reviewer: opencode gpt-5.6-sol (Codex quota-locked).

Testing

  • TDD throughout; traversal corpus incl. existing-target escape cases proves pre-filesystem rejection; symlink cases attempted on every platform (skipped only on privilege failure); FIFO regression test; stored-vs-inline parity with stub upstreams, options, and scope restriction.
  • go test -race ./internal/... green; server-edition green; lint v2 (CI config) 0 issues; e2e 65/65; both edition builds + GOOS=linux/windows cross-builds.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 14, 2026

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: 5e4f309
Status: ✅  Deploy successful!
Preview URL: https://415a0dce.mcpproxy-docs.pages.dev
Branch Preview URL: https://097-stored-scripts.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 77.16049% with 111 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
cmd/mcpproxy/code_cmd.go 59.32% 43 Missing and 5 partials ⚠️
internal/codescripts/codescripts.go 80.00% 34 Missing and 3 partials ⚠️
internal/cliclient/client.go 62.50% 6 Missing and 6 partials ⚠️
internal/server/mcp_code_execution.go 90.47% 3 Missing and 3 partials ⚠️
internal/httpapi/code_scripts.go 50.00% 3 Missing and 1 partial ⚠️
internal/server/code_exec_dispatch.go 80.00% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: 097-stored-scripts

Available Artifacts

  • archive-darwin-amd64 (29 MB)
  • archive-darwin-arm64 (26 MB)
  • archive-linux-amd64 (17 MB)
  • archive-linux-arm64 (15 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (25 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (22 MB)
  • installer-dmg-darwin-arm64 (20 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 31835259705 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@Dumbris
Dumbris force-pushed the 096-batched-call-tools branch from 882a79a to 51ee14b Compare August 14, 2026 18:28
@Dumbris
Dumbris force-pushed the 097-stored-scripts branch from 4888486 to 904f7fd Compare August 14, 2026 18:28
Dumbris added 11 commits August 14, 2026 22:52
…or-driven MCP discovery, active-config-file authority, REST surface pinned

Related #986
…ckstart

Related #986

Key decisions from code research: os.Root confinement with Lstat regular-file
policy (os.Root follows in-root symlinks; caller O_NOFOLLOW is silently
defeated); new leaf package internal/codescripts as single owner of script
semantics; scripts-dir authority per surface incl. the mainServer-nil and
loadCodeConfig path traps; three registration sites (incl. disabled stub);
GetConfigPath already on ServerController so the REST listing needs no
interface/mock churn; discovery is error-driven with static registrations.
…OLLOW, explicit config-path authority, handler-only resolution, LimitReader bound, CLI language Changed tracking

Related #986
…erywhere symlink tests stated uniformly

Related #986
Related #986

code_execution (MCP + REST) accepts script: "<name>" as an alternative to
inline code, resolving from <active-config-dir>/scripts/. The new
internal/codescripts package is the single owner of script semantics: pure
token name validation (the confinement boundary — invalid names are
rejected before any filesystem access), platform no-follow open (Unix
O_NOFOLLOW atomic; Windows Lstat best-effort), 256KB bound via
LimitReader(max+1), one validated read per invocation, listing with
ok/ambiguous/invalid statuses, and extension-derived language with
explicit-contradiction rejection.

## Changes
- internal/codescripts: new package + traversal-corpus/symlink/freshness
  tests (symlink cases attempted on every platform, skipped only on
  privilege failure)
- internal/server: script XOR code handler seam (handler is the only
  execution-time resolver on every surface), explicit config-file-path
  authority via WithConfigFilePath at construction, shared
  codeExecRecordArguments (history/activity carry resolved source as code
  plus additive script name), registrations on all three sites incl. the
  disabled stub, error-driven discovery (not-found lists <=20 names +
  total) documented in the tool description
- internal/httpapi: Script field with XOR 400 INVALID_REQUEST; new
  GET /api/v1/code/scripts listing endpoint (auth inherited)
- cmd/mcpproxy + internal/cliclient: --script flag (3-way exclusion),
  language sent only when the flag was explicitly set, name-over-the-wire
  in both daemon and standalone modes, mcpproxy code scripts list
- oas: swagger regen; docs: stored-scripts sections sourced from the
  implementation's actual strings

## Testing
- TDD throughout (red observed first per task)
- go test -race ./internal/... green; server-edition green; lint v2 0
  issues; e2e 65/65; both edition builds + GOOS=linux/windows cross-builds
…tive resolution

Related #986

Review finding: with no explicit path, no mainServer path, and a nil
config, filepath.Join("", name+ext) produced a bare relative path
resolved against the process working directory. An empty authority now
means no scripts, reported as the standard not-found error.
…n, case-exact resolution, typed 4xx REST mapping, test hardening

Related #986

- enable_code_execution=false now refuses code_execution on EVERY surface
  (REST /code/exec, /tools/call, CallToolDirect) via a gate at the shared
  handler chokepoint, with one message everywhere; the typed refusal
  travels through a context capture box (Spec-093 shed pattern) so REST
  answers 403 FEATURE_DISABLED without string matching. Pre-existing gap
  for inline code; closed here because stored scripts widened it.
- Unix open gains O_NONBLOCK so a FIFO dropped into scripts/ fails as
  non-regular instead of blocking the handler until a writer appears.
- Resolve now matches directory entries by exact byte comparison
  (os.ReadDir) before opening, so case-insensitive filesystems can no
  longer execute a script (backdoor.JS) that every listing and discovery
  surface reports as nonexistent.
- Stored-script resolution failures surface as 4xx over REST (404
  not-found carrying the discovery list, 400 invalid/ambiguous) instead
  of 500 EXECUTION_FAILED.
- code scripts list prints a stderr notice when it falls back to a local
  listing because the daemon is unreachable.
- Tests: traversal corpus extended with existing-target escape cases,
  stored-vs-inline parity deepened (stub upstream, options, scope), -o
  json listing branch covered, FIFO regression test.
@Dumbris
Dumbris force-pushed the 097-stored-scripts branch from 904f7fd to 5e4f309 Compare August 14, 2026 19:52
@Dumbris
Dumbris changed the base branch from 096-batched-call-tools to main August 14, 2026 19:52

@github-actions github-actions 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.

Approved (Model B): Paperclip review verdicts = ACCEPT and qa-gate green at this head SHA. Arming auto-merge; GitHub merges when all required checks pass.

@github-actions
github-actions Bot merged commit bfd43e7 into main Aug 14, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants