|
8 | 8 | } from "@modelcontextprotocol/sdk/types.js"; |
9 | 9 | import { randomBytes } from 'crypto'; |
10 | 10 | import { join } from 'path'; |
11 | | -import { mkdir, writeFile, appendFile, readFile, access } from 'fs/promises'; |
| 11 | +import { mkdir, writeFile, appendFile, readFile, access, unlink } from 'fs/promises'; |
12 | 12 | import { exec, ExecOptions } from 'child_process'; |
13 | 13 | import { promisify } from 'util'; |
14 | 14 | import { platform } from 'os'; |
@@ -53,6 +53,39 @@ await mkdir(CODE_STORAGE_DIR, { recursive: true }); |
53 | 53 |
|
54 | 54 | const execAsync = promisify(exec); |
55 | 55 |
|
| 56 | +// Standardized tool response shape |
| 57 | +type ToolResponse = { |
| 58 | + type: 'text'; |
| 59 | + text: string; |
| 60 | + isError: boolean; |
| 61 | +}; |
| 62 | + |
| 63 | +function makeResponse(payload: any, isError = false): ToolResponse { |
| 64 | + return { |
| 65 | + type: 'text', |
| 66 | + text: typeof payload === 'string' ? payload : JSON.stringify(payload), |
| 67 | + isError, |
| 68 | + }; |
| 69 | +} |
| 70 | + |
| 71 | +/** |
| 72 | + * Run a shell command and capture stdout/stderr even on non-zero exit codes. |
| 73 | + * This ensures we return useful information when a process fails. |
| 74 | + */ |
| 75 | +async function runCommand(command: string, options: ExecOptions & { cwd?: string; env?: NodeJS.ProcessEnv } = {}) { |
| 76 | + try { |
| 77 | + const { stdout, stderr } = await execAsync(command, options); |
| 78 | + return { stdout: stdout ?? '', stderr: stderr ?? '' }; |
| 79 | + } catch (err: any) { |
| 80 | + // err may contain stdout/stderr properties for failed commands |
| 81 | + return { |
| 82 | + stdout: err.stdout ?? '', |
| 83 | + stderr: err.stderr ?? (err.message ? String(err.message) : String(err)), |
| 84 | + error: err.message ?? String(err) |
| 85 | + }; |
| 86 | + } |
| 87 | +} |
| 88 | + |
56 | 89 | /** |
57 | 90 | * Get platform-specific command for environment activation and execution |
58 | 91 | */ |
@@ -120,24 +153,15 @@ async function executeCode(code: string, filePath: string) { |
120 | 153 | const pythonCmd = platform() === 'win32' ? `python -u "${filePath}"` : `python3 -u "${filePath}"`; |
121 | 154 | const { command, options } = getPlatformSpecificCommand(pythonCmd); |
122 | 155 |
|
123 | | - // Execute code |
124 | | - const { stdout, stderr } = await execAsync(command, { |
| 156 | + // Execute code (capture stdout/stderr even on non-zero exit) |
| 157 | + const { stdout, stderr } = await runCommand(command, { |
125 | 158 | cwd: CODE_STORAGE_DIR, |
126 | 159 | env: { ...process.env, PYTHONUNBUFFERED: '1' }, |
127 | 160 | ...options |
128 | 161 | }); |
129 | 162 |
|
130 | | - const response = { |
131 | | - status: stderr ? 'error' : 'success', |
132 | | - output: stderr || stdout, |
133 | | - file_path: filePath |
134 | | - }; |
135 | | - |
136 | | - return { |
137 | | - type: 'text', |
138 | | - text: JSON.stringify(response), |
139 | | - isError: !!stderr |
140 | | - }; |
| 163 | + const status = stderr ? 'error' : 'success'; |
| 164 | + return makeResponse({ status, output: stderr || stdout, file_path: filePath }, !!stderr); |
141 | 165 | } catch (error) { |
142 | 166 | const response = { |
143 | 167 | status: 'error', |
@@ -165,24 +189,15 @@ async function executeCodeFromFile(filePath: string) { |
165 | 189 | const pythonCmd = platform() === 'win32' ? `python -u "${filePath}"` : `python3 -u "${filePath}"`; |
166 | 190 | const { command, options } = getPlatformSpecificCommand(pythonCmd); |
167 | 191 |
|
168 | | - // Execute code with unbuffered Python |
169 | | - const { stdout, stderr } = await execAsync(command, { |
| 192 | + // Execute code with unbuffered Python and capture stderr/stdout |
| 193 | + const { stdout, stderr } = await runCommand(command, { |
170 | 194 | cwd: CODE_STORAGE_DIR, |
171 | 195 | env: { ...process.env, PYTHONUNBUFFERED: '1' }, |
172 | 196 | ...options |
173 | 197 | }); |
174 | 198 |
|
175 | | - const response = { |
176 | | - status: stderr ? 'error' : 'success', |
177 | | - output: stderr || stdout, |
178 | | - file_path: filePath |
179 | | - }; |
180 | | - |
181 | | - return { |
182 | | - type: 'text', |
183 | | - text: JSON.stringify(response), |
184 | | - isError: !!stderr |
185 | | - }; |
| 199 | + const status = stderr ? 'error' : 'success'; |
| 200 | + return makeResponse({ status, output: stderr || stdout, file_path: filePath }, !!stderr); |
186 | 201 | } catch (error) { |
187 | 202 | const response = { |
188 | 203 | status: 'error', |
@@ -351,26 +366,22 @@ async function installDependencies(packages: string[]) { |
351 | 366 | // Get platform-specific command |
352 | 367 | const { command, options } = getPlatformSpecificCommand(installCmd); |
353 | 368 |
|
354 | | - // Execute installation with unbuffered Python |
355 | | - const { stdout, stderr } = await execAsync(command, { |
| 369 | + // Execute installation and capture stdout/stderr |
| 370 | + const { stdout, stderr } = await runCommand(command, { |
356 | 371 | cwd: CODE_STORAGE_DIR, |
357 | 372 | env: { ...process.env, PYTHONUNBUFFERED: '1' }, |
358 | 373 | ...options |
359 | 374 | }); |
360 | 375 |
|
361 | 376 | const response = { |
362 | | - status: 'success', |
| 377 | + status: stderr ? 'error' : 'success', |
363 | 378 | env_type: ENV_CONFIG.type, |
364 | 379 | installed_packages: packages, |
365 | 380 | output: stdout, |
366 | 381 | warnings: stderr || undefined |
367 | 382 | }; |
368 | 383 |
|
369 | | - return { |
370 | | - type: 'text', |
371 | | - text: JSON.stringify(response), |
372 | | - isError: false |
373 | | - }; |
| 384 | + return makeResponse(response, !!stderr); |
374 | 385 | } catch (error) { |
375 | 386 | const response = { |
376 | 387 | status: 'error', |
@@ -460,21 +471,28 @@ print(json.dumps(results)) |
460 | 471 | const pythonCmd = platform() === 'win32' ? `python -u "${checkScriptPath}"` : `python3 -u "${checkScriptPath}"`; |
461 | 472 | const { command, options } = getPlatformSpecificCommand(pythonCmd); |
462 | 473 |
|
463 | | - const { stdout, stderr } = await execAsync(command, { |
464 | | - cwd: CODE_STORAGE_DIR, |
465 | | - env: { ...process.env, PYTHONUNBUFFERED: '1' }, |
466 | | - ...options |
467 | | - }); |
| 474 | + // Run the command and ensure we remove the temporary script afterwards |
| 475 | + let stdout = ''; |
| 476 | + let stderr = ''; |
| 477 | + try { |
| 478 | + const runResult = await runCommand(command, { |
| 479 | + cwd: CODE_STORAGE_DIR, |
| 480 | + env: { ...process.env, PYTHONUNBUFFERED: '1' }, |
| 481 | + ...options |
| 482 | + }); |
| 483 | + stdout = runResult.stdout ?? ''; |
| 484 | + stderr = runResult.stderr ?? ''; |
| 485 | + } finally { |
| 486 | + // Best-effort cleanup of the temporary check script |
| 487 | + try { |
| 488 | + await unlink(checkScriptPath); |
| 489 | + } catch (e) { |
| 490 | + // ignore cleanup errors |
| 491 | + } |
| 492 | + } |
468 | 493 |
|
469 | 494 | if (stderr) { |
470 | | - return { |
471 | | - type: 'text', |
472 | | - text: JSON.stringify({ |
473 | | - status: 'error', |
474 | | - error: stderr |
475 | | - }), |
476 | | - isError: true |
477 | | - }; |
| 495 | + return makeResponse({ status: 'error', error: stderr }, true); |
478 | 496 | } |
479 | 497 |
|
480 | 498 | // Parse the package information |
|
0 commit comments