@mandujs/mcp 0.32.3 → 0.33.0
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 +2 -2
- package/src/tools/deploy-plan.ts +330 -0
- package/src/tools/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@mandujs/core": "^0.
|
|
37
|
+
"@mandujs/core": "^0.47.0",
|
|
38
38
|
"@mandujs/ate": "^0.25.1",
|
|
39
39
|
"@mandujs/skills": "^0.19.0",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tools — deploy intent inspection & compile.
|
|
3
|
+
*
|
|
4
|
+
* Issue #250 — Phase 1 follow-up.
|
|
5
|
+
*
|
|
6
|
+
* Two read-mostly tools agents use to drive Mandu's deploy pipeline:
|
|
7
|
+
*
|
|
8
|
+
* - `mandu.deploy.plan` — run the heuristic inferer over the routes
|
|
9
|
+
* manifest and the existing intent cache. Returns the next cache
|
|
10
|
+
* + a per-route diff. Default is read-only (`apply: false`); pass
|
|
11
|
+
* `apply: true` to atomically write `.mandu/deploy.intent.json`.
|
|
12
|
+
*
|
|
13
|
+
* - `mandu.deploy.compile` — compile the manifest + cache into a
|
|
14
|
+
* concrete `vercel.json` (other targets land in subsequent
|
|
15
|
+
* phases). Returns the config object, the per-route summary, and
|
|
16
|
+
* compile warnings. Read-only — never writes vercel.json.
|
|
17
|
+
*
|
|
18
|
+
* The two tools share the same `@mandujs/core/deploy` engine the CLI
|
|
19
|
+
* uses, so agents and humans see identical results. Going through the
|
|
20
|
+
* core API rather than spawning the CLI keeps the response structured
|
|
21
|
+
* and avoids a child-process roundtrip.
|
|
22
|
+
*
|
|
23
|
+
* @see packages/cli/src/commands/deploy/plan.ts — interactive CLI
|
|
24
|
+
* @see packages/cli/src/commands/deploy/adapters/vercel.ts — adapter
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
28
|
+
import {
|
|
29
|
+
compileVercelJson,
|
|
30
|
+
emptyDeployIntentCache,
|
|
31
|
+
isStaticIntentValidFor,
|
|
32
|
+
loadDeployIntentCache,
|
|
33
|
+
planDeploy,
|
|
34
|
+
saveDeployIntentCache,
|
|
35
|
+
VercelCompileError,
|
|
36
|
+
buildDeployInferenceContext,
|
|
37
|
+
type DeployIntent,
|
|
38
|
+
type DeployIntentCache,
|
|
39
|
+
type PlanDiffEntry,
|
|
40
|
+
type VercelCompileResult,
|
|
41
|
+
} from "@mandujs/core/deploy";
|
|
42
|
+
import { generateManifest, loadManifest, type RoutesManifest } from "@mandujs/core";
|
|
43
|
+
import path from "node:path";
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the routes manifest for the project.
|
|
47
|
+
*
|
|
48
|
+
* Prefer the on-disk `.mandu/routes.manifest.json` (fast, written by
|
|
49
|
+
* `mandu build` / `mandu dev`). When that's missing, scan `app/`
|
|
50
|
+
* directly via `generateManifest()` so the tool keeps working in a
|
|
51
|
+
* fresh checkout. Returns `{ manifest, error }` — the error surface
|
|
52
|
+
* is `null` when at least one path succeeded, otherwise it carries
|
|
53
|
+
* the most specific reason so the MCP response can guide the agent.
|
|
54
|
+
*/
|
|
55
|
+
async function resolveManifestForTooling(
|
|
56
|
+
projectRoot: string,
|
|
57
|
+
): Promise<{ manifest: RoutesManifest | null; error: string | null }> {
|
|
58
|
+
const manifestPath = path.join(projectRoot, ".mandu", "routes.manifest.json");
|
|
59
|
+
try {
|
|
60
|
+
const result = await loadManifest(manifestPath);
|
|
61
|
+
if (result.success && result.data) {
|
|
62
|
+
return { manifest: result.data, error: null };
|
|
63
|
+
}
|
|
64
|
+
// Fall through to fs scan if the on-disk manifest is missing/malformed.
|
|
65
|
+
} catch {
|
|
66
|
+
// Fall through.
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const result = await generateManifest(projectRoot);
|
|
70
|
+
return { manifest: result.manifest, error: null };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
manifest: null,
|
|
74
|
+
error: err instanceof Error ? err.message : String(err),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── deploy.plan ──────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
interface DeployPlanInput {
|
|
82
|
+
/**
|
|
83
|
+
* When true, write the next cache to `.mandu/deploy.intent.json`.
|
|
84
|
+
* Default false — agents should review the diff and call again with
|
|
85
|
+
* `apply: true` only after the human sign-off.
|
|
86
|
+
*/
|
|
87
|
+
apply?: boolean;
|
|
88
|
+
/** Force re-inference even on unchanged source hashes. */
|
|
89
|
+
reinfer?: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface DeployPlanResultPayload {
|
|
93
|
+
/** ISO timestamp the inferer wrote into the next cache. */
|
|
94
|
+
generated_at: string;
|
|
95
|
+
/** Identifier of the inferer (`heuristic` for now; brain in M4). */
|
|
96
|
+
brain_model: string;
|
|
97
|
+
/** Per-route diff suitable for rendering as a table. */
|
|
98
|
+
diff: Array<{
|
|
99
|
+
route_id: string;
|
|
100
|
+
pattern: string;
|
|
101
|
+
kind: PlanDiffEntry["kind"];
|
|
102
|
+
runtime?: DeployIntent["runtime"];
|
|
103
|
+
previous_runtime?: DeployIntent["runtime"];
|
|
104
|
+
rationale?: string;
|
|
105
|
+
source?: "explicit" | "inferred";
|
|
106
|
+
}>;
|
|
107
|
+
/** Validation warnings (intent vs route shape). */
|
|
108
|
+
warnings: string[];
|
|
109
|
+
/** Total intents in the next cache. */
|
|
110
|
+
intent_count: number;
|
|
111
|
+
/** Whether the cache file was actually written. */
|
|
112
|
+
applied: boolean;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function deployPlanHandler(
|
|
116
|
+
projectRoot: string,
|
|
117
|
+
input: DeployPlanInput,
|
|
118
|
+
): Promise<DeployPlanResultPayload | { error: string; hint?: string }> {
|
|
119
|
+
const apply = input.apply === true;
|
|
120
|
+
const reinfer = input.reinfer === true;
|
|
121
|
+
|
|
122
|
+
const { manifest, error: manifestError } = await resolveManifestForTooling(projectRoot);
|
|
123
|
+
if (!manifest) {
|
|
124
|
+
return {
|
|
125
|
+
error: `Routes manifest could not be resolved: ${manifestError ?? "unknown reason"}`,
|
|
126
|
+
hint: "Run `mandu build` (or ensure `app/` exists with at least one route) before calling deploy tools.",
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
let previous: DeployIntentCache;
|
|
131
|
+
try {
|
|
132
|
+
previous = await loadDeployIntentCache(projectRoot);
|
|
133
|
+
} catch (err) {
|
|
134
|
+
return {
|
|
135
|
+
error: `Failed to load .mandu/deploy.intent.json: ${err instanceof Error ? err.message : String(err)}`,
|
|
136
|
+
hint: "Delete or restore the file, then call `mandu.deploy.plan` again.",
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const result = await planDeploy({
|
|
141
|
+
rootDir: projectRoot,
|
|
142
|
+
manifest,
|
|
143
|
+
previous,
|
|
144
|
+
reinfer,
|
|
145
|
+
brainModel: "heuristic",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Validate every entry against route shape so agents see the same
|
|
149
|
+
// warnings the CLI surfaces.
|
|
150
|
+
const warnings: string[] = [];
|
|
151
|
+
for (const route of manifest.routes) {
|
|
152
|
+
const entry = result.cache.intents[route.id];
|
|
153
|
+
if (!entry) continue;
|
|
154
|
+
const ctx = await buildDeployInferenceContext(projectRoot, route);
|
|
155
|
+
const validation = isStaticIntentValidFor(entry.intent, {
|
|
156
|
+
isDynamic: ctx.isDynamic,
|
|
157
|
+
hasGenerateStaticParams: ctx.hasGenerateStaticParams,
|
|
158
|
+
kind: ctx.kind,
|
|
159
|
+
});
|
|
160
|
+
if (!validation.ok) {
|
|
161
|
+
warnings.push(`${route.id} (${route.pattern}): ${validation.reason}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (apply) {
|
|
166
|
+
await saveDeployIntentCache(projectRoot, result.cache);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
generated_at: result.cache.generatedAt,
|
|
171
|
+
brain_model: result.cache.brainModel,
|
|
172
|
+
diff: result.diff.map((d) => ({
|
|
173
|
+
route_id: d.routeId,
|
|
174
|
+
pattern: d.pattern,
|
|
175
|
+
kind: d.kind,
|
|
176
|
+
runtime: d.next?.intent.runtime,
|
|
177
|
+
previous_runtime: d.previous?.intent.runtime,
|
|
178
|
+
rationale: d.next?.rationale,
|
|
179
|
+
source: d.next?.source,
|
|
180
|
+
})),
|
|
181
|
+
warnings,
|
|
182
|
+
intent_count: Object.keys(result.cache.intents).length,
|
|
183
|
+
applied: apply,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── deploy.compile ───────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
interface DeployCompileInput {
|
|
190
|
+
/** Adapter target. Only `vercel` is supported in M3. */
|
|
191
|
+
target?: "vercel";
|
|
192
|
+
/** Project name override (Mandu bookkeeping; not emitted into output). */
|
|
193
|
+
project_name?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface DeployCompileResultPayload {
|
|
197
|
+
target: "vercel";
|
|
198
|
+
/** Concrete vercel.json contents the adapter would write. */
|
|
199
|
+
config: VercelCompileResult["config"];
|
|
200
|
+
/** Compile warnings (e.g. #248 runtime gaps). */
|
|
201
|
+
warnings: string[];
|
|
202
|
+
/** Per-route summary the CLI prints in dry-run. */
|
|
203
|
+
per_route: VercelCompileResult["perRoute"];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function deployCompileHandler(
|
|
207
|
+
projectRoot: string,
|
|
208
|
+
input: DeployCompileInput,
|
|
209
|
+
): Promise<DeployCompileResultPayload | { error: string; hint?: string; routes?: ReadonlyArray<{ route_id: string; reason: string }> }> {
|
|
210
|
+
const target = input.target ?? "vercel";
|
|
211
|
+
if (target !== "vercel") {
|
|
212
|
+
return {
|
|
213
|
+
error: `Target "${target}" not supported yet. Phase 1 ships the Vercel compiler only.`,
|
|
214
|
+
hint: "Pass target=\"vercel\" or wait for the Fly compiler in Phase 2.",
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const { manifest, error: manifestError } = await resolveManifestForTooling(projectRoot);
|
|
219
|
+
if (!manifest) {
|
|
220
|
+
return {
|
|
221
|
+
error: `Routes manifest could not be resolved: ${manifestError ?? "unknown reason"}`,
|
|
222
|
+
hint: "Run `mandu build` (or ensure `app/` exists with at least one route) before calling deploy tools.",
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let cache: DeployIntentCache;
|
|
227
|
+
try {
|
|
228
|
+
cache = await loadDeployIntentCache(projectRoot);
|
|
229
|
+
} catch (err) {
|
|
230
|
+
return {
|
|
231
|
+
error: `Failed to load .mandu/deploy.intent.json: ${err instanceof Error ? err.message : String(err)}`,
|
|
232
|
+
hint: "Run `mandu.deploy.plan` (with apply=true) first, then re-run `mandu.deploy.compile`.",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (Object.keys(cache.intents).length === 0) {
|
|
236
|
+
cache = emptyDeployIntentCache();
|
|
237
|
+
return {
|
|
238
|
+
error: "Intent cache is empty",
|
|
239
|
+
hint: "Run `mandu.deploy.plan` with apply=true to populate .mandu/deploy.intent.json first.",
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const projectName = input.project_name ?? deriveProjectName(projectRoot);
|
|
244
|
+
try {
|
|
245
|
+
const result = compileVercelJson(manifest, cache, { projectName });
|
|
246
|
+
return {
|
|
247
|
+
target: "vercel",
|
|
248
|
+
config: result.config,
|
|
249
|
+
warnings: result.warnings,
|
|
250
|
+
per_route: result.perRoute,
|
|
251
|
+
};
|
|
252
|
+
} catch (err) {
|
|
253
|
+
if (err instanceof VercelCompileError) {
|
|
254
|
+
return {
|
|
255
|
+
error: "Vercel compile failed",
|
|
256
|
+
hint: "Resolve the per-route reasons below — usually by re-running `mandu.deploy.plan` with apply=true after editing the offending route.",
|
|
257
|
+
routes: err.routes.map((r) => ({ route_id: r.routeId, reason: r.reason })),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
throw err;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function deriveProjectName(projectRoot: string): string {
|
|
265
|
+
// Mirror the CLI's default — last path segment, sanitised.
|
|
266
|
+
const last = projectRoot.split(/[\\/]/).filter(Boolean).pop() ?? "mandu-project";
|
|
267
|
+
return last.toLowerCase().replace(/[^a-z0-9-_]/g, "-").slice(0, 100);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── MCP definitions + handlers ───────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
export const deployPlanToolDefinitions: Tool[] = [
|
|
273
|
+
{
|
|
274
|
+
name: "mandu.deploy.plan",
|
|
275
|
+
description:
|
|
276
|
+
"Infer DeployIntent for every route via the offline heuristic and (optionally) write `.mandu/deploy.intent.json`. Returns the per-route diff plus validation warnings. Default `apply: false` is read-only — agents should review before persisting. The `reinfer` flag forces re-inference even on unchanged source hashes.",
|
|
277
|
+
annotations: {
|
|
278
|
+
readOnlyHint: false,
|
|
279
|
+
},
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {
|
|
283
|
+
apply: {
|
|
284
|
+
type: "boolean",
|
|
285
|
+
description:
|
|
286
|
+
"When true, atomically write the next cache to `.mandu/deploy.intent.json`. Default false (preview-only).",
|
|
287
|
+
},
|
|
288
|
+
reinfer: {
|
|
289
|
+
type: "boolean",
|
|
290
|
+
description: "Force re-inference even on unchanged source hashes.",
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
required: [],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
name: "mandu.deploy.compile",
|
|
298
|
+
description:
|
|
299
|
+
"Compile the routes manifest + `.mandu/deploy.intent.json` into a concrete `vercel.json`. Returns the config object, per-route summary, and compile warnings (e.g. runtime gaps from issue #248). Read-only — does not write vercel.json. Phase 1 supports `target: \"vercel\"` only.",
|
|
300
|
+
annotations: {
|
|
301
|
+
readOnlyHint: true,
|
|
302
|
+
},
|
|
303
|
+
inputSchema: {
|
|
304
|
+
type: "object",
|
|
305
|
+
properties: {
|
|
306
|
+
target: {
|
|
307
|
+
type: "string",
|
|
308
|
+
enum: ["vercel"],
|
|
309
|
+
description: "Adapter target. Phase 1 supports vercel only.",
|
|
310
|
+
},
|
|
311
|
+
project_name: {
|
|
312
|
+
type: "string",
|
|
313
|
+
description:
|
|
314
|
+
"Project name override (Mandu bookkeeping; never emitted into vercel.json). Defaults to the directory name.",
|
|
315
|
+
},
|
|
316
|
+
},
|
|
317
|
+
required: [],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
export function deployPlanTools(projectRoot: string) {
|
|
323
|
+
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
324
|
+
"mandu.deploy.plan": async (args) =>
|
|
325
|
+
deployPlanHandler(projectRoot, args as DeployPlanInput),
|
|
326
|
+
"mandu.deploy.compile": async (args) =>
|
|
327
|
+
deployCompileHandler(projectRoot, args as DeployCompileInput),
|
|
328
|
+
};
|
|
329
|
+
return handlers;
|
|
330
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -63,6 +63,7 @@ export { compositeTools, compositeToolDefinitions } from "./composite.js";
|
|
|
63
63
|
// Phase 14.3 — AI/agent loop-closure tool suite
|
|
64
64
|
export { runTestsTools, runTestsToolDefinitions } from "./run-tests.js";
|
|
65
65
|
export { deployPreviewTools, deployPreviewToolDefinitions } from "./deploy-preview.js";
|
|
66
|
+
export { deployPlanTools, deployPlanToolDefinitions } from "./deploy-plan.js";
|
|
66
67
|
export { aiBriefTools, aiBriefToolDefinitions } from "./ai-brief.js";
|
|
67
68
|
export { loopCloseTools, loopCloseToolDefinitions } from "./loop-close.js";
|
|
68
69
|
// #243 — docs search/get for agents grounding answers in real framework docs
|
|
@@ -137,6 +138,7 @@ import { kitchenTools, kitchenToolDefinitions } from "./kitchen.js";
|
|
|
137
138
|
import { compositeTools, compositeToolDefinitions } from "./composite.js";
|
|
138
139
|
import { runTestsTools, runTestsToolDefinitions } from "./run-tests.js";
|
|
139
140
|
import { deployPreviewTools, deployPreviewToolDefinitions } from "./deploy-preview.js";
|
|
141
|
+
import { deployPlanTools, deployPlanToolDefinitions } from "./deploy-plan.js";
|
|
140
142
|
import { aiBriefTools, aiBriefToolDefinitions } from "./ai-brief.js";
|
|
141
143
|
import { loopCloseTools, loopCloseToolDefinitions } from "./loop-close.js";
|
|
142
144
|
import { docsTools, docsToolDefinitions } from "./docs.js";
|
|
@@ -254,6 +256,8 @@ const TOOL_MODULES: ToolModule[] = [
|
|
|
254
256
|
// Phase 14.3 — AI/agent loop-closure suite
|
|
255
257
|
{ category: "run-tests", definitions: runTestsToolDefinitions, handlers: runTestsTools },
|
|
256
258
|
{ category: "deploy-preview", definitions: deployPreviewToolDefinitions, handlers: deployPreviewTools },
|
|
259
|
+
// #250 — DeployIntent inspection / compile (Phase 1)
|
|
260
|
+
{ category: "deploy-plan", definitions: deployPlanToolDefinitions, handlers: deployPlanTools },
|
|
257
261
|
{ category: "ai-brief", definitions: aiBriefToolDefinitions, handlers: aiBriefTools },
|
|
258
262
|
{ category: "loop-close", definitions: loopCloseToolDefinitions, handlers: loopCloseTools },
|
|
259
263
|
{ category: "docs", definitions: docsToolDefinitions, handlers: docsTools },
|