swallowkit 2.0.0-alpha.2 → 2.0.0-alpha.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/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +2 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +39 -27
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -2
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +43 -122
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +2 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +45 -16
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +4 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +40 -2
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +27 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +19 -1
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +62 -35
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +4 -1
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +49 -18
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +1 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +20 -2
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +1 -1
- package/dist/core/project/react-project.d.ts.map +1 -1
- package/dist/core/project/react-project.js +45 -24
- package/dist/core/project/react-project.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +9 -1
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.js +8 -8
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +11 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +279 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +24 -1
- package/dist/core/verify/index.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +39 -15
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +3 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +137 -13
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/utils/azure-cli.d.ts +44 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +164 -0
- package/dist/utils/azure-cli.js.map +1 -0
- package/dist/utils/package-manager.js +1 -1
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-loop-phase34.test.ts +29 -0
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +199 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +57 -0
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +49 -3
- package/src/__tests__/package-manager.test.ts +2 -2
- package/src/__tests__/plan-apply.test.ts +68 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +19 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +40 -31
- package/src/cli/commands/init.ts +48 -125
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +53 -17
- package/src/cli/commands/scaffold.ts +40 -2
- package/src/cli/index.ts +26 -3
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-operations.ts +86 -42
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +4 -1
- package/src/core/project/agent-templates/AGENTS.md +39 -3
- package/src/core/project/agent-templates/CLAUDE.md +3 -1
- package/src/core/project/agent-templates/copilot-instructions.md +3 -1
- package/src/core/project/boundaries.ts +60 -18
- package/src/core/project/capabilities.ts +22 -2
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +49 -27
- package/src/core/project/workflows.ts +9 -1
- package/src/core/scaffold/auth-generator.ts +8 -8
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +305 -105
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +23 -1
- package/src/machine/index.ts +42 -18
- package/src/mcp/index.ts +156 -14
- package/src/mcp/version-selection.ts +163 -0
- package/src/utils/azure-cli.ts +154 -0
- package/src/utils/package-manager.ts +1 -1
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
package/src/mcp/index.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import { spawn } from "child_process";
|
|
3
4
|
import * as path from "path";
|
|
4
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
6
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
7
|
import * as z from "zod/v4";
|
|
7
8
|
import type { MachineNextAction, MachineOperationStatus, MachineResponse } from "../machine/contracts";
|
|
8
9
|
import { getSwallowKitVersion } from "../version";
|
|
10
|
+
import { detectFromProject, getCommands } from "../utils/package-manager";
|
|
11
|
+
import {
|
|
12
|
+
MCP_DELEGATED_FROM_ENV,
|
|
13
|
+
McpVersionContext,
|
|
14
|
+
buildPinnedUpdateNotice,
|
|
15
|
+
decideMcpLaunch,
|
|
16
|
+
resolveProjectSwallowKit,
|
|
17
|
+
} from "./version-selection";
|
|
9
18
|
|
|
10
19
|
interface MachineSuccessPayload<TData> {
|
|
11
20
|
ok: true;
|
|
@@ -70,6 +79,29 @@ async function executeMachineCommand<TData>(
|
|
|
70
79
|
return parsed;
|
|
71
80
|
}
|
|
72
81
|
|
|
82
|
+
function defaultVersionContext(): McpVersionContext {
|
|
83
|
+
return {
|
|
84
|
+
mcpVersion: getSwallowKitVersion(),
|
|
85
|
+
requestedVersion: "latest",
|
|
86
|
+
projectVersion: null,
|
|
87
|
+
selection: "running-version",
|
|
88
|
+
delegated: false,
|
|
89
|
+
warnings: [],
|
|
90
|
+
nextActions: [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function mergeNextActions(
|
|
95
|
+
first: MachineNextAction[] | undefined,
|
|
96
|
+
second: MachineNextAction[]
|
|
97
|
+
): MachineNextAction[] {
|
|
98
|
+
const merged = [...(first ?? [])];
|
|
99
|
+
for (const action of second) {
|
|
100
|
+
if (!merged.some((existing) => existing.command === action.command)) merged.push(action);
|
|
101
|
+
}
|
|
102
|
+
return merged;
|
|
103
|
+
}
|
|
104
|
+
|
|
73
105
|
function withOperationStatus<TData>(response: MachineSuccessPayload<TData>): Record<string, unknown> {
|
|
74
106
|
const data = response.data && typeof response.data === "object"
|
|
75
107
|
? (response.data as Record<string, unknown>)
|
|
@@ -92,25 +124,41 @@ function jsonTextContent(value: unknown): { content: Array<{ type: "text"; text:
|
|
|
92
124
|
};
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
function
|
|
96
|
-
const value = data && typeof data === "object"
|
|
127
|
+
function withVersionMetadata(data: unknown, context: McpVersionContext): Record<string, unknown> {
|
|
128
|
+
const value = data && typeof data === "object" && !Array.isArray(data)
|
|
129
|
+
? data as Record<string, unknown>
|
|
130
|
+
: { data };
|
|
97
131
|
const metadata = value.metadata && typeof value.metadata === "object"
|
|
98
132
|
? value.metadata as Record<string, unknown>
|
|
99
133
|
: {};
|
|
134
|
+
const existingNextActions = Array.isArray(value.nextActions) ? value.nextActions as MachineNextAction[] : undefined;
|
|
135
|
+
const nextActions = mergeNextActions(existingNextActions, context.nextActions);
|
|
100
136
|
|
|
101
137
|
return {
|
|
102
138
|
...value,
|
|
139
|
+
...(nextActions.length > 0 ? { nextActions } : {}),
|
|
103
140
|
metadata: {
|
|
104
141
|
...metadata,
|
|
105
|
-
swallowkitVersion:
|
|
142
|
+
swallowkitVersion: context.mcpVersion,
|
|
143
|
+
mcp: {
|
|
144
|
+
version: context.mcpVersion,
|
|
145
|
+
requestedVersion: context.requestedVersion,
|
|
146
|
+
projectVersion: context.projectVersion,
|
|
147
|
+
selection: context.selection,
|
|
148
|
+
delegated: context.delegated,
|
|
149
|
+
...(context.delegatedFrom ? { delegatedFrom: context.delegatedFrom } : {}),
|
|
150
|
+
},
|
|
151
|
+
...(context.warnings.length > 0 ? { warnings: [...context.warnings] } : {}),
|
|
106
152
|
},
|
|
107
|
-
}
|
|
153
|
+
};
|
|
108
154
|
}
|
|
109
155
|
|
|
110
156
|
export function buildSwallowKitToolDefinitions(
|
|
111
|
-
runMachineCli: MachineCliRunner = defaultMachineCliRunner
|
|
157
|
+
runMachineCli: MachineCliRunner = defaultMachineCliRunner,
|
|
158
|
+
versionContext: McpVersionContext = defaultVersionContext()
|
|
112
159
|
): ToolDefinition[] {
|
|
113
|
-
|
|
160
|
+
const withSwallowKitMetadata = (data: unknown) => withVersionMetadata(data, versionContext);
|
|
161
|
+
const definitions: ToolDefinition[] = [
|
|
114
162
|
{
|
|
115
163
|
name: "swallowkit_inspect_project",
|
|
116
164
|
description: "Return framework-owned SwallowKit project metadata.",
|
|
@@ -169,8 +217,9 @@ export function buildSwallowKitToolDefinitions(
|
|
|
169
217
|
functionsDir: z.string().optional(),
|
|
170
218
|
apiDir: z.string().optional(),
|
|
171
219
|
apiOnly: z.boolean().optional(),
|
|
220
|
+
skipInfra: z.boolean().optional().describe("Do not create or update anything under infra/; the container migration stays pending."),
|
|
172
221
|
}),
|
|
173
|
-
handler: async ({ model, functionsDir, apiDir, apiOnly }: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
|
|
222
|
+
handler: async ({ model, functionsDir, apiDir, apiOnly, skipInfra }: { model: string; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
|
|
174
223
|
const args = ["generate", "scaffold", model];
|
|
175
224
|
if (functionsDir) {
|
|
176
225
|
args.push("--functions-dir", functionsDir);
|
|
@@ -181,6 +230,9 @@ export function buildSwallowKitToolDefinitions(
|
|
|
181
230
|
if (apiOnly) {
|
|
182
231
|
args.push("--api-only");
|
|
183
232
|
}
|
|
233
|
+
if (skipInfra) {
|
|
234
|
+
args.push("--skip-infra");
|
|
235
|
+
}
|
|
184
236
|
|
|
185
237
|
const response = await executeMachineCommand(args, runMachineCli);
|
|
186
238
|
return jsonTextContent(response.data);
|
|
@@ -195,14 +247,16 @@ export function buildSwallowKitToolDefinitions(
|
|
|
195
247
|
functionsDir: z.string().optional(),
|
|
196
248
|
apiDir: z.string().optional(),
|
|
197
249
|
apiOnly: z.boolean().optional(),
|
|
250
|
+
skipInfra: z.boolean().optional().describe("Exclude all infra/ changes (container migration) from the plan."),
|
|
198
251
|
}),
|
|
199
|
-
handler: async ({ model, models, functionsDir, apiDir, apiOnly }: { model?: string; models?: string[]; functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
|
|
252
|
+
handler: async ({ model, models, functionsDir, apiDir, apiOnly, skipInfra }: { model?: string; models?: string[]; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
|
|
200
253
|
const targets = models && models.length > 0 ? models : model ? [model] : [];
|
|
201
254
|
if (targets.length === 0) throw new Error("Provide model or models.");
|
|
202
255
|
const args = ["plan", "scaffold", ...targets];
|
|
203
256
|
if (functionsDir) args.push("--functions-dir", functionsDir);
|
|
204
257
|
if (apiDir) args.push("--api-dir", apiDir);
|
|
205
258
|
if (apiOnly) args.push("--api-only");
|
|
259
|
+
if (skipInfra) args.push("--skip-infra");
|
|
206
260
|
|
|
207
261
|
const response = await executeMachineCommand(args, runMachineCli);
|
|
208
262
|
return jsonTextContent(withOperationStatus(response));
|
|
@@ -218,8 +272,9 @@ export function buildSwallowKitToolDefinitions(
|
|
|
218
272
|
functionsDir: z.string().optional(),
|
|
219
273
|
apiDir: z.string().optional(),
|
|
220
274
|
apiOnly: z.boolean().optional(),
|
|
275
|
+
skipInfra: z.boolean().optional().describe("Do not create or update anything under infra/ (defaults to the plan's setting)."),
|
|
221
276
|
}),
|
|
222
|
-
handler: async ({ model, planId, approve, functionsDir, apiDir, apiOnly }: { model?: string; planId?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean }) => {
|
|
277
|
+
handler: async ({ model, planId, approve, functionsDir, apiDir, apiOnly, skipInfra }: { model?: string; planId?: string; approve?: boolean; functionsDir?: string; apiDir?: string; apiOnly?: boolean; skipInfra?: boolean }) => {
|
|
223
278
|
const args = ["apply", "scaffold"];
|
|
224
279
|
if (model) args.push(model);
|
|
225
280
|
if (planId) args.push("--plan", planId);
|
|
@@ -227,6 +282,7 @@ export function buildSwallowKitToolDefinitions(
|
|
|
227
282
|
if (functionsDir) args.push("--functions-dir", functionsDir);
|
|
228
283
|
if (apiDir) args.push("--api-dir", apiDir);
|
|
229
284
|
if (apiOnly) args.push("--api-only");
|
|
285
|
+
if (skipInfra) args.push("--skip-infra");
|
|
230
286
|
|
|
231
287
|
const response = await executeMachineCommand(args, runMachineCli);
|
|
232
288
|
return jsonTextContent(withOperationStatus(response));
|
|
@@ -391,16 +447,28 @@ export function buildSwallowKitToolDefinitions(
|
|
|
391
447
|
},
|
|
392
448
|
},
|
|
393
449
|
];
|
|
450
|
+
|
|
451
|
+
// Every response reports which SwallowKit version answered and why it was selected.
|
|
452
|
+
return definitions.map((definition) => ({
|
|
453
|
+
...definition,
|
|
454
|
+
handler: async (input: unknown) => {
|
|
455
|
+
const result = await definition.handler(input);
|
|
456
|
+
return jsonTextContent(withSwallowKitMetadata(JSON.parse(result.content[0].text)));
|
|
457
|
+
},
|
|
458
|
+
}));
|
|
394
459
|
}
|
|
395
460
|
|
|
396
|
-
export function createSwallowKitMcpServer(
|
|
397
|
-
|
|
461
|
+
export function createSwallowKitMcpServer(
|
|
462
|
+
runMachineCli: MachineCliRunner = defaultMachineCliRunner,
|
|
463
|
+
versionContext: McpVersionContext = defaultVersionContext()
|
|
464
|
+
): McpServer {
|
|
465
|
+
const version = versionContext.mcpVersion;
|
|
398
466
|
const server = new McpServer({
|
|
399
467
|
name: "swallowkit-mcp",
|
|
400
468
|
version,
|
|
401
469
|
});
|
|
402
470
|
|
|
403
|
-
for (const tool of buildSwallowKitToolDefinitions(runMachineCli)) {
|
|
471
|
+
for (const tool of buildSwallowKitToolDefinitions(runMachineCli, versionContext)) {
|
|
404
472
|
server.registerTool(
|
|
405
473
|
tool.name,
|
|
406
474
|
{
|
|
@@ -414,9 +482,83 @@ export function createSwallowKitMcpServer(runMachineCli: MachineCliRunner = defa
|
|
|
414
482
|
return server;
|
|
415
483
|
}
|
|
416
484
|
|
|
485
|
+
function resolveExecCommand(cwd: string): string {
|
|
486
|
+
try {
|
|
487
|
+
return getCommands(detectFromProject(cwd)).exec;
|
|
488
|
+
} catch {
|
|
489
|
+
return "npx";
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function fetchPublishedLatest(): Promise<string | null> {
|
|
494
|
+
if (process.env.SWALLOWKIT_MCP_UPDATE_CHECK === "0") return null;
|
|
495
|
+
const registry = (process.env.npm_config_registry || "https://registry.npmjs.org").replace(/\/+$/, "");
|
|
496
|
+
try {
|
|
497
|
+
const response = await fetch(`${registry}/swallowkit/latest`, { signal: AbortSignal.timeout(3000) });
|
|
498
|
+
if (!response.ok) return null;
|
|
499
|
+
const body = await response.json() as { version?: unknown };
|
|
500
|
+
return typeof body.version === "string" ? body.version : null;
|
|
501
|
+
} catch {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function delegateToProjectMcp(entrypoint: string, fromVersion: string): void {
|
|
507
|
+
const child = spawn(process.execPath, [entrypoint], {
|
|
508
|
+
stdio: "inherit",
|
|
509
|
+
env: { ...process.env, [MCP_DELEGATED_FROM_ENV]: fromVersion },
|
|
510
|
+
});
|
|
511
|
+
for (const signal of ["SIGINT", "SIGTERM"] as const) {
|
|
512
|
+
process.on(signal, () => child.kill(signal));
|
|
513
|
+
}
|
|
514
|
+
child.on("error", (error) => {
|
|
515
|
+
console.error(`[swallowkit-mcp] failed to start the project-installed MCP: ${error.message}`);
|
|
516
|
+
process.exitCode = 1;
|
|
517
|
+
});
|
|
518
|
+
child.on("exit", (code, signal) => {
|
|
519
|
+
process.exitCode = code ?? (signal ? 1 : 0);
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
417
523
|
export async function runMcpServer(): Promise<void> {
|
|
418
|
-
|
|
419
|
-
const
|
|
524
|
+
const selfVersion = getSwallowKitVersion();
|
|
525
|
+
const cwd = process.cwd();
|
|
526
|
+
const execCommand = resolveExecCommand(cwd);
|
|
527
|
+
const decision = decideMcpLaunch({
|
|
528
|
+
selfVersion,
|
|
529
|
+
selfPackageRoot: path.resolve(__dirname, "..", ".."),
|
|
530
|
+
env: process.env,
|
|
531
|
+
project: resolveProjectSwallowKit(cwd),
|
|
532
|
+
execCommand,
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (decision.action === "delegate") {
|
|
536
|
+
console.error(
|
|
537
|
+
`[swallowkit-mcp] version: ${selfVersion}; delegating to the project-installed swallowkit-mcp ${decision.toVersion} (newer than the launcher).`
|
|
538
|
+
);
|
|
539
|
+
delegateToProjectMcp(decision.entrypoint, decision.fromVersion);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const context = decision.context;
|
|
544
|
+
// One write keeps the version line and its warnings together for log readers.
|
|
545
|
+
console.error([
|
|
546
|
+
`[swallowkit-mcp] version: ${selfVersion} (selection: ${context.selection}${context.delegatedFrom ? `, delegated from ${context.delegatedFrom}` : ""})`,
|
|
547
|
+
...context.warnings.map((warning) => `[swallowkit-mcp] warning: ${warning}`),
|
|
548
|
+
].join("\n"));
|
|
549
|
+
|
|
550
|
+
if (context.selection === "pinned") {
|
|
551
|
+
void fetchPublishedLatest().then((latest) => {
|
|
552
|
+
const notice = buildPinnedUpdateNotice(context, latest, execCommand);
|
|
553
|
+
if (notice) {
|
|
554
|
+
context.warnings.push(notice.warning);
|
|
555
|
+
context.nextActions.push(notice.nextAction);
|
|
556
|
+
console.error(`[swallowkit-mcp] warning: ${notice.warning}`);
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const server = createSwallowKitMcpServer(defaultMachineCliRunner, context);
|
|
420
562
|
const transport = new StdioServerTransport();
|
|
421
563
|
await server.connect(transport);
|
|
422
564
|
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime version selection for `swallowkit-mcp`.
|
|
3
|
+
*
|
|
4
|
+
* Policy: when launched as `latest` (or without an explicit version), use whichever is
|
|
5
|
+
* newer of this MCP package and the project-installed `swallowkit`. A newer project
|
|
6
|
+
* install takes over through its own `swallowkit-mcp`, so prerelease projects never get
|
|
7
|
+
* an older contract. An explicit SWALLOWKIT_MCP_VERSION is always honored.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import type { MachineNextAction } from "../machine/contracts";
|
|
13
|
+
import { MCP_LATEST, MCP_VERSION_ENV } from "../core/project/mcp-config";
|
|
14
|
+
import { compareSemver } from "../utils/semver";
|
|
15
|
+
|
|
16
|
+
export const MCP_DELEGATED_FROM_ENV = "SWALLOWKIT_MCP_DELEGATED_FROM";
|
|
17
|
+
|
|
18
|
+
export interface ProjectSwallowKitInstall {
|
|
19
|
+
version: string;
|
|
20
|
+
packageRoot: string;
|
|
21
|
+
mcpEntrypoint: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type McpVersionSelection =
|
|
25
|
+
| "running-version"
|
|
26
|
+
| "project-version"
|
|
27
|
+
| "pinned";
|
|
28
|
+
|
|
29
|
+
export interface McpVersionContext {
|
|
30
|
+
/** Version of the MCP server answering the request. */
|
|
31
|
+
mcpVersion: string;
|
|
32
|
+
/** SWALLOWKIT_MCP_VERSION as requested by the launcher (`latest` when unset). */
|
|
33
|
+
requestedVersion: string;
|
|
34
|
+
projectVersion: string | null;
|
|
35
|
+
selection: McpVersionSelection;
|
|
36
|
+
delegated: boolean;
|
|
37
|
+
delegatedFrom?: string;
|
|
38
|
+
warnings: string[];
|
|
39
|
+
nextActions: MachineNextAction[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type McpLaunchDecision =
|
|
43
|
+
| { action: "delegate"; entrypoint: string; fromVersion: string; toVersion: string }
|
|
44
|
+
| { action: "serve"; context: McpVersionContext };
|
|
45
|
+
|
|
46
|
+
export interface McpLaunchInput {
|
|
47
|
+
selfVersion: string;
|
|
48
|
+
selfPackageRoot: string;
|
|
49
|
+
env: NodeJS.ProcessEnv;
|
|
50
|
+
project: ProjectSwallowKitInstall | null;
|
|
51
|
+
execCommand?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function normalizeRoot(value: string): string {
|
|
55
|
+
const resolved = path.resolve(value);
|
|
56
|
+
try {
|
|
57
|
+
return fs.realpathSync.native(resolved).toLowerCase();
|
|
58
|
+
} catch {
|
|
59
|
+
return resolved.toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveProjectSwallowKit(cwd: string): ProjectSwallowKitInstall | null {
|
|
64
|
+
// Same lookup order as Node's node_modules resolution from the project directory.
|
|
65
|
+
let packageJsonPath: string | null = null;
|
|
66
|
+
for (let directory = path.resolve(cwd); ; directory = path.dirname(directory)) {
|
|
67
|
+
const candidate = path.join(directory, "node_modules", "swallowkit", "package.json");
|
|
68
|
+
if (fs.existsSync(candidate)) {
|
|
69
|
+
packageJsonPath = fs.realpathSync(candidate);
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (path.dirname(directory) === directory) return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
|
77
|
+
version?: string;
|
|
78
|
+
bin?: string | Record<string, string>;
|
|
79
|
+
};
|
|
80
|
+
if (!manifest.version) return null;
|
|
81
|
+
const packageRoot = path.dirname(packageJsonPath);
|
|
82
|
+
const bin = typeof manifest.bin === "object" ? manifest.bin["swallowkit-mcp"] : undefined;
|
|
83
|
+
const candidate = path.resolve(packageRoot, bin ?? path.join("dist", "mcp", "index.js"));
|
|
84
|
+
return {
|
|
85
|
+
version: manifest.version,
|
|
86
|
+
packageRoot,
|
|
87
|
+
mcpEntrypoint: fs.existsSync(candidate) ? candidate : null,
|
|
88
|
+
};
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function decideMcpLaunch(input: McpLaunchInput): McpLaunchDecision {
|
|
95
|
+
const { selfVersion, env, project } = input;
|
|
96
|
+
const exec = input.execCommand ?? "npx";
|
|
97
|
+
const requested = (env[MCP_VERSION_ENV] ?? "").trim() || MCP_LATEST;
|
|
98
|
+
const pinned = requested !== MCP_LATEST;
|
|
99
|
+
const delegatedFrom = (env[MCP_DELEGATED_FROM_ENV] ?? "").trim() || undefined;
|
|
100
|
+
const projectVersion = project?.version ?? null;
|
|
101
|
+
const projectNewer = projectVersion ? (compareSemver(projectVersion, selfVersion) ?? 0) > 0 : false;
|
|
102
|
+
const sameInstall = project ? normalizeRoot(project.packageRoot) === normalizeRoot(input.selfPackageRoot) : false;
|
|
103
|
+
|
|
104
|
+
if (!pinned && !delegatedFrom && project && projectNewer && !sameInstall && project.mcpEntrypoint) {
|
|
105
|
+
return { action: "delegate", entrypoint: project.mcpEntrypoint, fromVersion: selfVersion, toVersion: project.version };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const warnings: string[] = [];
|
|
109
|
+
const nextActions: MachineNextAction[] = [];
|
|
110
|
+
if (projectVersion && projectNewer) {
|
|
111
|
+
warnings.push(
|
|
112
|
+
`mcp-older-than-project: swallowkit-mcp ${selfVersion} is older than the project's swallowkit ${projectVersion}. ` +
|
|
113
|
+
"Its capabilities, boundaries, and plan/apply contracts may be outdated for this project."
|
|
114
|
+
);
|
|
115
|
+
nextActions.push({
|
|
116
|
+
command: `${exec} swallowkit machine inspect capabilities`,
|
|
117
|
+
description: "Use the project-local machine interface, which matches the installed SwallowKit version, until MCP is updated.",
|
|
118
|
+
});
|
|
119
|
+
nextActions.push({
|
|
120
|
+
command: `${exec} swallowkit configure-mcp`,
|
|
121
|
+
description: pinned
|
|
122
|
+
? `Re-select the MCP version (${MCP_VERSION_ENV} is pinned to ${requested}), then restart the MCP server.`
|
|
123
|
+
: "Pin the MCP launcher to the project version, then restart the MCP server.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
action: "serve",
|
|
129
|
+
context: {
|
|
130
|
+
mcpVersion: selfVersion,
|
|
131
|
+
requestedVersion: requested,
|
|
132
|
+
projectVersion,
|
|
133
|
+
selection: delegatedFrom ? "project-version" : pinned ? "pinned" : "running-version",
|
|
134
|
+
delegated: Boolean(delegatedFrom),
|
|
135
|
+
...(delegatedFrom ? { delegatedFrom } : {}),
|
|
136
|
+
warnings,
|
|
137
|
+
nextActions,
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* For an explicitly pinned launcher, report when the published `latest` has caught up
|
|
144
|
+
* with or passed the pinned version so the pin can be removed.
|
|
145
|
+
*/
|
|
146
|
+
export function buildPinnedUpdateNotice(
|
|
147
|
+
context: McpVersionContext,
|
|
148
|
+
publishedLatest: string | null,
|
|
149
|
+
execCommand = "npx"
|
|
150
|
+
): { warning: string; nextAction: MachineNextAction } | null {
|
|
151
|
+
if (context.selection !== "pinned" || !publishedLatest) return null;
|
|
152
|
+
const comparison = compareSemver(publishedLatest, context.requestedVersion);
|
|
153
|
+
if (comparison === null || comparison < 0) return null;
|
|
154
|
+
return {
|
|
155
|
+
warning:
|
|
156
|
+
`mcp-pin-outdated: ${MCP_VERSION_ENV} is pinned to ${context.requestedVersion}, but published latest is ${publishedLatest}. ` +
|
|
157
|
+
`Switch the launcher back to "${MCP_LATEST}".`,
|
|
158
|
+
nextAction: {
|
|
159
|
+
command: `${execCommand} swallowkit configure-mcp`,
|
|
160
|
+
description: `Re-select the MCP launcher version (returns to "${MCP_LATEST}" when it is at least the project version), then restart the MCP server.`,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure CLI invocation that works without a shell on every platform.
|
|
3
|
+
*
|
|
4
|
+
* On Windows `az` is a batch wrapper (az.cmd). Node cannot spawn it with `shell: false`
|
|
5
|
+
* (ENOENT for "az", EINVAL for "az.cmd"), so the wrapper is resolved explicitly:
|
|
6
|
+
* the MSI layout runs its bundled Python exactly like az.cmd does, and other wrappers
|
|
7
|
+
* run through cmd.exe with every argument quoted.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync, SpawnSyncOptions } from "child_process";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
|
|
14
|
+
export interface AzCliInvocation {
|
|
15
|
+
command: string;
|
|
16
|
+
prefixArgs: string[];
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
/** Batch wrapper that must be started through cmd.exe. */
|
|
19
|
+
batchScript?: string;
|
|
20
|
+
/** Resolved executable or wrapper, for diagnostics. */
|
|
21
|
+
source: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AzCliResult {
|
|
25
|
+
status: number | null;
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
28
|
+
error?: Error;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type AzCliAvailability =
|
|
32
|
+
| { available: true; invocation: AzCliInvocation }
|
|
33
|
+
| { available: false; reason: "not-found" | "not-runnable"; detail: string };
|
|
34
|
+
|
|
35
|
+
function findOnWindowsPath(name: string, env: NodeJS.ProcessEnv): string | null {
|
|
36
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
37
|
+
const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((ext) => ext.toLowerCase());
|
|
38
|
+
for (const directory of pathValue.split(";").filter(Boolean)) {
|
|
39
|
+
for (const extension of extensions) {
|
|
40
|
+
const candidate = path.join(directory.replace(/^"|"$/g, ""), `${name}${extension}`);
|
|
41
|
+
try {
|
|
42
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
43
|
+
} catch {
|
|
44
|
+
// Keep searching PATH.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveAzCliInvocation(
|
|
52
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
53
|
+
platform: NodeJS.Platform = process.platform
|
|
54
|
+
): AzCliInvocation | null {
|
|
55
|
+
if (platform !== "win32") return { command: "az", prefixArgs: [], source: "az" };
|
|
56
|
+
|
|
57
|
+
const resolved = findOnWindowsPath("az", env);
|
|
58
|
+
if (!resolved) return null;
|
|
59
|
+
const extension = path.extname(resolved).toLowerCase();
|
|
60
|
+
if (extension === ".exe" || extension === ".com") {
|
|
61
|
+
return { command: resolved, prefixArgs: [], source: resolved };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// MSI install: wbin\az.cmd runs "..\python.exe -IBm azure.cli %*" with AZ_INSTALLER=MSI.
|
|
65
|
+
const bundledPython = path.join(path.dirname(resolved), "..", "python.exe");
|
|
66
|
+
if (path.basename(path.dirname(resolved)).toLowerCase() === "wbin" && fs.existsSync(bundledPython)) {
|
|
67
|
+
return {
|
|
68
|
+
command: path.resolve(bundledPython),
|
|
69
|
+
prefixArgs: ["-IBm", "azure.cli"],
|
|
70
|
+
env: { AZ_INSTALLER: "MSI" },
|
|
71
|
+
source: resolved,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { command: env.ComSpec ?? "cmd.exe", prefixArgs: [], batchScript: resolved, source: resolved };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Builds the cmd.exe /s /c payload. Rejects characters cmd.exe would reinterpret even inside quotes. */
|
|
79
|
+
export function buildBatchCommandLine(script: string, args: string[]): string {
|
|
80
|
+
for (const value of [script, ...args]) {
|
|
81
|
+
if (/["%\r\n]/.test(value)) {
|
|
82
|
+
throw new Error(`Azure CLI argument cannot be passed safely through ${path.basename(script)}: ${JSON.stringify(value)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return `"${[script, ...args].map((value) => `"${value}"`).join(" ")}"`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let cachedInvocation: AzCliInvocation | null | undefined;
|
|
89
|
+
|
|
90
|
+
export function getAzCliInvocation(): AzCliInvocation | null {
|
|
91
|
+
if (cachedInvocation === undefined) cachedInvocation = resolveAzCliInvocation();
|
|
92
|
+
return cachedInvocation;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resetAzCliInvocationCache(): void {
|
|
96
|
+
cachedInvocation = undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Test hook: pin the resolved invocation (null = not found). */
|
|
100
|
+
export function setAzCliInvocationForTesting(invocation: AzCliInvocation | null | undefined): void {
|
|
101
|
+
cachedInvocation = invocation;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function runAzCli(
|
|
105
|
+
args: string[],
|
|
106
|
+
options: { timeout?: number; invocation?: AzCliInvocation | null } = {}
|
|
107
|
+
): AzCliResult {
|
|
108
|
+
const invocation = options.invocation === undefined ? getAzCliInvocation() : options.invocation;
|
|
109
|
+
if (!invocation) {
|
|
110
|
+
return { status: null, stdout: "", stderr: "", error: Object.assign(new Error("az was not found on PATH"), { code: "ENOENT" }) };
|
|
111
|
+
}
|
|
112
|
+
const spawnOptions: SpawnSyncOptions = {
|
|
113
|
+
shell: false,
|
|
114
|
+
encoding: "utf-8",
|
|
115
|
+
timeout: options.timeout ?? 300_000,
|
|
116
|
+
env: invocation.env ? { ...process.env, ...invocation.env } : process.env,
|
|
117
|
+
};
|
|
118
|
+
let result;
|
|
119
|
+
try {
|
|
120
|
+
result = invocation.batchScript
|
|
121
|
+
? spawnSync(invocation.command, ["/d", "/s", "/v:off", "/c", buildBatchCommandLine(invocation.batchScript, args)], {
|
|
122
|
+
...spawnOptions,
|
|
123
|
+
windowsVerbatimArguments: true,
|
|
124
|
+
})
|
|
125
|
+
: spawnSync(invocation.command, [...invocation.prefixArgs, ...args], spawnOptions);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return { status: null, stdout: "", stderr: "", error: error instanceof Error ? error : new Error(String(error)) };
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
status: result.status,
|
|
131
|
+
stdout: String(result.stdout ?? ""),
|
|
132
|
+
stderr: String(result.stderr ?? ""),
|
|
133
|
+
...(result.error ? { error: result.error } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function checkAzCliAvailability(): AzCliAvailability {
|
|
138
|
+
const invocation = getAzCliInvocation();
|
|
139
|
+
if (!invocation) {
|
|
140
|
+
return { available: false, reason: "not-found", detail: "az was not found on PATH." };
|
|
141
|
+
}
|
|
142
|
+
const result = runAzCli(["--version"], { timeout: 60_000, invocation });
|
|
143
|
+
if (result.status === 0) return { available: true, invocation };
|
|
144
|
+
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
|
145
|
+
if (errorCode === "ENOENT") {
|
|
146
|
+
return { available: false, reason: "not-found", detail: "az was not found on PATH." };
|
|
147
|
+
}
|
|
148
|
+
const output = `${result.stderr || result.stdout}`.trim().split(/\r?\n/).slice(-3).join(" ");
|
|
149
|
+
return {
|
|
150
|
+
available: false,
|
|
151
|
+
reason: "not-runnable",
|
|
152
|
+
detail: `${invocation.source} could not run "az --version" (${result.error?.message ?? `exit ${result.status}`})${output ? `: ${output}` : ""}`,
|
|
153
|
+
};
|
|
154
|
+
}
|