@skyramp/mcp 0.2.150-rc.ldw → 0.2.150-rc.ldw-3

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
@@ -37,6 +37,7 @@ import { registerOneClickTool } from "./tools/one-click/oneClickTool.js";
37
37
  import { registerEnrichTestWithMocksTool } from "./tools/enrichTestWithMocksTool.js";
38
38
  import { registerPreflightMockCheckTool } from "./tools/preflightMockCheckTool.js";
39
39
  import { registerGenerateEnrichedIntegrationTestTool } from "./tools/generateEnrichedIntegrationTestTool.js";
40
+ import { registerLocalDevWorkerComposeTool } from "./tools/localDevWorkerComposeTool.js";
40
41
  import { registerAnalysisResources } from "./resources/analysisResources.js";
41
42
  import { registerProgressResource } from "./resources/progressResource.js";
42
43
  import { AnalyticsService } from "./services/AnalyticsService.js";
@@ -198,7 +199,7 @@ const infrastructureTools = [
198
199
  registerTraceStopTool,
199
200
  ];
200
201
  if (isLocalDevEnabled()) {
201
- infrastructureTools.push(registerEnrichTestWithMocksTool, registerPreflightMockCheckTool, registerGenerateEnrichedIntegrationTestTool);
202
+ infrastructureTools.push(registerEnrichTestWithMocksTool, registerPreflightMockCheckTool, registerGenerateEnrichedIntegrationTestTool, registerLocalDevWorkerComposeTool);
202
203
  logger.info("Local-dev tools enabled via SKYRAMP_FEATURE_LOCAL_DEV");
203
204
  }
204
205
  if (isTestbotEnabled()) {
@@ -318,23 +318,24 @@ The tool returns instructions — it does NOT edit the file automatically. Apply
318
318
  .done()
319
319
  // ─── Phase 5: Deploy ────────────────────────────────────────────────────────
320
320
  .addPhase("deploy", "Deploy", { headerLevel: "##", stepFormat: "hash" })
321
- .step("START_ENVIRONMENT", "Bring up the service under test", (ctx) => `If a \`docker-compose\` file is present in the repository, the service under test and its real infrastructure (DB, cache, real broker, real first-party services) must be running — **but mocked downstream services must NOT be running.**
321
+ .step("START_ENVIRONMENT", "Bring up the service under test and the Skyramp worker", (ctx) => `Bring up the service under test and its real infrastructure (DB, cache, real broker, real first-party services) together with the Skyramp mock worker — **but mocked downstream services must NOT be running.**
322
322
 
323
- **Why this matters:** The Skyramp worker intercepts traffic via Docker DNS alias hijacking — it registers itself as the DNS alias for each mocked service's hostname on the Docker network. If the real service container is also up on the same hostname and port, the two will conflict: traffic may reach the real service instead of the mock, or port binding will fail entirely. This is the most common cause of mocks not being applied correctly.
323
+ **Why this matters:** The Skyramp worker runs as a Docker Compose service on the SUT's network and intercepts traffic via Docker DNS alias hijacking at mock-apply time — it takes over the DNS alias for each mocked service's hostname. If the real service container is also up on the same hostname and port, the two conflict: traffic may reach the real service instead of the mock, or port binding fails. This is the most common cause of mocks not being applied correctly. Running the worker as a compose service (rather than spawning it ad hoc) guarantees it joins the SUT's network and is torn down with the stack.
324
324
 
325
325
  Steps:
326
- 1. Run \`docker compose ps\` in \`${ctx.repositoryPath}\` to check current container state.
327
- 2. Identify which docker-compose service names correspond to services selected for mocking (e.g., if \`ams\` is selected for mocking, do not start the \`ams\` container).
328
- 3. Stop any mocked service containers that are already running:
329
- \`docker compose stop <mocked-service-1> <mocked-service-2> ...\`
330
- 4. Rebuild and bring up only the SUT and its real dependencies — start selectively:
331
- \`docker compose up -d --build <sut-service> <db> <cache> <broker> <real-first-party-services>\`
332
- Do **not** run plain \`docker compose up -d\` when it would start mocked downstream services or reuse a stale SUT image. If the compose file cannot start selectively, start the minimum required profile/services and then re-check that mocked service containers are stopped before executing tests. If the SUT service has no compose \`build\` context, record that local code cannot be rebuilt by compose before execution.
333
- 5. Wait for health checks to pass and verify the service under test responds (e.g., curl the health endpoint or send a minimal request).
334
-
335
- The \`skyramp_execute_test\` tool runs the enriched test in the language runner. The enriched test applies selected mocks at runtime through its language-specific helper before exercising the service.
336
-
337
- **Do NOT proceed to Execute until: (a) the service under test responds, and (b) all mocked service containers are confirmed stopped.**`)
326
+ 1. Call \`skyramp_setup_local_dev_worker\` with \`repositoryPath\` = \`${ctx.repositoryPath}\` (pass \`composeFile\` if the base docker-compose file is not at the repo root). It writes a worker compose override and returns \`composeFilePrefix\` (the \`-f <base> -f <override>\` flags), the resolved \`dockerNetwork\`, and the worker port. **Use the returned \`composeFilePrefix\` for every \`docker compose\` command below, and pass the returned \`dockerNetwork\` to test/mock generation (see the Generate phase).**
327
+ 2. Run \`docker compose <composeFilePrefix> ps\` to check current container state.
328
+ 3. Identify which docker-compose service names correspond to services selected for mocking (e.g., if \`ams\` is selected for mocking, do not start the \`ams\` container).
329
+ 4. Stop any mocked service containers that are already running:
330
+ \`docker compose <composeFilePrefix> stop <mocked-service-1> <mocked-service-2> ...\`
331
+ 5. Rebuild and bring up only the SUT, its real dependencies, and the worker — start selectively:
332
+ \`docker compose <composeFilePrefix> up -d --build <sut-service> <db> <cache> <broker> <real-first-party-services> skyramp\`
333
+ Always include the \`skyramp\` service. Do **not** run plain \`docker compose up -d\` when it would start mocked downstream services or reuse a stale SUT image. If the compose file cannot start selectively, start the minimum required profile/services plus \`skyramp\`, then re-check that mocked service containers are stopped before executing tests. If the SUT service has no compose \`build\` context, record that local code cannot be rebuilt by compose before execution.
334
+ 6. Wait for health checks to pass and verify (a) the service under test responds (e.g., curl the health endpoint) and (b) the \`skyramp\` worker container is running.
335
+
336
+ The \`skyramp_execute_test\` tool runs the enriched test in the language runner. Its generated Skyramp client connects to the worker — emitted with \`runtime="docker"\`, \`docker_network\`, and \`docker_skyramp_port\` (from the \`dockerNetwork\` and \`dockerWorkerPort\` passed to generation) — and applies the selected mocks before exercising the service.
337
+
338
+ **Do NOT proceed to Execute until: (a) the service under test responds, (b) the \`skyramp\` worker is running, and (c) all mocked service containers are confirmed stopped.**`)
338
339
  .subStep("DEPLOY_GRPC", "Handle gRPC dependencies", (_ctx) => `If the dependency map from Phase 1 includes gRPC services selected for mocking:
339
340
 
340
341
  To regenerate a single missing gRPC mock, call \`skyramp_mock_generation\` with \`protocol: "grpc"\`.
@@ -414,6 +415,7 @@ That's it. Pass \`token: ""\` so the executor injects \`SKYRAMP_TEST_TOKEN\` fro
414
415
  - **Generated test issue** — failure from wrong auth, missing field, or incorrect assertion in the generated test itself.
415
416
  - **Timeout** — execution did not complete; pass/fail is unknown.
416
417
  - **Issues found**: unmockable deps, generation failures, connection errors, gaps
418
+ - **Teardown**: remind the user that, when finished, tearing the stack down with \`docker compose <composeFilePrefix> down -v --remove-orphans\` (the same \`-f\` prefix returned by \`skyramp_setup_local_dev_worker\`) removes the SUT, real dependencies, **and the Skyramp worker** together — so no worker container or network is left behind. A plain \`docker compose down\` *without* the override prefix leaves the worker running (it is defined only in the override, so a base-only \`down\` treats it as an orphan) and the network fails to delete with "resource still in use"; always include the prefix. If the prefix is ever unavailable at teardown, \`docker compose down --remove-orphans\` against the base file still sweeps the worker as an orphan.
417
419
 
418
420
  Do NOT write results to a file — report inline to the user.`)
419
421
  .done();
@@ -63,6 +63,7 @@ ${sandboxWorkerBlock ? sandboxWorkerBlock + "\n" : ""}${serviceContext ? service
63
63
  - **Enrichment validation** — an integration test that has downstream mocks but lacks \`MOCK_SERVICES\` and the language-specific apply helper (\`apply_all_mocks(client)\` for Python or \`applyAllMocks(client)\` for TypeScript/JavaScript/Java) is incomplete. Stop and enrich it; never call \`skyramp_execute_test\` on an incomplete integration test.
64
64
  - **Do not delete mocks to pass** — if mock deployment fails, do NOT remove mock imports, empty \`MOCK_SERVICES\`, switch mocked downstreams to live Compose stubs, or report the resulting test as passed. Fix the mock wiring once; if still blocked, report the scenario as failed/blocked with the mock deployment error.
65
65
  - **Mocked services must not be running** — before execution, stop every Docker container for a service selected for mocking (\`docker compose stop <mocked-service>\`). The Skyramp worker takes over that service's DNS alias on the Docker network; a real container running on the same hostname and port will conflict, causing traffic to reach the real service instead of the mock or causing port binding failure. Bring up only the SUT and its real infrastructure (DB, cache, real broker, real first-party services); do NOT run \`docker compose up -d\` without immediately stopping mocked service containers.
66
+ - **Worker runs as a compose service** — the Deploy phase calls \`skyramp_setup_local_dev_worker\`, which adds the Skyramp worker to the SUT's Docker network via a compose override and returns the resolved \`dockerNetwork\` plus the \`-f\` \`composeFilePrefix\`. Pass that \`dockerNetwork\` (and \`dockerWorkerPort\`, default 35142) to \`skyramp_generate_enriched_integration_test\` so the generated client connects to that worker — generation emits the client with \`runtime="docker"\`, \`docker_network\`, and \`docker_skyramp_port\`. Use the \`composeFilePrefix\` for every \`docker compose\` command, and tear the stack down with the same prefix (\`docker compose <composeFilePrefix> down -v --remove-orphans\`) so the worker is removed with the stack — a base-only \`docker compose down\` leaves the worker (it lives only in the override) holding the network open. Never spawn the worker manually.
66
67
  - **Mock URL format** — the \`endpointURL\` in \`skyramp_mock_generation\` MUST use the original Docker service hostname (e.g., \`http://identity-service:4000\`, \`http://profile-service:50052\`). The Skyramp executor uses DNS alias hijacking — it takes over the service's DNS name on the Docker network so the service under test's requests are intercepted transparently. NEVER use \`localhost\`, \`127.0.0.1\`, \`0.0.0.0\`, \`host.docker.internal\`, or the worker address as the mock URL. For REST mocks, \`skyramp_preflight_mock_check\` returns blocking \`REST_LOOPBACK_URL\` when a deployable mock uses these hosts.
67
68
  - **REST mock routing** — Generated REST mock files may contain a fallback \`URL\` value such as \`http://localhost:8080\`; do not manually edit those files. The enrichment tool uses the mock generation \`# Command\` target to set each REST mock's \`mock.url\` back to the original Docker service origin before \`apply_mock()\`. If the command target is loopback, regenerate the mock with the original service hostname.
68
69
  - **gRPC mock routing** — gRPC mocks may target the real downstream service port (e.g., \`partner-accounts:50051\`). Apply gRPC mocks before the SUT starts or restart the SUT after applying them, and verify the Skyramp worker has the original endpoint host alias from \`endpointURL\` (\`partner-accounts\`, not the protobuf service name like \`PartnerAccountsService\`).
@@ -32,6 +32,7 @@ export const TOOLS_WITHOUT_PHASE = new Set([
32
32
  "skyramp_init_scan",
33
33
  "skyramp_init_workspace",
34
34
  "skyramp_one_click_tool",
35
+ "skyramp_setup_local_dev_worker",
35
36
  "skyramp_actions",
36
37
  "skyramp_start_trace_collection",
37
38
  "skyramp_stop_trace_collection",
@@ -16,6 +16,7 @@ export declare function executeGenerateEnrichedIntegrationTest(params: {
16
16
  output?: string;
17
17
  runtime?: string;
18
18
  dockerNetwork?: string;
19
+ dockerWorkerPort?: number;
19
20
  authHeader?: string;
20
21
  authScheme?: string;
21
22
  language?: string;
@@ -4,6 +4,7 @@ import { logger } from "../utils/logger.js";
4
4
  import { RuntimeEnvironment, runtimeEnvironmentInputSchema } from "../types/TestTypes.js";
5
5
  import { injectMockImports, isSupportedMockEnrichmentLanguage } from "./enrichTestWithMocksTool.js";
6
6
  import { isAuthorizationHeaderName } from "../utils/workspaceAuth.js";
7
+ import { WORKER_CONTROL_PORT } from "../utils/versions.js";
7
8
  import * as fs from "fs";
8
9
  import * as path from "path";
9
10
  const TOOL_NAME = "skyramp_generate_enriched_integration_test";
@@ -32,6 +33,13 @@ const enrichedIntegrationTestSchema = {
32
33
  .string()
33
34
  .optional()
34
35
  .describe("Docker network name for the test runtime."),
36
+ dockerWorkerPort: z
37
+ .number()
38
+ .int()
39
+ .min(0)
40
+ .max(65535)
41
+ .optional()
42
+ .describe("Skyramp worker control port the generated client connects to. Defaults to 35142 (the local-dev compose worker's published port)."),
35
43
  authHeader: z
36
44
  .string()
37
45
  .optional()
@@ -71,7 +79,7 @@ export function registerGenerateEnrichedIntegrationTestTool(server) {
71
79
  });
72
80
  }
73
81
  export async function executeGenerateEnrichedIntegrationTest(params) {
74
- const { scenarioFile, mockFiles, scenarioName, outputDir, output, runtime, dockerNetwork, authHeader, authScheme, language, framework, traceComments, } = params;
82
+ const { scenarioFile, mockFiles, scenarioName, outputDir, output, runtime, dockerNetwork, dockerWorkerPort, authHeader, authScheme, language, framework, traceComments, } = params;
75
83
  try {
76
84
  const resolvedLanguage = (language ?? "python").toLowerCase();
77
85
  if (!isSupportedMockEnrichmentLanguage(resolvedLanguage)) {
@@ -131,6 +139,7 @@ export async function executeGenerateEnrichedIntegrationTest(params) {
131
139
  runtime: runtime ?? RuntimeEnvironment.DOCKER,
132
140
  force: true,
133
141
  dockerNetwork,
142
+ dockerWorkerPort: (dockerWorkerPort ?? WORKER_CONTROL_PORT).toString(),
134
143
  authHeader: resolvedAuthHeader,
135
144
  authType: resolvedAuthType,
136
145
  language: resolvedLanguage,
@@ -0,0 +1,23 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export interface WorkerComposeResult {
3
+ baseComposeFile: string;
4
+ overrideFile: string;
5
+ dockerNetwork: string;
6
+ workerPort: number;
7
+ composeFilePrefix: string;
8
+ }
9
+ /**
10
+ * Generate the worker compose override and return the resolved network name and
11
+ * the `-f` prefix for `docker compose` commands. Throws on a missing compose file.
12
+ */
13
+ export declare function setupLocalDevWorker(repositoryPath: string, composeFile?: string): WorkerComposeResult;
14
+ /**
15
+ * Deterministically set up the Skyramp worker as a Docker Compose service for
16
+ * the local-dev workflow.
17
+ *
18
+ * This replaces the SDK's imperative worker spawn (which raced onto the wrong
19
+ * Docker network and leaked on teardown — Local-Dev Skill Findings #1, #9, #10).
20
+ * The worker becomes a compose service via a generated override file, so it joins
21
+ * the SUT's network declaratively and is removed by `docker compose down`.
22
+ */
23
+ export declare function registerLocalDevWorkerComposeTool(server: McpServer): void;
@@ -0,0 +1,191 @@
1
+ import { z } from "zod";
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+ import yaml from "js-yaml";
5
+ import { AnalyticsService } from "../services/AnalyticsService.js";
6
+ import { WORKER_DOCKER_IMAGE, WORKER_CONTROL_PORT } from "../utils/versions.js";
7
+ import { logger } from "../utils/logger.js";
8
+ const TOOL_NAME = "skyramp_setup_local_dev_worker";
9
+ // Relative path (under the repo) where the generated worker override is written.
10
+ const OVERRIDE_REL_PATH = ".skyramp/skyramp.worker.compose.yml";
11
+ const BASE_COMPOSE_CANDIDATES = [
12
+ "docker-compose.yml",
13
+ "docker-compose.yaml",
14
+ "compose.yml",
15
+ "compose.yaml",
16
+ ];
17
+ /**
18
+ * Docker Compose project name derivation: lowercase the directory basename and
19
+ * strip characters outside [a-z0-9_-] (Compose's normalization). COMPOSE_PROJECT_NAME
20
+ * overrides it when set. The default network is then `<project>_<networkKey>`, or
21
+ * `<project>_default` when the compose file declares no named networks.
22
+ */
23
+ function deriveProjectName(composeDir, env = process.env) {
24
+ const override = env.COMPOSE_PROJECT_NAME?.trim();
25
+ if (override)
26
+ return override;
27
+ return path
28
+ .basename(composeDir)
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9_-]/g, "");
31
+ }
32
+ /** Resolve the base compose file: explicit path wins, else discover at repo root. */
33
+ function resolveBaseCompose(repositoryPath, composeFile) {
34
+ if (composeFile) {
35
+ const abs = path.isAbsolute(composeFile)
36
+ ? composeFile
37
+ : path.join(repositoryPath, composeFile);
38
+ return fs.existsSync(abs) ? abs : undefined;
39
+ }
40
+ for (const candidate of BASE_COMPOSE_CANDIDATES) {
41
+ const abs = path.join(repositoryPath, candidate);
42
+ if (fs.existsSync(abs))
43
+ return abs;
44
+ }
45
+ return undefined;
46
+ }
47
+ /**
48
+ * Build the worker override compose document. The worker joins the same named
49
+ * network(s) the base declares (so it shares the SUT's network for DNS-alias
50
+ * hijacking at mock-apply time); when the base declares none, the worker joins
51
+ * the implicit `default` network like every other service. Only the control
52
+ * port (35142) is published to the host — mock ports stay internal to the
53
+ * docker network.
54
+ */
55
+ function buildOverrideDoc(networkKeys) {
56
+ const workerService = {
57
+ image: WORKER_DOCKER_IMAGE,
58
+ volumes: [
59
+ "skyramp:/etc/skyramp",
60
+ "/var/run/docker.sock:/var/run/docker.sock",
61
+ ],
62
+ ports: [`${WORKER_CONTROL_PORT}:${WORKER_CONTROL_PORT}`],
63
+ restart: "always",
64
+ };
65
+ const doc = {
66
+ services: { skyramp: workerService },
67
+ volumes: { skyramp: null },
68
+ };
69
+ if (networkKeys.length > 0) {
70
+ workerService.networks = networkKeys;
71
+ // Reference each base network by key so Compose merges (does not redefine) it.
72
+ doc.networks = Object.fromEntries(networkKeys.map((k) => [k, {}]));
73
+ }
74
+ return doc;
75
+ }
76
+ /**
77
+ * Resolve the actual Docker network name Compose uses for `key`:
78
+ * - an explicit `name:` is used verbatim (NOT project-scoped),
79
+ * - `external: true` networks are pre-existing and not project-scoped
80
+ * (use `external.name` for the legacy form, else the key),
81
+ * - otherwise Compose prefixes the project: `<project>_<key>`.
82
+ */
83
+ function resolveNetworkName(project, key, def) {
84
+ const d = def ?? {};
85
+ if (typeof d.name === "string" && d.name.length > 0)
86
+ return d.name;
87
+ if (d.external) {
88
+ if (typeof d.external === "object" && d.external.name) {
89
+ return d.external.name;
90
+ }
91
+ return key;
92
+ }
93
+ return `${project}_${key}`;
94
+ }
95
+ /**
96
+ * Generate the worker compose override and return the resolved network name and
97
+ * the `-f` prefix for `docker compose` commands. Throws on a missing compose file.
98
+ */
99
+ export function setupLocalDevWorker(repositoryPath, composeFile) {
100
+ const baseComposeFile = resolveBaseCompose(repositoryPath, composeFile);
101
+ if (!baseComposeFile) {
102
+ throw new Error(composeFile
103
+ ? `Compose file not found: ${composeFile}`
104
+ : `No docker-compose file found at ${repositoryPath} (looked for: ${BASE_COMPOSE_CANDIDATES.join(", ")}). Pass composeFile for a non-default location.`);
105
+ }
106
+ const parsed = yaml.load(fs.readFileSync(baseComposeFile, "utf-8"), {
107
+ schema: yaml.JSON_SCHEMA,
108
+ });
109
+ const networksObj = parsed?.networks ?? {};
110
+ const networkKeys = Object.keys(networksObj);
111
+ const composeDir = path.dirname(baseComposeFile);
112
+ const project = deriveProjectName(composeDir);
113
+ // The network the SDK client + executor target. First declared named network,
114
+ // else the implicit default network. Honor explicit `name:`/`external:` so the
115
+ // returned name matches the network Compose actually creates/uses.
116
+ const primaryKey = networkKeys[0] ?? "default";
117
+ const dockerNetwork = resolveNetworkName(project, primaryKey, networksObj[primaryKey]);
118
+ const overrideFile = path.join(repositoryPath, OVERRIDE_REL_PATH);
119
+ fs.mkdirSync(path.dirname(overrideFile), { recursive: true });
120
+ const overrideDoc = buildOverrideDoc(networkKeys);
121
+ const header = "# Generated by skyramp_setup_local_dev_worker — runs the Skyramp mock worker\n" +
122
+ "# as a compose service so it shares the SUT's network and is torn down with\n" +
123
+ "# the project. Layer it: docker compose -f <base> -f this up. Safe to delete.\n";
124
+ fs.writeFileSync(overrideFile, header + yaml.dump(overrideDoc, { lineWidth: -1 }), "utf-8");
125
+ return {
126
+ baseComposeFile,
127
+ overrideFile,
128
+ dockerNetwork,
129
+ workerPort: WORKER_CONTROL_PORT,
130
+ composeFilePrefix: `-f ${baseComposeFile} -f ${overrideFile}`,
131
+ };
132
+ }
133
+ /**
134
+ * Deterministically set up the Skyramp worker as a Docker Compose service for
135
+ * the local-dev workflow.
136
+ *
137
+ * This replaces the SDK's imperative worker spawn (which raced onto the wrong
138
+ * Docker network and leaked on teardown — Local-Dev Skill Findings #1, #9, #10).
139
+ * The worker becomes a compose service via a generated override file, so it joins
140
+ * the SUT's network declaratively and is removed by `docker compose down`.
141
+ */
142
+ export function registerLocalDevWorkerComposeTool(server) {
143
+ server.registerTool(TOOL_NAME, {
144
+ description: `Set up the Skyramp mock worker as a Docker Compose service for the local-dev workflow (run this in the Deploy phase, before bringing the stack up).
145
+
146
+ Writes a compose override file (.skyramp/skyramp.worker.compose.yml) that declares the worker as a service on the SUT's docker network, then returns the docker-compose \`-f\` prefix and the resolved docker network name.
147
+
148
+ Why: running the worker as a compose service makes it join the SUT's network declaratively (so the app can reach its mocks) and tears it down with \`docker compose down\` — replacing the fragile runtime worker spawn that landed on the wrong network and leaked on teardown.
149
+
150
+ Use the returned values like this:
151
+ - Bring up: \`docker compose <composeFilePrefix> up -d --build <sut-service> <real-deps...> skyramp\` (stop mocked-service containers first).
152
+ - Tear down: \`docker compose <composeFilePrefix> down -v\`.
153
+ - Pass \`dockerNetwork\` (and \`dockerWorkerPort\`=workerPort) to every test generation call so the generated client — emitted with runtime="docker", docker_network, docker_skyramp_port — connects to this worker.`,
154
+ inputSchema: {
155
+ repositoryPath: z
156
+ .string()
157
+ .refine((value) => path.isAbsolute(value), {
158
+ message: "repositoryPath must be an absolute path",
159
+ })
160
+ .describe("Absolute path to the repository (where docker-compose runs)."),
161
+ composeFile: z
162
+ .string()
163
+ .optional()
164
+ .describe("Path to the base docker-compose file (absolute, or relative to repositoryPath). Omit to auto-discover docker-compose.yml/.yaml or compose.yml/.yaml at the repo root. Provide this when the compose file lives in a subdirectory or has a non-default name."),
165
+ },
166
+ }, async (args) => {
167
+ try {
168
+ const result = setupLocalDevWorker(args.repositoryPath, args.composeFile);
169
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, undefined, {}).catch(() => { });
170
+ logger.info("Generated local-dev worker compose override", {
171
+ overrideFile: result.overrideFile,
172
+ dockerNetwork: result.dockerNetwork,
173
+ });
174
+ return {
175
+ content: [
176
+ {
177
+ type: "text",
178
+ text: JSON.stringify(result, null, 2),
179
+ },
180
+ ],
181
+ };
182
+ }
183
+ catch (err) {
184
+ const message = err instanceof Error ? err.message : String(err);
185
+ return {
186
+ isError: true,
187
+ content: [{ type: "text", text: message }],
188
+ };
189
+ }
190
+ });
191
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, afterEach } from "@jest/globals";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import yaml from "js-yaml";
6
+ import { setupLocalDevWorker } from "./localDevWorkerComposeTool.js";
7
+ import { WORKER_DOCKER_IMAGE } from "../utils/versions.js";
8
+ /** Create a project dir (named, for deterministic project-name derivation) with a compose file. */
9
+ function makeRepo(projectName, composeBody, fileName = "docker-compose.yml") {
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-worker-"));
11
+ const repo = path.join(root, projectName);
12
+ fs.mkdirSync(repo, { recursive: true });
13
+ fs.writeFileSync(path.join(repo, fileName), composeBody);
14
+ return { root, repo };
15
+ }
16
+ const NAMED_NETWORK_COMPOSE = `services:
17
+ monolith:
18
+ image: monolith
19
+ networks: [checkr_default]
20
+ networks:
21
+ checkr_default:
22
+ driver: bridge
23
+ `;
24
+ const DEFAULT_NETWORK_COMPOSE = `services:
25
+ monolith:
26
+ image: monolith
27
+ `;
28
+ // Network with an explicit `name:` — Compose uses that verbatim (not project-scoped).
29
+ const EXPLICIT_NAME_COMPOSE = `services:
30
+ monolith:
31
+ image: monolith
32
+ networks: [app]
33
+ networks:
34
+ app:
35
+ name: my_custom_net
36
+ `;
37
+ // External network — pre-existing, not project-scoped.
38
+ const EXTERNAL_NETWORK_COMPOSE = `services:
39
+ monolith:
40
+ image: monolith
41
+ networks: [shared]
42
+ networks:
43
+ shared:
44
+ external: true
45
+ `;
46
+ describe("setupLocalDevWorker", () => {
47
+ const created = [];
48
+ afterEach(() => {
49
+ while (created.length)
50
+ fs.rmSync(created.pop(), { recursive: true, force: true });
51
+ delete process.env.COMPOSE_PROJECT_NAME;
52
+ });
53
+ function loadOverride(file) {
54
+ return yaml.load(fs.readFileSync(file, "utf-8"));
55
+ }
56
+ it("attaches the worker to the base's named network and resolves <project>_<key>", () => {
57
+ const { root, repo } = makeRepo("demoapp", NAMED_NETWORK_COMPOSE);
58
+ created.push(root);
59
+ const result = setupLocalDevWorker(repo);
60
+ expect(result.dockerNetwork).toBe("demoapp_checkr_default");
61
+ expect(result.workerPort).toBe(35142);
62
+ expect(result.composeFilePrefix).toBe(`-f ${result.baseComposeFile} -f ${result.overrideFile}`);
63
+ const doc = loadOverride(result.overrideFile);
64
+ const worker = doc.services.skyramp;
65
+ expect(worker.image).toBe(WORKER_DOCKER_IMAGE);
66
+ expect(worker.ports).toEqual(["35142:35142"]); // only the control port published
67
+ expect(worker.volumes).toContain("/var/run/docker.sock:/var/run/docker.sock");
68
+ expect(worker.networks).toEqual(["checkr_default"]);
69
+ expect(doc.networks).toHaveProperty("checkr_default");
70
+ expect(doc.volumes).toHaveProperty("skyramp");
71
+ });
72
+ it("uses an explicit network name: verbatim (not project-scoped)", () => {
73
+ const { root, repo } = makeRepo("demoapp", EXPLICIT_NAME_COMPOSE);
74
+ created.push(root);
75
+ const result = setupLocalDevWorker(repo);
76
+ expect(result.dockerNetwork).toBe("my_custom_net");
77
+ expect(loadOverride(result.overrideFile).services.skyramp.networks).toEqual(["app"]);
78
+ });
79
+ it("uses the key (not project-scoped) for an external network", () => {
80
+ const { root, repo } = makeRepo("demoapp", EXTERNAL_NETWORK_COMPOSE);
81
+ created.push(root);
82
+ const result = setupLocalDevWorker(repo);
83
+ expect(result.dockerNetwork).toBe("shared");
84
+ });
85
+ it("joins the implicit default network when the base declares none", () => {
86
+ const { root, repo } = makeRepo("plainapp", DEFAULT_NETWORK_COMPOSE);
87
+ created.push(root);
88
+ const result = setupLocalDevWorker(repo);
89
+ expect(result.dockerNetwork).toBe("plainapp_default");
90
+ const doc = loadOverride(result.overrideFile);
91
+ // No named networks → worker has no explicit networks list (joins default).
92
+ expect(doc.services.skyramp.networks).toBeUndefined();
93
+ expect(doc.networks).toBeUndefined();
94
+ });
95
+ it("honors COMPOSE_PROJECT_NAME for the network name", () => {
96
+ const { root, repo } = makeRepo("ignored-basename", NAMED_NETWORK_COMPOSE);
97
+ created.push(root);
98
+ process.env.COMPOSE_PROJECT_NAME = "custom";
99
+ const result = setupLocalDevWorker(repo);
100
+ expect(result.dockerNetwork).toBe("custom_checkr_default");
101
+ });
102
+ it("resolves a non-root composeFile and builds the right -f prefix", () => {
103
+ const { root, repo } = makeRepo("subdirapp", NAMED_NETWORK_COMPOSE, "docker-compose.yml");
104
+ created.push(root);
105
+ // Move the compose into a subdir to simulate a non-default location.
106
+ const subdir = path.join(repo, "infra");
107
+ fs.mkdirSync(subdir);
108
+ const moved = path.join(subdir, "compose.yml");
109
+ fs.renameSync(path.join(repo, "docker-compose.yml"), moved);
110
+ const result = setupLocalDevWorker(repo, "infra/compose.yml");
111
+ expect(result.baseComposeFile).toBe(moved);
112
+ expect(result.composeFilePrefix).toContain(`-f ${moved}`);
113
+ // Project name comes from the compose file's directory (infra), not the repo root.
114
+ expect(result.dockerNetwork).toBe("infra_checkr_default");
115
+ });
116
+ it("throws a clear error when no compose file is found", () => {
117
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-worker-"));
118
+ created.push(root);
119
+ expect(() => setupLocalDevWorker(root)).toThrow(/No docker-compose file found/);
120
+ });
121
+ });
@@ -55,7 +55,7 @@ describe("dockerImageExistsLocally", () => {
55
55
  });
56
56
  });
57
57
  describe("pullDockerImage", () => {
58
- const IMAGE = "skyramp/executor:v1.3.30";
58
+ const IMAGE = "skyramp/executor:v1.3.32";
59
59
  beforeEach(() => jest.clearAllMocks());
60
60
  describe("on amd64 host", () => {
61
61
  const originalArch = process.arch;
@@ -1,3 +1,4 @@
1
- export declare const SKYRAMP_IMAGE_VERSION = "v1.3.30";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.30";
3
- export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.30";
1
+ export declare const SKYRAMP_IMAGE_VERSION = "v1.3.32";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.32";
3
+ export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.32";
4
+ export declare const WORKER_CONTROL_PORT = 35142;
@@ -1,3 +1,5 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.30";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.32";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
4
+ // Control port the Skyramp worker listens on (SDK `CONTAINER_PORT`).
5
+ export const WORKER_CONTROL_PORT = 35142;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.2.150-rc.ldw",
3
+ "version": "0.2.150-rc.ldw-3",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",
@@ -67,7 +67,7 @@
67
67
  "dependencies": {
68
68
  "@modelcontextprotocol/sdk": "^1.24.3",
69
69
  "@playwright/test": "^1.55.0",
70
- "@skyramp/skyramp": "1.3.30",
70
+ "@skyramp/skyramp": "1.3.32",
71
71
  "dockerode": "^5.0.0",
72
72
  "fast-glob": "^3.3.3",
73
73
  "js-yaml": "^4.1.1",