@evantahler/mcpx 0.21.5 → 0.21.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.ts +12 -1
- package/src/client/manager.ts +9 -4
- package/src/commands/exec.ts +3 -2
- package/src/commands/with-command.ts +3 -1
- package/src/shutdown.ts +65 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -22,8 +22,11 @@ import { registerSkillCommand } from "./commands/skill.ts";
|
|
|
22
22
|
import { registerTaskCommand } from "./commands/task.ts";
|
|
23
23
|
import { registerUpgradeCommand } from "./commands/upgrade.ts";
|
|
24
24
|
import { logger } from "./output/logger.ts";
|
|
25
|
+
import { ExitError, installSignalHandlers } from "./shutdown.ts";
|
|
25
26
|
import { maybeCheckForUpdate } from "./update/background.ts";
|
|
26
27
|
|
|
28
|
+
installSignalHandlers();
|
|
29
|
+
|
|
27
30
|
program
|
|
28
31
|
.name("mcpx")
|
|
29
32
|
.description("A command-line interface for MCP servers. curl for MCP.")
|
|
@@ -92,7 +95,15 @@ if (firstCommand && !knownCommands.has(firstCommand)) {
|
|
|
92
95
|
// Fire-and-forget background update check
|
|
93
96
|
const updateNotice = maybeCheckForUpdate();
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
try {
|
|
99
|
+
await program.parseAsync();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (err instanceof ExitError) {
|
|
102
|
+
process.exit(err.code);
|
|
103
|
+
}
|
|
104
|
+
logger.error(String(err));
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
96
107
|
|
|
97
108
|
// Print update notice after command output completes
|
|
98
109
|
process.on("beforeExit", async () => {
|
package/src/client/manager.ts
CHANGED
|
@@ -20,6 +20,7 @@ import pkg from "../../package.json";
|
|
|
20
20
|
import type { AuthFile, Prompt, Resource, ServerConfig, ServersFile, Tool } from "../config/schemas.ts";
|
|
21
21
|
import { isHttpServer, isStdioServer } from "../config/schemas.ts";
|
|
22
22
|
import { logger } from "../output/logger.ts";
|
|
23
|
+
import { register, unregister } from "../shutdown.ts";
|
|
23
24
|
import { handleElicitation } from "./elicitation.ts";
|
|
24
25
|
import { createHttpTransport } from "./http.ts";
|
|
25
26
|
import { McpOAuthProvider } from "./oauth.ts";
|
|
@@ -97,6 +98,7 @@ export class ServerManager {
|
|
|
97
98
|
this.logLevel = opts.logLevel ?? "warning";
|
|
98
99
|
this.json = opts.json ?? false;
|
|
99
100
|
this.noInteractive = opts.noInteractive ?? false;
|
|
101
|
+
register(this);
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
/** Get or create a connected client for a server */
|
|
@@ -492,16 +494,19 @@ export class ServerManager {
|
|
|
492
494
|
|
|
493
495
|
/** Disconnect all clients */
|
|
494
496
|
async close(): Promise<void> {
|
|
495
|
-
|
|
497
|
+
unregister(this);
|
|
498
|
+
// Close every transport — covers in-flight connects whose client hasn't been promoted
|
|
499
|
+
// into `this.clients` yet (otherwise stdio children would be orphaned on shutdown).
|
|
500
|
+
const closePromises = [...this.transports.values()].map(async (transport) => {
|
|
496
501
|
try {
|
|
497
|
-
await
|
|
502
|
+
await transport.close?.();
|
|
498
503
|
} catch {
|
|
499
504
|
// Ignore close errors
|
|
500
505
|
}
|
|
501
|
-
this.clients.delete(name);
|
|
502
|
-
this.transports.delete(name);
|
|
503
506
|
});
|
|
504
507
|
await Promise.allSettled(closePromises);
|
|
508
|
+
this.clients.clear();
|
|
509
|
+
this.transports.clear();
|
|
505
510
|
this.connecting.clear();
|
|
506
511
|
}
|
|
507
512
|
}
|
package/src/commands/exec.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
formatValidationErrors,
|
|
14
14
|
} from "../output/formatter.ts";
|
|
15
15
|
import { logger } from "../output/logger.ts";
|
|
16
|
+
import { ExitError } from "../shutdown.ts";
|
|
16
17
|
import { validateToolInput } from "../validation/schema.ts";
|
|
17
18
|
|
|
18
19
|
type ResolvedArgs =
|
|
@@ -268,10 +269,10 @@ export function registerExecCommand(program: Command) {
|
|
|
268
269
|
for (const elicitation of err.elicitations) {
|
|
269
270
|
await handleUrlElicitation(elicitation, elicitOptions);
|
|
270
271
|
}
|
|
271
|
-
|
|
272
|
+
throw new ExitError(1);
|
|
272
273
|
}
|
|
273
274
|
console.error(formatError(String(err), formatOptions));
|
|
274
|
-
|
|
275
|
+
throw new ExitError(1);
|
|
275
276
|
} finally {
|
|
276
277
|
await manager.close();
|
|
277
278
|
}
|
|
@@ -2,6 +2,7 @@ import type { Command } from "commander";
|
|
|
2
2
|
import { type AppContext, getContext } from "../context.ts";
|
|
3
3
|
import { formatError } from "../output/formatter.ts";
|
|
4
4
|
import { logger, type Spinner } from "../output/logger.ts";
|
|
5
|
+
import { ExitError } from "../shutdown.ts";
|
|
5
6
|
|
|
6
7
|
export interface CommandContext extends AppContext {
|
|
7
8
|
spinner: Spinner;
|
|
@@ -47,9 +48,10 @@ export function withCommand<TArgs extends unknown[]>(
|
|
|
47
48
|
try {
|
|
48
49
|
await handler({ ...appCtx, spinner }, ...args);
|
|
49
50
|
} catch (err) {
|
|
51
|
+
if (err instanceof ExitError) throw err;
|
|
50
52
|
spinner.error(options.errorLabel ?? "Failed");
|
|
51
53
|
console.error(formatError(String(err), formatOptions));
|
|
52
|
-
|
|
54
|
+
throw new ExitError(1);
|
|
53
55
|
} finally {
|
|
54
56
|
await manager.close();
|
|
55
57
|
}
|
package/src/shutdown.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { logger } from "./output/logger.ts";
|
|
2
|
+
|
|
3
|
+
export interface Closeable {
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class ExitError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
public readonly code: number,
|
|
10
|
+
message?: string,
|
|
11
|
+
) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "ExitError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const registry = new Set<Closeable>();
|
|
18
|
+
let shuttingDown = false;
|
|
19
|
+
|
|
20
|
+
export function register(c: Closeable): void {
|
|
21
|
+
registry.add(c);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function unregister(c: Closeable): void {
|
|
25
|
+
registry.delete(c);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HARD_TIMEOUT_MS = 8000;
|
|
29
|
+
|
|
30
|
+
async function runShutdown(exitCode: number): Promise<never> {
|
|
31
|
+
if (shuttingDown) {
|
|
32
|
+
process.exit(exitCode);
|
|
33
|
+
}
|
|
34
|
+
shuttingDown = true;
|
|
35
|
+
|
|
36
|
+
const closeAll = Promise.allSettled([...registry].map((c) => c.close()));
|
|
37
|
+
await Promise.race([closeAll, new Promise<void>((resolve) => setTimeout(resolve, HARD_TIMEOUT_MS).unref())]);
|
|
38
|
+
process.exit(exitCode);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let installed = false;
|
|
42
|
+
|
|
43
|
+
export function installSignalHandlers(): void {
|
|
44
|
+
if (installed) return;
|
|
45
|
+
installed = true;
|
|
46
|
+
|
|
47
|
+
process.on("SIGINT", () => {
|
|
48
|
+
void runShutdown(130);
|
|
49
|
+
});
|
|
50
|
+
process.on("SIGTERM", () => {
|
|
51
|
+
void runShutdown(143);
|
|
52
|
+
});
|
|
53
|
+
process.on("SIGHUP", () => {
|
|
54
|
+
void runShutdown(129);
|
|
55
|
+
});
|
|
56
|
+
process.on("uncaughtException", (err) => {
|
|
57
|
+
logger.error(`uncaught exception: ${err?.stack ?? String(err)}`);
|
|
58
|
+
void runShutdown(1);
|
|
59
|
+
});
|
|
60
|
+
process.on("unhandledRejection", (reason) => {
|
|
61
|
+
const detail = reason instanceof Error ? (reason.stack ?? reason.message) : String(reason);
|
|
62
|
+
logger.error(`unhandled rejection: ${detail}`);
|
|
63
|
+
void runShutdown(1);
|
|
64
|
+
});
|
|
65
|
+
}
|