diff --git a/CLAUDE.md b/CLAUDE.md index 10ba8079..69508d48 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -403,7 +403,7 @@ All server responses include a `health` field that provides consistent status in ## JavaScript Code Execution -The `code_execution` tool enables orchestrating multiple upstream MCP tools in a single request using sandboxed JavaScript (ES5.1+). +The `code_execution` tool enables orchestrating multiple upstream MCP tools in a single request using sandboxed JavaScript (ES2020+). Modern syntax is fully supported: arrow functions, const/let, template literals, destructuring, classes, for-of, optional chaining (?.), nullish coalescing (??), spread/rest, Promises, Symbols, Map/Set, Proxy/Reflect, and generators. ### Configuration @@ -554,7 +554,7 @@ Runtime detection (uvx→Python, npx→Node.js), image selection, environment pa Dynamic port allocation, RFC 8252 + PKCE, flow coordinator (`internal/oauth/coordinator.go`), automatic token refresh. See [docs/oauth-resource-autodetect.md](docs/oauth-resource-autodetect.md). ### Code Execution -Sandboxed JavaScript (ES5.1+), orchestrates multiple upstream tools in single request. See [docs/code_execution/overview.md](docs/code_execution/overview.md). +Sandboxed JavaScript (ES2020+), orchestrates multiple upstream tools in single request. See [docs/code_execution/overview.md](docs/code_execution/overview.md). ### Connection Management Exponential backoff, separate contexts for app vs server lifecycle, state machine: Disconnected → Connecting → Authenticating → Ready. diff --git a/docs/code_execution/api-reference.md b/docs/code_execution/api-reference.md index 4059f676..66714257 100644 --- a/docs/code_execution/api-reference.md +++ b/docs/code_execution/api-reference.md @@ -27,7 +27,7 @@ Complete reference for the `code_execution` MCP tool. "properties": { "code": { "type": "string", - "description": "JavaScript source code (ES5.1+) to execute..." + "description": "JavaScript source code (ES2020+) to execute..." }, "input": { "type": "object", @@ -97,7 +97,7 @@ Complete reference for the `code_execution` MCP tool. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `code` | string | **Yes** | JavaScript source code to execute (ES5.1+ syntax) | +| `code` | string | **Yes** | JavaScript source code to execute (ES2020+ syntax supported) | | `input` | object | No | Input data accessible as `input` global variable (default: `{}`) | | `options` | object | No | Execution options (see below) | @@ -260,7 +260,7 @@ var data = res.result; ### Available JavaScript Features -#### ES5.1 Standard Library +#### JavaScript Standard Library (ES2020+) ✅ **Available**: - **Objects**: `Object.keys()`, `Object.create()`, `Object.defineProperty()`, etc. @@ -277,7 +277,7 @@ var data = res.result; - **Filesystem**: No `fs` module or file I/O - **Network**: No `http`, `https`, `fetch`, or network access - **Process**: No `process` object or environment variables -- **ES6+**: No arrow functions, template literals, `async/await`, `Promise`, etc. +- **Node.js APIs**: No Node.js-specific APIs (Buffer, Stream, etc.) #### Type Conversions @@ -577,13 +577,12 @@ mcpproxy code exec --code="while(true){}" --timeout=1000 2>&1 # Save code to file for complex scripts cat > /tmp/script.js << 'EOF' -var users = ['octocat', 'torvalds']; -var results = []; -for (var i = 0; i < users.length; i++) { - var res = call_tool('github', 'get_user', {username: users[i]}); - if (res.ok) results.push(res.result.name); -} -return {names: results}; +const users = ['octocat', 'torvalds']; +const names = users + .map(username => call_tool('github', 'get_user', {username})) + .filter(res => res.ok) + .map(res => res.result.name); +return {names}; EOF mcpproxy code exec --file=/tmp/script.js @@ -597,7 +596,7 @@ mcpproxy code exec --file=/tmp/script.js - **Required**: `code` parameter must be provided - **Type**: Must be a string -- **Syntax**: Must be valid ES5.1 JavaScript +- **Syntax**: Must be valid JavaScript (ES2020+ supported) - **Serialization**: Return value must be JSON-serializable ### Input Validation diff --git a/docs/code_execution/examples.md b/docs/code_execution/examples.md index d5ae0931..36fce999 100644 --- a/docs/code_execution/examples.md +++ b/docs/code_execution/examples.md @@ -92,26 +92,22 @@ mcpproxy code exec \ // Request { "code": ` -var userRes = call_tool('github', 'get_user', {username: input.username}); +const userRes = call_tool('github', 'get_user', {username: input.username}); if (!userRes.ok) { return {error: 'Failed to get user: ' + userRes.error.message}; } -var reposRes = call_tool('github', 'list_repos', {user: input.username, limit: 5}); +const reposRes = call_tool('github', 'list_repos', {user: input.username, limit: 5}); if (!reposRes.ok) { return {error: 'Failed to get repos: ' + reposRes.error.message}; } +const { name, login, public_repos } = userRes.result; + return { - user: { - name: userRes.result.name, - login: userRes.result.login, - public_repos: userRes.result.public_repos - }, - repos: reposRes.result.map(function(r) { - return {name: r.name, stars: r.stargazers_count}; - }), - total_repos: userRes.result.public_repos + user: { name, login, public_repos }, + repos: reposRes.result.map(r => ({name: r.name, stars: r.stargazers_count})), + total_repos: public_repos }; `, "input": {"username": "octocat"} @@ -140,26 +136,22 @@ return { ```bash # Save to file for readability cat > /tmp/github_user_repos.js << 'EOF' -var userRes = call_tool('github', 'get_user', {username: input.username}); +const userRes = call_tool('github', 'get_user', {username: input.username}); if (!userRes.ok) { - return {error: 'Failed to get user: ' + userRes.error.message}; + return {error: `Failed to get user: ${userRes.error.message}`}; } -var reposRes = call_tool('github', 'list_repos', {user: input.username, limit: 5}); +const reposRes = call_tool('github', 'list_repos', {user: input.username, limit: 5}); if (!reposRes.ok) { - return {error: 'Failed to get repos: ' + reposRes.error.message}; + return {error: `Failed to get repos: ${reposRes.error.message}`}; } +const { name, login, public_repos } = userRes.result; + return { - user: { - name: userRes.result.name, - login: userRes.result.login, - public_repos: userRes.result.public_repos - }, - repos: reposRes.result.map(function(r) { - return {name: r.name, stars: r.stargazers_count}; - }), - total_repos: userRes.result.public_repos + user: { name, login, public_repos }, + repos: reposRes.result.map(r => ({name: r.name, stars: r.stargazers_count})), + total_repos: public_repos }; EOF @@ -176,25 +168,20 @@ mcpproxy code exec --file=/tmp/github_user_repos.js --input='{"username": "octoc // Request { "code": ` -var githubUser = call_tool('github', 'get_user', {username: input.username}); -var gitlabUser = call_tool('gitlab', 'get_user', {username: input.username}); -var bitbucketUser = call_tool('bitbucket', 'get_user', {username: input.username}); - -var results = { - github: githubUser.ok ? githubUser.result : null, - gitlab: gitlabUser.ok ? gitlabUser.result : null, - bitbucket: bitbucketUser.ok ? bitbucketUser.result : null -}; +const sources = ['github', 'gitlab', 'bitbucket']; +const profiles = {}; +const availableOn = []; -var availableOn = []; -if (results.github) availableOn.push('github'); -if (results.gitlab) availableOn.push('gitlab'); -if (results.bitbucket) availableOn.push('bitbucket'); +for (const source of sources) { + const res = call_tool(source, 'get_user', {username: input.username}); + profiles[source] = res.ok ? res.result : null; + if (res.ok) availableOn.push(source); +} return { username: input.username, available_on: availableOn, - profiles: results + profiles }; `, "input": {"username": "johndoe"} @@ -213,7 +200,7 @@ return { // Request { "code": ` -var res = call_tool('github', 'get_user', {username: input.username}); +const res = call_tool('github', 'get_user', {username: input.username}); if (!res.ok) { // Return structured error with details @@ -227,14 +214,8 @@ if (!res.ok) { }; } -return { - success: true, - data: { - name: res.result.name, - login: res.result.login, - bio: res.result.bio - } -}; +const { name, login, bio } = res.result; +return { success: true, data: { name, login, bio } }; `, "input": {"username": "this-user-definitely-does-not-exist-12345"} } @@ -317,28 +298,20 @@ return { // Request { "code": ` -var results = []; -var errors = []; +const results = []; +const errors = []; -for (var i = 0; i < input.repo_names.length; i++) { - var repoName = input.repo_names[i]; - var res = call_tool('github', 'get_repo', { +for (const repoName of input.repo_names) { + const res = call_tool('github', 'get_repo', { owner: input.owner, repo: repoName }); if (res.ok) { - results.push({ - name: repoName, - stars: res.result.stargazers_count, - forks: res.result.forks_count, - language: res.result.language - }); + const { stargazers_count: stars, forks_count: forks, language } = res.result; + results.push({ name: repoName, stars, forks, language }); } else { - errors.push({ - name: repoName, - error: res.error.message - }); + errors.push({ name: repoName, error: res.error.message }); } } @@ -378,22 +351,18 @@ return { **CLI Command**: ```bash cat > /tmp/batch_repos.js << 'EOF' -var results = []; -var errors = []; +const results = []; +const errors = []; -for (var i = 0; i < input.repo_names.length; i++) { - var repoName = input.repo_names[i]; - var res = call_tool('github', 'get_repo', { +for (const repoName of input.repo_names) { + const res = call_tool('github', 'get_repo', { owner: input.owner, repo: repoName }); if (res.ok) { - results.push({ - name: repoName, - stars: res.result.stargazers_count, - forks: res.result.forks_count - }); + const { stargazers_count: stars, forks_count: forks } = res.result; + results.push({ name: repoName, stars, forks }); } else { errors.push({name: repoName, error: res.error.message}); } @@ -467,7 +436,7 @@ return { // Request { "code": ` -var reposRes = call_tool('github', 'list_repos', { +const reposRes = call_tool('github', 'list_repos', { user: input.username, limit: 100 }); @@ -476,26 +445,25 @@ if (!reposRes.ok) { return {error: reposRes.error.message}; } -var repos = reposRes.result; -var totalStars = 0; -var totalForks = 0; -var languages = {}; -var activeRepos = 0; - -for (var i = 0; i < repos.length; i++) { - var repo = repos[i]; +const repos = reposRes.result; +const totalStars = repos.reduce((sum, r) => sum + (r.stargazers_count ?? 0), 0); +const totalForks = repos.reduce((sum, r) => sum + (r.forks_count ?? 0), 0); +const languages = {}; - totalStars += repo.stargazers_count || 0; - totalForks += repo.forks_count || 0; - - var lang = repo.language || 'Unknown'; - languages[lang] = (languages[lang] || 0) + 1; +let activeRepos = 0; +for (const repo of repos) { + const lang = repo.language ?? 'Unknown'; + languages[lang] = (languages[lang] ?? 0) + 1; if (!repo.archived && repo.pushed_at) { activeRepos++; } } +const sorted = [...repos].sort((a, b) => + (b.stargazers_count ?? 0) - (a.stargazers_count ?? 0) +); + return { username: input.username, total_repos: repos.length, @@ -504,10 +472,8 @@ return { total_stars: totalStars, total_forks: totalForks, avg_stars: repos.length > 0 ? Math.round(totalStars / repos.length) : 0, - languages: languages, - most_popular_repo: repos.sort(function(a, b) { - return (b.stargazers_count || 0) - (a.stargazers_count || 0); - })[0].name + languages, + most_popular_repo: sorted[0]?.name ?? 'N/A' }; `, "input": {"username": "octocat"} @@ -546,7 +512,7 @@ return { // Request { "code": ` -var reposRes = call_tool('github', 'list_repos', { +const reposRes = call_tool('github', 'list_repos', { user: input.username, limit: 50 }); @@ -556,34 +522,26 @@ if (!reposRes.ok) { } // Filter: only non-archived repos updated recently -var cutoffDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // 90 days ago +const cutoffDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000); // 90 days ago -var activeRepos = reposRes.result.filter(function(repo) { - if (repo.archived) return false; - if (!repo.pushed_at) return false; +const activeRepos = reposRes.result.filter(repo => + !repo.archived && repo.pushed_at && new Date(repo.pushed_at) > cutoffDate +); - var pushedDate = new Date(repo.pushed_at); - return pushedDate > cutoffDate; -}); - -// Map: extract relevant fields -var simplified = activeRepos.map(function(repo) { - return { +// Map, transform, and sort by stars descending +const simplified = activeRepos + .map(repo => ({ name: repo.name, - description: repo.description || 'No description', - language: repo.language || 'Unknown', + description: repo.description ?? 'No description', + language: repo.language ?? 'Unknown', stars: repo.stargazers_count, last_updated: repo.pushed_at - }; -}); - -// Sort by stars descending -simplified.sort(function(a, b) { - return b.stars - a.stars; -}); + })) + .sort((a, b) => b.stars - a.stars) + .slice(0, 10); // Top 10 return { - repos: simplified.slice(0, 10), // Top 10 + repos: simplified, total_active: activeRepos.length, total_repos: reposRes.result.length }; @@ -604,31 +562,24 @@ return { // Request { "code": ` -var users = ['octocat', 'torvalds', 'nonexistent-user-xyz']; -var successful = []; -var failed = []; +const users = ['octocat', 'torvalds', 'nonexistent-user-xyz']; +const successful = []; +const failed = []; -for (var i = 0; i < users.length; i++) { - var username = users[i]; - var res = call_tool('github', 'get_user', {username: username}); +for (const username of users) { + const res = call_tool('github', 'get_user', {username}); if (res.ok) { - successful.push({ - username: username, - name: res.result.name, - public_repos: res.result.public_repos - }); + const { name, public_repos } = res.result; + successful.push({ username, name, public_repos }); } else { - failed.push({ - username: username, - reason: res.error.message - }); + failed.push({ username, reason: res.error.message }); } } return { - successful: successful, - failed: failed, + successful, + failed, summary: { success_count: successful.length, failure_count: failed.length, @@ -672,18 +623,16 @@ return { // Request { "code": ` -var items = input.items; -var results = []; +const { items } = input; +const results = []; -for (var i = 0; i < items.length; i++) { +for (const item of items) { // Simulate delay by doing some computation - for (var j = 0; j < 1000000; j++) { + for (let j = 0; j < 1000000; j++) { // Busy wait } - var res = call_tool('api-server', 'process_item', { - id: items[i] - }); + const res = call_tool('api-server', 'process_item', { id: item }); if (res.ok) { results.push(res.result); @@ -710,7 +659,7 @@ return {processed: results, count: results.length}; { "code": ` // Get user's repos -var reposRes = call_tool('github', 'list_repos', { +const reposRes = call_tool('github', 'list_repos', { user: input.username, limit: 5 }); @@ -719,24 +668,20 @@ if (!reposRes.ok) { return {error: reposRes.error.message}; } -var reposWithContributors = []; - // For each repo, fetch contributors -for (var i = 0; i < reposRes.result.length; i++) { - var repo = reposRes.result[i]; - - var contributorsRes = call_tool('github', 'list_contributors', { +const reposWithContributors = reposRes.result.map(repo => { + const contributorsRes = call_tool('github', 'list_contributors', { owner: input.username, repo: repo.name, limit: 3 }); - reposWithContributors.push({ + return { name: repo.name, stars: repo.stargazers_count, contributors: contributorsRes.ok ? contributorsRes.result : [] - }); -} + }; +}); return { username: input.username, diff --git a/docs/code_execution/overview.md b/docs/code_execution/overview.md index acf2f418..d34af31b 100644 --- a/docs/code_execution/overview.md +++ b/docs/code_execution/overview.md @@ -52,8 +52,8 @@ if (!user.ok) { // Loop with accumulation const results = []; -for (let i = 0; i < input.repos.length; i++) { - const repo = call_tool('github', 'get_repo', {name: input.repos[i]}); +for (const repoName of input.repos) { + const repo = call_tool('github', 'get_repo', {name: repoName}); if (repo.ok) { results.push(repo.result); } @@ -69,11 +69,9 @@ const repos = call_tool('github', 'list_repos', {user: input.username}); if (!repos.ok) return repos; // Filter and transform -const activeRepos = repos.result.filter(function(r) { - return !r.archived && r.pushed_at > input.since; -}).map(function(r) { - return {name: r.name, stars: r.stargazers_count, language: r.language}; -}); +const activeRepos = repos.result + .filter(r => !r.archived && r.pushed_at > input.since) + .map(r => ({name: r.name, stars: r.stargazers_count, language: r.language})); return {repos: activeRepos, total: activeRepos.length}; ``` @@ -147,12 +145,12 @@ The JavaScript execution environment is **heavily sandboxed** to prevent securit - Filesystem access - No `fs` module - Network access - No `http` or `fetch` - Environment variables - No `process.env` -- Node.js built-ins - ES5.1+ standard library only +- Node.js built-ins - JavaScript standard library only ✅ **Available:** - `input` - Global variable with request input data - `call_tool(serverName, toolName, args)` - Function to call upstream MCP tools -- ES5.1+ standard library (Array, Object, String, Math, Date, JSON, etc.) +- Modern JavaScript (ES2020+) standard library including Array, Object, String, Math, Date, JSON, Map, Set, Symbol, Promise, Proxy, Reflect ### Configuration & Limits @@ -269,7 +267,7 @@ return { ```javascript // Try primary server, fallback to secondary -var result = call_tool('primary-db', 'query', {sql: input.query}); +let result = call_tool('primary-db', 'query', {sql: input.query}); if (!result.ok) { // Primary failed, try backup @@ -283,16 +281,16 @@ return result.ok ? result.result : {error: 'Both databases unavailable'}; ```javascript // Fetch details for multiple items -var results = []; -var errors = []; +const results = []; +const errors = []; -for (var i = 0; i < input.ids.length; i++) { - var res = call_tool('api-server', 'get_item', {id: input.ids[i]}); +for (const id of input.ids) { + const res = call_tool('api-server', 'get_item', {id}); if (res.ok) { results.push(res.result); } else { - errors.push({id: input.ids[i], error: res.error}); + errors.push({id, error: res.error}); } } @@ -308,25 +306,23 @@ return { ```javascript // Get repos and compute statistics -var reposRes = call_tool('github', 'list_repos', {user: input.username}); +const reposRes = call_tool('github', 'list_repos', {user: input.username}); if (!reposRes.ok) return reposRes; -var repos = reposRes.result; -var totalStars = 0; -var languages = {}; - -for (var i = 0; i < repos.length; i++) { - totalStars += repos[i].stargazers_count || 0; +const repos = reposRes.result; +const totalStars = repos.reduce((sum, r) => sum + (r.stargazers_count ?? 0), 0); +const languages = {}; - var lang = repos[i].language || 'Unknown'; - languages[lang] = (languages[lang] || 0) + 1; +for (const repo of repos) { + const lang = repo.language ?? 'Unknown'; + languages[lang] = (languages[lang] ?? 0) + 1; } return { total_repos: repos.length, total_stars: totalStars, avg_stars: Math.round(totalStars / repos.length), - languages: languages + languages }; ``` @@ -380,7 +376,7 @@ code_execution({ ## Best Practices ### 1. Keep Code Simple -- Use ES5.1 syntax (no arrow functions, template literals, or async/await) +- Use modern JavaScript syntax (arrow functions, const/let, template literals, destructuring are all supported) - Avoid deeply nested logic - Prefer explicit error handling over implicit failures diff --git a/docs/code_execution/troubleshooting.md b/docs/code_execution/troubleshooting.md index 7f0c2171..5d1f562f 100644 --- a/docs/code_execution/troubleshooting.md +++ b/docs/code_execution/troubleshooting.md @@ -109,46 +109,25 @@ mcpproxy call tool --tool-name=retrieve_tools --json_args='{"query":"code execut **Common Causes**: 1. Missing closing brackets/braces 2. Invalid JavaScript syntax -3. Using ES6+ features (not supported) **Solutions**: **Problem**: Missing brackets ```javascript // Bad -var x = { value: 1, missing: 2 +const x = { value: 1, missing: 2 // Good -var x = { value: 1, missing: 2 }; +const x = { value: 1, missing: 2 }; ``` -**Problem**: ES6 arrow functions +**Problem**: Incomplete expressions ```javascript -// Bad (ES6) -var doubled = arr.map(x => x * 2); - -// Good (ES5) -var doubled = arr.map(function(x) { return x * 2; }); -``` - -**Problem**: Template literals -```javascript -// Bad (ES6) -var msg = `Hello, ${name}!`; - -// Good (ES5) -var msg = 'Hello, ' + name + '!'; -``` - -**Problem**: Const/let -```javascript -// Bad (ES6) -const x = 42; -let y = 10; +// Bad +const msg = `Hello, ${ -// Good (ES5) -var x = 42; -var y = 10; +// Good +const msg = `Hello, ${name}!`; ``` --- @@ -729,7 +708,7 @@ return result; ### Use Error Boundaries -Wrap risky operations in try-catch (though not available in ES5, use checks instead): +Wrap risky operations in try-catch or use checks: ```javascript // Check before accessing diff --git a/docs/features/code-execution.md b/docs/features/code-execution.md index b22b6573..cc97eb58 100644 --- a/docs/features/code-execution.md +++ b/docs/features/code-execution.md @@ -9,7 +9,7 @@ keywords: [code, execution, javascript, orchestration] # Code Execution -The `code_execution` tool enables orchestrating multiple upstream MCP tools in a single request using sandboxed JavaScript (ES5.1+). +The `code_execution` tool enables orchestrating multiple upstream MCP tools in a single request using sandboxed JavaScript (ES2020+). ## Overview @@ -180,14 +180,14 @@ Increase the timeout or optimize your code: ### Syntax Errors -Use ES5.1 syntax (no arrow functions, let/const, template literals): +Modern JavaScript syntax (ES2020+) is fully supported, including arrow functions, const/let, template literals, destructuring, optional chaining, and nullish coalescing: ```javascript -// Wrong +// All of these work const result = () => call_tool('server', 'tool'); - -// Correct -var result = function() { return call_tool('server', 'tool'); }; +const name = user?.profile?.name ?? 'unknown'; +const msg = `Hello, ${name}!`; +const { data, error } = response; ``` ### Tool Not Found diff --git a/go.mod b/go.mod index a17bea7d..72e8e430 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/blevesearch/bleve/v2 v2.5.2 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 - github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7 + github.com/dop251/goja v0.0.0-20260305124333-6a7976c22267 github.com/gen2brain/beeep v0.11.1 github.com/go-chi/chi/v5 v5.2.3 github.com/golang-jwt/jwt/v5 v5.3.0 @@ -35,6 +35,7 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 go.uber.org/zap v1.27.0 golang.org/x/mod v0.26.0 + golang.org/x/sys v0.38.0 golang.org/x/term v0.37.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 @@ -141,7 +142,6 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect golang.org/x/net v0.43.0 // indirect - golang.org/x/sys v0.38.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.35.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 // indirect diff --git a/go.sum b/go.sum index ad802c88..b4474e67 100644 --- a/go.sum +++ b/go.sum @@ -92,8 +92,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7 h1:jxmXU5V9tXxJnydU5v/m9SG8TRUa/Z7IXODBpMs/P+U= -github.com/dop251/goja v0.0.0-20251103141225-af2ceb9156d7/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= +github.com/dop251/goja v0.0.0-20260305124333-6a7976c22267 h1:Kfmq11A6DLHD8XoOeljWjzWg/rrujeaLHWSb8u7+2qQ= +github.com/dop251/goja v0.0.0-20260305124333-6a7976c22267/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o= diff --git a/internal/jsruntime/modern_js_test.go b/internal/jsruntime/modern_js_test.go new file mode 100644 index 00000000..01225fa3 --- /dev/null +++ b/internal/jsruntime/modern_js_test.go @@ -0,0 +1,545 @@ +package jsruntime + +import ( + "context" + "testing" +) + +// TestModernJSArrowFunctions verifies arrow function syntax works +func TestModernJSArrowFunctions(t *testing.T) { + caller := newMockToolCaller() + + tests := []struct { + name string + code string + }{ + { + "basic arrow function", + `const double = (x) => x * 2; ({ result: double(21) })`, + }, + { + "arrow with implicit return", + `const items = [1, 2, 3]; ({ result: items.map(x => x * 2) })`, + }, + { + "arrow with block body", + `const greet = (name) => { return "Hello, " + name; }; ({ result: greet("world") })`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Execute(context.Background(), caller, tt.code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + }) + } +} + +// TestModernJSConstLet verifies const and let declarations work +func TestModernJSConstLet(t *testing.T) { + caller := newMockToolCaller() + + tests := []struct { + name string + code string + }{ + { + "const declaration", + `const x = 42; ({ result: x })`, + }, + { + "let declaration", + `let x = 10; x = 20; ({ result: x })`, + }, + { + "block scoping with let", + `let x = 1; { let x = 2; } ({ result: x })`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Execute(context.Background(), caller, tt.code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + }) + } +} + +// TestModernJSTemplateLiterals verifies template literal syntax works +func TestModernJSTemplateLiterals(t *testing.T) { + caller := newMockToolCaller() + + code := "const name = 'world'; ({ result: `Hello, ${name}!` })" + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["result"] != "Hello, world!" { + t.Errorf("expected 'Hello, world!', got %v", resultMap["result"]) + } +} + +// TestModernJSDestructuring verifies destructuring assignment works +func TestModernJSDestructuring(t *testing.T) { + caller := newMockToolCaller() + + tests := []struct { + name string + code string + }{ + { + "object destructuring", + `const { a, b } = { a: 1, b: 2 }; ({ result: a + b })`, + }, + { + "array destructuring", + `const [x, y, z] = [10, 20, 30]; ({ result: x + y + z })`, + }, + { + "nested destructuring", + `const { user: { name } } = { user: { name: "Alice" } }; ({ result: name })`, + }, + { + "destructuring with defaults", + `const { a = 1, b = 2 } = { a: 10 }; ({ result: a + b })`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Execute(context.Background(), caller, tt.code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + }) + } +} + +// TestModernJSSpreadRest verifies spread and rest operators work +func TestModernJSSpreadRest(t *testing.T) { + caller := newMockToolCaller() + + tests := []struct { + name string + code string + }{ + { + "array spread", + `const a = [1, 2]; const b = [...a, 3, 4]; ({ result: b.length })`, + }, + { + "object spread", + `const a = { x: 1 }; const b = { ...a, y: 2 }; ({ result: b.x + b.y })`, + }, + { + "rest parameters", + `function sum(...nums) { return nums.reduce((a, b) => a + b, 0); } ({ result: sum(1, 2, 3, 4) })`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Execute(context.Background(), caller, tt.code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + }) + } +} + +// TestModernJSDefaultParameters verifies default parameter values work +func TestModernJSDefaultParameters(t *testing.T) { + caller := newMockToolCaller() + + code := `function greet(name = "world") { return "Hello, " + name; } ({ result: greet() })` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["result"] != "Hello, world" { + t.Errorf("expected 'Hello, world', got %v", resultMap["result"]) + } +} + +// TestModernJSClasses verifies ES6 class syntax works +func TestModernJSClasses(t *testing.T) { + caller := newMockToolCaller() + + code := ` + class Animal { + constructor(name) { + this.name = name; + } + speak() { + return this.name + " makes a sound"; + } + } + + class Dog extends Animal { + speak() { + return this.name + " barks"; + } + } + + const dog = new Dog("Rex"); + ({ result: dog.speak() }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["result"] != "Rex barks" { + t.Errorf("expected 'Rex barks', got %v", resultMap["result"]) + } +} + +// TestModernJSForOf verifies for-of loop works +func TestModernJSForOf(t *testing.T) { + caller := newMockToolCaller() + + code := ` + const items = [10, 20, 30]; + let sum = 0; + for (const item of items) { + sum += item; + } + ({ result: sum }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + sumVal := toInt64(resultMap["result"]) + if sumVal != 60 { + t.Errorf("expected 60, got %v", resultMap["result"]) + } +} + +// TestModernJSPromises verifies Promise support works +func TestModernJSPromises(t *testing.T) { + caller := newMockToolCaller() + + // Goja supports Promise constructor but execution is synchronous + code := ` + const p = new Promise((resolve) => { + resolve(42); + }); + ({ result: typeof Promise !== 'undefined' }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["result"] != true { + t.Errorf("expected Promise to be defined, got %v", resultMap["result"]) + } +} + +// TestModernJSSymbols verifies Symbol support +func TestModernJSSymbols(t *testing.T) { + caller := newMockToolCaller() + + code := ` + const sym = Symbol('test'); + ({ result: typeof sym === 'symbol', description: sym.toString() }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["result"] != true { + t.Errorf("expected typeof Symbol to be 'symbol', got %v", resultMap["result"]) + } +} + +// TestModernJSMapSet verifies Map and Set work +func TestModernJSMapSet(t *testing.T) { + caller := newMockToolCaller() + + tests := []struct { + name string + code string + }{ + { + "Map basics", + ` + const m = new Map(); + m.set('key1', 'value1'); + m.set('key2', 'value2'); + ({ size: m.size, hasKey1: m.has('key1'), value: m.get('key1') }) + `, + }, + { + "Set basics", + ` + const s = new Set([1, 2, 3, 2, 1]); + ({ size: s.size, has2: s.has(2), has4: s.has(4) }) + `, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := Execute(context.Background(), caller, tt.code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + }) + } +} + +// TestModernJSOptionalChaining verifies optional chaining (?.) works +func TestModernJSOptionalChaining(t *testing.T) { + caller := newMockToolCaller() + + code := ` + const obj = { user: { name: "Alice" } }; + const missing = { user: null }; + ({ + found: obj.user?.name, + notFound: missing.user?.name ?? "default", + deepMissing: obj.foo?.bar?.baz ?? "fallback" + }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["found"] != "Alice" { + t.Errorf("expected 'Alice', got %v", resultMap["found"]) + } + if resultMap["notFound"] != "default" { + t.Errorf("expected 'default', got %v", resultMap["notFound"]) + } + if resultMap["deepMissing"] != "fallback" { + t.Errorf("expected 'fallback', got %v", resultMap["deepMissing"]) + } +} + +// TestModernJSNullishCoalescing verifies nullish coalescing (??) works +func TestModernJSNullishCoalescing(t *testing.T) { + caller := newMockToolCaller() + + code := ` + const a = null ?? "default_a"; + const b = undefined ?? "default_b"; + const c = 0 ?? "default_c"; + const d = "" ?? "default_d"; + ({ a, b, c, d }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["a"] != "default_a" { + t.Errorf("expected 'default_a', got %v", resultMap["a"]) + } + if resultMap["b"] != "default_b" { + t.Errorf("expected 'default_b', got %v", resultMap["b"]) + } + // 0 is not null/undefined, so ?? should return 0 + if toInt64(resultMap["c"]) != 0 { + t.Errorf("expected 0, got %v", resultMap["c"]) + } + // "" is not null/undefined, so ?? should return "" + if resultMap["d"] != "" { + t.Errorf("expected '', got %v", resultMap["d"]) + } +} + +// TestModernJSGenerators verifies generator function support +func TestModernJSGenerators(t *testing.T) { + caller := newMockToolCaller() + + code := ` + function* range(start, end) { + for (let i = start; i < end; i++) { + yield i; + } + } + + const nums = []; + for (const n of range(1, 5)) { + nums.push(n); + } + ({ result: nums }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + nums := resultMap["result"].([]interface{}) + if len(nums) != 4 { + t.Errorf("expected 4 numbers, got %d", len(nums)) + } +} + +// TestModernJSProxyReflect verifies Proxy and Reflect support +func TestModernJSProxyReflect(t *testing.T) { + caller := newMockToolCaller() + + code := ` + const handler = { + get: function(target, prop) { + return prop in target ? target[prop] : "default"; + } + }; + + const obj = new Proxy({ name: "Alice" }, handler); + ({ + name: obj.name, + missing: obj.missing, + hasReflect: typeof Reflect !== 'undefined' + }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["name"] != "Alice" { + t.Errorf("expected 'Alice', got %v", resultMap["name"]) + } + if resultMap["missing"] != "default" { + t.Errorf("expected 'default', got %v", resultMap["missing"]) + } + if resultMap["hasReflect"] != true { + t.Errorf("expected Reflect to be defined") + } +} + +// TestModernJSComputedPropertyNames verifies computed property names work +func TestModernJSComputedPropertyNames(t *testing.T) { + caller := newMockToolCaller() + + code := ` + const key = "dynamic"; + const obj = { [key]: "value", ["computed_" + 1]: true }; + ({ result: obj.dynamic, computed: obj.computed_1 }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["result"] != "value" { + t.Errorf("expected 'value', got %v", resultMap["result"]) + } +} + +// TestModernJSShorthandProperties verifies shorthand property syntax works +func TestModernJSShorthandProperties(t *testing.T) { + caller := newMockToolCaller() + + code := ` + const name = "Alice"; + const age = 30; + const obj = { name, age }; + ({ result: obj.name + " is " + obj.age }) + ` + result := Execute(context.Background(), caller, code, ExecutionOptions{}) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["result"] != "Alice is 30" { + t.Errorf("expected 'Alice is 30', got %v", resultMap["result"]) + } +} + +// TestModernJSWithToolCalls verifies modern JS syntax works with tool calling +func TestModernJSWithToolCalls(t *testing.T) { + caller := newMockToolCaller() + caller.results["github:get_user"] = map[string]interface{}{ + "login": "octocat", + "id": 583231, + "name": "The Octocat", + } + caller.results["github:list_repos"] = []interface{}{ + map[string]interface{}{"name": "repo1", "stars": 100}, + map[string]interface{}{"name": "repo2", "stars": 200}, + } + + code := ` + const userRes = call_tool("github", "get_user", { username: input.username }); + if (!userRes.ok) throw new Error("Failed: " + userRes.error.message); + + const reposRes = call_tool("github", "list_repos", { user: input.username }); + if (!reposRes.ok) throw new Error("Failed: " + reposRes.error.message); + + const repos = reposRes.result.map(r => ({ + name: r.name, + stars: r.stars + })); + + const totalStars = repos.reduce((sum, r) => sum + r.stars, 0); + + ({ + user: userRes.result.name, + repos, + totalStars, + summary: ` + "`${userRes.result.name} has ${repos.length} repos with ${totalStars} stars`" + ` + }) + ` + opts := ExecutionOptions{ + Input: map[string]interface{}{ + "username": "octocat", + }, + } + + result := Execute(context.Background(), caller, code, opts) + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap := result.Value.(map[string]interface{}) + if resultMap["user"] != "The Octocat" { + t.Errorf("expected 'The Octocat', got %v", resultMap["user"]) + } + if toInt64(resultMap["totalStars"]) != 300 { + t.Errorf("expected totalStars=300, got %v", resultMap["totalStars"]) + } + expectedSummary := "The Octocat has 2 repos with 300 stars" + if resultMap["summary"] != expectedSummary { + t.Errorf("expected summary=%q, got %v", expectedSummary, resultMap["summary"]) + } +} + +// toInt64 converts a numeric interface{} to int64 +func toInt64(v interface{}) int64 { + switch val := v.(type) { + case int64: + return val + case float64: + return int64(val) + case int: + return int64(val) + default: + return 0 + } +} diff --git a/internal/server/mcp.go b/internal/server/mcp.go index ed6a8079..fd2da01a 100644 --- a/internal/server/mcp.go +++ b/internal/server/mcp.go @@ -448,12 +448,12 @@ func (p *MCPProxyServer) registerTools(_ bool) { // code_execution - JavaScript code execution for multi-tool orchestration (feature-flagged) if p.config.EnableCodeExecution { codeExecutionTool := mcp.NewTool("code_execution", - mcp.WithDescription("Execute JavaScript code that orchestrates multiple upstream MCP tools in a single request. Use this when you need to combine results from 2+ tools, implement conditional logic, loops, or data transformations that would require multiple round-trips otherwise.\n\n**When to use**: Multi-step workflows with data transformation, conditional logic, error handling, or iterating over results.\n**When NOT to use**: Single tool calls (use call_tool directly), long-running operations (>2 minutes).\n\n**Available in JavaScript**:\n- `input` global: Your input data passed via the 'input' parameter\n- `call_tool(serverName, toolName, args)`: Call upstream tools (returns {ok, result} or {ok, error})\n- Standard ES5.1+ JavaScript (no require(), filesystem, or network access)\n\n**Security**: Sandboxed execution with timeout enforcement. Respects existing quarantine and server restrictions."), + mcp.WithDescription("Execute JavaScript code that orchestrates multiple upstream MCP tools in a single request. Use this when you need to combine results from 2+ tools, implement conditional logic, loops, or data transformations that would require multiple round-trips otherwise.\n\n**When to use**: Multi-step workflows with data transformation, conditional logic, error handling, or iterating over results.\n**When NOT to use**: Single tool calls (use call_tool directly), long-running operations (>2 minutes).\n\n**Available in JavaScript**:\n- `input` global: Your input data passed via the 'input' parameter\n- `call_tool(serverName, toolName, args)`: Call upstream tools (returns {ok, result} or {ok, error})\n- Modern JavaScript (ES2020+): arrow functions, const/let, template literals, destructuring, classes, for-of, optional chaining (?.), nullish coalescing (??), spread/rest, Promises, Symbols, Map/Set, Proxy/Reflect (no require(), filesystem, or network access)\n\n**Security**: Sandboxed execution with timeout enforcement. Respects existing quarantine and server restrictions."), mcp.WithTitleAnnotation("Code Execution"), mcp.WithDestructiveHintAnnotation(true), mcp.WithString("code", mcp.Required(), - mcp.Description("JavaScript source code (ES5.1+) to execute. Use `input` to access input data and `call_tool(serverName, toolName, args)` to invoke upstream tools. Return value must be JSON-serializable. Example: `const res = call_tool('github', 'get_user', {username: input.username}); if (!res.ok) throw new Error(res.error.message); ({user: res.result, timestamp: Date.now()})`"), + mcp.Description("JavaScript source code (ES2020+) to execute. Supports modern syntax: arrow functions, const/let, template literals, destructuring, optional chaining, nullish coalescing. Use `input` to access input data and `call_tool(serverName, toolName, args)` to invoke upstream tools. Return value must be JSON-serializable. Example: `const res = call_tool('github', 'get_user', {username: input.username}); if (!res.ok) throw new Error(res.error.message); ({user: res.result, timestamp: Date.now()})`"), ), mcp.WithObject("input", mcp.Description("Input data accessible as global `input` variable in JavaScript code (default: {})"),