@skyramp/mcp 0.5.0-rc.2 → 0.5.0-rc.4

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/build/index.js CHANGED
@@ -7,6 +7,8 @@ import { registerTraceTool } from "./tools/trace/startTraceCollectionTool.js";
7
7
  import { registerTraceStopTool } from "./tools/trace/stopTraceCollectionTool.js";
8
8
  import { registerExecuteSkyrampTestTool } from "./tools/executeSkyrampTestTool.js";
9
9
  import { registerExecuteFleetTool } from "./tools/executeFleetTool.js";
10
+ import { registerFleetLifecycleTools } from "./tools/fleetLifecycleTools.js";
11
+ import { fleetRuns } from "./services/FleetRunRegistry.js";
10
12
  import { AUTH_PLACEHOLDER_TOKEN } from "./types/TestTypes.js";
11
13
  import { AUTH_CONFLICT_ERROR_MSG } from "./prompts/test-recommendation/recommendationSections.js";
12
14
  import { logger } from "./utils/logger.js";
@@ -242,6 +244,7 @@ async function createServer({ era }) {
242
244
  registerLogoutTool,
243
245
  registerExecuteSkyrampTestTool,
244
246
  registerExecuteFleetTool,
247
+ registerFleetLifecycleTools,
245
248
  registerTraceTool,
246
249
  registerTraceStopTool,
247
250
  ];
@@ -328,11 +331,18 @@ async function createServer({ era }) {
328
331
  runInitTrigger();
329
332
  return server;
330
333
  }
334
+ // A crashing process must not stay alive anywhere near the full graceful
335
+ // shutdown's 30s grace — it still gives workers a moment to cancel, but caps
336
+ // it low enough that a crash can't hang the process.
337
+ const CRASH_CLEANUP_WAIT_MS = 3_000;
338
+ /** Interrupt running fleets (bounded wait) so an exit does not leave workers loading. */
339
+ const cancelFleets = () => fleetRuns.cancelAll(CRASH_CLEANUP_WAIT_MS).catch(() => { });
331
340
  // Global error handlers for crash telemetry
332
341
  process.on("uncaughtException", async (error) => {
333
342
  // Don't send analytics for EPIPE errors
334
343
  const isEpipeError = "code" in error && error.code === "EPIPE";
335
344
  if (isEpipeError) {
345
+ await cancelFleets();
336
346
  process.exit(0);
337
347
  }
338
348
  logger.critical("Uncaught exception - MCP server crashing", {
@@ -345,6 +355,7 @@ process.on("uncaughtException", async (error) => {
345
355
  catch (telemetryError) {
346
356
  logger.error("Failed to send crash telemetry", { telemetryError });
347
357
  }
358
+ await cancelFleets();
348
359
  process.exit(1);
349
360
  });
350
361
  // Handle unhandled promise rejections — without this handler Node.js 15+
@@ -354,6 +365,7 @@ process.on("unhandledRejection", async (reason) => {
354
365
  const error = reason instanceof Error ? reason : new Error(String(reason));
355
366
  // EPIPE means the client already disconnected — exit cleanly
356
367
  if ("code" in error && error.code === "EPIPE") {
368
+ await cancelFleets();
357
369
  process.exit(0);
358
370
  }
359
371
  // gRPC/Docker connectivity errors — log and survive.
@@ -400,7 +412,9 @@ async function main() {
400
412
  const cleanupPlaywright = playwrightService
401
413
  ? playwrightService.shutdown().catch(() => { })
402
414
  : Promise.resolve();
403
- cleanupPlaywright.finally(() => {
415
+ // A running fleet is interrupted so it can cancel its workers, not left loading them.
416
+ const cleanupFleets = fleetRuns.cancelAll().catch(() => { });
417
+ Promise.all([cleanupPlaywright, cleanupFleets]).finally(() => {
404
418
  handle.close().finally(() => {
405
419
  process.exit(0);
406
420
  });
@@ -1,4 +1,5 @@
1
1
  import Docker from "dockerode";
2
+ import { Writable } from "stream";
2
3
  /**
3
4
  * Where the mounted pieces live inside the executor, per container platform.
4
5
  *
@@ -11,8 +12,12 @@ export type ContainerPlatform = "linux" | "windows";
11
12
  interface Layout {
12
13
  work: string;
13
14
  certs: string;
14
- /** The directory a branch-built library jar is mounted from. */
15
+ /** The directory a branch-built library jar or package is mounted from. */
15
16
  overrideDir: string;
17
+ /** The native library the image's SDKs load. */
18
+ nativeLib: string;
19
+ /** Where a branch-built npm SDK is installed, ahead of the image's on NODE_PATH. */
20
+ npmOverride: string;
16
21
  /** The directory a credential pool outside the work directory is mounted from. */
17
22
  poolDir: string;
18
23
  imageClasses: string;
@@ -49,11 +54,20 @@ export interface FleetExecutionOptions {
49
54
  executorImage?: string;
50
55
  platform?: ContainerPlatform;
51
56
  entryClass?: string;
52
- certsDir: string;
57
+ /** Required only when the fleet deploys on a remote engine, which is mTLS. */
58
+ certsDir?: string;
59
+ /** Opt-in: the mounted daemon socket gives the fleet root-level access to this host. */
60
+ localDocker?: boolean;
53
61
  credentials?: string;
54
62
  libraryJar?: string;
63
+ /** A branch-built pip (.whl, .tar.gz) or npm (.tgz) SDK for a Python, TS or JS fleet. */
64
+ libraryPackage?: string;
55
65
  timeoutSec?: number;
56
66
  env?: Record<string, string>;
67
+ /** Internal live-output sink, fed whole lines; never supplied through a tool's JSON schema. */
68
+ onOutput?: (chunk: string) => void;
69
+ /** Internal: aborting it interrupts the run, then kills it after CANCEL_GRACE_MS. */
70
+ signal?: AbortSignal;
57
71
  }
58
72
  export interface FleetExecutionResult {
59
73
  passed: boolean;
@@ -62,17 +76,47 @@ export interface FleetExecutionResult {
62
76
  entryClass: string;
63
77
  exitCode: number | null;
64
78
  report: string;
79
+ /** Only the last MAX_BUFFERED_OUTPUT characters; the complete run is at outputLog. */
65
80
  output: string;
66
81
  result?: unknown;
67
82
  timedOut?: boolean;
83
+ cancelled?: boolean;
84
+ /** The run's complete output on the host, when it could be written. */
85
+ outputLog?: string;
68
86
  }
69
- /** A simple (unqualified) Java class name. */
70
- export declare const JAVA_CLASS_NAME: RegExp;
87
+ /** How long an interrupted fleet gets to cancel its workers before it is killed. */
88
+ export declare const CANCEL_GRACE_MS = 20000;
71
89
  /**
72
- * The generated fleet manager: the one class in the directory that JUnit runs. The journey
73
- * and the load class travel with it because they sit beside it, and they are compiled but
74
- * never selected -- the journey is an input the fleet ships to the worker, and the load
75
- * class only supplies the scenario.
90
+ * A sink for one of the container's streams that passes on whole lines only. Docker
91
+ * interleaves stdout and stderr frames, so a line is only whole within its own stream:
92
+ * one shared sink could split a SKYRAMP_LIMIT line around a stderr frame.
93
+ */
94
+ export declare function lineSink(emit: (line: string) => void): {
95
+ sink: Writable;
96
+ flush: () => void;
97
+ };
98
+ /**
99
+ * A simple Java class name, or a Python, TypeScript or JavaScript fleet script's file name.
100
+ *
101
+ * No `$`: the name is interpolated unquoted into the `sh`/PowerShell command built below, and
102
+ * `$` there is a variable sigil, not a literal character.
103
+ */
104
+ export declare const FLEET_ENTRY_NAME: RegExp;
105
+ export type FleetLanguage = "java" | "python" | "typescript" | "javascript";
106
+ /** The language a fleet entry is written in: a script by its extension, else a Java class. */
107
+ export declare function fleetLanguage(entry: string): FleetLanguage;
108
+ /**
109
+ * The SDK a branch-built package replaces: a pip sdist or wheel for Python, an npm tarball
110
+ * for TypeScript and JavaScript. Java takes a jar through libraryJar instead.
111
+ */
112
+ export declare function packageLanguages(pkg: string): FleetLanguage[];
113
+ /** The file an entry lives in, relative to the fleet directory. */
114
+ export declare function fleetFileName(entry: string): string;
115
+ /**
116
+ * The generated fleet manager: the JUnit class (Java) or the script (Python, TypeScript)
117
+ * the run starts. The journey and the load file travel with it because they sit beside it
118
+ * -- the journey is an input the fleet ships to the worker, and the load file only supplies
119
+ * the scenario.
76
120
  */
77
121
  export declare function findEntryClass(directory: string, explicit?: string): string;
78
122
  /**
@@ -84,12 +128,25 @@ export declare function findEntryClass(directory: string, explicit?: string): st
84
128
  */
85
129
  export declare function assertNoPinnedImage(fleetFile: string): void;
86
130
  /**
87
- * The fleet reaches the worker it deployed at an address derived from the Docker daemon:
88
- * the daemon's host for a remote engine, `localhost` for a local socket. Inside this
89
- * container `localhost` is the container, so a local-socket fleet cannot reach the worker it
90
- * just deployed and would hang until the timeout instead of failing.
131
+ * Which Docker daemons the fleet deploys its workers through, read off its hosts.
132
+ *
133
+ * `remote`: a host names a tcp or http(s) engine. That connection is mTLS, so the run needs
134
+ * the client certificates.
135
+ *
136
+ * `local`: a host deploys with no remote engine, which is the daemon this tool runs the
137
+ * executor on. The fleet reaches it through the daemon's socket and needs no certificates:
138
+ * the way to try a fleet on one's own machine.
139
+ *
140
+ * A host that carries an address drives a worker that already runs and deploys nothing, so
141
+ * it counts as neither. Throws when no host, a non-literal dockerHost, or a host built from a
142
+ * spread or a bare variable leaves the engine unknown -- the last of those would otherwise
143
+ * read as local with no dockerHost found, mounting the daemon's socket for a host that may
144
+ * actually be remote.
91
145
  */
92
- export declare function assertRemoteDaemon(fleetFile: string): void;
146
+ export declare function fleetDaemons(fleetFile: string): {
147
+ remote: boolean;
148
+ local: boolean;
149
+ };
93
150
  /**
94
151
  * The image the run happens in.
95
152
  *
@@ -105,10 +162,24 @@ export interface ContainerCommand {
105
162
  /** The shell script argv ends with -- named, because its index differs per platform. */
106
163
  script: string;
107
164
  }
108
- /** javac, then the JUnit console runner, both against the image's classpath. */
109
- export declare function buildCommand(entryClass: string, overrideJar: string | undefined, platform?: ContainerPlatform): ContainerCommand;
110
- /** The lines the fleet printed, which is what a reader wants out of the JUnit noise. */
165
+ /**
166
+ * Java: javac, then the JUnit console runner, both against the image's classpath. Python,
167
+ * TypeScript and JavaScript: the fleet script, run from the work directory so its imports
168
+ * of the load file resolve beside it -- after installing a branch-built SDK package over
169
+ * the image's own, when one is mounted.
170
+ */
171
+ export declare function buildCommand(entryClass: string, overrideJar: string | undefined, platform?: ContainerPlatform, overridePackage?: string): ContainerCommand;
172
+ /**
173
+ * The lines the fleet printed, which is what a reader wants out of the JUnit noise: the
174
+ * one-line digest, or the report blocks (name in column one, continuation lines indented).
175
+ */
111
176
  export declare function extractReport(output: string): string;
177
+ /**
178
+ * The report the library rendered itself, carried in the structured result as `report`.
179
+ * Preferred over the stdout filter: it is the complete text, not whatever survived the
180
+ * JUnit runner's console, and it is the same text every SDK prints.
181
+ */
182
+ export declare function embeddedReport(result: unknown): string | undefined;
112
183
  /**
113
184
  * Where a path inside the container lands on the host.
114
185
  *