diff --git a/CLAUDE.md b/CLAUDE.md index b7b8c11b..e76bbff4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -633,6 +633,8 @@ See `docs/prerelease-builds.md` for download instructions. - BBolt database (`~/.mcpproxy/config.db`) — new `agent_tokens` bucket (028-agent-tokens) - Go 1.24 (toolchain go1.24.10) + TypeScript 5.9 / Vue 3.5 + Chi router, BBolt, Zap logging, mcp-go, golang-jwt/jwt/v5, Vue 3, Pinia, DaisyUI (024-teams-multiuser-oauth) - BBolt database (`~/.mcpproxy/config.db`) - new buckets for users, sessions, user servers (024-teams-multiuser-oauth) +- Go 1.24 (toolchain go1.24.10) + `github.com/dop251/goja` (existing JS sandbox), `github.com/evanw/esbuild` (new - TypeScript transpilation), `github.com/mark3labs/mcp-go` (MCP protocol), `github.com/spf13/cobra` (CLI) (033-typescript-code-execution) +- N/A (no new storage requirements) (033-typescript-code-execution) ## Recent Changes - 001-update-version-display: Added Go 1.24 (toolchain go1.24.10) diff --git a/cmd/mcpproxy/code_cmd.go b/cmd/mcpproxy/code_cmd.go index 90f549ee..7b658465 100644 --- a/cmd/mcpproxy/code_cmd.go +++ b/cmd/mcpproxy/code_cmd.go @@ -28,16 +28,19 @@ import ( var ( codeCmd = &cobra.Command{ Use: "code", - Short: "JavaScript code execution for multi-tool orchestration", - Long: "Execute JavaScript code that orchestrates multiple upstream MCP tools in a single request", + Short: "JavaScript/TypeScript code execution for multi-tool orchestration", + Long: "Execute JavaScript or TypeScript code that orchestrates multiple upstream MCP tools in a single request", } codeExecCmd = &cobra.Command{ Use: "exec", - Short: "Execute JavaScript code", - Long: `Execute JavaScript code that can orchestrate multiple upstream MCP tools. + Short: "Execute JavaScript or TypeScript code", + Long: `Execute JavaScript or TypeScript code that can orchestrate multiple upstream MCP tools. -The JavaScript code has access to: +Use --language typescript to write TypeScript code with type annotations, +interfaces, enums, and generics. Types are automatically stripped before execution. + +The code has access to: - input: Global variable containing the input data (from --input or --input-file) - call_tool(serverName, toolName, args): Function to invoke upstream MCP tools @@ -63,6 +66,7 @@ Exit codes: codeAllowedSrvs []string codeLogLevel string codeConfigPath string + codeLanguage string ) // GetCodeCommand returns the code command for adding to the root command @@ -84,14 +88,21 @@ func init() { codeExecCmd.Flags().StringSliceVar(&codeAllowedSrvs, "allowed-servers", []string{}, "Comma-separated list of allowed server names (empty = all allowed)") codeExecCmd.Flags().StringVarP(&codeLogLevel, "log-level", "l", "info", "Log level (trace, debug, info, warn, error)") codeExecCmd.Flags().StringVarP(&codeConfigPath, "config", "c", "", "Path to MCP configuration file (default: ~/.mcpproxy/mcp_config.json)") + codeExecCmd.Flags().StringVar(&codeLanguage, "language", "javascript", "Source code language: javascript, typescript") // Add examples codeExecCmd.Example = ` # Execute inline code with input mcpproxy code exec --code="({ result: input.value * 2 })" --input='{"value": 21}' + # Execute TypeScript code + mcpproxy code exec --language typescript --code="const x: number = 42; ({ result: x })" + # Execute code from file mcpproxy code exec --file=script.js --input-file=params.json + # Execute TypeScript from file + mcpproxy code exec --language typescript --file=script.ts --input-file=params.json + # Call upstream tools mcpproxy code exec --code="call_tool('github', 'get_user', {username: input.user})" --input='{"user":"octocat"}' @@ -196,6 +207,7 @@ func runCodeExecClientMode(dataDir, code string, input map[string]interface{}, l codeTimeout, codeMaxToolCalls, codeAllowedSrvs, + cliclient.CodeExecOptions{Language: codeLanguage}, ) if err != nil { // T029: Use formatErrorWithRequestID to include request_id in error output @@ -274,6 +286,11 @@ func runCodeExecStandalone(globalConfig *config.Config, code string, input map[s }, } + // Pass language if not the default + if codeLanguage != "" && codeLanguage != "javascript" { + args["language"] = codeLanguage + } + // Call the code_execution tool result, err := mcpProxy.CallBuiltInTool(ctx, "code_execution", args) if err != nil { @@ -330,6 +347,10 @@ func validateOptions() error { fmt.Fprintf(os.Stderr, "Error: max-tool-calls cannot be negative\n") return fmt.Errorf("invalid max-tool-calls") } + if codeLanguage != "javascript" && codeLanguage != "typescript" { + fmt.Fprintf(os.Stderr, "Error: unsupported language %q. Supported languages: javascript, typescript\n", codeLanguage) + return fmt.Errorf("invalid language") + } return nil } diff --git a/docs/code_execution/api-reference.md b/docs/code_execution/api-reference.md index 66714257..2004d24e 100644 --- a/docs/code_execution/api-reference.md +++ b/docs/code_execution/api-reference.md @@ -1,6 +1,6 @@ -# JavaScript Code Execution - API Reference +# Code Execution - API Reference -Complete reference for the `code_execution` MCP tool. +Complete reference for the `code_execution` MCP tool (JavaScript and TypeScript). ## Table of Contents @@ -27,11 +27,17 @@ Complete reference for the `code_execution` MCP tool. "properties": { "code": { "type": "string", - "description": "JavaScript source code (ES2020+) to execute..." + "description": "JavaScript or TypeScript source code (ES2020+) to execute..." + }, + "language": { + "type": "string", + "description": "Source code language. When set to 'typescript', the code is automatically transpiled to JavaScript before execution.", + "enum": ["javascript", "typescript"], + "default": "javascript" }, "input": { "type": "object", - "description": "Input data accessible as global `input` variable in JavaScript code", + "description": "Input data accessible as global `input` variable in code", "default": {} }, "options": { @@ -77,6 +83,16 @@ Complete reference for the `code_execution` MCP tool. } ``` +### TypeScript Request + +```json +{ + "code": "const x: number = 42; const msg: string = 'hello'; ({ result: x, message: msg })", + "language": "typescript", + "input": {} +} +``` + ### Full Request with Options ```json @@ -97,7 +113,8 @@ Complete reference for the `code_execution` MCP tool. | Parameter | Type | Required | Description | |-----------|------|----------|-------------| -| `code` | string | **Yes** | JavaScript source code to execute (ES2020+ syntax supported) | +| `code` | string | **Yes** | JavaScript or TypeScript source code to execute (ES2020+ syntax supported) | +| `language` | string | No | Source language: `"javascript"` (default) or `"typescript"` | | `input` | object | No | Input data accessible as `input` global variable (default: `{}`) | | `options` | object | No | Execution options (see below) | diff --git a/docs/code_execution/overview.md b/docs/code_execution/overview.md index d34af31b..718ed3ce 100644 --- a/docs/code_execution/overview.md +++ b/docs/code_execution/overview.md @@ -1,8 +1,10 @@ -# JavaScript Code Execution - Overview +# Code Execution - Overview ## What is Code Execution? -The `code_execution` tool enables LLM agents to orchestrate multiple upstream MCP tools in a single request using JavaScript. Instead of making multiple round-trips to the model, you can execute complex multi-step workflows with conditional logic, loops, and data transformations—all within a single execution context. +The `code_execution` tool enables LLM agents to orchestrate multiple upstream MCP tools in a single request using JavaScript or TypeScript. Instead of making multiple round-trips to the model, you can execute complex multi-step workflows with conditional logic, loops, and data transformations—all within a single execution context. + +**TypeScript support**: Set `language: "typescript"` to write code with type annotations, interfaces, enums, and generics. Types are automatically stripped before execution with near-zero overhead (<5ms). ## When to Use Code Execution @@ -213,9 +215,12 @@ mcpproxy serve ### 3. Test with CLI ```bash -# Simple test +# Simple JavaScript test mcpproxy code exec --code="({ result: input.value * 2 })" --input='{"value": 21}' +# TypeScript test +mcpproxy code exec --language typescript --code="const x: number = 42; ({ result: x })" + # Call upstream tool mcpproxy code exec --code="call_tool('github', 'get_user', {username: input.user})" --input='{"user":"octocat"}' ``` @@ -373,6 +378,59 @@ code_execution({ // Returns: {ok: false, error: {code: "MAX_TOOL_CALLS_EXCEEDED", message: "..."}} ``` +## TypeScript Support + +You can write code execution scripts in TypeScript by setting the `language` parameter to `"typescript"`. TypeScript types are automatically stripped before execution using esbuild, with near-zero transpilation overhead. + +### Supported TypeScript Features + +- Type annotations: `const x: number = 42` +- Interfaces: `interface User { name: string; age: number; }` +- Type aliases: `type StringOrNumber = string | number` +- Generics: `function identity(arg: T): T { return arg; }` +- Enums: `enum Direction { Up = "UP", Down = "DOWN" }` +- Namespaces: `namespace MyLib { export const value = 42; }` +- Type assertions: `const x = value as string` + +### TypeScript via MCP Tool + +```json +{ + "name": "code_execution", + "arguments": { + "code": "interface User { name: string; }\nconst user: User = { name: input.username };\n({ greeting: 'Hello ' + user.name })", + "language": "typescript", + "input": {"username": "Alice"} + } +} +``` + +### TypeScript via REST API + +```bash +curl -X POST http://127.0.0.1:8080/api/v1/code/exec \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-key" \ + -d '{ + "code": "const x: number = 42; ({ result: x })", + "language": "typescript" + }' +``` + +### TypeScript via CLI + +```bash +mcpproxy code exec --language typescript \ + --code="const x: number = 42; ({ result: x })" +``` + +### Important Notes + +- TypeScript support uses type-stripping only (no type checking or semantic validation) +- Valid JavaScript is also valid TypeScript, so you can always use `language: "typescript"` even for plain JS +- The transpiled output runs in the same ES5.1+ goja sandbox with all existing capabilities +- Transpilation errors return the `TRANSPILE_ERROR` error code with line/column information + ## Best Practices ### 1. Keep Code Simple diff --git a/go.mod b/go.mod index 72e8e430..3f6be6b9 100644 --- a/go.mod +++ b/go.mod @@ -82,6 +82,7 @@ require ( github.com/dlclark/regexp2 v1.11.4 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/esiqveland/notify v0.13.3 // indirect + github.com/evanw/esbuild v0.27.3 // indirect github.com/fsnotify/fsnotify v1.8.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect diff --git a/go.sum b/go.sum index b4474e67..7126418b 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,8 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 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= github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE= +github.com/evanw/esbuild v0.27.3 h1:dH/to9tBKybig6hl25hg4SKIWP7U8COdJKbGEwnUkmU= +github.com/evanw/esbuild v0.27.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= @@ -318,6 +320,7 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= diff --git a/internal/cliclient/client.go b/internal/cliclient/client.go index 06320e16..9015ddf1 100644 --- a/internal/cliclient/client.go +++ b/internal/cliclient/client.go @@ -147,7 +147,12 @@ func parseAPIError(errorMsg, requestID string) error { return &APIError{Message: errorMsg, RequestID: requestID} } -// CodeExec executes JavaScript code via the daemon API. +// CodeExecOptions contains optional parameters for code execution via the daemon API. +type CodeExecOptions struct { + Language string // Source language: "javascript" (default) or "typescript" +} + +// CodeExec executes JavaScript or TypeScript code via the daemon API. func (c *Client) CodeExec( ctx context.Context, code string, @@ -155,6 +160,7 @@ func (c *Client) CodeExec( timeoutMS int, maxToolCalls int, allowedServers []string, + opts ...CodeExecOptions, ) (*CodeExecResult, error) { // Build request body reqBody := map[string]interface{}{ @@ -167,6 +173,11 @@ func (c *Client) CodeExec( }, } + // Apply optional language parameter + if len(opts) > 0 && opts[0].Language != "" && opts[0].Language != "javascript" { + reqBody["language"] = opts[0].Language + } + bodyBytes, err := json.Marshal(reqBody) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) diff --git a/internal/httpapi/code_exec.go b/internal/httpapi/code_exec.go index 2d764feb..0b879036 100644 --- a/internal/httpapi/code_exec.go +++ b/internal/httpapi/code_exec.go @@ -15,9 +15,10 @@ import ( // CodeExecRequest represents the request body for code execution. type CodeExecRequest struct { - Code string `json:"code"` - Input map[string]interface{} `json:"input"` - Options CodeExecOptions `json:"options"` + Code string `json:"code"` + Language string `json:"language,omitempty"` // "javascript" (default) or "typescript" + Input map[string]interface{} `json:"input"` + Options CodeExecOptions `json:"options"` } // CodeExecOptions represents execution options. @@ -75,6 +76,13 @@ func (h *CodeExecHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // Validate language if provided + if req.Language != "" && req.Language != "javascript" && req.Language != "typescript" { + h.writeError(w, r, http.StatusBadRequest, "INVALID_LANGUAGE", + fmt.Sprintf("Unsupported language %q. Supported languages: javascript, typescript", req.Language)) + return + } + // Set defaults if req.Input == nil { req.Input = make(map[string]interface{}) @@ -99,6 +107,11 @@ func (h *CodeExecHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { }, } + // Pass language if specified + if req.Language != "" { + args["language"] = req.Language + } + // Call the code_execution built-in tool result, err := h.toolCaller.CallTool(ctx, "code_execution", args) if err != nil { diff --git a/internal/httpapi/code_exec_test.go b/internal/httpapi/code_exec_test.go index dd3a07b7..7256e61d 100644 --- a/internal/httpapi/code_exec_test.go +++ b/internal/httpapi/code_exec_test.go @@ -29,22 +29,22 @@ func (m *mockController) CallTool(ctx context.Context, toolName string, args map } // Stub implementations for other ServerController methods -func (m *mockController) IsRunning() bool { return true } -func (m *mockController) IsReady() bool { return true } -func (m *mockController) GetListenAddress() string { return "" } -func (m *mockController) GetUpstreamStats() map[string]interface{} { return nil } -func (m *mockController) StartServer(ctx context.Context) error { return nil } -func (m *mockController) StopServer() error { return nil } -func (m *mockController) GetStatus() interface{} { return nil } -func (m *mockController) StatusChannel() <-chan interface{} { return nil } -func (m *mockController) EventsChannel() <-chan interface{} { return nil } +func (m *mockController) IsRunning() bool { return true } +func (m *mockController) IsReady() bool { return true } +func (m *mockController) GetListenAddress() string { return "" } +func (m *mockController) GetUpstreamStats() map[string]interface{} { return nil } +func (m *mockController) StartServer(ctx context.Context) error { return nil } +func (m *mockController) StopServer() error { return nil } +func (m *mockController) GetStatus() interface{} { return nil } +func (m *mockController) StatusChannel() <-chan interface{} { return nil } +func (m *mockController) EventsChannel() <-chan interface{} { return nil } func (m *mockController) GetAllServers() ([]map[string]interface{}, error) { return nil, nil } -func (m *mockController) EnableServer(serverName string, enabled bool) error { return nil } -func (m *mockController) RestartServer(serverName string) error { return nil } -func (m *mockController) ForceReconnectAllServers(reason string) error { return nil } -func (m *mockController) GetDockerRecoveryStatus() interface{} { return nil } +func (m *mockController) EnableServer(serverName string, enabled bool) error { return nil } +func (m *mockController) RestartServer(serverName string) error { return nil } +func (m *mockController) ForceReconnectAllServers(reason string) error { return nil } +func (m *mockController) GetDockerRecoveryStatus() interface{} { return nil } func (m *mockController) QuarantineServer(serverName string, quarantined bool) error { return nil } @@ -61,17 +61,19 @@ func (m *mockController) SearchTools(query string, limit int) ([]map[string]inte func (m *mockController) GetServerLogs(serverName string, tail int) ([]contracts.LogEntry, error) { return nil, nil } -func (m *mockController) ReloadConfiguration() error { return nil } -func (m *mockController) GetConfigPath() string { return "" } -func (m *mockController) GetLogDir() string { return "" } -func (m *mockController) TriggerOAuthLogin(serverName string) error { return nil } -func (m *mockController) GetSecretResolver() interface{} { return nil } -func (m *mockController) GetCurrentConfig() interface{} { return nil } +func (m *mockController) ReloadConfiguration() error { return nil } +func (m *mockController) GetConfigPath() string { return "" } +func (m *mockController) GetLogDir() string { return "" } +func (m *mockController) TriggerOAuthLogin(serverName string) error { return nil } +func (m *mockController) GetSecretResolver() interface{} { return nil } +func (m *mockController) GetCurrentConfig() interface{} { return nil } func (m *mockController) NotifySecretsChanged(ctx context.Context, operation, secretName string) error { return nil } -func (m *mockController) GetToolCalls(limit, offset int) (interface{}, int, error) { return nil, 0, nil } -func (m *mockController) GetToolCallByID(id string) (interface{}, error) { return nil, nil } +func (m *mockController) GetToolCalls(limit, offset int) (interface{}, int, error) { + return nil, 0, nil +} +func (m *mockController) GetToolCallByID(id string) (interface{}, error) { return nil, nil } func (m *mockController) GetServerToolCalls(serverName string, limit int) (interface{}, error) { return nil, nil } @@ -90,21 +92,21 @@ func (m *mockController) ListRegistries() ([]interface{}, error) { return nil, n func (m *mockController) SearchRegistryServers(registryID, tag, query string, limit int) ([]interface{}, error) { return nil, nil } -func (m *mockController) GetManagementService() interface{} { return nil } -func (m *mockController) GetRuntime() interface{} { return nil } +func (m *mockController) GetManagementService() interface{} { return nil } +func (m *mockController) GetRuntime() interface{} { return nil } func (m *mockController) GetSessions(limit, offset int) (interface{}, int, error) { return nil, 0, nil } -func (m *mockController) GetSessionByID(id string) (interface{}, error) { return nil, nil } -func (m *mockController) GetRecentSessions(limit int) (interface{}, int, error) { return nil, 0, nil } +func (m *mockController) GetSessionByID(id string) (interface{}, error) { return nil, nil } +func (m *mockController) GetRecentSessions(limit int) (interface{}, int, error) { return nil, 0, nil } func (m *mockController) GetToolCallsBySession(sessionID string, limit, offset int) (interface{}, int, error) { return nil, 0, nil } -func (m *mockController) GetVersionInfo() interface{} { return nil } -func (m *mockController) RefreshVersionInfo() interface{} { return nil } -func (m *mockController) DiscoverServerTools(_ context.Context, _ string) error { return nil } -func (m *mockController) AddServer(_ context.Context, _ interface{}) error { return nil } -func (m *mockController) RemoveServer(_ context.Context, _ string) error { return nil } +func (m *mockController) GetVersionInfo() interface{} { return nil } +func (m *mockController) RefreshVersionInfo() interface{} { return nil } +func (m *mockController) DiscoverServerTools(_ context.Context, _ string) error { return nil } +func (m *mockController) AddServer(_ context.Context, _ interface{}) error { return nil } +func (m *mockController) RemoveServer(_ context.Context, _ string) error { return nil } func (m *mockController) ListActivities(_ interface{}) (interface{}, int, error) { return nil, 0, nil } -func (m *mockController) GetActivity(_ string) (interface{}, error) { return nil, nil } +func (m *mockController) GetActivity(_ string) (interface{}, error) { return nil, nil } func (m *mockController) StreamActivities(_ interface{}) <-chan interface{} { ch := make(chan interface{}) close(ch) @@ -192,6 +194,131 @@ func TestCodeExecHandler_MissingCode(t *testing.T) { assert.Contains(t, response, "error") } +func TestCodeExecHandler_TypeScriptSuccess(t *testing.T) { + // Given: TypeScript code execution request + reqBody := map[string]interface{}{ + "code": "const x: number = 42; ({ result: x })", + "language": "typescript", + "input": map[string]interface{}{}, + "options": map[string]interface{}{ + "timeout_ms": 60000, + "max_tool_calls": 10, + }, + } + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/code/exec", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + // Mock controller that verifies language is passed through + mockCtrl := &mockController{ + callToolFunc: func(ctx context.Context, toolName string, args map[string]interface{}) (interface{}, error) { + assert.Equal(t, "code_execution", toolName) + // Verify language parameter is passed + assert.Equal(t, "typescript", args["language"]) + // Return success + execResult := map[string]interface{}{ + "ok": true, + "value": 42, + } + resultJSON, _ := json.Marshal(execResult) + return []interface{}{ + map[string]interface{}{ + "type": "text", + "text": string(resultJSON), + }, + }, nil + }, + } + + logger := zap.NewNop().Sugar() + handler := httpapi.NewCodeExecHandler(mockCtrl, logger) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + err := json.Unmarshal(recorder.Body.Bytes(), &response) + require.NoError(t, err) + assert.True(t, response["ok"].(bool)) +} + +func TestCodeExecHandler_NoLanguageDefaultsToJavaScript(t *testing.T) { + // Given: Request without language field + reqBody := map[string]interface{}{ + "code": "({ result: 42 })", + "input": map[string]interface{}{}, + } + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/code/exec", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + mockCtrl := &mockController{ + callToolFunc: func(ctx context.Context, toolName string, args map[string]interface{}) (interface{}, error) { + // Verify no language parameter is passed (backward compat) + _, hasLanguage := args["language"] + assert.False(t, hasLanguage, "language should not be in args when not specified") + execResult := map[string]interface{}{ + "ok": true, + "value": 42, + } + resultJSON, _ := json.Marshal(execResult) + return []interface{}{ + map[string]interface{}{ + "type": "text", + "text": string(resultJSON), + }, + }, nil + }, + } + + logger := zap.NewNop().Sugar() + handler := httpapi.NewCodeExecHandler(mockCtrl, logger) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusOK, recorder.Code) + + var response map[string]interface{} + err := json.Unmarshal(recorder.Body.Bytes(), &response) + require.NoError(t, err) + assert.True(t, response["ok"].(bool)) +} + +func TestCodeExecHandler_InvalidLanguage(t *testing.T) { + // Given: Request with invalid language + reqBody := map[string]interface{}{ + "code": "({ result: 42 })", + "language": "python", + "input": map[string]interface{}{}, + } + bodyBytes, _ := json.Marshal(reqBody) + + req := httptest.NewRequest(http.MethodPost, "/api/v1/code/exec", bytes.NewReader(bodyBytes)) + req.Header.Set("Content-Type", "application/json") + + mockCtrl := &mockController{} + logger := zap.NewNop().Sugar() + handler := httpapi.NewCodeExecHandler(mockCtrl, logger) + + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, req) + + assert.Equal(t, http.StatusBadRequest, recorder.Code) + + var response map[string]interface{} + err := json.Unmarshal(recorder.Body.Bytes(), &response) + require.NoError(t, err) + assert.False(t, response["ok"].(bool)) + errorMap := response["error"].(map[string]interface{}) + assert.Equal(t, "INVALID_LANGUAGE", errorMap["code"]) + assert.Contains(t, errorMap["message"], "python") +} + func TestCodeExecHandler_ExecutionError(t *testing.T) { // Given: Code with syntax error (returned from code_execution tool) reqBody := map[string]interface{}{ diff --git a/internal/jsruntime/errors.go b/internal/jsruntime/errors.go index 0c94d245..fe6e34be 100644 --- a/internal/jsruntime/errors.go +++ b/internal/jsruntime/errors.go @@ -23,6 +23,12 @@ const ( // ErrorCodeSerializationError indicates result cannot be JSON-serialized ErrorCodeSerializationError ErrorCode = "SERIALIZATION_ERROR" + + // ErrorCodeTranspileError indicates TypeScript transpilation failed + ErrorCodeTranspileError ErrorCode = "TRANSPILE_ERROR" + + // ErrorCodeInvalidLanguage indicates an unsupported language was specified + ErrorCodeInvalidLanguage ErrorCode = "INVALID_LANGUAGE" ) // JsError represents a JavaScript execution error with message, stack trace, and error code diff --git a/internal/jsruntime/runtime.go b/internal/jsruntime/runtime.go index 709aaa3b..508fdab2 100644 --- a/internal/jsruntime/runtime.go +++ b/internal/jsruntime/runtime.go @@ -17,6 +17,7 @@ type ExecutionOptions struct { MaxToolCalls int // Maximum number of call_tool() invocations (0 = unlimited) AllowedServers []string // Whitelist of allowed server names (empty = all allowed) ExecutionID string // Unique execution ID for logging (auto-generated if empty) + Language string // Source language: "javascript" (default) or "typescript" // Auth enforcement (Spec 031) AuthContext *AuthInfo // Auth context for permission enforcement (nil = no restrictions) @@ -101,13 +102,28 @@ type ToolCallRecord struct { ErrorDetail interface{} `json:"error_details,omitempty"` } -// Execute runs JavaScript code in a sandboxed environment with tool call capabilities +// Execute runs JavaScript or TypeScript code in a sandboxed environment with tool call capabilities. +// When opts.Language is "typescript", the code is transpiled to JavaScript before execution. func Execute(ctx context.Context, caller ToolCaller, code string, opts ExecutionOptions) *Result { // Generate execution ID if not provided if opts.ExecutionID == "" { opts.ExecutionID = uuid.New().String() } + // Validate language parameter + if langErr := ValidateLanguage(opts.Language); langErr != nil { + return NewErrorResult(langErr) + } + + // Transpile TypeScript to JavaScript if needed + if opts.Language == "typescript" { + transpiled, transpileErr := TranspileTypeScript(code) + if transpileErr != nil { + return NewErrorResult(transpileErr) + } + code = transpiled + } + // Create execution context execCtx := &ExecutionContext{ ExecutionID: opts.ExecutionID, diff --git a/internal/jsruntime/runtime_test.go b/internal/jsruntime/runtime_test.go index 29ca9699..f94801ee 100644 --- a/internal/jsruntime/runtime_test.go +++ b/internal/jsruntime/runtime_test.go @@ -325,6 +325,177 @@ func TestExecuteNonSerializableResult(t *testing.T) { } } +// TestExecuteTypeScript tests TypeScript code execution via Execute() +func TestExecuteTypeScript(t *testing.T) { + caller := newMockToolCaller() + code := `const x: number = 42; const msg: string = "hello"; ({ result: x, message: msg })` + opts := ExecutionOptions{ + Language: "typescript", + } + + result := Execute(context.Background(), caller, code, opts) + + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + resultMap, ok := result.Value.(map[string]interface{}) + if !ok { + t.Fatalf("expected result to be a map, got %T", result.Value) + } + + var resultNum int64 + switch v := resultMap["result"].(type) { + case int64: + resultNum = v + case float64: + resultNum = int64(v) + } + if resultNum != 42 { + t.Errorf("expected result=42, got %v", resultMap["result"]) + } + if resultMap["message"] != "hello" { + t.Errorf("expected message='hello', got %v", resultMap["message"]) + } +} + +// TestExecuteTypeScriptWithCallTool tests TypeScript code that uses call_tool() +func TestExecuteTypeScriptWithCallTool(t *testing.T) { + caller := newMockToolCaller() + caller.results["github:get_user"] = map[string]interface{}{ + "login": "octocat", + "id": 583231, + } + + code := ` + interface ToolResult { + ok: boolean; + result?: any; + error?: any; + } + const res: ToolResult = call_tool("github", "get_user", { username: "octocat" }); + if (!res.ok) throw new Error("Failed"); + ({ user: res.result }) + ` + opts := ExecutionOptions{ + Language: "typescript", + } + + result := Execute(context.Background(), caller, code, opts) + + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } + + if len(caller.calls) != 1 { + t.Fatalf("expected 1 tool call, got %d", len(caller.calls)) + } +} + +// TestExecuteJavaScriptWithLanguageExplicit tests that language: "javascript" works as before +func TestExecuteJavaScriptWithLanguageExplicit(t *testing.T) { + caller := newMockToolCaller() + code := `({ result: 42 })` + opts := ExecutionOptions{ + Language: "javascript", + } + + result := Execute(context.Background(), caller, code, opts) + + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } +} + +// TestExecuteEmptyLanguageDefaultsToJavaScript tests that empty language works as JavaScript +func TestExecuteEmptyLanguageDefaultsToJavaScript(t *testing.T) { + caller := newMockToolCaller() + code := `({ result: 42 })` + opts := ExecutionOptions{ + Language: "", // empty = default = javascript + } + + result := Execute(context.Background(), caller, code, opts) + + if !result.Ok { + t.Fatalf("expected ok=true, got error: %v", result.Error) + } +} + +// TestExecuteInvalidLanguage tests that an invalid language returns an error +func TestExecuteInvalidLanguage(t *testing.T) { + caller := newMockToolCaller() + code := `({ result: 42 })` + opts := ExecutionOptions{ + Language: "python", + } + + result := Execute(context.Background(), caller, code, opts) + + if result.Ok { + t.Fatalf("expected ok=false for invalid language, got ok=true") + } + if result.Error.Code != ErrorCodeInvalidLanguage { + t.Errorf("expected error code INVALID_LANGUAGE, got %s", result.Error.Code) + } +} + +// TestExecuteTypeScriptTranspileError tests that transpilation errors are returned properly +func TestExecuteTypeScriptTranspileError(t *testing.T) { + caller := newMockToolCaller() + code := `const x: number = ;` // invalid TypeScript + opts := ExecutionOptions{ + Language: "typescript", + } + + result := Execute(context.Background(), caller, code, opts) + + if result.Ok { + t.Fatalf("expected ok=false for transpile error, got ok=true") + } + if result.Error.Code != ErrorCodeTranspileError { + t.Errorf("expected error code TRANSPILE_ERROR, got %s", result.Error.Code) + } +} + +// TestExecuteTypeScriptWithInput tests TypeScript with input variable +func TestExecuteTypeScriptWithInput(t *testing.T) { + caller := newMockToolCaller() + code := ` + interface Input { value: number; name: string; } + const inp = input as Input; + ({ doubled: inp.value * 2, greeting: "Hello " + inp.name }) + ` + opts := ExecutionOptions{ + Language: "typescript", + Input: map[string]interface{}{ + "value": 21, + "name": "World", + }, + } + + 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{}) + var doubled int64 + switch v := resultMap["doubled"].(type) { + case int64: + doubled = v + case float64: + doubled = int64(v) + } + if doubled != 42 { + t.Errorf("expected doubled=42, got %v", resultMap["doubled"]) + } + if resultMap["greeting"] != "Hello World" { + t.Errorf("expected greeting='Hello World', got %v", resultMap["greeting"]) + } +} + // TestExecuteSandboxRestrictions tests that require() and other APIs are blocked func TestExecuteSandboxRestrictions(t *testing.T) { caller := newMockToolCaller() diff --git a/internal/jsruntime/typescript.go b/internal/jsruntime/typescript.go new file mode 100644 index 00000000..c41d7953 --- /dev/null +++ b/internal/jsruntime/typescript.go @@ -0,0 +1,56 @@ +package jsruntime + +import ( + "fmt" + "strings" + + "github.com/evanw/esbuild/pkg/api" +) + +// SupportedLanguages lists all valid language values for code execution. +var SupportedLanguages = []string{"javascript", "typescript"} + +// TranspileTypeScript transpiles TypeScript code to JavaScript using esbuild. +// It performs type-stripping only (no type checking or semantic validation). +// Returns the transpiled JavaScript code on success, or a JsError on failure. +func TranspileTypeScript(code string) (string, *JsError) { + result := api.Transform(code, api.TransformOptions{ + Loader: api.LoaderTS, + }) + + if len(result.Errors) > 0 { + // Build error message from esbuild errors with location info + var messages []string + for _, err := range result.Errors { + if err.Location != nil { + messages = append(messages, fmt.Sprintf( + "line %d, column %d: %s", + err.Location.Line, + err.Location.Column, + err.Text, + )) + } else { + messages = append(messages, err.Text) + } + } + errMsg := fmt.Sprintf("TypeScript transpilation failed: %s", strings.Join(messages, "; ")) + return "", NewJsError(ErrorCodeTranspileError, errMsg) + } + + return string(result.Code), nil +} + +// ValidateLanguage checks if the given language string is supported. +// Returns nil if valid, or a JsError if not. +func ValidateLanguage(language string) *JsError { + switch language { + case "", "javascript", "typescript": + return nil + default: + return NewJsError( + ErrorCodeInvalidLanguage, + fmt.Sprintf("Unsupported language %q. Supported languages: %s", + language, strings.Join(SupportedLanguages, ", ")), + ) + } +} diff --git a/internal/jsruntime/typescript_test.go b/internal/jsruntime/typescript_test.go new file mode 100644 index 00000000..70e3066c --- /dev/null +++ b/internal/jsruntime/typescript_test.go @@ -0,0 +1,229 @@ +package jsruntime + +import ( + "strings" + "testing" +) + +func TestTranspileTypeScript_BasicTypeAnnotation(t *testing.T) { + code := `const x: number = 42; x;` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error, got: %v", jsErr) + } + // The output should not contain ": number" + if strings.Contains(result, ": number") { + t.Errorf("expected type annotation to be stripped, got: %s", result) + } + // The output should still contain the value assignment + if !strings.Contains(result, "42") { + t.Errorf("expected output to contain '42', got: %s", result) + } +} + +func TestTranspileTypeScript_InterfaceRemoved(t *testing.T) { + code := ` +interface User { + name: string; + age: number; +} +const user: User = { name: "Alice", age: 30 }; +user; +` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error, got: %v", jsErr) + } + if strings.Contains(result, "interface") { + t.Errorf("expected interface to be removed, got: %s", result) + } + if !strings.Contains(result, "Alice") { + t.Errorf("expected output to contain 'Alice', got: %s", result) + } +} + +func TestTranspileTypeScript_GenericsRemoved(t *testing.T) { + code := ` +function identity(arg: T): T { + return arg; +} +const result = identity(42); +result; +` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error, got: %v", jsErr) + } + // Generics should be stripped + if strings.Contains(result, "") { + t.Errorf("expected generics to be stripped, got: %s", result) + } + if !strings.Contains(result, "42") { + t.Errorf("expected output to contain '42', got: %s", result) + } +} + +func TestTranspileTypeScript_EnumProducesJS(t *testing.T) { + code := ` +enum Direction { + Up = "UP", + Down = "DOWN", + Left = "LEFT", + Right = "RIGHT" +} +const d: Direction = Direction.Up; +d; +` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error, got: %v", jsErr) + } + // Enum should produce JavaScript output (not just be removed) + if !strings.Contains(result, "UP") { + t.Errorf("expected enum values in output, got: %s", result) + } + // Type annotation should be stripped + if strings.Contains(result, ": Direction") { + t.Errorf("expected type annotation stripped, got: %s", result) + } +} + +func TestTranspileTypeScript_NamespaceProducesJS(t *testing.T) { + code := ` +namespace MyNamespace { + export const value = 42; +} +MyNamespace.value; +` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error, got: %v", jsErr) + } + if !strings.Contains(result, "42") { + t.Errorf("expected namespace value in output, got: %s", result) + } +} + +func TestTranspileTypeScript_PlainJavaScriptPassthrough(t *testing.T) { + code := `var x = 42; x;` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error, got: %v", jsErr) + } + if !strings.Contains(result, "42") { + t.Errorf("expected output to contain '42', got: %s", result) + } +} + +func TestTranspileTypeScript_InvalidCode(t *testing.T) { + code := `const x: number = ;` // invalid: missing value + _, jsErr := TranspileTypeScript(code) + if jsErr == nil { + t.Fatal("expected transpilation error, got nil") + } + if jsErr.Code != ErrorCodeTranspileError { + t.Errorf("expected error code TRANSPILE_ERROR, got %s", jsErr.Code) + } + // Error should mention line information + if !strings.Contains(jsErr.Message, "line") { + t.Errorf("expected error to contain line info, got: %s", jsErr.Message) + } +} + +func TestTranspileTypeScript_EmptyCode(t *testing.T) { + code := `` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error for empty code, got: %v", jsErr) + } + // Empty input should produce empty or whitespace-only output + if strings.TrimSpace(result) != "" { + t.Errorf("expected empty output, got: %q", result) + } +} + +func TestTranspileTypeScript_TypeAlias(t *testing.T) { + code := ` +type StringOrNumber = string | number; +const val: StringOrNumber = "hello"; +val; +` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error, got: %v", jsErr) + } + if strings.Contains(result, "StringOrNumber") { + t.Errorf("expected type alias to be removed, got: %s", result) + } + if !strings.Contains(result, "hello") { + t.Errorf("expected output to contain 'hello', got: %s", result) + } +} + +func TestTranspileTypeScript_AsExpression(t *testing.T) { + code := `const x = (42 as number); x;` + result, jsErr := TranspileTypeScript(code) + if jsErr != nil { + t.Fatalf("expected no error, got: %v", jsErr) + } + if strings.Contains(result, " as ") { + t.Errorf("expected 'as' expression to be stripped, got: %s", result) + } + if !strings.Contains(result, "42") { + t.Errorf("expected output to contain '42', got: %s", result) + } +} + +func TestValidateLanguage(t *testing.T) { + tests := []struct { + name string + language string + wantErr bool + }{ + {"empty defaults to javascript", "", false}, + {"javascript is valid", "javascript", false}, + {"typescript is valid", "typescript", false}, + {"python is invalid", "python", true}, + {"TypeScript (wrong case) is invalid", "TypeScript", true}, + {"js is invalid", "js", true}, + {"ts is invalid", "ts", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := ValidateLanguage(tt.language) + if tt.wantErr && err == nil { + t.Errorf("expected error for language %q, got nil", tt.language) + } + if !tt.wantErr && err != nil { + t.Errorf("expected no error for language %q, got: %v", tt.language, err) + } + if tt.wantErr && err != nil && err.Code != ErrorCodeInvalidLanguage { + t.Errorf("expected error code INVALID_LANGUAGE, got %s", err.Code) + } + }) + } +} + +func BenchmarkTranspileTypeScript(b *testing.B) { + // Generate a ~10KB TypeScript code sample + var code strings.Builder + code.WriteString("interface Config { host: string; port: number; debug: boolean; }\n") + code.WriteString("type Result = { ok: true; value: T } | { ok: false; error: string };\n") + for i := 0; i < 200; i++ { + code.WriteString("const val") + code.WriteString(strings.Repeat("x", 3)) + code.WriteString(": number = ") + code.WriteString("42;\n") + } + code.WriteString("({ result: 42 });\n") + codeStr := code.String() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, jsErr := TranspileTypeScript(codeStr) + if jsErr != nil { + b.Fatalf("transpilation failed: %v", jsErr) + } + } +} diff --git a/internal/server/mcp.go b/internal/server/mcp.go index e83d1ea3..04492923 100644 --- a/internal/server/mcp.go +++ b/internal/server/mcp.go @@ -453,18 +453,22 @@ func (p *MCPProxyServer) registerTools(_ bool) { ) p.server.AddTool(readCacheTool, p.handleReadCache) - // code_execution - JavaScript code execution for multi-tool orchestration (feature-flagged) + // code_execution - JavaScript/TypeScript 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- 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.WithDescription("Execute JavaScript or TypeScript 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 code**:\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**TypeScript support**: Set `language: \"typescript\"` to write TypeScript code with type annotations, interfaces, enums, and generics. Types are automatically stripped before execution.\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 (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.Description("JavaScript or TypeScript 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.WithString("language", + mcp.Description("Source code language. When set to 'typescript', the code is automatically transpiled to JavaScript before execution. Type annotations are stripped, enums and namespaces are converted to JavaScript equivalents. Default: 'javascript'."), + mcp.Enum("javascript", "typescript"), ), mcp.WithObject("input", - mcp.Description("Input data accessible as global `input` variable in JavaScript code (default: {})"), + mcp.Description("Input data accessible as global `input` variable in code (default: {})"), ), mcp.WithObject("options", mcp.Description("Execution options: timeout_ms (1-600000, default: 120000), max_tool_calls (>= 0, 0=unlimited), allowed_servers (array of server names, empty=all allowed)"), diff --git a/internal/server/mcp_code_execution.go b/internal/server/mcp_code_execution.go index a3492bb2..041afe3e 100644 --- a/internal/server/mcp_code_execution.go +++ b/internal/server/mcp_code_execution.go @@ -34,6 +34,11 @@ func (p *MCPProxyServer) handleCodeExecution(ctx context.Context, request mcp.Ca // Get all arguments args := request.GetArguments() + // Extract language (optional, default: "javascript") + if language, ok := args["language"].(string); ok && language != "" { + options.Language = language + } + // Extract input (optional) - this is an object input, ok := args["input"].(map[string]interface{}) if !ok || input == nil { @@ -165,6 +170,12 @@ func (p *MCPProxyServer) handleCodeExecution(ctx context.Context, request mcp.Ca }() } + // Determine effective language for logging + effectiveLanguage := options.Language + if effectiveLanguage == "" { + effectiveLanguage = "javascript" + } + // Inject auth context for permission enforcement (Spec 031) if authCtx := auth.AuthContextFromContext(ctx); authCtx != nil { options.AuthContext = &jsruntime.AuthInfo{ @@ -177,9 +188,10 @@ func (p *MCPProxyServer) handleCodeExecution(ctx context.Context, request mcp.Ca options.ToolAnnotationFunc = p.lookupToolPermission } - // Execute JavaScript - p.logger.Info("executing JavaScript code", + // Execute code + p.logger.Info("executing code", zap.String("execution_id", options.ExecutionID), + zap.String("language", effectiveLanguage), zap.Int("code_length", len(code)), zap.Int("timeout_ms", options.TimeoutMs), zap.Int("max_tool_calls", options.MaxToolCalls), @@ -276,8 +288,9 @@ func (p *MCPProxyServer) handleCodeExecution(ctx context.Context, request mcp.Ca ServerName: "mcpproxy", // Built-in tool ToolName: "code_execution", Arguments: map[string]interface{}{ - "code": code, - "input": options.Input, + "code": code, + "input": options.Input, + "language": effectiveLanguage, }, Response: result, Duration: int64(executionDuration), @@ -321,8 +334,9 @@ func (p *MCPProxyServer) handleCodeExecution(ctx context.Context, request mcp.Ca } } codeExecArgs := map[string]interface{}{ - "code": code, - "input": options.Input, + "code": code, + "input": options.Input, + "language": effectiveLanguage, } p.emitActivityInternalToolCall("code_execution", "", "", "", sessionID, parentCallID, status, errorMsg, executionDuration.Milliseconds(), codeExecArgs, result, nil) diff --git a/internal/server/mcp_code_execution_test.go b/internal/server/mcp_code_execution_test.go index 165c6c68..22b169dc 100644 --- a/internal/server/mcp_code_execution_test.go +++ b/internal/server/mcp_code_execution_test.go @@ -88,3 +88,187 @@ func TestCodeExecution_WithMainServer(t *testing.T) { // For now, we skip it as the existing integration tests cover this case t.Skip("Covered by existing integration tests") } + +// TestCodeExecution_TypeScript tests TypeScript execution via the MCP tool +func TestCodeExecution_TypeScript(t *testing.T) { + tmpDir := t.TempDir() + logger := zap.NewNop() + + cfg := &config.Config{ + DataDir: tmpDir, + EnableCodeExecution: true, + CodeExecutionPoolSize: 1, + ToolResponseLimit: 10000, + Servers: []*config.ServerConfig{}, + } + + storageManager, err := storage.NewManager(tmpDir, logger.Sugar()) + require.NoError(t, err) + defer storageManager.Close() + + indexManager, err := index.NewManager(tmpDir, logger) + require.NoError(t, err) + defer indexManager.Close() + + secretResolver := secret.NewResolver() + upstreamManager := upstream.NewManager(logger, cfg, storageManager.GetBoltDB(), secretResolver, storageManager) + + cacheManager, err := cache.NewManager(storageManager.GetDB(), logger) + require.NoError(t, err) + defer cacheManager.Close() + + truncator := truncate.NewTruncator(cfg.ToolResponseLimit) + + mcpProxy := server.NewMCPProxyServer( + storageManager, + indexManager, + upstreamManager, + cacheManager, + truncator, + logger, + nil, + false, + cfg, + ) + defer mcpProxy.Close() + + ctx := context.Background() + args := map[string]interface{}{ + "code": "const x: number = 42; const msg: string = \"hello\"; ({ result: x, message: msg })", + "language": "typescript", + "input": map[string]interface{}{}, + "options": map[string]interface{}{ + "timeout_ms": 10000, + "max_tool_calls": 0, + }, + } + + result, err := mcpProxy.CallBuiltInTool(ctx, "code_execution", args) + + require.NoError(t, err, "CallBuiltInTool should not error") + assert.NotNil(t, result, "Result should not be nil") + assert.Greater(t, len(result.Content), 0, "Result should have content") +} + +// TestCodeExecution_JavaScriptBackwardCompat tests that JavaScript still works without language param +func TestCodeExecution_JavaScriptBackwardCompat(t *testing.T) { + tmpDir := t.TempDir() + logger := zap.NewNop() + + cfg := &config.Config{ + DataDir: tmpDir, + EnableCodeExecution: true, + CodeExecutionPoolSize: 1, + ToolResponseLimit: 10000, + Servers: []*config.ServerConfig{}, + } + + storageManager, err := storage.NewManager(tmpDir, logger.Sugar()) + require.NoError(t, err) + defer storageManager.Close() + + indexManager, err := index.NewManager(tmpDir, logger) + require.NoError(t, err) + defer indexManager.Close() + + secretResolver := secret.NewResolver() + upstreamManager := upstream.NewManager(logger, cfg, storageManager.GetBoltDB(), secretResolver, storageManager) + + cacheManager, err := cache.NewManager(storageManager.GetDB(), logger) + require.NoError(t, err) + defer cacheManager.Close() + + truncator := truncate.NewTruncator(cfg.ToolResponseLimit) + + mcpProxy := server.NewMCPProxyServer( + storageManager, + indexManager, + upstreamManager, + cacheManager, + truncator, + logger, + nil, + false, + cfg, + ) + defer mcpProxy.Close() + + ctx := context.Background() + // No "language" key - backward compatible + args := map[string]interface{}{ + "code": "({ result: input.value * 2 })", + "input": map[string]interface{}{"value": 21}, + "options": map[string]interface{}{ + "timeout_ms": 10000, + "max_tool_calls": 0, + }, + } + + result, err := mcpProxy.CallBuiltInTool(ctx, "code_execution", args) + + require.NoError(t, err, "CallBuiltInTool should not error") + assert.NotNil(t, result, "Result should not be nil") + assert.Greater(t, len(result.Content), 0, "Result should have content") +} + +// TestCodeExecution_InvalidLanguage tests that an invalid language returns an error +func TestCodeExecution_InvalidLanguage(t *testing.T) { + tmpDir := t.TempDir() + logger := zap.NewNop() + + cfg := &config.Config{ + DataDir: tmpDir, + EnableCodeExecution: true, + CodeExecutionPoolSize: 1, + ToolResponseLimit: 10000, + Servers: []*config.ServerConfig{}, + } + + storageManager, err := storage.NewManager(tmpDir, logger.Sugar()) + require.NoError(t, err) + defer storageManager.Close() + + indexManager, err := index.NewManager(tmpDir, logger) + require.NoError(t, err) + defer indexManager.Close() + + secretResolver := secret.NewResolver() + upstreamManager := upstream.NewManager(logger, cfg, storageManager.GetBoltDB(), secretResolver, storageManager) + + cacheManager, err := cache.NewManager(storageManager.GetDB(), logger) + require.NoError(t, err) + defer cacheManager.Close() + + truncator := truncate.NewTruncator(cfg.ToolResponseLimit) + + mcpProxy := server.NewMCPProxyServer( + storageManager, + indexManager, + upstreamManager, + cacheManager, + truncator, + logger, + nil, + false, + cfg, + ) + defer mcpProxy.Close() + + ctx := context.Background() + args := map[string]interface{}{ + "code": "({ result: 42 })", + "language": "python", + "input": map[string]interface{}{}, + "options": map[string]interface{}{ + "timeout_ms": 10000, + "max_tool_calls": 0, + }, + } + + result, err := mcpProxy.CallBuiltInTool(ctx, "code_execution", args) + + // The tool returns an error in the result content, not as a Go error + require.NoError(t, err, "CallBuiltInTool should not return Go error") + assert.NotNil(t, result, "Result should not be nil") + assert.Greater(t, len(result.Content), 0, "Result should have content") +} diff --git a/internal/testutil/binary.go b/internal/testutil/binary.go index 4dcb65a8..1e856b45 100644 --- a/internal/testutil/binary.go +++ b/internal/testutil/binary.go @@ -157,8 +157,8 @@ func (env *BinaryTestEnv) Start() { // Start the binary env.cmd = exec.Command(env.binaryPath, "serve", "--config="+env.configPath, "--log-level=debug") env.cmd.Env = append(os.Environ(), - "MCPPROXY_DISABLE_OAUTH=true", // Disable OAuth for testing - "MCPPROXY_API_KEY="+TestAPIKey, // Set API key for testing + "MCPPROXY_DISABLE_OAUTH=true", // Disable OAuth for testing + "MCPPROXY_API_KEY="+TestAPIKey, // Set API key for testing ) err := env.cmd.Start() @@ -253,7 +253,7 @@ func (env *BinaryTestEnv) waitForToolIndexing() { bodyStr := string(body) // Check if we have success response and at least one server if strings.Contains(bodyStr, `"success":true`) && - strings.Contains(bodyStr, `"servers":`) { + strings.Contains(bodyStr, `"servers":`) { // Server API is working, that's enough // Don't strictly require tools to be indexed as they might index slowly env.t.Log("Server API is responding, proceeding with tests") @@ -433,6 +433,7 @@ func createTestConfig(t *testing.T, configPath string, port int, dataDir string) "NPM_CONFIG_PREFIX" ] }, + "quarantine_enabled": false, "docker_isolation": { "enabled": false } diff --git a/specs/033-typescript-code-execution/checklists/requirements.md b/specs/033-typescript-code-execution/checklists/requirements.md new file mode 100644 index 00000000..6535efff --- /dev/null +++ b/specs/033-typescript-code-execution/checklists/requirements.md @@ -0,0 +1,36 @@ +# Specification Quality Checklist: TypeScript Code Execution Support + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-03-10 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All items pass validation. Spec is ready for `/speckit.plan`. +- The spec deliberately excludes auto-detection of TypeScript syntax (mentioned in Assumptions) to keep the interface explicit and predictable. This is a scoping decision, not a gap. +- Performance target (5ms) is stated as a user-facing outcome rather than an implementation benchmark. diff --git a/specs/033-typescript-code-execution/contracts/cli.md b/specs/033-typescript-code-execution/contracts/cli.md new file mode 100644 index 00000000..a923f299 --- /dev/null +++ b/specs/033-typescript-code-execution/contracts/cli.md @@ -0,0 +1,45 @@ +# CLI Contract: Code Execution + +**Feature**: 033-typescript-code-execution + +## mcpproxy code exec + +### New Flag + +``` +--language string Source code language: javascript, typescript (default "javascript") +``` + +### Usage Examples + +```bash +# Execute TypeScript inline +mcpproxy code exec --language typescript --code "const x: number = 42; ({ result: x })" + +# Execute TypeScript from file +mcpproxy code exec --language typescript --file script.ts --input='{"name": "world"}' + +# Default behavior unchanged (JavaScript) +mcpproxy code exec --code "({ result: input.value * 2 })" --input='{"value": 21}' +``` + +### Output + +Output format is unchanged. TypeScript transpilation errors are reported via the existing error format: + +```json +{ + "ok": false, + "error": { + "code": "TRANSPILE_ERROR", + "message": "TypeScript transpilation failed at line 3, column 5: ..." + } +} +``` + +### Exit Codes + +No changes to exit codes: +- `0`: Successful execution +- `1`: Execution failed (includes transpilation errors) +- `2`: Invalid arguments or configuration diff --git a/specs/033-typescript-code-execution/contracts/mcp-tool-schema.md b/specs/033-typescript-code-execution/contracts/mcp-tool-schema.md new file mode 100644 index 00000000..9dc563a7 --- /dev/null +++ b/specs/033-typescript-code-execution/contracts/mcp-tool-schema.md @@ -0,0 +1,54 @@ +# MCP Tool Schema Contract: code_execution + +**Feature**: 033-typescript-code-execution + +## Updated Tool Schema + +The `code_execution` MCP tool gains a new `language` parameter: + +```json +{ + "name": "code_execution", + "description": "Execute JavaScript or TypeScript code that orchestrates multiple upstream MCP tools...", + "inputSchema": { + "type": "object", + "required": ["code"], + "properties": { + "code": { + "type": "string", + "description": "JavaScript or TypeScript source code to execute..." + }, + "language": { + "type": "string", + "description": "Source code language. When set to 'typescript', the code is automatically transpiled to JavaScript before execution. Type annotations are stripped, enums and namespaces are converted to JavaScript equivalents.", + "enum": ["javascript", "typescript"], + "default": "javascript" + }, + "input": { + "type": "object", + "description": "Input data accessible as global `input` variable" + }, + "options": { + "type": "object", + "description": "Execution options: timeout_ms, max_tool_calls, allowed_servers" + } + } + } +} +``` + +## New Error Code + +```json +{ + "code": "TRANSPILE_ERROR", + "message": "TypeScript transpilation failed at line 5, column 10: Type 'string' is not assignable...", + "stack": "" +} +``` + +## Backward Compatibility + +- Omitting `language` parameter: code executes as JavaScript (existing behavior) +- Setting `language: "javascript"`: identical to omitting it +- All existing tool call arguments continue to work unchanged diff --git a/specs/033-typescript-code-execution/contracts/rest-api.md b/specs/033-typescript-code-execution/contracts/rest-api.md new file mode 100644 index 00000000..e27731cb --- /dev/null +++ b/specs/033-typescript-code-execution/contracts/rest-api.md @@ -0,0 +1,65 @@ +# REST API Contract: Code Execution + +**Feature**: 033-typescript-code-execution + +## POST /api/v1/code/exec + +### Updated Request Body + +```json +{ + "code": "const greeting: string = 'hello'; ({ result: greeting })", + "language": "typescript", + "input": {}, + "options": { + "timeout_ms": 120000, + "max_tool_calls": 0, + "allowed_servers": [] + } +} +``` + +| Field | Type | Required | Default | Description | +|------------|--------|----------|----------------|------------------------------------------| +| code | string | yes | - | Source code to execute | +| language | string | no | "javascript" | Source language: "javascript", "typescript" | +| input | object | no | {} | Input data | +| options | object | no | defaults | Execution options | + +### Response (unchanged) + +Success: +```json +{ + "ok": true, + "result": { "result": "hello" }, + "stats": {} +} +``` + +Transpilation error: +```json +{ + "ok": false, + "error": { + "code": "TRANSPILE_ERROR", + "message": "TypeScript transpilation failed: [error details with line/column]" + } +} +``` + +Invalid language: +```json +{ + "ok": false, + "error": { + "code": "INVALID_LANGUAGE", + "message": "Unsupported language 'python'. Supported languages: javascript, typescript" + } +} +``` + +### Backward Compatibility + +- Omitting `language` field: request processed as JavaScript (existing behavior) +- All existing request/response formats unchanged diff --git a/specs/033-typescript-code-execution/data-model.md b/specs/033-typescript-code-execution/data-model.md new file mode 100644 index 00000000..5e12d5f7 --- /dev/null +++ b/specs/033-typescript-code-execution/data-model.md @@ -0,0 +1,67 @@ +# Data Model: TypeScript Code Execution Support + +**Feature**: 033-typescript-code-execution +**Date**: 2026-03-10 + +## Entities + +### ExecutionOptions (modified) + +The existing `ExecutionOptions` struct in `internal/jsruntime/runtime.go` gains a new `Language` field. + +| Field | Type | Default | Description | +|----------------|----------|----------------|--------------------------------------------------| +| Input | map | {} | Input data accessible as global `input` variable | +| TimeoutMs | int | 120000 | Execution timeout in milliseconds | +| MaxToolCalls | int | 0 (unlimited) | Maximum call_tool() invocations | +| AllowedServers | []string | [] (all) | Whitelist of allowed server names | +| ExecutionID | string | auto-generated | Unique execution ID for logging | +| **Language** | string | "javascript" | Source language: "javascript" or "typescript" | + +### TranspileResult (new) + +Represents the output of TypeScript-to-JavaScript transpilation. + +| Field | Type | Description | +|---------|--------|-------------------------------------------------------------| +| Code | string | Transpiled JavaScript code (empty on error) | +| Errors | []TranspileError | List of transpilation errors (empty on success) | + +### TranspileError (new) + +Represents a single transpilation error with source location. + +| Field | Type | Description | +|---------|--------|--------------------------------------------------| +| Message | string | Human-readable error message | +| Line | int | 1-based line number in original TypeScript source | +| Column | int | 0-based column number in original TypeScript source | + +### CodeExecRequest (modified - REST API) + +The existing `CodeExecRequest` struct in `internal/httpapi/code_exec.go` gains a new `Language` field. + +| Field | Type | Default | Description | +|----------|-----------------|--------------|--------------------------------------| +| Code | string | required | Source code to execute | +| Input | map | {} | Input data | +| Options | CodeExecOptions | defaults | Execution options | +| **Language** | string | "javascript" | Source language: "javascript" or "typescript" | + +## State Transitions + +N/A - This feature is stateless. Each request is independently processed: + +1. Receive code + language parameter +2. If language == "typescript": transpile to JavaScript +3. Execute JavaScript in goja sandbox +4. Return result + +No persistent state changes, no database modifications, no event emissions. + +## Validation Rules + +- `language` must be one of: `"javascript"`, `"typescript"` (case-sensitive) +- Invalid `language` values return an error with code `INVALID_ARGS` listing valid options +- Empty `language` defaults to `"javascript"` +- When `language` is `"typescript"`, transpilation errors produce `TRANSPILE_ERROR` error code diff --git a/specs/033-typescript-code-execution/plan.md b/specs/033-typescript-code-execution/plan.md new file mode 100644 index 00000000..97597712 --- /dev/null +++ b/specs/033-typescript-code-execution/plan.md @@ -0,0 +1,88 @@ +# Implementation Plan: TypeScript Code Execution Support + +**Branch**: `033-typescript-code-execution` | **Date**: 2026-03-10 | **Spec**: [spec.md](./spec.md) +**Input**: Feature specification from `/specs/033-typescript-code-execution/spec.md` + +## Summary + +Add TypeScript language support to MCPProxy's code_execution feature by integrating esbuild's Go API for fast type-stripping transpilation. When users specify `language: "typescript"`, the submitted code is transpiled to JavaScript before execution in the existing goja sandbox. This adds a `language` parameter to the MCP tool schema, REST API, and CLI command while maintaining full backward compatibility with existing JavaScript code execution. + +## Technical Context + +**Language/Version**: Go 1.24 (toolchain go1.24.10) +**Primary Dependencies**: `github.com/dop251/goja` (existing JS sandbox), `github.com/evanw/esbuild` (new - TypeScript transpilation), `github.com/mark3labs/mcp-go` (MCP protocol), `github.com/spf13/cobra` (CLI) +**Storage**: N/A (no new storage requirements) +**Testing**: `go test -race` (unit and integration tests) +**Target Platform**: macOS, Linux, Windows (cross-platform Go binary) +**Project Type**: Single Go project with modular internal packages +**Performance Goals**: TypeScript transpilation overhead < 5ms for code under 10KB +**Constraints**: Transpiled output must be ES5.1+ compatible for goja sandbox; full backward compatibility with existing JavaScript execution +**Scale/Scope**: Single feature addition touching 6-8 files across 4 packages + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +| Principle | Status | Notes | +|-----------|--------|-------| +| I. Performance at Scale | PASS | esbuild Go API transpiles in <1ms; well under 5ms target. No impact on tool indexing or search. | +| II. Actor-Based Concurrency | PASS | Transpilation is synchronous and stateless - runs within existing execution goroutine. No new locks or shared state needed. | +| III. Configuration-Driven Architecture | PASS | No new config options needed. TypeScript support is always available when code_execution is enabled. The `language` parameter is per-request, not per-config. | +| IV. Security by Default | PASS | esbuild only strips types - no code generation beyond what TypeScript syntax requires (enums, namespaces). Output executes in the same goja sandbox with all existing restrictions. | +| V. Test-Driven Development (TDD) | PASS | Comprehensive unit tests planned for transpilation layer, integration tests for MCP tool and REST API. | +| VI. Documentation Hygiene | PASS | Updates planned for CLAUDE.md, code_execution docs, and API reference. | +| Separation of Concerns | PASS | Transpilation layer is isolated in `internal/jsruntime/typescript.go`. No impact on core/tray split. | +| Event-Driven Updates | N/A | No state changes or events involved. | +| DDD Layering | PASS | Transpilation logic stays in domain layer (`internal/jsruntime`), API changes in presentation layer (`internal/httpapi`, `internal/server`). | +| Upstream Client Modularity | N/A | No changes to upstream client layers. | + +**Gate result**: PASS - no violations. + +## Project Structure + +### Documentation (this feature) + +```text +specs/033-typescript-code-execution/ +├── plan.md # This file +├── research.md # Phase 0 output +├── data-model.md # Phase 1 output +├── quickstart.md # Phase 1 output +├── contracts/ # Phase 1 output +└── tasks.md # Phase 2 output (created by /speckit.tasks) +``` + +### Source Code (repository root) + +```text +internal/jsruntime/ +├── runtime.go # MODIFY - Add language parameter to Execute(), call transpiler +├── errors.go # MODIFY - Add ErrorCodeTranspileError error code +├── typescript.go # NEW - TypeScript transpilation via esbuild +├── typescript_test.go # NEW - Unit tests for transpilation +├── runtime_test.go # MODIFY - Add TypeScript execution tests +├── pool.go # NO CHANGE +└── pool_test.go # NO CHANGE + +internal/server/ +├── mcp.go # MODIFY - Add 'language' parameter to code_execution tool schema +├── mcp_code_execution.go # MODIFY - Parse and pass language parameter +└── mcp_code_execution_test.go # MODIFY - Add TypeScript test cases + +internal/httpapi/ +├── code_exec.go # MODIFY - Add 'language' field to CodeExecRequest +└── code_exec_test.go # MODIFY - Add TypeScript test cases + +cmd/mcpproxy/ +└── code_cmd.go # MODIFY - Add --language flag + +docs/code_execution/ +├── overview.md # MODIFY - Document TypeScript support +└── api-reference.md # MODIFY - Document language parameter +``` + +**Structure Decision**: This is a focused feature addition to existing packages. No new packages or directories needed. The transpilation layer (`typescript.go`) is a new file in the existing `internal/jsruntime/` package, keeping related code together. + +## Complexity Tracking + +No constitution violations to justify. The feature adds a single new dependency (esbuild) and a thin transpilation layer with no new abstractions, patterns, or architectural changes. diff --git a/specs/033-typescript-code-execution/quickstart.md b/specs/033-typescript-code-execution/quickstart.md new file mode 100644 index 00000000..95861498 --- /dev/null +++ b/specs/033-typescript-code-execution/quickstart.md @@ -0,0 +1,59 @@ +# Quickstart: TypeScript Code Execution + +**Feature**: 033-typescript-code-execution + +## Development Setup + +```bash +# 1. Switch to feature branch +git checkout 033-typescript-code-execution + +# 2. Add esbuild dependency +go get github.com/evanw/esbuild + +# 3. Verify build +go build ./cmd/mcpproxy + +# 4. Run tests +go test ./internal/jsruntime/... -v -race +go test ./internal/server/... -v -race +go test ./internal/httpapi/... -v -race +``` + +## Implementation Order + +1. **internal/jsruntime/typescript.go** - Transpilation layer (new file) +2. **internal/jsruntime/typescript_test.go** - Unit tests (new file) +3. **internal/jsruntime/errors.go** - Add `ErrorCodeTranspileError` +4. **internal/jsruntime/runtime.go** - Add `Language` to `ExecutionOptions`, call transpiler +5. **internal/jsruntime/runtime_test.go** - TypeScript execution tests +6. **internal/server/mcp.go** - Add `language` to tool schema +7. **internal/server/mcp_code_execution.go** - Parse language parameter +8. **internal/server/mcp_code_execution_test.go** - MCP integration tests +9. **internal/httpapi/code_exec.go** - Add `Language` to request struct +10. **internal/httpapi/code_exec_test.go** - REST API tests +11. **cmd/mcpproxy/code_cmd.go** - Add `--language` flag +12. **docs/** - Update documentation + +## Quick Verification + +```bash +# Build +go build -o mcpproxy ./cmd/mcpproxy + +# Test TypeScript via CLI (requires code_execution enabled in config) +./mcpproxy code exec --language typescript --code "const x: number = 42; ({ result: x })" +# Expected: {"ok": true, "value": {"result": 42}} + +# Test JavaScript still works (backward compat) +./mcpproxy code exec --code "({ result: input.value * 2 })" --input='{"value": 21}' +# Expected: {"ok": true, "value": {"result": 42}} +``` + +## Key Files to Read First + +1. `internal/jsruntime/runtime.go` - Current execution flow (understand `Execute()` function) +2. `internal/server/mcp.go` lines 448-466 - Tool schema registration +3. `internal/server/mcp_code_execution.go` - MCP handler (language parameter parsing goes here) +4. `internal/httpapi/code_exec.go` - REST API handler +5. `cmd/mcpproxy/code_cmd.go` - CLI command diff --git a/specs/033-typescript-code-execution/research.md b/specs/033-typescript-code-execution/research.md new file mode 100644 index 00000000..cddf6639 --- /dev/null +++ b/specs/033-typescript-code-execution/research.md @@ -0,0 +1,82 @@ +# Research: TypeScript Code Execution Support + +**Feature**: 033-typescript-code-execution +**Date**: 2026-03-10 + +## R1: esbuild Go API for TypeScript Transpilation + +**Decision**: Use `github.com/evanw/esbuild` Go API with `api.Transform()` for TypeScript-to-JavaScript transpilation. + +**Rationale**: +- esbuild's Go API is a native Go library (no external process spawning needed) +- `api.Transform()` performs in-memory string-to-string transformation, ideal for this use case +- Transpilation is extremely fast: typically <1ms for typical code sizes +- Supports all TypeScript syntax: type annotations, interfaces, enums, generics, namespaces +- The `api.LoaderTS` loader strips types without type-checking (exactly what we need) +- Zero runtime dependencies - compiles into the Go binary + +**Alternatives considered**: +- `swc` (Rust-based): Requires CGO or external binary, adds build complexity +- `tsc` (TypeScript compiler): Requires Node.js runtime, 100x+ slower +- Manual regex type stripping: Fragile, won't handle enums/namespaces correctly +- `babel`: Requires Node.js runtime + +**Key API usage**: +```go +import "github.com/evanw/esbuild/pkg/api" + +result := api.Transform(code, api.TransformOptions{ + Loader: api.LoaderTS, + Target: api.ES2015, // ES5 target not available; ES2015 is closest compatible +}) +``` + +## R2: esbuild Target Compatibility with goja + +**Decision**: Use `api.ESNext` as the esbuild target, since goja supports ES5.1+ and esbuild's type-stripping doesn't downlevel JavaScript features regardless of target. + +**Rationale**: +- esbuild's `Loader: api.LoaderTS` mode primarily strips type annotations +- The `Target` option controls JavaScript syntax downleveling (e.g., arrow functions, template literals) +- Since goja already supports ES5.1+ including many ES6+ features, using ESNext avoids unnecessary transformations +- TypeScript enums and namespaces produce JavaScript regardless of target, and the output is compatible with goja +- If specific ES6+ features cause goja issues, they would already be a problem with plain JavaScript execution + +**Alternatives considered**: +- `api.ES5`: Not available in esbuild (minimum is ES2015) +- `api.ES2015`: Would add unnecessary downleveling; goja handles most ES2015+ features + +## R3: Error Handling for Transpilation Failures + +**Decision**: Add a new `ErrorCodeTranspileError` to the existing error code enum. Transpilation errors include source location (line, column) from esbuild's error messages. + +**Rationale**: +- esbuild provides structured error messages with file/line/column information +- Reusing `ErrorCodeSyntaxError` would conflate TypeScript type errors with JavaScript syntax errors +- A dedicated error code allows clients to distinguish transpilation failures from runtime errors +- Error messages should reference the original TypeScript source location, not the transpiled output + +## R4: Language Parameter Design + +**Decision**: Add `language` as a top-level string parameter (not nested in `options`) to the code_execution tool schema, REST API request body, and CLI flags. + +**Rationale**: +- The language determines how the code is processed before execution - it's a fundamental property of the request, not an execution option +- Top-level placement matches the prominence of the `code` parameter (they're directly related) +- Simple string enum (`"javascript"`, `"typescript"`) is easy to validate and extend later +- Default value `"javascript"` ensures full backward compatibility + +**Alternatives considered**: +- Nested in `options` object: Less visible, semantically wrong (it's not an execution option) +- Auto-detection from code content: Unreliable, opaque to users, hard to debug +- Separate tool (`code_execution_ts`): Duplicates tool logic, harder to maintain + +## R5: Performance Validation Approach + +**Decision**: Validate the <5ms transpilation target using benchmark tests and log transpilation duration for production observability. + +**Rationale**: +- esbuild benchmarks show sub-millisecond performance for typical code sizes +- Adding a Go benchmark test (`BenchmarkTranspile`) provides reproducible evidence +- Logging transpilation duration alongside execution duration gives operators visibility +- No separate timeout needed - transpilation time counts toward the existing execution timeout diff --git a/specs/033-typescript-code-execution/spec.md b/specs/033-typescript-code-execution/spec.md new file mode 100644 index 00000000..120726e6 --- /dev/null +++ b/specs/033-typescript-code-execution/spec.md @@ -0,0 +1,135 @@ +# Feature Specification: TypeScript Code Execution Support + +**Feature Branch**: `033-typescript-code-execution` +**Created**: 2026-03-10 +**Status**: Draft +**Input**: User description: "Add TypeScript language support to MCPProxy's code_execution feature. When users submit TypeScript code (detected by .ts file hints, explicit language parameter, or TypeScript-specific syntax like type annotations), automatically transpile it to JavaScript using esbuild's Go API before executing in the goja sandbox. The transpilation should be transparent - users write TypeScript, it gets stripped to JS and executed. Add a language parameter to the code_execution MCP tool schema (values: 'javascript', 'typescript', default: 'javascript'). When language is 'typescript' or auto-detected, run esbuild transform first. Performance target: less than 5ms transpilation overhead." + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Execute TypeScript Code via MCP Tool (Priority: P1) + +A developer using an AI agent (e.g., Claude, Cursor) wants to write code_execution scripts using TypeScript syntax for type safety and readability. They submit TypeScript code with type annotations, interfaces, and typed parameters through the `code_execution` MCP tool, specifying `language: "typescript"`. The system transparently strips the types and executes the resulting JavaScript in the existing sandbox, returning the same result format as plain JavaScript execution. + +**Why this priority**: This is the core value proposition. Without the ability to execute TypeScript through the MCP tool interface, no other feature in this spec delivers value. This is the primary interface used by AI agents. + +**Independent Test**: Can be fully tested by sending a `code_execution` MCP tool call with `language: "typescript"` and TypeScript code containing type annotations, and verifying the result matches expected output. + +**Acceptance Scenarios**: + +1. **Given** code_execution is enabled, **When** a user submits TypeScript code with `language: "typescript"` (e.g., `const x: number = 42; ({ result: x })`), **Then** the system strips type annotations, executes the resulting JavaScript, and returns `{ ok: true, value: { result: 42 } }`. +2. **Given** code_execution is enabled, **When** a user submits TypeScript code with interfaces and typed function parameters, **Then** the types are stripped and the logic executes correctly. +3. **Given** code_execution is enabled, **When** a user submits TypeScript code with syntax errors in the type annotations, **Then** the system returns a clear error indicating the transpilation failed with the specific error location. +4. **Given** code_execution is enabled, **When** a user submits `language: "javascript"` (or omits the language parameter), **Then** the code executes exactly as before with no transpilation step (backward compatible). + +--- + +### User Story 2 - Execute TypeScript Code via REST API (Priority: P2) + +A developer or automation tool uses the REST API endpoint (`POST /api/v1/code/exec`) to execute TypeScript code. They include a `language` field in the request body set to `"typescript"`. The system transpiles and executes the code, returning results in the same response format. + +**Why this priority**: The REST API is the secondary interface for code execution, used by integrations and the CLI. It must support the same language parameter as the MCP tool. + +**Independent Test**: Can be fully tested by sending a POST request to `/api/v1/code/exec` with `language: "typescript"` and TypeScript code, verifying the JSON response. + +**Acceptance Scenarios**: + +1. **Given** the REST API is available, **When** a user sends a POST to `/api/v1/code/exec` with `{ "code": "const x: number = 42; ({ result: x })", "language": "typescript" }`, **Then** the response contains `{ "ok": true, "result": { "result": 42 } }`. +2. **Given** the REST API is available, **When** a user sends a request without the `language` field, **Then** the system defaults to JavaScript execution (backward compatible). + +--- + +### User Story 3 - Execute TypeScript Code via CLI (Priority: P3) + +A developer uses the CLI command `mcpproxy code exec` to run TypeScript code locally, specifying the language via a `--language` flag. This is useful for testing and debugging code_execution scripts. + +**Why this priority**: The CLI is a convenience interface primarily for local development and debugging. It adds value but is not the primary use case. + +**Independent Test**: Can be fully tested by running `mcpproxy code exec --language typescript --code "const x: number = 42; ({ result: x })"` and checking the output. + +**Acceptance Scenarios**: + +1. **Given** the CLI is available, **When** a user runs `mcpproxy code exec --language typescript --code "..."`, **Then** the TypeScript code is transpiled and executed, with results printed to stdout. +2. **Given** the CLI is available, **When** a user omits the `--language` flag, **Then** the code is treated as JavaScript (backward compatible). + +--- + +### Edge Cases + +- What happens when TypeScript code uses features not supported by the sandbox runtime (e.g., `async/await`, ES modules)? The transpiler strips types but the resulting JS must still be valid for the ES5.1+ sandbox. Unsupported JS features should produce a clear runtime error from the sandbox, not the transpiler. +- What happens when the transpilation itself takes longer than expected? The transpilation time counts toward the overall execution timeout, ensuring no separate timeout mechanism is needed. +- What happens when code contains no TypeScript-specific syntax but `language: "typescript"` is specified? The transpiler should handle it gracefully - valid JavaScript is also valid TypeScript, so transpilation succeeds as a no-op type strip. +- What happens with TypeScript `enum` declarations? Enums produce JavaScript output (not just type stripping), so they should work correctly after transpilation. +- What happens with TypeScript `namespace` declarations? These also produce JavaScript output and should be handled by the transpiler. +- What happens when an invalid `language` value is provided (e.g., `"python"`)? The system should return a clear error indicating the language is not supported, listing valid options. + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST accept a `language` parameter in the `code_execution` MCP tool schema with allowed values `"javascript"` and `"typescript"`, defaulting to `"javascript"`. +- **FR-002**: System MUST accept a `language` field in the REST API `POST /api/v1/code/exec` request body with the same allowed values and default. +- **FR-003**: System MUST accept a `--language` flag in the `mcpproxy code exec` CLI command with the same allowed values and default. +- **FR-004**: When `language` is `"typescript"`, the system MUST transpile the submitted code from TypeScript to JavaScript before execution in the sandbox. +- **FR-005**: The transpilation MUST strip all TypeScript-specific syntax (type annotations, interfaces, type aliases, generics, enums, namespaces) and produce valid JavaScript. +- **FR-006**: The transpiled JavaScript MUST be executed in the existing sandbox with all existing capabilities (input variable, call_tool function, timeout enforcement, tool call limits). +- **FR-007**: When `language` is `"javascript"` or not specified, the system MUST execute code exactly as before with no transpilation step, preserving full backward compatibility. +- **FR-008**: Transpilation errors MUST be returned to the user with clear error messages including the error location (line and column number) in the original TypeScript source. +- **FR-009**: The system MUST reject unsupported `language` values with an error message listing the supported languages. +- **FR-010**: The transpilation overhead MUST be less than 5 milliseconds for typical code submissions (under 10KB of source code). +- **FR-011**: The system MUST log the language used and transpilation duration in execution logs for observability. +- **FR-012**: The activity log record for code_execution calls MUST include the language parameter used. + +### Key Entities + +- **Language Parameter**: A string field added to the code execution request indicating the source language. Values: `"javascript"` (default), `"typescript"`. Affects whether a transpilation step runs before sandbox execution. +- **Transpilation Result**: The output of converting TypeScript to JavaScript, containing either the transpiled JavaScript code or error details (message, line, column). + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Users can submit TypeScript code with type annotations through any interface (MCP tool, REST API, CLI) and receive correct execution results within the same response time expectations as JavaScript (less than 5ms additional overhead for transpilation). +- **SC-002**: 100% backward compatibility - all existing JavaScript code execution requests continue to work identically without any changes to client code or configuration. +- **SC-003**: TypeScript transpilation errors provide actionable feedback including the error location (line and column) in the original source, enabling users to fix issues on first attempt in 90% of cases. +- **SC-004**: The `language` parameter is captured in activity logs, enabling operators to track TypeScript vs JavaScript usage patterns. + +## Assumptions + +- TypeScript transpilation uses type-stripping only (no type checking or semantic validation). This is intentional for performance - the goal is to allow TypeScript syntax, not to provide a full TypeScript compiler. +- The transpiler targets ES5.1+ output compatible with the existing goja sandbox runtime. +- No new configuration options are needed for enabling/disabling TypeScript support - it is always available when code_execution is enabled. +- The `language` parameter is a simple string enum, not a complex object. Auto-detection of TypeScript syntax is not included in this spec to keep the interface explicit and predictable. + +## Commit Message Conventions *(mandatory)* + +When committing changes for this feature, follow these guidelines: + +### Issue References +- Use: `Related #[issue-number]` - Links the commit to the issue without auto-closing +- Do NOT use: `Fixes #[issue-number]`, `Closes #[issue-number]`, `Resolves #[issue-number]` - These auto-close issues on merge + +**Rationale**: Issues should only be closed manually after verification and testing in production, not automatically on merge. + +### Co-Authorship +- Do NOT include: `Co-Authored-By: Claude ` +- Do NOT include: "Generated with Claude Code" + +**Rationale**: Commit authorship should reflect the human contributors, not the AI tools used. + +### Example Commit Message +``` +feat: [brief description of change] + +Related #[issue-number] + +[Detailed description of what was changed and why] + +## Changes +- [Bulleted list of key changes] +- [Each change on a new line] + +## Testing +- [Test results summary] +- [Key test scenarios covered] +``` diff --git a/specs/033-typescript-code-execution/tasks.md b/specs/033-typescript-code-execution/tasks.md new file mode 100644 index 00000000..03a9c412 --- /dev/null +++ b/specs/033-typescript-code-execution/tasks.md @@ -0,0 +1,174 @@ +# Tasks: TypeScript Code Execution Support + +**Input**: Design documents from `/specs/033-typescript-code-execution/` +**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ + +**Tests**: Included - the constitution (V. Test-Driven Development) requires tests for all features. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Phase 1: Setup + +**Purpose**: Add esbuild dependency and foundational types + +- [ ] T001 Add esbuild dependency by running `go get github.com/evanw/esbuild` and verify `go.mod` / `go.sum` are updated +- [ ] T002 Add `ErrorCodeTranspileError` error code constant to `internal/jsruntime/errors.go` +- [ ] T003 Add `Language` field (string, default "javascript") to `ExecutionOptions` struct in `internal/jsruntime/runtime.go` + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Core transpilation layer that ALL user stories depend on + +- [ ] T004 Create `internal/jsruntime/typescript.go` with `TranspileTypeScript(code string) (string, error)` function using esbuild `api.Transform()` with `Loader: api.LoaderTS`. Return transpiled JS code on success; return `*JsError` with `ErrorCodeTranspileError` on failure, including line/column from esbuild error messages. +- [ ] T005 Create `internal/jsruntime/typescript_test.go` with unit tests: (a) basic type annotation stripping, (b) interfaces removed, (c) generics removed, (d) enums produce valid JS, (e) namespaces produce valid JS, (f) plain JavaScript passes through unchanged, (g) invalid TypeScript returns error with line/column, (h) empty code input, (i) performance benchmark `BenchmarkTranspileTypeScript` verifying <5ms for 10KB code +- [ ] T006 Modify `Execute()` function in `internal/jsruntime/runtime.go` to check `opts.Language`: if `"typescript"`, call `TranspileTypeScript(code)` before passing to `executeWithVM()`. If transpilation fails, return the transpile error. If `"javascript"` or empty, execute as before (no transpilation). If unsupported language value, return error with code `INVALID_ARGS` listing valid options. +- [ ] T007 Add TypeScript execution tests to `internal/jsruntime/runtime_test.go`: (a) TypeScript code with types executes correctly via `Execute()`, (b) JavaScript code with `Language: "javascript"` works unchanged, (c) empty `Language` defaults to JavaScript, (d) invalid language returns error, (e) TypeScript with `call_tool()` works, (f) TypeScript transpilation error returns `TRANSPILE_ERROR` code, (g) transpilation overhead is logged + +**Checkpoint**: Transpilation layer is complete and tested. All user stories can now proceed. + +--- + +## Phase 3: User Story 1 - Execute TypeScript Code via MCP Tool (Priority: P1) MVP + +**Goal**: AI agents can submit TypeScript code through the `code_execution` MCP tool with `language: "typescript"` and get correct results. + +**Independent Test**: Send a `code_execution` MCP tool call with `language: "typescript"` and TypeScript code containing type annotations; verify the result matches expected output. + +### Tests for User Story 1 + +- [ ] T008 [P] [US1] Add test cases to `internal/server/mcp_code_execution_test.go`: (a) TypeScript code with `language: "typescript"` executes correctly, (b) JavaScript code without language parameter works unchanged, (c) invalid language returns error, (d) TypeScript transpilation error returns proper MCP error format, (e) language parameter is passed through to activity log + +### Implementation for User Story 1 + +- [ ] T009 [US1] Add `language` string parameter to the `code_execution` tool schema in `internal/server/mcp.go` (around line 450) using `mcp.WithString("language", mcp.Description("..."), mcp.Enum("javascript", "typescript"))` with description from contracts/mcp-tool-schema.md. Update the tool description to mention TypeScript support. +- [ ] T010 [US1] Modify `handleCodeExecution()` in `internal/server/mcp_code_execution.go` to: (a) parse `language` from `request.GetArguments()` with default `"javascript"`, (b) set `options.Language` before calling `jsruntime.Execute()`, (c) log the language used alongside existing execution logging, (d) include `language` in the activity record arguments map + +**Checkpoint**: TypeScript execution works via MCP tool. AI agents can use `language: "typescript"`. + +--- + +## Phase 4: User Story 2 - Execute TypeScript Code via REST API (Priority: P2) + +**Goal**: Developers and automation tools can execute TypeScript code through `POST /api/v1/code/exec` with a `language` field. + +**Independent Test**: Send a POST request to `/api/v1/code/exec` with `language: "typescript"` and TypeScript code; verify the JSON response. + +### Tests for User Story 2 + +- [ ] T011 [P] [US2] Add test cases to `internal/httpapi/code_exec_test.go`: (a) TypeScript code with `language: "typescript"` returns successful result, (b) request without `language` field defaults to JavaScript, (c) invalid language returns `INVALID_LANGUAGE` error, (d) TypeScript transpilation error returns proper error response + +### Implementation for User Story 2 + +- [ ] T012 [US2] Add `Language` field (`json:"language"`) to `CodeExecRequest` struct in `internal/httpapi/code_exec.go` +- [ ] T013 [US2] Modify `ServeHTTP()` in `internal/httpapi/code_exec.go` to: (a) pass `req.Language` in the `args` map as `"language"` key when calling `h.toolCaller.CallTool()`, (b) validate language if non-empty (must be "javascript" or "typescript"), returning `INVALID_LANGUAGE` error for unsupported values + +**Checkpoint**: TypeScript execution works via REST API. Integrations and automation tools can use `language: "typescript"`. + +--- + +## Phase 5: User Story 3 - Execute TypeScript Code via CLI (Priority: P3) + +**Goal**: Developers can run TypeScript code locally via `mcpproxy code exec --language typescript`. + +**Independent Test**: Run `mcpproxy code exec --language typescript --code "const x: number = 42; ({ result: x })"` and verify output. + +### Implementation for User Story 3 + +- [ ] T014 [US3] Add `--language` flag (string, default "javascript") to `codeExecCmd` in `cmd/mcpproxy/code_cmd.go` using `codeExecCmd.Flags().StringVar()`. Add validation in `validateOptions()` to reject unsupported language values. +- [ ] T015 [US3] Pass the language flag value in the `args` map in both `runCodeExecStandalone()` and `runCodeExecClientMode()` functions in `cmd/mcpproxy/code_cmd.go`. Update the command description and examples to include TypeScript usage. + +**Checkpoint**: TypeScript execution works via CLI. Developers can test TypeScript code locally. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +**Purpose**: Documentation, backward compatibility verification, and build validation + +- [ ] T016 [P] Update `docs/code_execution/overview.md` to document TypeScript support: what it is, how to use it, language parameter, limitations (type-stripping only, no type checking) +- [ ] T017 [P] Update `docs/code_execution/api-reference.md` to document the `language` parameter in the MCP tool schema and REST API request body +- [ ] T018 Verify full build succeeds: `go build ./cmd/mcpproxy` (personal edition) and `go build -tags server ./cmd/mcpproxy` (server edition) +- [ ] T019 Run complete test suite: `go test ./internal/jsruntime/... -v -race && go test ./internal/server/... -v -race && go test ./internal/httpapi/... -v -race` + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies - can start immediately +- **Foundational (Phase 2)**: Depends on Setup (Phase 1) completion - BLOCKS all user stories +- **User Story 1 (Phase 3)**: Depends on Foundational (Phase 2) completion +- **User Story 2 (Phase 4)**: Depends on Foundational (Phase 2) completion - can run in parallel with US1 +- **User Story 3 (Phase 5)**: Depends on Foundational (Phase 2) completion - can run in parallel with US1/US2 +- **Polish (Phase 6)**: Depends on all user stories being complete + +### User Story Dependencies + +- **User Story 1 (P1)**: Can start after Phase 2. No dependencies on other stories. **This is the MVP.** +- **User Story 2 (P2)**: Can start after Phase 2. Independent of US1 (uses same foundational transpiler). The REST handler calls the MCP tool internally, so US1 implementation (T009, T010) must be complete first. +- **User Story 3 (P3)**: Can start after Phase 2. Independent of US1/US2 in standalone mode. Client mode relies on daemon having US1/US2 changes, but the CLI flag addition is independent. + +### Within Each User Story + +- Tests should be written and verified to fail before implementation +- Schema/model changes before handler logic +- Handler logic before integration + +### Parallel Opportunities + +- T002 and T003 can run in parallel (different files) +- T004 and T005 can be developed together (new file + its tests) +- T008 and T011 can run in parallel (different test files) +- T016 and T017 can run in parallel (different doc files) +- User Stories 1, 2, and 3 can theoretically be developed in parallel after Phase 2 (though US2 depends on US1 for the MCP handler) + +--- + +## Parallel Example: User Story 1 + +```bash +# Test and implementation can be developed in sequence: +Task T008: "Add TypeScript test cases to internal/server/mcp_code_execution_test.go" +# then +Task T009: "Add language parameter to code_execution tool schema in internal/server/mcp.go" +Task T010: "Modify handleCodeExecution() to parse and pass language parameter" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Setup (T001-T003) +2. Complete Phase 2: Foundational (T004-T007) +3. Complete Phase 3: User Story 1 (T008-T010) +4. **STOP and VALIDATE**: Test TypeScript execution via MCP tool +5. Deploy/demo if ready + +### Incremental Delivery + +1. Complete Setup + Foundational -> Transpilation layer ready +2. Add User Story 1 -> TypeScript works via MCP tool (MVP!) +3. Add User Story 2 -> TypeScript works via REST API +4. Add User Story 3 -> TypeScript works via CLI +5. Polish -> Docs updated, full build validated + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- [Story] label maps task to specific user story for traceability +- Each user story is independently completable and testable after the foundational phase +- The esbuild dependency adds ~5MB to the binary but provides near-zero transpilation latency +- All tasks preserve backward compatibility - JavaScript execution is never affected diff --git a/web/frontend/dist/index.html b/web/frontend/dist/index.html index 77b894d3..c47cf84d 100644 --- a/web/frontend/dist/index.html +++ b/web/frontend/dist/index.html @@ -5,8 +5,8 @@ MCPProxy Control Panel - - + +