Skip to content

Commit 0978c26

Browse files
committed
Refactor error handling for tools
1 parent 8048307 commit 0978c26

File tree

3 files changed

+180
-175
lines changed

3 files changed

+180
-175
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ npx -y mcp-devcontainers
3232

3333
## MCP Transport
3434

35-
- Start server: `npm run start` - Launches the MCP server with **stdio transport**
36-
- Start SSE server: `npm run start sse` - Runs the MCP server with **Server-Sent Events transport**
37-
- Start Streamable HTTP server: `npm run start http` - Starts the MCP server with **Streamable HTTP transport**
35+
- Start server: `npm start` - Launches the MCP server with **stdio transport**
36+
- Start SSE server: `npm start sse` - Runs the MCP server with **Server-Sent Events transport**
37+
- Start Streamable HTTP server: `npm start http` - Starts the MCP server with **Streamable HTTP transport**
3838

3939
## 📚 Tools
4040

src/devcontainer.ts

Lines changed: 68 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,89 @@
1-
import { spawn, SpawnOptions } from 'child_process';
2-
import fs from 'fs';
1+
import { spawn, SpawnOptions } from "child_process";
2+
import fs from "fs";
3+
import { z } from "zod";
34
import path from "path";
45
import { createRequire } from "module";
56

6-
export const NULL_DEVICE = '/dev/null';
7+
const NULL_DEVICE = "/dev/null";
78

89
type CommandResult = Promise<string>;
910

10-
interface DevcontainerOptions {
11-
stdioFilePath?: string;
12-
}
11+
const WS_FOLDER_DESC = "Path to the workspace folder (string)";
12+
const STDIO_FILE_PATH = `Path for output logs (string), default is ${NULL_DEVICE}`;
13+
const COMMAND = "Command to execute (array of string)";
1314

14-
interface DevContainerUpOptions extends DevcontainerOptions {
15-
workspaceFolder: string;
16-
}
15+
export const DevUpSchema = z.object({
16+
workspaceFolder: z.string().describe(WS_FOLDER_DESC),
17+
stdioFilePath: z.string().describe(STDIO_FILE_PATH).optional(),
18+
});
1719

18-
interface DevContainerRunUserCommandsOptions extends DevcontainerOptions {
19-
workspaceFolder: string;
20-
}
20+
export const DevRunSchema = z.object({
21+
workspaceFolder: z.string().describe(WS_FOLDER_DESC),
22+
stdioFilePath: z.string().describe(STDIO_FILE_PATH).optional(),
23+
});
2124

22-
interface DevContainerExecOptions extends DevcontainerOptions {
23-
workspaceFolder: string;
24-
command: string[];
25-
}
25+
export const DevExecSchema = z.object({
26+
workspaceFolder: z.string().describe(WS_FOLDER_DESC),
27+
stdioFilePath: z.string().describe(STDIO_FILE_PATH).optional(),
28+
command: z.array(z.string()).min(1).describe(COMMAND),
29+
});
30+
31+
type DevUpArgs = z.infer<typeof DevUpSchema>;
32+
type DevRunArgs = z.infer<typeof DevRunSchema>;
33+
type DevExecArgs = z.infer<typeof DevExecSchema>;
2634

2735
const require = createRequire(import.meta.url);
2836

2937
function devcontainerBinaryPath(): string {
3038
try {
31-
const pkgPath = require.resolve('@devcontainers/cli/package.json');
39+
const pkgPath = require.resolve("@devcontainers/cli/package.json");
3240
const pkg = require(pkgPath);
3341
return path.join(path.dirname(pkgPath), pkg.bin.devcontainer);
3442
} catch (error) {
35-
throw new Error('Failed to locate devcontainer CLI: ' + (error as Error).message);
43+
throw new Error(
44+
"Failed to locate devcontainer CLI: " + (error as Error).message
45+
);
3646
}
3747
}
3848

39-
function createOutputStream(stdioFilePath: string = NULL_DEVICE): fs.WriteStream {
49+
function createOutputStream(
50+
stdioFilePath: string = NULL_DEVICE
51+
): fs.WriteStream {
4052
try {
41-
return fs.createWriteStream(stdioFilePath, { flags: 'w' })
53+
return fs.createWriteStream(stdioFilePath, { flags: "w" });
4254
} catch (error) {
43-
throw new Error(`Failed to create output stream: ${(error as Error).message}`);
55+
throw new Error(
56+
`Failed to create output stream: ${(error as Error).message}`
57+
);
4458
}
4559
}
4660

47-
async function runCommand(args: string[], stdoutStream: fs.WriteStream): CommandResult {
61+
async function runCommand(
62+
args: string[],
63+
stdoutStream: fs.WriteStream
64+
): CommandResult {
4865
return new Promise((resolve, reject) => {
4966
const binaryPath = devcontainerBinaryPath();
50-
const child = spawn('node', [binaryPath, ...args], {
51-
stdio: ['ignore', 'pipe', 'pipe'],
67+
const child = spawn("node", [binaryPath, ...args], {
68+
stdio: ["ignore", "pipe", "pipe"],
5269
} as SpawnOptions);
5370

5471
const stdoutData: string[] = [];
5572
const stderrData: string[] = [];
5673

57-
child.on('error', (error) => {
74+
child.on("error", (error) => {
5875
cleanup(error);
5976
reject(new Error(`Process spawn failed: ${error.message}`));
6077
});
6178

6279
// Pipe stdout to the stream as before, but also collect it
63-
child.stdout?.on('data', (data) => {
64-
stdoutData.push(data.toString())
80+
child.stdout?.on("data", (data) => {
81+
stdoutData.push(data.toString());
6582
stdoutStream.write(data);
6683
});
6784

6885
// Collect stderr data instead of piping to process.stderr
69-
child.stderr?.on('data', (data) => {
86+
child.stderr?.on("data", (data) => {
7087
stderrData.push(data.toString().trim());
7188
});
7289

@@ -81,39 +98,47 @@ async function runCommand(args: string[], stdoutStream: fs.WriteStream): Command
8198
}
8299
};
83100

84-
child.on('close', (code, signal) => {
101+
child.on("close", (code, signal) => {
85102
cleanup();
86103
if (code === 0) {
87-
resolve(`success with code ${code}\n-------\n${stdoutData.join('\n\n')}`);
104+
resolve(
105+
`success with code ${code}\n-------\n${stdoutData.join("\n\n")}`
106+
);
88107
} else {
89108
const reason = signal
90109
? `terminated by signal ${signal}`
91110
: `exited with code ${code}`;
92-
111+
93112
// Combine the error message with the collected stderr output
94-
const errorMessage = `Command failed: devcontainer ${args.join(' ')} (${reason})\n-------\n${stderrData.join('\n\n')}`;
113+
const errorMessage = `Command failed: devcontainer ${args.join(
114+
" "
115+
)} (${reason})\n-------\n${stderrData.join("\n\n")}`;
95116
reject(new Error(errorMessage));
96117
}
97118
});
98119
});
99120
}
100121

101-
export async function devUp(options: DevContainerUpOptions): CommandResult {
122+
export async function devUp(options: DevUpArgs): CommandResult {
102123
const stream = createOutputStream(options.stdioFilePath);
103-
return runCommand(['up', '--workspace-folder', options.workspaceFolder], stream);
124+
return runCommand(
125+
["up", "--workspace-folder", options.workspaceFolder],
126+
stream
127+
);
104128
}
105129

106-
export async function devRunUserCommands(options: DevContainerRunUserCommandsOptions): CommandResult {
130+
export async function devRunUserCommands(options: DevRunArgs): CommandResult {
107131
const stream = createOutputStream(options.stdioFilePath);
108-
return runCommand(['run-user-commands', '--workspace-folder', options.workspaceFolder], stream);
132+
return runCommand(
133+
["run-user-commands", "--workspace-folder", options.workspaceFolder],
134+
stream
135+
);
109136
}
110137

111-
export async function devExec(options: DevContainerExecOptions): CommandResult {
138+
export async function devExec(options: DevExecArgs): CommandResult {
112139
const stream = createOutputStream(options.stdioFilePath);
113-
return runCommand([
114-
'exec',
115-
'--workspace-folder',
116-
options.workspaceFolder,
117-
...options.command
118-
], stream);
140+
return runCommand(
141+
["exec", "--workspace-folder", options.workspaceFolder, ...options.command],
142+
stream
143+
);
119144
}

0 commit comments

Comments
 (0)