docs/code_execution/troubleshooting.md
Common issues, error messages, and solutions for the code_execution tool.
Symptom:
Error: code_execution is disabled in configuration. Set 'enable_code_execution: true' in config file.
Cause: The code_execution feature is disabled by default for security.
Solution:
~/.mcpproxy/mcp_config.json)"enable_code_execution": truepkill mcpproxy && mcpproxy serveExample Configuration:
{
"enable_code_execution": true,
"code_execution_timeout_ms": 120000,
"code_execution_max_tool_calls": 0,
"code_execution_pool_size": 10,
"code_execution_max_parallel": 8,
"mcpServers": [...]
}
Symptom: LLM agent lists available tools but code_execution is missing.
Cause: Feature is not enabled or server didn't restart after configuration change.
Solution:
"enable_code_execution": trueVerification:
# Check if code_execution tool is registered
mcpproxy call tool --tool-name=retrieve_tools --json_args='{"query":"code execution"}'
Symptom:
{
"ok": false,
"error": {
"message": "timeout_ms must be between 1 and 600000 milliseconds"
}
}
Cause: Invalid timeout_ms value in request options.
Solution: Use a timeout between 1ms and 600000ms (10 minutes):
{
"code": "...",
"options": {
"timeout_ms": 120000 // 2 minutes (valid)
}
}
Symptom: mcpproxy refuses to load the configuration file:
config validation failed: code_execution_max_parallel: must be between 1 and 32 (or 0 for default)
Cause: code_execution_max_parallel is outside the supported range.
Solution: Use a value between 1 and 32 (or omit the key / set 0 for the
default of 8):
{
"code_execution_max_parallel": 8
}
The same range applies to the per-batch override —
call_tools(requests, {max_parallel: 40}) returns a single INVALID_ARGS
envelope instead of dispatching.
Symptom:
{
"ok": false,
"error": {
"code": "SYNTAX_ERROR",
"message": "SyntaxError: Unexpected token '{'",
"stack": ""
}
}
Common Causes:
Solutions:
Problem: Missing brackets
// Bad
const x = { value: 1, missing: 2
// Good
const x = { value: 1, missing: 2 };
Problem: Incomplete expressions
// Bad
const msg = `Hello, ${
// Good
const msg = `Hello, ${name}!`;
Symptom: Code execution fails with "end of input" error.
Cause: Incomplete JavaScript expression or statement.
Solution: Ensure all blocks are properly closed:
// Bad
var x = function() {
return 42;
// Missing closing brace
// Good
var x = function() {
return 42;
};
Symptom:
{
"ok": false,
"error": {
"code": "RUNTIME_ERROR",
"message": "TypeError: Cannot read property 'name' of undefined",
"stack": "..."
}
}
Cause: Accessing properties on undefined or null values.
Solution: Always check values before accessing properties:
// Bad
var name = input.user.name; // Crashes if input.user is undefined
// Good
var name = input.user ? input.user.name : 'Unknown';
// Better
var name = 'Unknown';
if (input && input.user && input.user.name) {
name = input.user.name;
}
For Tool Calls:
// Bad
var user = call_tool('github', 'get_user', {username: input.username});
var name = user.result.name; // Crashes if user.ok is false
// Good
var user = call_tool('github', 'get_user', {username: input.username});
if (!user.ok) {
return {error: user.error.message};
}
var name = user.result.name;
Symptom:
{
"ok": false,
"error": {
"code": "RUNTIME_ERROR",
"message": "ReferenceError: myVariable is not defined",
"stack": "..."
}
}
Cause: Using variables or functions that don't exist.
Common Mistakes:
Problem: Undefined variables
// Bad
return result; // 'result' was never declared
// Good
var result = 42;
return result;
Problem: Using unavailable functions
// Bad
var data = require('fs').readFileSync('file.txt'); // require() not available
// Bad
setTimeout(function() { doWork(); }, 1000); // setTimeout() not available
// Good
var data = call_tool('filesystem', 'read_file', {path: 'file.txt'});
Symptom: Execution fails with stack overflow.
Cause: Infinite recursion or very deep recursion.
Solution: Check for infinite loops or recursive calls:
// Bad
function factorial(n) {
return n * factorial(n - 1); // No base case!
}
// Good
function factorial(n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1);
}
// Better (iterative)
function factorial(n) {
var result = 1;
for (var i = 2; i <= n; i++) {
result *= i;
}
return result;
}
Symptom:
{
"ok": false,
"error": {
"code": "TIMEOUT",
"message": "JavaScript execution timed out",
"stack": ""
}
}
Common Causes:
Solutions:
Problem: Infinite loop
// Bad
while (true) {
// This will timeout
}
// Bad
for (var i = 0; i < 1000000000; i++) {
// This takes too long
}
// Good
for (var i = 0; i < input.items.length; i++) {
// Bounded loop
}
Problem: Insufficient timeout for workload
// Bad: Default 2-minute timeout for 100 tool calls
{
"code": "for(var i=0;i<100;i++){call_tool(...)}",
"options": {"timeout_ms": 120000} // Might not be enough
}
// Good: Increase timeout for heavy workloads
{
"code": "for(var i=0;i<100;i++){call_tool(...)}",
"options": {"timeout_ms": 300000} // 5 minutes
}
Problem: Slow upstream tools
// Solution: Reduce number of calls or increase timeout
{
"code": "...",
"options": {
"timeout_ms": 300000, // Increase timeout
"max_tool_calls": 50 // Limit calls to prevent runaway
}
}
Symptom:
{
"ok": false,
"error": {
"code": "MAX_TOOL_CALLS_EXCEEDED",
"message": "Exceeded maximum tool calls limit (10)",
"stack": ""
}
}
Cause: Code called call_tool() more times than max_tool_calls allows.
Every element of a call_tools() batch counts as one call too, checked in input
order — tail elements over the limit come back as per-slot
MAX_TOOL_CALLS_EXCEEDED errors and are never dispatched.
Solution:
Option 1: Increase limit
{
"code": "for(var i=0;i<20;i++){call_tool(...)}",
"options": {"max_tool_calls": 25} // Set higher than needed
}
Option 2: Reduce tool calls
// Bad: Call tool for every item
for (var i = 0; i < 1000; i++) {
call_tool('api', 'process', {id: items[i]});
}
// Good: Batch items if tool supports it
var batchSize = 100;
for (var i = 0; i < items.length; i += batchSize) {
var batch = items.slice(i, i + batchSize);
call_tool('api', 'process_batch', {items: batch});
}
Symptom:
{
"ok": false,
"error": {
"code": "SERVER_NOT_ALLOWED",
"message": "Server 'gitlab' is not in the allowed servers list",
"stack": ""
}
}
Cause: Attempted to call a server not in allowed_servers option.
Solution:
Option 1: Add server to allowed list
{
"code": "call_tool('gitlab', 'get_user', {username: 'test'})",
"options": {
"allowed_servers": ["github", "gitlab"] // Add gitlab
}
}
Option 2: Remove restriction (allow all servers)
{
"code": "call_tool('gitlab', 'get_user', {username: 'test'})",
"options": {
"allowed_servers": [] // Empty array = all servers allowed
}
}
Symptom: call_tool() returns error saying server doesn't exist.
Cause: Server name is incorrect or server is not configured.
Solution:
// Bad
call_tool('GitHub', 'get_user', {}); // Wrong case
// Good
call_tool('github', 'get_user', {}); // Correct name
mcpproxy call tool --tool-name=upstream_servers --json_args='{"operation":"list"}'
mcpproxy call tool --tool-name=upstream_servers \
--json_args='{"operation":"add","name":"github","url":"https://api.github.com/mcp","protocol":"http","enabled":true}'
Symptom: call_tools() returns a single envelope instead of an array of slots:
{
"ok": false,
"error": {
"code": "INVALID_ARGS",
"message": "call_tools: element 3: must be an object with server and tool"
}
}
Cause: The batch itself is malformed, so nothing was dispatched and no budget
was consumed. Triggers: requests is not an array, an element is not an object
with non-empty server/tool strings, a supplied args is not an object, the
array has a sparse hole, options is not an object, max_parallel is not an
integer in 1-32, or the batch exceeds 100 elements.
Solution: Fix the element the message names, then check the result shape before mapping over it:
var slots = call_tools(requests, {max_parallel: 8});
if (!Array.isArray(slots)) {
// whole batch rejected — slots is {ok:false, error:{...}}
({error: slots.error.message});
} else {
({results: slots});
}
For more than 100 items, chunk the array and issue one call_tools() per chunk.
Symptom: A call_tools() batch returns one success and many failed slots
mentioning a concurrency limit:
[
{"ok": true, "result": {...}},
{"ok": false, "error": {
"code": "UPSTREAM_ERROR",
"message": "upstream server \"fragile-db\" is busy: its concurrency limit (1) is saturated (queue_full) — please retry shortly"
}}
]
Cause: The target server has a per-server concurrency limit
(max_concurrent_requests) with no queue_size. Batching never bypasses
those limits: calls over the cap are shed immediately, which is the server's
configured policy — one shed error per overflow element.
Solution: Give the server queue headroom, or lower the batch concurrency to match its cap:
{
"mcpServers": [
{ "name": "fragile-db", "command": "db-mcp", "max_concurrent_requests": 1, "queue_size": 20 }
]
}
// Or match the cap from the script
call_tools(requests, {max_parallel: 1});
Queued calls wait up to queue_timeout, and that wall-clock wait happens inside
the script's timeout_ms budget — size queue_size and timeout_ms together.
These apply to invocations that name a stored script
(script: "<name>", --script <name>) instead of sending code inline. One
command answers most of them:
mcpproxy code scripts list # names, paths, statuses, and the directory that was read
Symptom:
Provide exactly one of 'code' (inline source) or 'script' (the name of a script stored in the 'scripts' directory next to mcpproxy's config file) — not both, not neither.
Over REST the same rule is an HTTP 400 before dispatch:
{"ok": false, "error": {"code": "INVALID_REQUEST", "message": "Provide exactly one of 'code' (inline source) or 'script' (the name of a stored script)"}}
Cause: The request carried both code and script, or neither. JSON Schema
cannot express "exactly one of", so neither field is schema-required and the
rule is enforced by the tool.
Solution: Send one source. On the CLI, --code, --file and --script are
mutually exclusive (--code, --file and --script are mutually exclusive, exit
code 2).
Symptom:
Cannot execute stored script: stored script "fetch-pr" not found in /Users/me/.mcpproxy/scripts. Available scripts (3): daily-report, fetch-prs, triage
Or, with an empty/absent directory:
Cannot execute stored script: stored script "fetch-pr" not found: no stored scripts in /Users/me/.mcpproxy/scripts (create fetch-pr.js or fetch-pr.ts there)
Cause: No <name>.js / <name>.ts in the scripts directory. Usually a typo
(names are case-sensitive), a file that is not a script (uppercase or other
extension: .JS, .mjs, .jsx are ignored), or the wrong directory — the
scripts directory follows the active config file, not --data-dir.
Solution: This error is the discovery mechanism — it lists the first 20 available names alphabetically plus the total, so an MCP client can recover the name set from the failed call. For the full picture, including where the daemon looked:
mcpproxy code scripts list
mcpproxy code scripts list --config /etc/mcpproxy/mcp_config.json # a non-default config
If the directory in the message is not the one you authored in, start the daemon
with the config file you meant (mcpproxy serve --config …) — with
~/.mcpproxy/mcp_config.json the scripts live in ~/.mcpproxy/scripts/.
mcpproxy never creates the directory itself; mkdir -p it.
Symptom:
Cannot execute stored script: invalid script name "../../etc/passwd": character "." is not allowed (names are 1-64 characters of A-Z, a-z, 0-9, '-' or '_' — a name, never a path)
Cause: script is a name, never a path. Separators, .., dots,
extensions, non-ASCII characters and names longer than 64 characters are
rejected before the filesystem is touched at all.
Solution: Pass the base name only — fetch-prs, not fetch-prs.js,
./fetch-prs.js, or /abs/path/fetch-prs.js.
Symptom:
Cannot execute stored script: stored script "triage" is ambiguous: /Users/me/.mcpproxy/scripts/triage.js and /Users/me/.mcpproxy/scripts/triage.ts both exist — remove one
Cause: Both extensions exist for one name — often a leftover after converting a script from JavaScript to TypeScript. Ambiguity is never resolved silently.
Solution: Delete (or rename) one of the two files. mcpproxy code scripts list flags such names with status ambiguous before you hit them at runtime.
Symptom:
Cannot execute stored script: stored script "big-report" (/Users/me/.mcpproxy/scripts/big-report.js) is oversized: scripts are limited to 262144 bytes
Cause:
| Reason | Meaning |
|---|---|
oversized | The file exceeds the 256 KB stored-script bound (inline code has no such bound; this one exists purely to bound the daemon-side read) |
empty | Zero bytes — commonly a half-finished redirect (> script.js) |
unreadable | Permissions or an I/O error; the detail carries the OS error |
non-regular | The path is a symlink, directory, or device — scripts must be regular files |
Solution: Split an oversized workflow into several scripts (or move bulk
data into input), finish the write, fix permissions, or replace the symlink
with the real file. Copy, do not link:
cp /shared/workflows/report.js ~/.mcpproxy/scripts/report.js
The scripts directory itself may be a symlink — it is operator-controlled; only the script file may not be.
Symptom:
Cannot execute stored script: stored script "daily-report" is a .ts file (typescript) but language "javascript" was requested — omit 'language' or set it to "typescript"
Cause: The extension is authoritative for a stored script, and the explicit
language contradicted it.
Solution: Omit language entirely — the extension decides. (The CLI already
forwards --language only when you set it explicitly, so its javascript
default cannot trigger this.)
Cause: Almost always the file was not replaced where the daemon looks, or the edit went to a different scripts directory. There is no cache and no watcher: every invocation opens and reads the file once, so a completed replacement is visible to the very next call — no restart, nothing to flush.
Solution: Confirm the path with mcpproxy code scripts list (it always
prints the directory it read), then edit by atomic replace so no invocation
can observe a half-written file:
tmp=$(mktemp ~/.mcpproxy/scripts/.report.XXXXXX)
cp new-report.js "$tmp" && mv "$tmp" ~/.mcpproxy/scripts/report.js
Editing in place while an invocation is reading is the one unsupported case: that run gets whatever the read returned (validated, but unspecified).
Cause: Working as designed. v1 has no write path for stored scripts — no MCP tool, no REST endpoint, no CLI verb creates, updates, or deletes them. Code running in the sandbox has no filesystem access either.
Solution: Author scripts with your normal filesystem tooling (editor, scp,
configuration management). GET /api/v1/code/scripts and mcpproxy code scripts list are read-only views of the result.
Symptom:
{
"ok": false,
"error": {
"code": "SERIALIZATION_ERROR",
"message": "Result contains non-JSON-serializable values (functions, circular references, etc.)",
"stack": ""
}
}
Cause: JavaScript return value contains functions, circular references, or other non-JSON types.
Solutions:
Problem: Returning functions
// Bad
return {
calculate: function() { return 42; }
};
// Good
return {
result: 42
};
Problem: Returning undefined
// Bad
return undefined; // Not JSON-serializable
// Good
return null; // JSON-serializable
Problem: Circular references
// Bad
var obj = {value: 42};
obj.self = obj; // Circular reference
return obj;
// Good
return {value: 42};
Problem: Special objects (Date, RegExp)
// Bad
return {
created: new Date() // Date object not JSON-serializable
};
// Good
return {
created: new Date().toISOString() // String is JSON-serializable
};
Symptom: Code takes longer than expected to execute.
Diagnosis:
Solutions:
Reduce tool calls:
// Bad: N tool calls
for (var i = 0; i < items.length; i++) {
call_tool('api', 'get', {id: items[i]});
}
// Good: 1 batch tool call
call_tool('api', 'get_batch', {ids: items});
Run independent calls in parallel (when the tool has no bulk variant):
// Bad: N sequential calls — latency is the sum
for (var i = 0; i < items.length; i++) {
call_tool('api', 'get', {id: items[i]});
}
// Good: one batch — latency is about the slowest call
var slots = call_tools(items.map(function (id) {
return {server: 'api', tool: 'get', args: {id: id}};
}), {max_parallel: 8});
Only batch calls that do not depend on each other. Raise
code_execution_max_parallel (or options.max_parallel, max 32) if upstreams
can take the pressure — and check the target server's max_concurrent_requests /
queue_size first (see the queue_full entry above).
Cache repeated calls:
// Bad: Call same tool multiple times
for (var i = 0; i < items.length; i++) {
var config = call_tool('api', 'get_config', {}); // Repeated call
process(items[i], config);
}
// Good: Call once, reuse result
var config = call_tool('api', 'get_config', {});
if (!config.ok) return config;
for (var i = 0; i < items.length; i++) {
process(items[i], config.result);
}
Optimize loops:
// Bad: Inefficient nested loops
for (var i = 0; i < arr1.length; i++) {
for (var j = 0; j < arr2.length; j++) {
if (arr1[i] === arr2[j]) {
results.push(arr1[i]);
}
}
}
// Good: Use object for O(1) lookup
var set = {};
for (var i = 0; i < arr2.length; i++) {
set[arr2[i]] = true;
}
for (var i = 0; i < arr1.length; i++) {
if (set[arr1[i]]) {
results.push(arr1[i]);
}
}
Symptom: Requests take longer to start, or you see "waiting for VM" logs.
Cause: More concurrent executions than pool size allows.
Solution: Increase pool size in configuration:
{
"code_execution_pool_size": 20 // Increase from default 10
}
Trade-offs:
Recommendation: Monitor pool usage and adjust based on load.
CLI:
mcpproxy code exec --code="..." --log-level=debug
Server: Edit config to set log level:
{
"log_level": "debug"
}
What you'll see:
var user = call_tool('github', 'get_user', {username: input.username});
console.log('User response:', JSON.stringify(user)); // Logs to server logs
if (!user.ok) {
console.log('Error occurred:', user.error);
return {error: user.error.message};
}
console.log('User name:', user.result.name);
return {name: user.result.name};
Where to find logs: ~/.mcpproxy/logs/main.log (or platform-specific log directory)
Start simple and build up:
// Step 1: Verify input access
return input;
// Step 2: Test tool call
var res = call_tool('github', 'get_user', {username: 'octocat'});
return res;
// Step 3: Add error handling
var res = call_tool('github', 'get_user', {username: 'octocat'});
if (!res.ok) return {error: res.error.message};
return res.result;
// Step 4: Add data transformation
var res = call_tool('github', 'get_user', {username: 'octocat'});
if (!res.ok) return {error: res.error.message};
return {
name: res.result.name,
repos: res.result.public_repos
};
Test that your return value is JSON-serializable:
var result =;
// This will throw if result is not serializable
var json = JSON.stringify(result);
// If it succeeds, return it
return result;
Wrap risky operations in try-catch or use checks:
// Check before accessing
if (res && res.ok && res.result && res.result.name) {
return {name: res.result.name};
} else {
return {error: 'Invalid response structure'};
}
// For loops, check each iteration
for (var i = 0; i < items.length; i++) {
if (!items[i]) {
console.log('Skipping null item at index', i);
continue;
}
// Process items[i]
}
Use the CLI to reproduce issues quickly:
# Save problematic code to file
cat > /tmp/debug.js << 'EOF'
var user = call_tool('github', 'get_user', {username: input.username});
console.log('Response:', JSON.stringify(user));
return user;
EOF
# Run with debug logging
mcpproxy code exec \
--file=/tmp/debug.js \
--input='{"username":"octocat"}' \
--log-level=debug
If you're still stuck after trying these solutions:
~/.mcpproxy/logs/main.log for detailed error messages**Environment**:
- MCPProxy version: (run `mcpproxy --version`)
- OS: (macOS/Linux/Windows)
- Configuration: (relevant config fields)
**Code**:
```javascript
// Your JavaScript code here
Input:
{
"input": {...}
}
Expected: What you expected to happen
Actual: What actually happened (include full error message)
Logs: Relevant lines from ~/.mcpproxy/logs/main.log
---
## Next Steps
- **Examples**: See [examples.md](examples.md) for working code samples
- **API Reference**: See [api-reference.md](api-reference.md) for complete schema
- **Overview**: See [overview.md](overview.md) for architecture and best practices