Skip to content

feat(mcp): add Code Mode (execute_script) with a sandboxed V8 engine - #136

Merged
spelech merged 2 commits into
mainfrom
feat/code-mode-execute-script
Nov 12, 2026
Merged

spelech merged 2 commits into
mainfrom
feat/code-mode-execute-script

Conversation

@spelech

@spelech spelech commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Problem

An agent that traces a problem makes many small tool calls that depend on each other: find the route, trace its handler, trace the callers of each repository method, read a file. Each call is one round trip, and each intermediate result goes into the agent's context. In a debugging test on a real repository, the agent with ContextCortex was slower than an agent with only grep and file reads, mostly because of these round trips.

Fix

New tool execute_script (Code Mode). It uses the same pattern as Code Mode (execute_script) in model-context-gateway, so a script also runs unchanged through the gateway.

  • Input: script (alias code): an async JavaScript body; top-level await and return work. arguments (alias args): an input object, available as args and arguments.
  • Tool calls: await tools.<tool>(args), await cc.callTool("<tool>", args), and the gateway forms tools.contextcortex.<tool>(args) and mcg.callTool(...). Results are parsed JSON. A tool error (an unknown tool, unknown or missing arguments, or a result with an error field) throws an Error that the script can catch.
  • Result: {result, logs, executionTimeMs, toolCallsCount}. On a failure: {error: "script_failed", message, logs, executionTimeMs, toolCallsCount}. Line numbers in errors count from the first line of the script. Results larger than 100,000 characters are cut and marked truncated.
  • Sandbox (app/services/script_sandbox.py): a new V8 isolate per run (mini-racer==0.14.1, ISC license, released 2026-02-01). The script reaches Python only through one async bridge function; values cross as JSON strings. It has no file, network, or process access (require, process, and fetch are not defined).
  • Limits (same defaults and ranges as the gateway): CODE_MODE_TIMEOUT_SECONDS (30, 1..300), CODE_MODE_MAX_TOOL_CALLS (50, 1..500), CODE_MODE_MAX_MEMORY_MB (64, 8..512). CODE_MODE_ENABLED=false turns it off. V8 has no statement counter, so the timeout stops endless loops.
  • Security: every tool call goes through the normal handler, so its role check applies. execute_script cannot call itself.
  • Types: new resource contextcortex://code-mode/sdk.d.ts, with one typed declaration per tool, made from the tool schemas, with the argument descriptions as JSDoc.
  • Packaging: mini-racer is in requirements.txt and pinned in python-constraints.txt. contextcortex.spec collects py_mini_racer (the V8 library and the ICU data file), which mini-racer loads from _MEIPASS/py_mini_racer in a PyInstaller build.
  • Docs: README, docs/reference/mcp-tools.md (new section 20 with an example), docs/mcp-tools-and-resources.md, and the tool counts (19 → 20).

Tests

tests/test_execute_script.py (13 tests, run locally with mini-racer 0.14.1):

  • Tool calls, logs, args/arguments, the gateway forms, and cc.callTool.
  • Caught and uncaught tool errors (logs before the error are kept), script errors with correct line numbers, syntax errors, and no return.
  • Limits: an endless loop stops at the timeout, a memory loop stops at the memory limit, and the tool call limit stops a loop.
  • No host access from the script.
  • Dispatch checks: recursion, unknown tool, unknown argument, and missing argument.
  • End to end: a script calls the real trace_path handler on an SQLite graph and returns the callers of two methods.
  • The environment limits and the TypeScript declarations.

tests/test_mcp_pagination_and_resources.py now expects 20 tools. I also built sdk.d.ts from the real registered tools (19 declarations; execute_script is not listed).

The portable bundle is built only in CI, so the release build is the first check that the V8 library loads from the bundle.

🤖 Generated with Claude Code

Steve Pelech and others added 2 commits November 12, 2026 15:43
An agent that traces a problem makes many small tool calls that depend on
each other. Each round trip costs time and tokens. Code Mode runs one
agent-written async JavaScript body that calls the tools on the server and
returns only the final result.

The design follows Code Mode in model-context-gateway: `script`/`code` and
`arguments`/`args`; `tools.<tool>(args)`, `cc.callTool(name, args)`, and the
gateway forms `tools.contextcortex.<tool>` and `mcg.callTool`; parsed JSON
results; a tool error throws a JavaScript Error; console logs; the result
envelope {result, logs, executionTimeMs, toolCallsCount}; no recursive
execute_script; and limits on time (30 s), tool calls (50), and memory
(64 MB), set with CODE_MODE_* environment variables.

The sandbox is a new V8 isolate per run (mini-racer 0.14.1). The script
reaches Python only through one async bridge function, and values cross as
JSON strings. Every tool call goes through the normal handler, so its role
check applies. The resource contextcortex://code-mode/sdk.d.ts gives one
typed declaration per tool, made from the tool schemas.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
# Conflicts:
#	REQUIREMENTS.md
#	docs/mcp-tools-and-resources.md
@spelech
spelech merged commit 1f60139 into main Nov 12, 2026
16 checks passed
@spelech
spelech deleted the feat/code-mode-execute-script branch November 12, 2026 22:15
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.

1 participant