Skip to content

Commit 72bfb05

Browse files
committed
feat: add TypeScript language support to code_execution tool
Enable LLM agents to write code execution scripts in TypeScript with type annotations, interfaces, enums, generics, and namespaces. Types are automatically stripped via esbuild before execution in the goja sandbox with near-zero transpilation overhead (<5ms). Changes across all three surfaces (MCP tool, REST API, CLI): - New `language` parameter on code_execution tool (enum: javascript, typescript) - esbuild-based transpilation layer in internal/jsruntime/typescript.go - TRANSPILE_ERROR and INVALID_LANGUAGE error codes for clear diagnostics - Backward compatible: omitting language defaults to JavaScript - 38 jsruntime tests, 6 httpapi tests, 5 server tests all passing Spec: specs/033-typescript-code-execution/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e04bf28 commit 72bfb05

27 files changed

Lines changed: 1848 additions & 107 deletions

CLAUDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,8 @@ See `docs/prerelease-builds.md` for download instructions.
615615
- BBolt database (`~/.mcpproxy/config.db`) — new `agent_tokens` bucket (028-agent-tokens)
616616
- 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)
617617
- BBolt database (`~/.mcpproxy/config.db`) - new buckets for users, sessions, user servers (024-teams-multiuser-oauth)
618+
- 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)
619+
- N/A (no new storage requirements) (033-typescript-code-execution)
618620

619621
## Recent Changes
620622
- 001-update-version-display: Added Go 1.24 (toolchain go1.24.10)

cmd/mcpproxy/code_cmd.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,19 @@ import (
2828
var (
2929
codeCmd = &cobra.Command{
3030
Use: "code",
31-
Short: "JavaScript code execution for multi-tool orchestration",
32-
Long: "Execute JavaScript code that orchestrates multiple upstream MCP tools in a single request",
31+
Short: "JavaScript/TypeScript code execution for multi-tool orchestration",
32+
Long: "Execute JavaScript or TypeScript code that orchestrates multiple upstream MCP tools in a single request",
3333
}
3434

3535
codeExecCmd = &cobra.Command{
3636
Use: "exec",
37-
Short: "Execute JavaScript code",
38-
Long: `Execute JavaScript code that can orchestrate multiple upstream MCP tools.
37+
Short: "Execute JavaScript or TypeScript code",
38+
Long: `Execute JavaScript or TypeScript code that can orchestrate multiple upstream MCP tools.
3939
40-
The JavaScript code has access to:
40+
Use --language typescript to write TypeScript code with type annotations,
41+
interfaces, enums, and generics. Types are automatically stripped before execution.
42+
43+
The code has access to:
4144
- input: Global variable containing the input data (from --input or --input-file)
4245
- call_tool(serverName, toolName, args): Function to invoke upstream MCP tools
4346
@@ -63,6 +66,7 @@ Exit codes:
6366
codeAllowedSrvs []string
6467
codeLogLevel string
6568
codeConfigPath string
69+
codeLanguage string
6670
)
6771

6872
// GetCodeCommand returns the code command for adding to the root command
@@ -84,14 +88,21 @@ func init() {
8488
codeExecCmd.Flags().StringSliceVar(&codeAllowedSrvs, "allowed-servers", []string{}, "Comma-separated list of allowed server names (empty = all allowed)")
8589
codeExecCmd.Flags().StringVarP(&codeLogLevel, "log-level", "l", "info", "Log level (trace, debug, info, warn, error)")
8690
codeExecCmd.Flags().StringVarP(&codeConfigPath, "config", "c", "", "Path to MCP configuration file (default: ~/.mcpproxy/mcp_config.json)")
91+
codeExecCmd.Flags().StringVar(&codeLanguage, "language", "javascript", "Source code language: javascript, typescript")
8792

8893
// Add examples
8994
codeExecCmd.Example = ` # Execute inline code with input
9095
mcpproxy code exec --code="({ result: input.value * 2 })" --input='{"value": 21}'
9196
97+
# Execute TypeScript code
98+
mcpproxy code exec --language typescript --code="const x: number = 42; ({ result: x })"
99+
92100
# Execute code from file
93101
mcpproxy code exec --file=script.js --input-file=params.json
94102
103+
# Execute TypeScript from file
104+
mcpproxy code exec --language typescript --file=script.ts --input-file=params.json
105+
95106
# Call upstream tools
96107
mcpproxy code exec --code="call_tool('github', 'get_user', {username: input.user})" --input='{"user":"octocat"}'
97108
@@ -196,6 +207,7 @@ func runCodeExecClientMode(dataDir, code string, input map[string]interface{}, l
196207
codeTimeout,
197208
codeMaxToolCalls,
198209
codeAllowedSrvs,
210+
cliclient.CodeExecOptions{Language: codeLanguage},
199211
)
200212
if err != nil {
201213
// T029: Use formatErrorWithRequestID to include request_id in error output
@@ -274,6 +286,11 @@ func runCodeExecStandalone(globalConfig *config.Config, code string, input map[s
274286
},
275287
}
276288

289+
// Pass language if not the default
290+
if codeLanguage != "" && codeLanguage != "javascript" {
291+
args["language"] = codeLanguage
292+
}
293+
277294
// Call the code_execution tool
278295
result, err := mcpProxy.CallBuiltInTool(ctx, "code_execution", args)
279296
if err != nil {
@@ -330,6 +347,10 @@ func validateOptions() error {
330347
fmt.Fprintf(os.Stderr, "Error: max-tool-calls cannot be negative\n")
331348
return fmt.Errorf("invalid max-tool-calls")
332349
}
350+
if codeLanguage != "javascript" && codeLanguage != "typescript" {
351+
fmt.Fprintf(os.Stderr, "Error: unsupported language %q. Supported languages: javascript, typescript\n", codeLanguage)
352+
return fmt.Errorf("invalid language")
353+
}
333354
return nil
334355
}
335356

docs/code_execution/api-reference.md

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# JavaScript Code Execution - API Reference
1+
# Code Execution - API Reference
22

3-
Complete reference for the `code_execution` MCP tool.
3+
Complete reference for the `code_execution` MCP tool (JavaScript and TypeScript).
44

55
## Table of Contents
66

@@ -27,11 +27,17 @@ Complete reference for the `code_execution` MCP tool.
2727
"properties": {
2828
"code": {
2929
"type": "string",
30-
"description": "JavaScript source code (ES5.1+) to execute..."
30+
"description": "JavaScript or TypeScript source code to execute..."
31+
},
32+
"language": {
33+
"type": "string",
34+
"description": "Source code language. When set to 'typescript', the code is automatically transpiled to JavaScript before execution.",
35+
"enum": ["javascript", "typescript"],
36+
"default": "javascript"
3137
},
3238
"input": {
3339
"type": "object",
34-
"description": "Input data accessible as global `input` variable in JavaScript code",
40+
"description": "Input data accessible as global `input` variable in code",
3541
"default": {}
3642
},
3743
"options": {
@@ -77,6 +83,16 @@ Complete reference for the `code_execution` MCP tool.
7783
}
7884
```
7985

86+
### TypeScript Request
87+
88+
```json
89+
{
90+
"code": "const x: number = 42; const msg: string = 'hello'; ({ result: x, message: msg })",
91+
"language": "typescript",
92+
"input": {}
93+
}
94+
```
95+
8096
### Full Request with Options
8197

8298
```json
@@ -97,7 +113,8 @@ Complete reference for the `code_execution` MCP tool.
97113

98114
| Parameter | Type | Required | Description |
99115
|-----------|------|----------|-------------|
100-
| `code` | string | **Yes** | JavaScript source code to execute (ES5.1+ syntax) |
116+
| `code` | string | **Yes** | JavaScript or TypeScript source code to execute |
117+
| `language` | string | No | Source language: `"javascript"` (default) or `"typescript"` |
101118
| `input` | object | No | Input data accessible as `input` global variable (default: `{}`) |
102119
| `options` | object | No | Execution options (see below) |
103120

docs/code_execution/overview.md

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
# JavaScript Code Execution - Overview
1+
# Code Execution - Overview
22

33
## What is Code Execution?
44

5-
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.
5+
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.
6+
7+
**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).
68

79
## When to Use Code Execution
810

@@ -215,9 +217,12 @@ mcpproxy serve
215217
### 3. Test with CLI
216218

217219
```bash
218-
# Simple test
220+
# Simple JavaScript test
219221
mcpproxy code exec --code="({ result: input.value * 2 })" --input='{"value": 21}'
220222

223+
# TypeScript test
224+
mcpproxy code exec --language typescript --code="const x: number = 42; ({ result: x })"
225+
221226
# Call upstream tool
222227
mcpproxy code exec --code="call_tool('github', 'get_user', {username: input.user})" --input='{"user":"octocat"}'
223228
```
@@ -377,6 +382,59 @@ code_execution({
377382
// Returns: {ok: false, error: {code: "MAX_TOOL_CALLS_EXCEEDED", message: "..."}}
378383
```
379384

385+
## TypeScript Support
386+
387+
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.
388+
389+
### Supported TypeScript Features
390+
391+
- Type annotations: `const x: number = 42`
392+
- Interfaces: `interface User { name: string; age: number; }`
393+
- Type aliases: `type StringOrNumber = string | number`
394+
- Generics: `function identity<T>(arg: T): T { return arg; }`
395+
- Enums: `enum Direction { Up = "UP", Down = "DOWN" }`
396+
- Namespaces: `namespace MyLib { export const value = 42; }`
397+
- Type assertions: `const x = value as string`
398+
399+
### TypeScript via MCP Tool
400+
401+
```json
402+
{
403+
"name": "code_execution",
404+
"arguments": {
405+
"code": "interface User { name: string; }\nconst user: User = { name: input.username };\n({ greeting: 'Hello ' + user.name })",
406+
"language": "typescript",
407+
"input": {"username": "Alice"}
408+
}
409+
}
410+
```
411+
412+
### TypeScript via REST API
413+
414+
```bash
415+
curl -X POST http://127.0.0.1:8080/api/v1/code/exec \
416+
-H "Content-Type: application/json" \
417+
-H "X-API-Key: your-key" \
418+
-d '{
419+
"code": "const x: number = 42; ({ result: x })",
420+
"language": "typescript"
421+
}'
422+
```
423+
424+
### TypeScript via CLI
425+
426+
```bash
427+
mcpproxy code exec --language typescript \
428+
--code="const x: number = 42; ({ result: x })"
429+
```
430+
431+
### Important Notes
432+
433+
- TypeScript support uses type-stripping only (no type checking or semantic validation)
434+
- Valid JavaScript is also valid TypeScript, so you can always use `language: "typescript"` even for plain JS
435+
- The transpiled output runs in the same ES5.1+ goja sandbox with all existing capabilities
436+
- Transpilation errors return the `TRANSPILE_ERROR` error code with line/column information
437+
380438
## Best Practices
381439

382440
### 1. Keep Code Simple

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ require (
8181
github.com/dlclark/regexp2 v1.11.4 // indirect
8282
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
8383
github.com/esiqveland/notify v0.13.3 // indirect
84+
github.com/evanw/esbuild v0.27.3 // indirect
8485
github.com/fsnotify/fsnotify v1.8.0 // indirect
8586
github.com/go-logr/logr v1.4.3 // indirect
8687
github.com/go-logr/stdr v1.2.2 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,8 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
9898
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
9999
github.com/esiqveland/notify v0.13.3 h1:QCMw6o1n+6rl+oLUfg8P1IIDSFsDEb2WlXvVvIJbI/o=
100100
github.com/esiqveland/notify v0.13.3/go.mod h1:hesw/IRYTO0x99u1JPweAl4+5mwXJibQVUcP0Iu5ORE=
101+
github.com/evanw/esbuild v0.27.3 h1:dH/to9tBKybig6hl25hg4SKIWP7U8COdJKbGEwnUkmU=
102+
github.com/evanw/esbuild v0.27.3/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
101103
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
102104
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
103105
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
@@ -318,6 +320,7 @@ golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
318320
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
319321
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
320322
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
323+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
321324
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
322325
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
323326
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=

internal/cliclient/client.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,20 @@ func parseAPIError(errorMsg, requestID string) error {
147147
return &APIError{Message: errorMsg, RequestID: requestID}
148148
}
149149

150-
// CodeExec executes JavaScript code via the daemon API.
150+
// CodeExecOptions contains optional parameters for code execution via the daemon API.
151+
type CodeExecOptions struct {
152+
Language string // Source language: "javascript" (default) or "typescript"
153+
}
154+
155+
// CodeExec executes JavaScript or TypeScript code via the daemon API.
151156
func (c *Client) CodeExec(
152157
ctx context.Context,
153158
code string,
154159
input map[string]interface{},
155160
timeoutMS int,
156161
maxToolCalls int,
157162
allowedServers []string,
163+
opts ...CodeExecOptions,
158164
) (*CodeExecResult, error) {
159165
// Build request body
160166
reqBody := map[string]interface{}{
@@ -167,6 +173,11 @@ func (c *Client) CodeExec(
167173
},
168174
}
169175

176+
// Apply optional language parameter
177+
if len(opts) > 0 && opts[0].Language != "" && opts[0].Language != "javascript" {
178+
reqBody["language"] = opts[0].Language
179+
}
180+
170181
bodyBytes, err := json.Marshal(reqBody)
171182
if err != nil {
172183
return nil, fmt.Errorf("failed to marshal request: %w", err)
@@ -211,7 +222,7 @@ func (c *Client) CallTool(
211222
) (*CallToolResult, error) {
212223
// Build request body (REST API format)
213224
reqBody := map[string]interface{}{
214-
"tool_name": toolName,
225+
"tool_name": toolName,
215226
"arguments": args,
216227
}
217228

internal/httpapi/code_exec.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,10 @@ import (
1515

1616
// CodeExecRequest represents the request body for code execution.
1717
type CodeExecRequest struct {
18-
Code string `json:"code"`
19-
Input map[string]interface{} `json:"input"`
20-
Options CodeExecOptions `json:"options"`
18+
Code string `json:"code"`
19+
Language string `json:"language,omitempty"` // "javascript" (default) or "typescript"
20+
Input map[string]interface{} `json:"input"`
21+
Options CodeExecOptions `json:"options"`
2122
}
2223

2324
// CodeExecOptions represents execution options.
@@ -75,6 +76,13 @@ func (h *CodeExecHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
7576
return
7677
}
7778

79+
// Validate language if provided
80+
if req.Language != "" && req.Language != "javascript" && req.Language != "typescript" {
81+
h.writeError(w, r, http.StatusBadRequest, "INVALID_LANGUAGE",
82+
fmt.Sprintf("Unsupported language %q. Supported languages: javascript, typescript", req.Language))
83+
return
84+
}
85+
7886
// Set defaults
7987
if req.Input == nil {
8088
req.Input = make(map[string]interface{})
@@ -99,6 +107,11 @@ func (h *CodeExecHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
99107
},
100108
}
101109

110+
// Pass language if specified
111+
if req.Language != "" {
112+
args["language"] = req.Language
113+
}
114+
102115
// Call the code_execution built-in tool
103116
result, err := h.toolCaller.CallTool(ctx, "code_execution", args)
104117
if err != nil {

0 commit comments

Comments
 (0)