Skip to content

Commit 587ab4a

Browse files
committed
Merge origin/main into 033-typescript-code-execution
Resolve merge conflicts between TypeScript code execution feature and main branch changes (routing modes PR#327, tool quarantine PR#328, goja update PR#330). Conflicts resolved by keeping both sides: - internal/jsruntime/runtime.go: Added Language field alongside auth context fields from Spec 031 - internal/server/mcp.go: Combined TypeScript+ES2020+ tool descriptions and kept language parameter alongside updated code description - internal/server/mcp_code_execution.go: Kept language logging alongside auth context injection from Spec 031 - docs/code_execution/api-reference.md: Merged TypeScript language parameter docs with ES2020+ description updates Also fixed binary E2E test config to disable tool-level quarantine (quarantine_enabled: false), matching the approach in non-binary E2E tests. Without this, the new tool quarantine feature from Spec 032 causes tools to be quarantined during tests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2 parents 72bfb05 + 93cc912 commit 587ab4a

74 files changed

Lines changed: 5585 additions & 418 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ go test -race ./internal/... -v # Race detection
174174
mcpproxy upstream list # List all servers
175175
mcpproxy upstream logs <name> # View logs (--tail, --follow)
176176
mcpproxy upstream restart <name> # Restart server (supports --all)
177+
mcpproxy upstream inspect <name> # Inspect tool approval status (Spec 032)
178+
mcpproxy upstream approve <name> # Approve pending/changed tools (Spec 032)
177179
mcpproxy doctor # Run health checks
178180
```
179181

@@ -300,6 +302,7 @@ See [docs/configuration.md](docs/configuration.md) for complete reference.
300302
- **`call_tool_destructive`** - Proxy destructive tool calls to upstream servers (Spec 018)
301303
- **`code_execution`** - Execute JavaScript to orchestrate multiple tools (disabled by default)
302304
- **`upstream_servers`** - CRUD operations for server management
305+
- **`quarantine_security`** - Security quarantine management: list/inspect quarantined servers, inspect/approve/approve-all tools (Spec 032)
303306

304307
**Tool Format**: `<serverName>:<toolName>` (e.g., `github:create_issue`)
305308

@@ -333,6 +336,9 @@ See [docs/configuration.md](docs/configuration.md) for complete reference.
333336
| `GET /api/v1/tokens/{name}` | Get agent token details |
334337
| `DELETE /api/v1/tokens/{name}` | Revoke agent token |
335338
| `POST /api/v1/tokens/{name}/regenerate` | Regenerate agent token secret |
339+
| `POST /api/v1/servers/{id}/tools/approve` | Approve pending/changed tools (Spec 032) |
340+
| `GET /api/v1/servers/{id}/tools/{tool}/diff` | View tool description/schema changes (Spec 032) |
341+
| `GET /api/v1/servers/{id}/tools/export` | Export tool approval records (Spec 032) |
336342
| `GET /events` | SSE stream for live updates |
337343

338344
**Authentication**: Use `X-API-Key` header or `?apikey=` query parameter.
@@ -403,7 +409,7 @@ All server responses include a `health` field that provides consistent status in
403409

404410
## JavaScript Code Execution
405411

406-
The `code_execution` tool enables orchestrating multiple upstream MCP tools in a single request using sandboxed JavaScript (ES5.1+).
412+
The `code_execution` tool enables orchestrating multiple upstream MCP tools in a single request using sandboxed JavaScript (ES2020+). Modern syntax is fully supported: arrow functions, const/let, template literals, destructuring, classes, for-of, optional chaining (?.), nullish coalescing (??), spread/rest, Promises, Symbols, Map/Set, Proxy/Reflect, and generators.
407413

408414
### Configuration
409415

@@ -439,6 +445,7 @@ See `docs/code_execution/` for complete guides:
439445
- **`require_mcp_auth`**: When enabled, `/mcp` endpoint rejects unauthenticated requests (default: false for backward compatibility)
440446
- **Quarantine system**: New servers quarantined until manually approved
441447
- **Tool Poisoning Attack (TPA) protection**: Automatic detection of malicious descriptions
448+
- **Tool-level quarantine (Spec 032)**: SHA-256 hash-based change detection for individual tool descriptions/schemas. New tools start as "pending", changed tools marked as "changed" (rug pull detection). Configurable via `quarantine_enabled` (global) and `skip_quarantine` (per-server).
442449

443450
See [docs/features/agent-tokens.md](docs/features/agent-tokens.md) and [docs/features/security-quarantine.md](docs/features/security-quarantine.md) for details.
444451

@@ -554,14 +561,25 @@ Runtime detection (uvx→Python, npx→Node.js), image selection, environment pa
554561
Dynamic port allocation, RFC 8252 + PKCE, flow coordinator (`internal/oauth/coordinator.go`), automatic token refresh. See [docs/oauth-resource-autodetect.md](docs/oauth-resource-autodetect.md).
555562

556563
### Code Execution
557-
Sandboxed JavaScript (ES5.1+), orchestrates multiple upstream tools in single request. See [docs/code_execution/overview.md](docs/code_execution/overview.md).
564+
Sandboxed JavaScript (ES2020+), orchestrates multiple upstream tools in single request. See [docs/code_execution/overview.md](docs/code_execution/overview.md).
558565

559566
### Connection Management
560567
Exponential backoff, separate contexts for app vs server lifecycle, state machine: Disconnected → Connecting → Authenticating → Ready.
561568

562569
### Tool Indexing
563570
Full rebuild on server changes, hash-based change detection, background indexing.
564571

572+
### Tool-Level Quarantine (Spec 032)
573+
SHA-256 hash-based approval system for individual tools. Key files:
574+
- `internal/storage/models.go` - `ToolApprovalRecord` model and `ToolApprovalBucket`
575+
- `internal/storage/bbolt.go` - CRUD operations for tool approvals
576+
- `internal/runtime/tool_quarantine.go` - Hash calculation, approval checking, blocking logic
577+
- `internal/runtime/lifecycle.go` - Integration in `applyDifferentialToolUpdate()`
578+
- `internal/server/mcp.go` - Tool-level blocking in `handleCallToolVariant()` and MCP tool operations
579+
- `internal/httpapi/server.go` - REST API endpoints for inspection/approval
580+
- `internal/config/config.go` - `QuarantineEnabled` (global) and `SkipQuarantine` (per-server)
581+
- `frontend/src/views/ServerDetail.vue` - Web UI quarantine panel
582+
565583
### Signal Handling
566584
Graceful shutdown, context cancellation, Docker cleanup, double shutdown protection.
567585

cmd/mcpproxy/doctor_cmd.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,23 @@ func displaySecurityFeaturesStatus() {
421421
return
422422
}
423423

424+
// Routing Mode status (Spec 031)
425+
routingMode := cfg.RoutingMode
426+
if routingMode == "" {
427+
routingMode = config.RoutingModeRetrieveTools
428+
}
429+
fmt.Printf(" Routing Mode: %s\n", routingMode)
430+
switch routingMode {
431+
case config.RoutingModeDirect:
432+
fmt.Println(" All upstream tools exposed directly via /mcp endpoint")
433+
case config.RoutingModeCodeExecution:
434+
fmt.Println(" JS orchestration via code_execution tool")
435+
default:
436+
fmt.Println(" BM25 search via retrieve_tools + call_tool variants")
437+
}
438+
fmt.Printf(" Endpoints: /mcp/all (direct), /mcp/code (code_execution), /mcp/call (retrieve_tools)\n")
439+
fmt.Println()
440+
424441
// Sensitive Data Detection status
425442
sddConfig := cfg.SensitiveDataDetection
426443
if sddConfig == nil || sddConfig.IsEnabled() {

cmd/mcpproxy/status_cmd.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ type StatusInfo struct {
2828
UptimeSeconds float64 `json:"uptime_seconds,omitempty"`
2929
APIKey string `json:"api_key"`
3030
WebUIURL string `json:"web_ui_url"`
31+
RoutingMode string `json:"routing_mode"`
3132
Servers *ServerCounts `json:"servers,omitempty"`
3233
SocketPath string `json:"socket_path,omitempty"`
3334
ConfigPath string `json:"config_path,omitempty"`
@@ -158,11 +159,17 @@ func collectStatusFromDaemon(cfg *config.Config, socketPath, configPath string)
158159
defer cancel()
159160

160161
info := &StatusInfo{
161-
State: "Running",
162-
Edition: Edition,
163-
APIKey: cfg.APIKey,
164-
SocketPath: socketPath,
165-
ConfigPath: configPath,
162+
State: "Running",
163+
Edition: Edition,
164+
APIKey: cfg.APIKey,
165+
RoutingMode: cfg.RoutingMode,
166+
SocketPath: socketPath,
167+
ConfigPath: configPath,
168+
}
169+
170+
// Apply routing mode default if empty
171+
if info.RoutingMode == "" {
172+
info.RoutingMode = config.RoutingModeRetrieveTools
166173
}
167174

168175
// Add teams info if available
@@ -220,13 +227,19 @@ func collectStatusFromConfig(cfg *config.Config, socketPath, configPath string)
220227
listenAddr = "127.0.0.1:8080"
221228
}
222229

230+
routingMode := cfg.RoutingMode
231+
if routingMode == "" {
232+
routingMode = config.RoutingModeRetrieveTools
233+
}
234+
223235
info := &StatusInfo{
224-
State: "Not running",
225-
Edition: Edition,
226-
ListenAddr: listenAddr + " (configured)",
227-
APIKey: cfg.APIKey,
228-
WebUIURL: statusBuildWebUIURL(listenAddr, cfg.APIKey),
229-
ConfigPath: configPath,
236+
State: "Not running",
237+
Edition: Edition,
238+
ListenAddr: listenAddr + " (configured)",
239+
APIKey: cfg.APIKey,
240+
WebUIURL: statusBuildWebUIURL(listenAddr, cfg.APIKey),
241+
RoutingMode: routingMode,
242+
ConfigPath: configPath,
230243
}
231244

232245
info.TeamsInfo = collectTeamsInfo(cfg)
@@ -351,6 +364,7 @@ func printStatusTable(info *StatusInfo) {
351364
}
352365

353366
fmt.Printf(" %-12s %s\n", "API Key:", info.APIKey)
367+
fmt.Printf(" %-12s %s\n", "Routing:", info.RoutingMode)
354368
fmt.Printf(" %-12s %s\n", "Web UI:", info.WebUIURL)
355369

356370
if info.Servers != nil {

cmd/mcpproxy/status_cmd_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,129 @@ func TestStatusJSONOutput(t *testing.T) {
449449
}
450450
}
451451

452+
func TestStatusRoutingModeInTable(t *testing.T) {
453+
tests := []struct {
454+
name string
455+
routingMode string
456+
expected string
457+
}{
458+
{
459+
name: "retrieve_tools mode",
460+
routingMode: "retrieve_tools",
461+
expected: "retrieve_tools",
462+
},
463+
{
464+
name: "direct mode",
465+
routingMode: "direct",
466+
expected: "direct",
467+
},
468+
{
469+
name: "code_execution mode",
470+
routingMode: "code_execution",
471+
expected: "code_execution",
472+
},
473+
}
474+
475+
for _, tt := range tests {
476+
t.Run(tt.name, func(t *testing.T) {
477+
info := &StatusInfo{
478+
State: "Running",
479+
Edition: "personal",
480+
ListenAddr: "127.0.0.1:8080",
481+
APIKey: "a1b2****a1b2",
482+
WebUIURL: "http://127.0.0.1:8080/ui/?apikey=test",
483+
RoutingMode: tt.routingMode,
484+
}
485+
486+
old := os.Stdout
487+
r, w, _ := os.Pipe()
488+
os.Stdout = w
489+
490+
printStatusTable(info)
491+
492+
w.Close()
493+
os.Stdout = old
494+
495+
buf := make([]byte, 4096)
496+
n, _ := r.Read(buf)
497+
output := string(buf[:n])
498+
499+
if !strings.Contains(output, "Routing:") {
500+
t.Errorf("expected output to contain 'Routing:', output:\n%s", output)
501+
}
502+
if !strings.Contains(output, tt.expected) {
503+
t.Errorf("expected output to contain %q, output:\n%s", tt.expected, output)
504+
}
505+
})
506+
}
507+
}
508+
509+
func TestStatusRoutingModeInJSON(t *testing.T) {
510+
info := &StatusInfo{
511+
State: "Running",
512+
Edition: "personal",
513+
ListenAddr: "127.0.0.1:8080",
514+
APIKey: "testkey",
515+
WebUIURL: "http://127.0.0.1:8080/ui/",
516+
RoutingMode: "direct",
517+
}
518+
519+
old := os.Stdout
520+
r, w, _ := os.Pipe()
521+
os.Stdout = w
522+
523+
err := printStatusJSON(info)
524+
525+
w.Close()
526+
os.Stdout = old
527+
528+
if err != nil {
529+
t.Fatalf("printStatusJSON failed: %v", err)
530+
}
531+
532+
buf := make([]byte, 8192)
533+
n, _ := r.Read(buf)
534+
output := string(buf[:n])
535+
536+
var result StatusInfo
537+
if jsonErr := json.Unmarshal([]byte(output), &result); jsonErr != nil {
538+
t.Fatalf("invalid JSON: %v\nOutput: %s", jsonErr, output)
539+
}
540+
541+
if result.RoutingMode != "direct" {
542+
t.Errorf("expected routing_mode 'direct', got %q", result.RoutingMode)
543+
}
544+
}
545+
546+
func TestCollectStatusFromConfigRoutingMode(t *testing.T) {
547+
t.Run("uses config routing mode", func(t *testing.T) {
548+
cfg := &config.Config{
549+
Listen: "127.0.0.1:8080",
550+
APIKey: "testkey",
551+
RoutingMode: "direct",
552+
}
553+
554+
info := collectStatusFromConfig(cfg, "/tmp/test.sock", "/tmp/config.json")
555+
556+
if info.RoutingMode != "direct" {
557+
t.Errorf("expected routing mode 'direct', got %q", info.RoutingMode)
558+
}
559+
})
560+
561+
t.Run("defaults to retrieve_tools when empty", func(t *testing.T) {
562+
cfg := &config.Config{
563+
Listen: "127.0.0.1:8080",
564+
APIKey: "testkey",
565+
}
566+
567+
info := collectStatusFromConfig(cfg, "/tmp/test.sock", "/tmp/config.json")
568+
569+
if info.RoutingMode != config.RoutingModeRetrieveTools {
570+
t.Errorf("expected routing mode %q, got %q", config.RoutingModeRetrieveTools, info.RoutingMode)
571+
}
572+
})
573+
}
574+
452575
// parseTestDuration is a helper to parse duration strings for tests.
453576
func parseTestDuration(s string) (time.Duration, error) {
454577
return time.ParseDuration(s)

0 commit comments

Comments
 (0)