@mandujs/mcp 0.37.1 → 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 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
- | `mandu_validate_slot` | Validate slot against constraints |
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.1",
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.1",
38
- "@mandujs/ate": "^0.26.0",
39
- "@mandujs/skills": "^0.20.0",
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 the declarative pattern: island('visible', Component)
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 { island } from "@mandujs/core/client";
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 island("visible", <Messages />);
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/i.test(message);
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
- progressToken,
444
- } = args as {
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
- progressToken?: string | number;
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
- } = args as {
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
- return await runFullPipeline({
537
- repoRoot, baseURL, oracleLevel, ci, useImpactAnalysis,
538
- base, head, autoHeal, tsconfigPath, routeGlobs, buildSalt,
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 {
@@ -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 island() wrapper.",
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 { island } from "@mandujs/core/client";
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
- export default island("${strategy}", ${name}Inner);
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
+ }
@@ -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 as ToolModule["handlers"], requiresServer: true },
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 as ToolModule["handlers"], requiresServer: true },
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 as ToolModule["handlers"], acceptsServer: true },
212
- { category: "ate-phase5", definitions: atePhase5ToolDefinitions, handlers: createAtePhase5Handlers as unknown as ToolModule["handlers"] },
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 as ToolModule["handlers"], acceptsServer: true },
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
- for (const module of TOOL_MODULES) {
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 = (module.handlers as (root: string, srv: Server, mon: ActivityMonitor) => Record<string, (args: Record<string, unknown>) => Promise<unknown>>)(
328
- projectRoot,
329
- server!,
330
- monitor!,
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 {
@@ -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 the port parsed from the
63
- * currently running `mandu dev` stdout; falls back to mandu config; final
64
- * default is 3333. Shared by every Kitchen-backed MCP tool so they all
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(projectRoot: string): Promise<string> {
68
- let port: number | undefined;
69
-
70
- const serverState = getDevServerState();
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
- validateSlotConstraints,
4
- DEFAULT_SLOT_CONSTRAINTS,
5
- API_SLOT_CONSTRAINTS,
6
- READONLY_SLOT_CONSTRAINTS,
7
- type SlotConstraints,
8
- } from "@mandujs/core";
9
-
10
- export const slotValidationToolDefinitions: Tool[] = [
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
- const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
68
- "mandu.slot.validate": async (args: Record<string, unknown>) => {
69
- const { file, preset, constraints: customConstraints } = args as {
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["mandu_validate_slot"] = handlers["mandu.slot.validate"];
196
- handlers["mandu_get_slot_constraints"] = handlers["mandu.slot.constraints"];
197
-
87
+ };
88
+
89
+ // Backward-compatible aliases
90
+ handlers["mandu_get_slot_constraints"] = handlers["mandu.slot.constraints"];
91
+
198
92
  return handlers;
199
93
  }