@mandujs/mcp 0.37.0 â 0.37.2
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/README.md +3 -4
- package/package.json +4 -4
- package/src/prompts.ts +4 -4
- package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +3 -3
- package/src/tools/ate.ts +37 -24
- package/src/tools/composite.ts +13 -11
- package/src/tools/index.ts +61 -30
- package/src/tools/kitchen.ts +72 -32
- package/src/tools/slot-validation.ts +19 -125
package/README.md
CHANGED
|
@@ -136,10 +136,9 @@ bunx @mandujs/mcp --root /path/to/project
|
|
|
136
136
|
|
|
137
137
|
### Semantic Slots (RFC-001) đ
|
|
138
138
|
|
|
139
|
-
| Tool | Description |
|
|
140
|
-
|------|-------------|
|
|
141
|
-
| `
|
|
142
|
-
| `mandu_validate_slots` | Batch validate multiple slots |
|
|
139
|
+
| Tool | Description |
|
|
140
|
+
|------|-------------|
|
|
141
|
+
| `mandu_get_slot_constraints` | Get recommended slot constraint presets |
|
|
143
142
|
|
|
144
143
|
### Architecture Negotiation (RFC-001) đ
|
|
145
144
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.2",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@mandujs/core": "^0.54.
|
|
38
|
-
"@mandujs/ate": "^
|
|
39
|
-
"@mandujs/skills": "^0.20.
|
|
37
|
+
"@mandujs/core": "^0.54.2",
|
|
38
|
+
"@mandujs/ate": "^0.26.1",
|
|
39
|
+
"@mandujs/skills": "^0.20.1",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
41
41
|
},
|
|
42
42
|
"engines": {
|
package/src/prompts.ts
CHANGED
|
@@ -48,10 +48,10 @@ Follow these steps using Mandu MCP tools:
|
|
|
48
48
|
1. Read current route manifest: Resource mandu://routes
|
|
49
49
|
2. Read project config: Resource mandu://config
|
|
50
50
|
3. Negotiate the feature spec: Tool mandu.negotiate
|
|
51
|
-
4. Generate scaffold: Tool mandu.negotiate.scaffold
|
|
52
|
-
5. If client interactivity needed, create island: Tool mandu_create_island
|
|
53
|
-
Use
|
|
54
|
-
6. If data requirements exist, create slot: Tool mandu_create_slot
|
|
51
|
+
4. Generate scaffold: Tool mandu.negotiate.scaffold
|
|
52
|
+
5. If client interactivity needed, create island: Tool mandu_create_island
|
|
53
|
+
Use @mandujs/core/client with wrapComponent(Component) or island({ setup, render })
|
|
54
|
+
6. If data requirements exist, create slot: Tool mandu_create_slot
|
|
55
55
|
Slots are server-side data loaders that run before render
|
|
56
56
|
7. If API is exposed, define contract: Tool mandu_create_contract
|
|
57
57
|
Contracts are Zod schemas for validation and OpenAPI generation
|
|
@@ -138,8 +138,8 @@ export function createServerSupabase() {
|
|
|
138
138
|
Mandu ė client island ėė Realtime ęĩŦë
:
|
|
139
139
|
|
|
140
140
|
```tsx
|
|
141
|
-
// app/messages/MessagesIsland.client.tsx
|
|
142
|
-
import {
|
|
141
|
+
// app/messages/MessagesIsland.client.tsx
|
|
142
|
+
import { wrapComponent } from "@mandujs/core/client";
|
|
143
143
|
import { useEffect, useState } from "react";
|
|
144
144
|
import { supabase } from "@/lib/supabase";
|
|
145
145
|
|
|
@@ -174,7 +174,7 @@ function Messages() {
|
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
export default
|
|
177
|
+
export default wrapComponent(Messages);
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
### 3.3 Storage
|
package/src/tools/ate.ts
CHANGED
|
@@ -105,7 +105,11 @@ export const ateToolDefinitions: Tool[] = [
|
|
|
105
105
|
type: "string",
|
|
106
106
|
description: "Dev server URL (default: http://localhost:3333). Must match the running mandu dev server.",
|
|
107
107
|
},
|
|
108
|
-
ci: { type: "boolean", description: "CI mode: stricter timeouts, no interactive prompts" },
|
|
108
|
+
ci: { type: "boolean", description: "CI mode: stricter timeouts, no interactive prompts" },
|
|
109
|
+
timeoutMs: {
|
|
110
|
+
type: "number",
|
|
111
|
+
description: "Hard timeout for the whole Playwright process in milliseconds (default: 600000).",
|
|
112
|
+
},
|
|
109
113
|
headless: { type: "boolean", description: "Run browsers headlessly (default: true)" },
|
|
110
114
|
browsers: {
|
|
111
115
|
type: "array",
|
|
@@ -260,7 +264,11 @@ export const ateToolDefinitions: Tool[] = [
|
|
|
260
264
|
enum: ["L0", "L1", "L2", "L3"],
|
|
261
265
|
description: "Assertion depth: L0=smoke, L1=HTTP status, L2=contract schema, L3=full behavioral",
|
|
262
266
|
},
|
|
263
|
-
ci: { type: "boolean", description: "CI mode: stricter timeouts" },
|
|
267
|
+
ci: { type: "boolean", description: "CI mode: stricter timeouts" },
|
|
268
|
+
timeoutMs: {
|
|
269
|
+
type: "number",
|
|
270
|
+
description: "Hard timeout for the whole Playwright process in milliseconds (default: 600000).",
|
|
271
|
+
},
|
|
264
272
|
useImpactAnalysis: {
|
|
265
273
|
type: "boolean",
|
|
266
274
|
description: "Run impact analysis first and only test changed routes (faster in CI)",
|
|
@@ -378,7 +386,7 @@ export function ateTools(projectRoot: string, server?: Server) {
|
|
|
378
386
|
return await ateRun(input);
|
|
379
387
|
} catch (err) {
|
|
380
388
|
const message = err instanceof Error ? err.message : String(err);
|
|
381
|
-
const isTimeout = /timed out
|
|
389
|
+
const isTimeout = /timeout|timed out|íėėė/i.test(message);
|
|
382
390
|
const snap = tracker.snapshot();
|
|
383
391
|
const partial: PartialRunResults = {
|
|
384
392
|
runId: snap.runId ?? `unknown-${Date.now()}`,
|
|
@@ -439,19 +447,21 @@ export function ateTools(projectRoot: string, server?: Server) {
|
|
|
439
447
|
browsers,
|
|
440
448
|
onlyFiles,
|
|
441
449
|
onlyRoutes,
|
|
442
|
-
grep,
|
|
443
|
-
|
|
444
|
-
|
|
450
|
+
grep,
|
|
451
|
+
timeoutMs,
|
|
452
|
+
progressToken,
|
|
453
|
+
} = args as {
|
|
445
454
|
repoRoot: string;
|
|
446
455
|
baseURL?: string;
|
|
447
456
|
ci?: boolean;
|
|
448
457
|
headless?: boolean;
|
|
449
458
|
browsers?: ("chromium" | "firefox" | "webkit")[];
|
|
450
459
|
onlyFiles?: string[];
|
|
451
|
-
onlyRoutes?: string[];
|
|
452
|
-
grep?: string;
|
|
453
|
-
|
|
454
|
-
|
|
460
|
+
onlyRoutes?: string[];
|
|
461
|
+
grep?: string;
|
|
462
|
+
timeoutMs?: number;
|
|
463
|
+
progressToken?: string | number;
|
|
464
|
+
};
|
|
455
465
|
return await runWithObservability(
|
|
456
466
|
{
|
|
457
467
|
repoRoot,
|
|
@@ -459,10 +469,11 @@ export function ateTools(projectRoot: string, server?: Server) {
|
|
|
459
469
|
ci,
|
|
460
470
|
headless,
|
|
461
471
|
browsers,
|
|
462
|
-
onlyFiles,
|
|
463
|
-
onlyRoutes,
|
|
464
|
-
grep,
|
|
465
|
-
|
|
472
|
+
onlyFiles,
|
|
473
|
+
onlyRoutes,
|
|
474
|
+
grep,
|
|
475
|
+
timeoutMs,
|
|
476
|
+
},
|
|
466
477
|
{ progressToken },
|
|
467
478
|
);
|
|
468
479
|
},
|
|
@@ -518,9 +529,10 @@ export function ateTools(projectRoot: string, server?: Server) {
|
|
|
518
529
|
},
|
|
519
530
|
"mandu.ate.auto_pipeline": async (args: Record<string, unknown>) => {
|
|
520
531
|
const {
|
|
521
|
-
repoRoot, baseURL, oracleLevel, ci, useImpactAnalysis,
|
|
522
|
-
base, head, autoHeal, tsconfigPath, routeGlobs, buildSalt,
|
|
523
|
-
|
|
532
|
+
repoRoot, baseURL, oracleLevel, ci, useImpactAnalysis,
|
|
533
|
+
base, head, autoHeal, tsconfigPath, routeGlobs, buildSalt,
|
|
534
|
+
timeoutMs,
|
|
535
|
+
} = args as {
|
|
524
536
|
repoRoot: string;
|
|
525
537
|
baseURL?: string;
|
|
526
538
|
oracleLevel?: OracleLevel;
|
|
@@ -530,13 +542,14 @@ export function ateTools(projectRoot: string, server?: Server) {
|
|
|
530
542
|
head?: string;
|
|
531
543
|
autoHeal?: boolean;
|
|
532
544
|
tsconfigPath?: string;
|
|
533
|
-
routeGlobs?: string[];
|
|
534
|
-
buildSalt?: string;
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
545
|
+
routeGlobs?: string[];
|
|
546
|
+
buildSalt?: string;
|
|
547
|
+
timeoutMs?: number;
|
|
548
|
+
};
|
|
549
|
+
return await runFullPipeline({
|
|
550
|
+
repoRoot, baseURL, oracleLevel, ci, useImpactAnalysis,
|
|
551
|
+
base, head, autoHeal, tsconfigPath, routeGlobs, buildSalt, timeoutMs,
|
|
552
|
+
});
|
|
540
553
|
},
|
|
541
554
|
"mandu.ate.feedback": async (args: Record<string, unknown>) => {
|
|
542
555
|
const { repoRoot, runId, autoApply } = args as {
|
package/src/tools/composite.ts
CHANGED
|
@@ -60,10 +60,10 @@ export const compositeToolDefinitions: Tool[] = [
|
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
62
|
{
|
|
63
|
-
name: "mandu.island.add",
|
|
64
|
-
description:
|
|
65
|
-
"Create an island component with correct @mandujs/core/client imports and hydration strategy. " +
|
|
66
|
-
"Generates a .island.tsx file in app/{route}/ with the
|
|
63
|
+
name: "mandu.island.add",
|
|
64
|
+
description:
|
|
65
|
+
"Create an island component with correct @mandujs/core/client imports and hydration strategy. " +
|
|
66
|
+
"Generates a .island.tsx file in app/{route}/ with the wrapComponent() island definition.",
|
|
67
67
|
annotations: {
|
|
68
68
|
destructiveHint: true,
|
|
69
69
|
readOnlyHint: false,
|
|
@@ -474,10 +474,10 @@ function generateMiddlewareSource(preset: string, options: Record<string, string
|
|
|
474
474
|
return templates[preset] ?? templates.default;
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
function generateIslandSource(name: string, strategy: string): string {
|
|
478
|
-
return `"use client";
|
|
479
|
-
import {
|
|
480
|
-
import { useState } from "react";
|
|
477
|
+
function generateIslandSource(name: string, strategy: string): string {
|
|
478
|
+
return `"use client";
|
|
479
|
+
import { wrapComponent } from "@mandujs/core/client";
|
|
480
|
+
import { useState } from "react";
|
|
481
481
|
|
|
482
482
|
interface ${name}Props {
|
|
483
483
|
[key: string]: unknown;
|
|
@@ -493,6 +493,8 @@ function ${name}Inner(props: ${name}Props) {
|
|
|
493
493
|
);
|
|
494
494
|
}
|
|
495
495
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
496
|
+
// Route hydration priority should be set on the page/manifest route.
|
|
497
|
+
// Suggested priority for this island: "${strategy}".
|
|
498
|
+
export default wrapComponent(${name}Inner);
|
|
499
|
+
`;
|
|
500
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -162,10 +162,10 @@ import {
|
|
|
162
162
|
/**
|
|
163
163
|
* ëęĩŦ ëǍë ė ëŗ´
|
|
164
164
|
*/
|
|
165
|
-
interface ToolModule {
|
|
166
|
-
category: string;
|
|
167
|
-
definitions: Tool[];
|
|
168
|
-
handlers: (
|
|
165
|
+
export interface ToolModule {
|
|
166
|
+
category: string;
|
|
167
|
+
definitions: Tool[];
|
|
168
|
+
handlers: (
|
|
169
169
|
projectRoot: string,
|
|
170
170
|
server?: Server,
|
|
171
171
|
monitor?: ActivityMonitor
|
|
@@ -188,7 +188,7 @@ interface ToolModule {
|
|
|
188
188
|
/**
|
|
189
189
|
* ëší¸ė¸ ëęĩŦ ëǍë ëĒŠëĄ
|
|
190
190
|
*/
|
|
191
|
-
const TOOL_MODULES: ToolModule[] = [
|
|
191
|
+
export const TOOL_MODULES: ToolModule[] = [
|
|
192
192
|
{ category: "spec", definitions: specToolDefinitions, handlers: specTools },
|
|
193
193
|
{ category: "generate", definitions: generateToolDefinitions, handlers: generateTools },
|
|
194
194
|
{ category: "transaction", definitions: transactionToolDefinitions, handlers: transactionTools },
|
|
@@ -200,18 +200,18 @@ const TOOL_MODULES: ToolModule[] = [
|
|
|
200
200
|
{ category: "slot", definitions: slotToolDefinitions, handlers: slotTools },
|
|
201
201
|
{ category: "hydration", definitions: hydrationToolDefinitions, handlers: hydrationTools },
|
|
202
202
|
{ category: "contract", definitions: contractToolDefinitions, handlers: contractTools },
|
|
203
|
-
{ category: "brain", definitions: brainToolDefinitions, handlers: brainTools
|
|
203
|
+
{ category: "brain", definitions: brainToolDefinitions, handlers: brainTools, requiresServer: true },
|
|
204
204
|
{ category: "runtime", definitions: runtimeToolDefinitions, handlers: runtimeTools },
|
|
205
205
|
{ category: "seo", definitions: seoToolDefinitions, handlers: seoTools },
|
|
206
|
-
{ category: "project", definitions: projectToolDefinitions, handlers: projectTools
|
|
206
|
+
{ category: "project", definitions: projectToolDefinitions, handlers: projectTools, requiresServer: true },
|
|
207
207
|
// ate + ate-run accept an optional Server so notifications/progress
|
|
208
208
|
// can flow (issue #238). `acceptsServer: true` forwards the server
|
|
209
209
|
// when available but still registers when it isn't â callers that
|
|
210
210
|
// boot without an MCP transport get progress no-oped silently.
|
|
211
|
-
{ category: "ate", definitions: ateToolDefinitions, handlers: ateTools
|
|
212
|
-
{ category: "ate-phase5", definitions: atePhase5ToolDefinitions, handlers: createAtePhase5Handlers
|
|
211
|
+
{ category: "ate", definitions: ateToolDefinitions, handlers: ateTools, acceptsServer: true },
|
|
212
|
+
{ category: "ate-phase5", definitions: atePhase5ToolDefinitions, handlers: createAtePhase5Handlers },
|
|
213
213
|
{ category: "ate-context", definitions: ateContextToolDefinitions, handlers: ateContextTools },
|
|
214
|
-
{ category: "ate-run", definitions: ateRunToolDefinitions, handlers: ateRunTools
|
|
214
|
+
{ category: "ate-run", definitions: ateRunToolDefinitions, handlers: ateRunTools, acceptsServer: true },
|
|
215
215
|
{ category: "ate-flakes", definitions: ateFlakesToolDefinitions, handlers: ateFlakesTools },
|
|
216
216
|
{ category: "ate-prompt", definitions: atePromptToolDefinitions, handlers: atePromptTools },
|
|
217
217
|
{ category: "ate-exemplar", definitions: ateExemplarToolDefinitions, handlers: ateExemplarTools },
|
|
@@ -282,10 +282,41 @@ const TOOL_MODULES: ToolModule[] = [
|
|
|
282
282
|
definitions: extractContractToolDefinitions,
|
|
283
283
|
handlers: extractContractTools,
|
|
284
284
|
},
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
export function validateBuiltinToolModules(
|
|
288
|
+
modules: readonly ToolModule[] = TOOL_MODULES
|
|
289
|
+
): string[] {
|
|
290
|
+
const issues: string[] = [];
|
|
291
|
+
const categories = new Set<string>();
|
|
292
|
+
const toolNames = new Map<string, string>();
|
|
293
|
+
|
|
294
|
+
for (const module of modules) {
|
|
295
|
+
if (categories.has(module.category)) {
|
|
296
|
+
issues.push(`duplicate tool category: ${module.category}`);
|
|
297
|
+
}
|
|
298
|
+
categories.add(module.category);
|
|
299
|
+
|
|
300
|
+
if (module.definitions.length === 0) {
|
|
301
|
+
issues.push(`tool category has no definitions: ${module.category}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const definition of module.definitions) {
|
|
305
|
+
const previousCategory = toolNames.get(definition.name);
|
|
306
|
+
if (previousCategory) {
|
|
307
|
+
issues.push(
|
|
308
|
+
`duplicate tool definition: ${definition.name} in ${previousCategory} and ${module.category}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
toolNames.set(definition.name, module.category);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return issues;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* ëší¸ė¸ ëęĩŦë¤ė ë ė§ė¤í¸ëĻŦė ëąëĄ
|
|
289
320
|
*
|
|
290
321
|
* @param projectRoot - íëĄė í¸ ëŖ¨í¸ ę˛ŊëĄ
|
|
291
322
|
* @param server - MCP Server ė¸ė¤í´ė¤ (ė í, brain/project ëęĩŦė íė)
|
|
@@ -306,11 +337,15 @@ export function registerBuiltinTools(
|
|
|
306
337
|
monitor?: ActivityMonitor,
|
|
307
338
|
options?: { profile?: McpProfile }
|
|
308
339
|
): void {
|
|
309
|
-
const allowedCategories = options?.profile
|
|
310
|
-
? getProfileCategories(options.profile)
|
|
311
|
-
: null;
|
|
312
|
-
|
|
313
|
-
|
|
340
|
+
const allowedCategories = options?.profile
|
|
341
|
+
? getProfileCategories(options.profile)
|
|
342
|
+
: null;
|
|
343
|
+
const definitionIssues = validateBuiltinToolModules();
|
|
344
|
+
if (definitionIssues.length > 0) {
|
|
345
|
+
throw new Error(`Invalid MCP tool module registry:\n${definitionIssues.join("\n")}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const module of TOOL_MODULES) {
|
|
314
349
|
// Profile filtering: skip categories not in the allowed list
|
|
315
350
|
if (allowedCategories && !allowedCategories.includes(module.category)) {
|
|
316
351
|
continue;
|
|
@@ -321,17 +356,13 @@ export function registerBuiltinTools(
|
|
|
321
356
|
continue;
|
|
322
357
|
}
|
|
323
358
|
|
|
324
|
-
try {
|
|
325
|
-
let handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>>;
|
|
326
|
-
if (module.requiresServer) {
|
|
327
|
-
handlers =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
);
|
|
332
|
-
} else if (module.acceptsServer) {
|
|
333
|
-
// Forward the Server when available; fall back to just projectRoot.
|
|
334
|
-
handlers = server
|
|
359
|
+
try {
|
|
360
|
+
let handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>>;
|
|
361
|
+
if (module.requiresServer) {
|
|
362
|
+
handlers = module.handlers(projectRoot, server, monitor);
|
|
363
|
+
} else if (module.acceptsServer) {
|
|
364
|
+
// Forward the Server when available; fall back to just projectRoot.
|
|
365
|
+
handlers = server
|
|
335
366
|
? module.handlers(projectRoot, server)
|
|
336
367
|
: module.handlers(projectRoot);
|
|
337
368
|
} else {
|
package/src/tools/kitchen.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Enables any MCP-compatible agent to read client-side errors in real-time.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import { loadManduConfig } from "@mandujs/core";
|
|
9
|
-
import { getDevServerState } from "./project.js";
|
|
7
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { loadManduConfig } from "@mandujs/core";
|
|
9
|
+
import { getDevServerState } from "./project.js";
|
|
10
|
+
import { readRuntimeControl } from "../utils/runtime-control.js";
|
|
10
11
|
|
|
11
12
|
export const kitchenToolDefinitions: Tool[] = [
|
|
12
13
|
{
|
|
@@ -19,10 +20,18 @@ export const kitchenToolDefinitions: Tool[] = [
|
|
|
19
20
|
inputSchema: {
|
|
20
21
|
type: "object",
|
|
21
22
|
properties: {
|
|
22
|
-
clear: {
|
|
23
|
-
type: "boolean",
|
|
24
|
-
description: "Clear errors after reading (default: false)",
|
|
25
|
-
},
|
|
23
|
+
clear: {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
description: "Clear errors after reading (default: false)",
|
|
26
|
+
},
|
|
27
|
+
baseURL: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Explicit dev server URL. Overrides runtime-control/config discovery.",
|
|
30
|
+
},
|
|
31
|
+
port: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Explicit dev server port. Overrides runtime-control/config discovery.",
|
|
34
|
+
},
|
|
26
35
|
},
|
|
27
36
|
required: [],
|
|
28
37
|
},
|
|
@@ -47,11 +56,19 @@ export const kitchenToolDefinitions: Tool[] = [
|
|
|
47
56
|
description:
|
|
48
57
|
"Include the extended diagnose report. Default true. Set false to lower latency when a11y_hints / package_export_gaps are noisy.",
|
|
49
58
|
},
|
|
50
|
-
includeDiff: {
|
|
51
|
-
type: "boolean",
|
|
52
|
-
description:
|
|
53
|
-
"Include git diff against MANDU_DIFF_BASE (default HEAD). Default true. Set false to skip when git is unavailable.",
|
|
54
|
-
},
|
|
59
|
+
includeDiff: {
|
|
60
|
+
type: "boolean",
|
|
61
|
+
description:
|
|
62
|
+
"Include git diff against MANDU_DIFF_BASE (default HEAD). Default true. Set false to skip when git is unavailable.",
|
|
63
|
+
},
|
|
64
|
+
baseURL: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Explicit dev server URL. Overrides runtime-control/config discovery.",
|
|
67
|
+
},
|
|
68
|
+
port: {
|
|
69
|
+
type: "number",
|
|
70
|
+
description: "Explicit dev server port. Overrides runtime-control/config discovery.",
|
|
71
|
+
},
|
|
55
72
|
},
|
|
56
73
|
required: [],
|
|
57
74
|
},
|
|
@@ -59,15 +76,32 @@ export const kitchenToolDefinitions: Tool[] = [
|
|
|
59
76
|
];
|
|
60
77
|
|
|
61
78
|
/**
|
|
62
|
-
* Resolve the dev server base URL. Prefers
|
|
63
|
-
*
|
|
64
|
-
* default
|
|
65
|
-
* agree on where to fetch from.
|
|
79
|
+
* Resolve the dev server base URL. Prefers explicit args, then
|
|
80
|
+
* `.mandu/runtime-control.json` from the running dev/start process, then
|
|
81
|
+
* captured stdout, then config/default 3333. Shared by every Kitchen-backed
|
|
82
|
+
* MCP tool so they all agree on where to fetch from.
|
|
66
83
|
*/
|
|
67
|
-
async function resolveDevServerBaseUrl(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
async function resolveDevServerBaseUrl(
|
|
85
|
+
projectRoot: string,
|
|
86
|
+
args: { baseURL?: unknown; port?: unknown } = {},
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
if (typeof args.baseURL === "string" && args.baseURL.trim()) {
|
|
89
|
+
return args.baseURL.trim().replace(/\/+$/, "");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const explicitPort = normalizePort(args.port);
|
|
93
|
+
if (explicitPort) {
|
|
94
|
+
return `http://localhost:${explicitPort}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const control = await readRuntimeControl(projectRoot);
|
|
98
|
+
if (control?.baseUrl) {
|
|
99
|
+
return control.baseUrl.replace(/\/+$/, "");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let port: number | undefined;
|
|
103
|
+
|
|
104
|
+
const serverState = getDevServerState();
|
|
71
105
|
if (serverState) {
|
|
72
106
|
for (const line of serverState.output) {
|
|
73
107
|
const portMatch = line.match(/https?:\/\/localhost:(\d+)/);
|
|
@@ -81,15 +115,21 @@ async function resolveDevServerBaseUrl(projectRoot: string): Promise<string> {
|
|
|
81
115
|
const config = await loadManduConfig(projectRoot);
|
|
82
116
|
port = config.server?.port ?? 3333;
|
|
83
117
|
}
|
|
84
|
-
|
|
85
|
-
return `http://localhost:${port}`;
|
|
86
|
-
}
|
|
118
|
+
|
|
119
|
+
return `http://localhost:${port}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizePort(value: unknown): number | undefined {
|
|
123
|
+
const raw = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : NaN;
|
|
124
|
+
if (!Number.isInteger(raw) || raw < 1 || raw > 65535) return undefined;
|
|
125
|
+
return raw;
|
|
126
|
+
}
|
|
87
127
|
|
|
88
128
|
export function kitchenTools(projectRoot: string) {
|
|
89
129
|
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
90
|
-
"mandu.kitchen.errors": async (args: Record<string, unknown>) => {
|
|
91
|
-
const { clear = false } = args as { clear?: boolean };
|
|
92
|
-
const baseUrl = await resolveDevServerBaseUrl(projectRoot);
|
|
130
|
+
"mandu.kitchen.errors": async (args: Record<string, unknown>) => {
|
|
131
|
+
const { clear = false } = args as { clear?: boolean };
|
|
132
|
+
const baseUrl = await resolveDevServerBaseUrl(projectRoot, args);
|
|
93
133
|
|
|
94
134
|
try {
|
|
95
135
|
// Fetch errors from Kitchen API
|
|
@@ -145,12 +185,12 @@ export function kitchenTools(projectRoot: string) {
|
|
|
145
185
|
*/
|
|
146
186
|
"mandu.devtools.context": async (args: Record<string, unknown>) => {
|
|
147
187
|
const {
|
|
148
|
-
includeBundle = true,
|
|
149
|
-
includeDiagnose = true,
|
|
150
|
-
includeDiff = true,
|
|
151
|
-
} = args as { includeBundle?: boolean; includeDiagnose?: boolean; includeDiff?: boolean };
|
|
152
|
-
|
|
153
|
-
const baseUrl = await resolveDevServerBaseUrl(projectRoot);
|
|
188
|
+
includeBundle = true,
|
|
189
|
+
includeDiagnose = true,
|
|
190
|
+
includeDiff = true,
|
|
191
|
+
} = args as { includeBundle?: boolean; includeDiagnose?: boolean; includeDiff?: boolean };
|
|
192
|
+
|
|
193
|
+
const baseUrl = await resolveDevServerBaseUrl(projectRoot, args);
|
|
154
194
|
const params = new URLSearchParams();
|
|
155
195
|
if (!includeBundle) params.set("bundle", "0");
|
|
156
196
|
if (!includeDiagnose) params.set("diagnose", "0");
|
|
@@ -1,49 +1,13 @@
|
|
|
1
|
-
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
{
|
|
12
|
-
name: "mandu.slot.validate",
|
|
13
|
-
description:
|
|
14
|
-
"Validate a slot file against semantic constraints (lines, complexity, patterns, imports).",
|
|
15
|
-
annotations: {
|
|
16
|
-
readOnlyHint: true,
|
|
17
|
-
},
|
|
18
|
-
inputSchema: {
|
|
19
|
-
type: "object",
|
|
20
|
-
properties: {
|
|
21
|
-
file: {
|
|
22
|
-
type: "string",
|
|
23
|
-
description: "Path to the slot file to validate",
|
|
24
|
-
},
|
|
25
|
-
preset: {
|
|
26
|
-
type: "string",
|
|
27
|
-
enum: ["default", "api", "readonly"],
|
|
28
|
-
description: "Constraint preset to use (default: 'default')",
|
|
29
|
-
},
|
|
30
|
-
constraints: {
|
|
31
|
-
type: "object",
|
|
32
|
-
description: "Custom constraints (overrides preset)",
|
|
33
|
-
properties: {
|
|
34
|
-
maxLines: { type: "number" },
|
|
35
|
-
maxCyclomaticComplexity: { type: "number" },
|
|
36
|
-
requiredPatterns: { type: "array", items: { type: "string" } },
|
|
37
|
-
forbiddenPatterns: { type: "array", items: { type: "string" } },
|
|
38
|
-
allowedImports: { type: "array", items: { type: "string" } },
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
required: ["file"],
|
|
43
|
-
},
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: "mandu.slot.constraints",
|
|
1
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SLOT_CONSTRAINTS,
|
|
4
|
+
API_SLOT_CONSTRAINTS,
|
|
5
|
+
READONLY_SLOT_CONSTRAINTS,
|
|
6
|
+
} from "@mandujs/core";
|
|
7
|
+
|
|
8
|
+
export const slotValidationToolDefinitions: Tool[] = [
|
|
9
|
+
{
|
|
10
|
+
name: "mandu.slot.constraints",
|
|
47
11
|
description:
|
|
48
12
|
"Get recommended slot constraint presets (default, api, readonly).",
|
|
49
13
|
annotations: {
|
|
@@ -63,79 +27,10 @@ export const slotValidationToolDefinitions: Tool[] = [
|
|
|
63
27
|
},
|
|
64
28
|
];
|
|
65
29
|
|
|
66
|
-
export function slotValidationTools(projectRoot: string) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
file: string;
|
|
71
|
-
preset?: "default" | "api" | "readonly";
|
|
72
|
-
constraints?: SlotConstraints;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
if (!file) {
|
|
76
|
-
return {
|
|
77
|
-
error: "File path is required",
|
|
78
|
-
tip: "Provide the path to the slot file to validate",
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// íëĻŦė
ė í
|
|
83
|
-
let constraints: SlotConstraints;
|
|
84
|
-
if (customConstraints) {
|
|
85
|
-
constraints = customConstraints;
|
|
86
|
-
} else {
|
|
87
|
-
switch (preset) {
|
|
88
|
-
case "api":
|
|
89
|
-
constraints = API_SLOT_CONSTRAINTS;
|
|
90
|
-
break;
|
|
91
|
-
case "readonly":
|
|
92
|
-
constraints = READONLY_SLOT_CONSTRAINTS;
|
|
93
|
-
break;
|
|
94
|
-
default:
|
|
95
|
-
constraints = DEFAULT_SLOT_CONSTRAINTS;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// íėŧ ę˛ŊëĄ ė ęˇí ë° ëŗ´ė ę˛ėĻ (LFI ë°Šė§)
|
|
100
|
-
const path = await import("path");
|
|
101
|
-
const rawPath = file.startsWith("/") || file.includes(":")
|
|
102
|
-
? file
|
|
103
|
-
: path.join(projectRoot, file);
|
|
104
|
-
const filePath = path.normalize(path.resolve(rawPath));
|
|
105
|
-
const normalizedRoot = path.normalize(path.resolve(projectRoot));
|
|
106
|
-
|
|
107
|
-
// ę˛ŊëĄę° íëĄė í¸ ëŖ¨í¸ ë´ė ėëė§ ę˛ėĻ
|
|
108
|
-
if (!filePath.startsWith(normalizedRoot)) {
|
|
109
|
-
return {
|
|
110
|
-
error: "Access denied: File path is outside project root",
|
|
111
|
-
tip: "Only files within the project directory can be validated",
|
|
112
|
-
requestedPath: file,
|
|
113
|
-
projectRoot: projectRoot,
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const result = await validateSlotConstraints(filePath, constraints);
|
|
118
|
-
|
|
119
|
-
return {
|
|
120
|
-
valid: result.valid,
|
|
121
|
-
file: result.filePath,
|
|
122
|
-
stats: result.stats,
|
|
123
|
-
violations: result.violations.map((v) => ({
|
|
124
|
-
type: v.type,
|
|
125
|
-
severity: v.severity,
|
|
126
|
-
message: v.message,
|
|
127
|
-
suggestion: v.suggestion,
|
|
128
|
-
line: v.line,
|
|
129
|
-
})),
|
|
130
|
-
suggestions: result.suggestions,
|
|
131
|
-
constraintsUsed: constraints,
|
|
132
|
-
tip: result.valid
|
|
133
|
-
? "â
Slot passes all constraints"
|
|
134
|
-
: "Fix violations before deployment. Use mandu.slot.constraints for guidance.",
|
|
135
|
-
};
|
|
136
|
-
},
|
|
137
|
-
|
|
138
|
-
"mandu.slot.constraints": async (args: Record<string, unknown>) => {
|
|
30
|
+
export function slotValidationTools(projectRoot: string) {
|
|
31
|
+
void projectRoot;
|
|
32
|
+
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
33
|
+
"mandu.slot.constraints": async (args: Record<string, unknown>) => {
|
|
139
34
|
const { preset } = args as { preset?: "default" | "api" | "readonly" };
|
|
140
35
|
|
|
141
36
|
const presets = {
|
|
@@ -189,11 +84,10 @@ Mandu.filling()
|
|
|
189
84
|
`.trim(),
|
|
190
85
|
};
|
|
191
86
|
},
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
// Backward-compatible aliases
|
|
195
|
-
handlers["
|
|
196
|
-
|
|
197
|
-
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Backward-compatible aliases
|
|
90
|
+
handlers["mandu_get_slot_constraints"] = handlers["mandu.slot.constraints"];
|
|
91
|
+
|
|
198
92
|
return handlers;
|
|
199
93
|
}
|