Skip to main content
Glama
runSandbox.ts17.1 kB
import { z } from 'zod'; import type { ToolDefinition } from '../types.js'; import { getSandboxClient, ExecutionState } from '@prodisco/sandbox-server/client'; import { searchToolsService } from './searchTools.js'; // ============================================================================ // Schema Definition // ============================================================================ const RunSandboxInputSchema = z.object({ // Mode selection - determines which operation to perform mode: z .enum(['execute', 'stream', 'async', 'status', 'cancel', 'list']) .default('execute') .optional() .describe( 'Execution mode: ' + '"execute" (default) - blocking execution, waits for completion; ' + '"stream" - real-time output streaming; ' + '"async" - start execution and return immediately with execution ID; ' + '"status" - get status and output of an async execution; ' + '"cancel" - cancel a running execution; ' + '"list" - list active and recent executions' ), // === Execute/Stream/Async mode parameters === code: z.string().optional() .describe('(execute/stream/async mode) TypeScript code to execute'), cached: z.string().optional() .describe('(execute/stream/async mode) Name of a cached script to execute (from searchTools results)'), timeout: z.number().int().positive().max(120000).default(30000).optional() .describe('(execute/stream/async mode) Execution timeout in milliseconds (default: 30000, max: 120000)'), // === Status mode parameters === executionId: z.string().optional() .describe('(status/cancel mode) Execution ID from async mode response'), wait: z.boolean().optional() .describe('(status mode) If true, wait for completion (long-poll)'), outputOffset: z.number().int().nonnegative().optional() .describe('(status mode) Offset in output buffer for incremental reads'), // === List mode parameters === states: z.array(z.enum(['pending', 'running', 'completed', 'failed', 'cancelled', 'timeout'])).optional() .describe('(list mode) Filter by execution states'), limit: z.number().int().positive().max(100).default(10).optional() .describe('(list mode) Maximum number of results'), includeCompletedWithinMs: z.number().int().nonnegative().optional() .describe('(list mode) Include completed executions from last N milliseconds'), }); // ============================================================================ // Result Types // ============================================================================ /** Helper to convert ExecutionState enum to string */ function stateToString(state: ExecutionState): string { switch (state) { case ExecutionState.EXECUTION_STATE_PENDING: return 'pending'; case ExecutionState.EXECUTION_STATE_RUNNING: return 'running'; case ExecutionState.EXECUTION_STATE_COMPLETED: return 'completed'; case ExecutionState.EXECUTION_STATE_FAILED: return 'failed'; case ExecutionState.EXECUTION_STATE_CANCELLED: return 'cancelled'; case ExecutionState.EXECUTION_STATE_TIMEOUT: return 'timeout'; default: return 'unknown'; } } /** Helper to convert string state to ExecutionState enum */ function stringToState(state: string): ExecutionState { switch (state) { case 'pending': return ExecutionState.EXECUTION_STATE_PENDING; case 'running': return ExecutionState.EXECUTION_STATE_RUNNING; case 'completed': return ExecutionState.EXECUTION_STATE_COMPLETED; case 'failed': return ExecutionState.EXECUTION_STATE_FAILED; case 'cancelled': return ExecutionState.EXECUTION_STATE_CANCELLED; case 'timeout': return ExecutionState.EXECUTION_STATE_TIMEOUT; default: return ExecutionState.EXECUTION_STATE_UNSPECIFIED; } } // Result type for execute mode (blocking execution) type ExecuteModeResult = { mode: 'execute'; success: boolean; output: string; error?: string; executionTimeMs: number; cachedScript?: string; cached?: { name: string; description: string; createdAtMs: number; contentHash: string; }; }; // Result type for stream mode (streaming execution) type StreamModeResult = { mode: 'stream'; success: boolean; output: string; errorOutput: string; error?: string; executionTimeMs: number; executionId: string; state: string; cached?: { name: string; description: string; createdAtMs: number; contentHash: string; }; }; // Result type for async mode (start async execution) type AsyncModeResult = { mode: 'async'; executionId: string; state: string; message: string; }; // Result type for status mode (get execution status) type StatusModeResult = { mode: 'status'; executionId: string; state: string; output: string; errorOutput: string; outputLength: number; errorOutputLength: number; result?: { success: boolean; error?: string; executionTimeMs: number; cached?: { name: string; description: string; createdAtMs: number; contentHash: string; }; }; }; // Result type for cancel mode (cancel execution) type CancelModeResult = { mode: 'cancel'; success: boolean; executionId: string; state: string; message?: string; }; // Result type for list mode (list executions) type ListModeResult = { mode: 'list'; executions: Array<{ executionId: string; state: string; startedAtMs: number; finishedAtMs?: number; codePreview: string; isCached: boolean; cachedName?: string; }>; totalCount: number; }; // Error result type type ErrorResult = { mode: string; success: false; error: string; }; // Union type for all modes type RunSandboxResult = | ExecuteModeResult | StreamModeResult | AsyncModeResult | StatusModeResult | CancelModeResult | ListModeResult | ErrorResult; // ============================================================================ // Mode Execution Functions // ============================================================================ /** * Execute mode - blocking execution, waits for completion */ async function executeExecuteMode( input: z.infer<typeof RunSandboxInputSchema> ): Promise<ExecuteModeResult | ErrorResult> { const { code, cached, timeout = 30000 } = input; if (!code && !cached) { return { mode: 'execute', success: false, error: 'Either "code" or "cached" must be provided for execute mode', }; } try { const client = getSandboxClient(); const result = await client.execute({ code, cached, timeoutMs: timeout, }); // Index newly cached scripts for searchability using CacheEntry metadata if (result.success && result.cached) { try { await searchToolsService.indexCacheEntry({ name: result.cached.name, description: result.cached.description, createdAtMs: result.cached.createdAtMs, contentHash: result.cached.contentHash, }); } catch { // Silently ignore indexing errors } } return { mode: 'execute', success: result.success, output: result.output, error: result.error, executionTimeMs: result.executionTimeMs, cachedScript: result.cached?.name ?? (cached ? cached : undefined), cached: result.cached, }; } catch (error) { return { mode: 'execute', success: false, error: `gRPC error: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Stream mode - real-time output streaming * Collects all chunks and returns complete output */ async function executeStreamMode( input: z.infer<typeof RunSandboxInputSchema> ): Promise<StreamModeResult | ErrorResult> { const { code, cached, timeout = 30000 } = input; if (!code && !cached) { return { mode: 'stream', success: false, error: 'Either "code" or "cached" must be provided for stream mode', }; } try { const client = getSandboxClient(); let output = ''; let errorOutput = ''; let executionId = ''; let finalResult: StreamModeResult | null = null; for await (const chunk of client.executeStream({ code, cached, timeoutMs: timeout, })) { executionId = chunk.executionId; if (chunk.type === 'output') { output += chunk.data as string; } else if (chunk.type === 'error') { errorOutput += chunk.data as string; } else if (chunk.type === 'result') { const resultData = chunk.data as { success: boolean; error?: string; executionTimeMs: number; state: ExecutionState; cached?: { name: string; description: string; createdAtMs: number; contentHash: string; }; }; // Index newly cached scripts if (resultData.success && resultData.cached) { try { await searchToolsService.indexCacheEntry({ name: resultData.cached.name, description: resultData.cached.description, createdAtMs: resultData.cached.createdAtMs, contentHash: resultData.cached.contentHash, }); } catch { // Silently ignore indexing errors } } finalResult = { mode: 'stream', success: resultData.success, output, errorOutput, error: resultData.error, executionTimeMs: resultData.executionTimeMs, executionId, state: stateToString(resultData.state), cached: resultData.cached, }; } } if (finalResult) { return finalResult; } return { mode: 'stream', success: false, output, errorOutput, error: 'Stream ended without final result', executionTimeMs: 0, executionId, state: 'unknown', }; } catch (error) { return { mode: 'stream', success: false, error: `gRPC error: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Async mode - start execution and return immediately with execution ID */ async function executeAsyncMode( input: z.infer<typeof RunSandboxInputSchema> ): Promise<AsyncModeResult | ErrorResult> { const { code, cached, timeout = 30000 } = input; if (!code && !cached) { return { mode: 'async', success: false, error: 'Either "code" or "cached" must be provided for async mode', }; } try { const client = getSandboxClient(); const result = await client.executeAsync({ code, cached, timeoutMs: timeout, }); return { mode: 'async', executionId: result.executionId, state: stateToString(result.state), message: `Execution started. Use mode: "status" with executionId: "${result.executionId}" to check progress.`, }; } catch (error) { return { mode: 'async', success: false, error: `gRPC error: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Status mode - get status and output of an async execution */ async function executeStatusMode( input: z.infer<typeof RunSandboxInputSchema> ): Promise<StatusModeResult | ErrorResult> { const { executionId, wait, outputOffset } = input; if (!executionId) { return { mode: 'status', success: false, error: 'executionId is required for status mode', }; } try { const client = getSandboxClient(); const status = await client.getExecution(executionId, { wait, outputOffset, }); // Index newly cached scripts if execution completed if (status.result?.cached) { try { await searchToolsService.indexCacheEntry({ name: status.result.cached.name, description: status.result.cached.description, createdAtMs: status.result.cached.createdAtMs, contentHash: status.result.cached.contentHash, }); } catch { // Silently ignore indexing errors } } return { mode: 'status', executionId: status.executionId, state: stateToString(status.state), output: status.output, errorOutput: status.errorOutput, outputLength: status.outputLength, errorOutputLength: status.errorOutputLength, result: status.result ? { success: status.result.success, error: status.result.error, executionTimeMs: status.result.executionTimeMs, cached: status.result.cached, } : undefined, }; } catch (error) { return { mode: 'status', success: false, error: `gRPC error: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * Cancel mode - cancel a running execution */ async function executeCancelMode( input: z.infer<typeof RunSandboxInputSchema> ): Promise<CancelModeResult | ErrorResult> { const { executionId } = input; if (!executionId) { return { mode: 'cancel', success: false, error: 'executionId is required for cancel mode', }; } try { const client = getSandboxClient(); const result = await client.cancelExecution(executionId); return { mode: 'cancel', success: result.success, executionId, state: stateToString(result.state), message: result.message, }; } catch (error) { return { mode: 'cancel', success: false, error: `gRPC error: ${error instanceof Error ? error.message : String(error)}`, }; } } /** * List mode - list active and recent executions */ async function executeListMode( input: z.infer<typeof RunSandboxInputSchema> ): Promise<ListModeResult | ErrorResult> { const { states, limit = 10, includeCompletedWithinMs } = input; try { const client = getSandboxClient(); const executions = await client.listExecutions({ states: states?.map(stringToState), limit, includeCompletedWithinMs, }); return { mode: 'list', executions: executions.map(e => ({ executionId: e.executionId, state: stateToString(e.state), startedAtMs: e.startedAtMs, finishedAtMs: e.finishedAtMs, codePreview: e.codePreview, isCached: e.isCached, cachedName: e.cachedName, })), totalCount: executions.length, }; } catch (error) { return { mode: 'list', success: false, error: `gRPC error: ${error instanceof Error ? error.message : String(error)}`, }; } } // ============================================================================ // Tool Definition // ============================================================================ export const runSandboxTool: ToolDefinition<RunSandboxResult, typeof RunSandboxInputSchema> = { name: 'kubernetes.runSandbox', description: 'Execute TypeScript code in a sandboxed environment for Kubernetes and Prometheus operations. ' + 'MODES: ' + '• execute (default): Blocking execution, waits for completion. ' + 'Params: code OR cached (required), timeout. ' + 'Example: { code: "console.log(\'hello\')" } ' + '• stream: Real-time output streaming, returns all output when complete. ' + 'Params: code OR cached (required), timeout. ' + 'Example: { mode: "stream", code: "for(let i=0;i<5;i++) console.log(i)" } ' + '• async: Start execution and return immediately with execution ID. ' + 'Params: code OR cached (required), timeout. ' + 'Example: { mode: "async", code: "longRunningTask()" } ' + '• status: Get status and output of an async execution. ' + 'Params: executionId (required), wait (optional, long-poll), outputOffset (optional). ' + 'Example: { mode: "status", executionId: "abc-123", wait: true } ' + '• cancel: Cancel a running execution. ' + 'Params: executionId (required). ' + 'Example: { mode: "cancel", executionId: "abc-123" } ' + '• list: List active and recent executions. ' + 'Params: states (optional), limit (optional), includeCompletedWithinMs (optional). ' + 'Example: { mode: "list", states: ["running"], limit: 5 } ' + 'The sandbox provides: k8s, kc (pre-configured KubeConfig), console, process.env, require("prometheus-query"). ' + 'Use searchTools first to discover APIs and find cached scripts.', schema: RunSandboxInputSchema, async execute(input) { const { mode = 'execute' } = input; switch (mode) { case 'execute': return executeExecuteMode(input); case 'stream': return executeStreamMode(input); case 'async': return executeAsyncMode(input); case 'status': return executeStatusMode(input); case 'cancel': return executeCancelMode(input); case 'list': return executeListMode(input); default: return { mode: mode as string, success: false, error: `Unknown mode: ${mode}`, }; } }, };

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/harche/ProDisco'

If you have feedback or need assistance with the MCP directory API, please join our Discord server