@mandujs/mcp 0.18.2 → 0.18.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.
@@ -22,23 +22,36 @@ type DevServerState = {
22
22
  };
23
23
 
24
24
  let devServerState: DevServerState | null = null;
25
+ let devServerStarting = false;
25
26
 
26
27
  function trimOutput(text: string, maxChars: number = 4000): string {
27
28
  if (text.length <= maxChars) return text;
28
29
  return text.slice(-maxChars);
29
30
  }
30
31
 
31
- async function runCommand(cmd: string[], cwd: string) {
32
+ const COMMAND_TIMEOUT_MS = 120_000; // 2 minutes
33
+
34
+ async function runCommand(cmd: string[], cwd: string, timeoutMs: number = COMMAND_TIMEOUT_MS) {
32
35
  const proc = spawn(cmd, {
33
36
  cwd,
34
37
  stdout: "pipe",
35
38
  stderr: "pipe",
36
39
  });
37
40
 
38
- const [stdout, stderr, exitCode] = await Promise.all([
39
- new Response(proc.stdout).text(),
40
- new Response(proc.stderr).text(),
41
- proc.exited,
41
+ const timeoutPromise = new Promise<never>((_, reject) =>
42
+ setTimeout(() => {
43
+ proc.kill();
44
+ reject(new Error(`Command timed out after ${timeoutMs}ms: ${cmd.join(" ")}`));
45
+ }, timeoutMs)
46
+ );
47
+
48
+ const [stdout, stderr, exitCode] = await Promise.race([
49
+ Promise.all([
50
+ new Response(proc.stdout).text(),
51
+ new Response(proc.stderr).text(),
52
+ proc.exited,
53
+ ]),
54
+ timeoutPromise,
42
55
  ]);
43
56
 
44
57
  return {
@@ -192,6 +205,16 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
192
205
 
193
206
  await fs.mkdir(baseDir, { recursive: true });
194
207
 
208
+ // Runtime whitelist validation for spawn arguments
209
+ const VALID_CSS = ["tailwind", "panda", "none"];
210
+ const VALID_UI = ["shadcn", "ark", "none"];
211
+ if (css !== undefined && !VALID_CSS.includes(css)) {
212
+ return { success: false, error: `Invalid css value: ${css}. Must be one of: ${VALID_CSS.join(", ")}` };
213
+ }
214
+ if (ui !== undefined && !VALID_UI.includes(ui)) {
215
+ return { success: false, error: `Invalid ui value: ${ui}. Must be one of: ${VALID_UI.join(", ")}` };
216
+ }
217
+
195
218
  const initArgs = ["@mandujs/cli", "init", name];
196
219
  if (minimal) {
197
220
  initArgs.push("--minimal");
@@ -201,7 +224,16 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
201
224
  if (theme) initArgs.push("--theme");
202
225
  }
203
226
 
204
- const initResult = await runCommand(["bunx", ...initArgs], baseDir);
227
+ let initResult: { exitCode: number | null; stdout: string; stderr: string };
228
+ try {
229
+ initResult = await runCommand(["bunx", ...initArgs], baseDir);
230
+ } catch (err) {
231
+ return {
232
+ success: false,
233
+ step: "init",
234
+ error: err instanceof Error ? err.message : String(err),
235
+ };
236
+ }
205
237
  if (initResult.exitCode !== 0) {
206
238
  return {
207
239
  success: false,
@@ -216,7 +248,16 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
216
248
 
217
249
  let installResult: { exitCode: number | null; stdout: string; stderr: string } | null = null;
218
250
  if (install !== false) {
219
- installResult = await runCommand(["bun", "install"], projectDir);
251
+ try {
252
+ installResult = await runCommand(["bun", "install"], projectDir);
253
+ } catch (err) {
254
+ return {
255
+ success: false,
256
+ step: "install",
257
+ projectDir,
258
+ error: err instanceof Error ? err.message : String(err),
259
+ };
260
+ }
220
261
  if (installResult.exitCode !== 0) {
221
262
  return {
222
263
  success: false,
@@ -253,53 +294,60 @@ export function projectTools(projectRoot: string, server?: Server, monitor?: Act
253
294
 
254
295
  mandu_dev_start: async (args: Record<string, unknown>) => {
255
296
  const { cwd } = args as { cwd?: string };
256
- if (devServerState) {
297
+ if (devServerState || devServerStarting) {
257
298
  return {
258
299
  success: false,
259
- message: "Dev server is already running",
260
- pid: devServerState.process.pid,
261
- cwd: devServerState.cwd,
300
+ message: devServerStarting
301
+ ? "Dev server is starting up, please wait"
302
+ : "Dev server is already running",
303
+ pid: devServerState?.process.pid,
304
+ cwd: devServerState?.cwd,
262
305
  };
263
306
  }
264
307
 
265
- const targetDir = cwd ? path.resolve(projectRoot, cwd) : projectRoot;
266
-
267
- const proc = spawn(["bun", "run", "dev"], {
268
- cwd: targetDir,
269
- stdout: "pipe",
270
- stderr: "pipe",
271
- stdin: "ignore",
272
- });
273
-
274
- const state: DevServerState = {
275
- process: proc,
276
- cwd: targetDir,
277
- startedAt: new Date(),
278
- output: [],
279
- maxLines: 50,
280
- };
281
- devServerState = state;
308
+ devServerStarting = true;
309
+ try {
310
+ const targetDir = cwd ? path.resolve(projectRoot, cwd) : projectRoot;
311
+
312
+ const proc = spawn(["bun", "run", "dev"], {
313
+ cwd: targetDir,
314
+ stdout: "pipe",
315
+ stderr: "pipe",
316
+ stdin: "ignore",
317
+ });
318
+
319
+ const state: DevServerState = {
320
+ process: proc,
321
+ cwd: targetDir,
322
+ startedAt: new Date(),
323
+ output: [],
324
+ maxLines: 50,
325
+ };
326
+ devServerState = state;
327
+
328
+ consumeStream(proc.stdout, state, "stdout", server).catch(() => {});
329
+ consumeStream(proc.stderr, state, "stderr", server).catch(() => {});
282
330
 
283
- consumeStream(proc.stdout, state, "stdout", server).catch(() => {});
284
- consumeStream(proc.stderr, state, "stderr", server).catch(() => {});
331
+ proc.exited.then(() => {
332
+ if (devServerState?.process === proc) {
333
+ devServerState = null;
334
+ }
335
+ }).catch(() => {});
285
336
 
286
- proc.exited.then(() => {
287
- if (devServerState?.process === proc) {
288
- devServerState = null;
337
+ if (monitor) {
338
+ monitor.logEvent("dev", `Dev server started (${targetDir})`);
289
339
  }
290
- }).catch(() => {});
291
340
 
292
- if (monitor) {
293
- monitor.logEvent("dev", `Dev server started (${targetDir})`);
341
+ return {
342
+ success: true,
343
+ pid: proc.pid,
344
+ cwd: targetDir,
345
+ startedAt: state.startedAt.toISOString(),
346
+ message: "Dev server started",
347
+ };
348
+ } finally {
349
+ devServerStarting = false;
294
350
  }
295
-
296
- return {
297
- success: true,
298
- pid: proc.pid,
299
- cwd: targetDir,
300
- startedAt: state.startedAt.toISOString(),
301
- message: "Dev server started",
302
- };
303
351
  },
304
352
 
305
353
  mandu_dev_stop: async () => {
@@ -1,10 +1,6 @@
1
1
  /**
2
2
  * Mandu MCP Runtime Tools
3
- * Runtime 설정 조회 관리 도구
4
- *
5
- * - Logger 설정 조회/변경
6
- * - Normalize 설정 조회
7
- * - Contract 옵션 확인
3
+ * Query and manage runtime configuration: logger settings and contract normalize options.
8
4
  */
9
5
 
10
6
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
@@ -17,8 +13,10 @@ export const runtimeToolDefinitions: Tool[] = [
17
13
  {
18
14
  name: "mandu_get_runtime_config",
19
15
  description:
20
- "Get current runtime configuration including logger and normalize settings. " +
21
- "Shows default values and any overrides from contracts.",
16
+ "Get the Mandu runtime configuration defaults for logger and normalize settings. " +
17
+ "Shows default values for every configurable option along with usage examples. " +
18
+ "Use this to understand the runtime before calling mandu_set_contract_normalize " +
19
+ "or mandu_generate_logger_config.",
22
20
  inputSchema: {
23
21
  type: "object",
24
22
  properties: {},
@@ -28,8 +26,11 @@ export const runtimeToolDefinitions: Tool[] = [
28
26
  {
29
27
  name: "mandu_get_contract_options",
30
28
  description:
31
- "Get normalize and coerce options for a specific contract. " +
32
- "These options control how request data is sanitized and type-converted.",
29
+ "Read the normalize and coerceQueryParams options currently set in a specific contract file. " +
30
+ "These options control how incoming request data is validated and sanitized: " +
31
+ "'normalize' removes or blocks undefined fields (Mass Assignment protection), " +
32
+ "'coerceQueryParams' auto-converts URL query string values to their declared schema types (e.g., '123' → number). " +
33
+ "Returns the parsed values and their effect, or defaults if no explicit options are set.",
33
34
  inputSchema: {
34
35
  type: "object",
35
36
  properties: {
@@ -44,8 +45,12 @@ export const runtimeToolDefinitions: Tool[] = [
44
45
  {
45
46
  name: "mandu_set_contract_normalize",
46
47
  description:
47
- "Set normalize mode for a contract. " +
48
- "Modes: 'strip' (remove undefined fields, default), 'strict' (error on undefined), 'passthrough' (allow all).",
48
+ "Set the normalize mode (and optionally coerceQueryParams) in a route's contract file. " +
49
+ "Normalize modes: " +
50
+ "'strip' (default, recommended) — removes any request fields not defined in the schema, preventing Mass Assignment attacks. " +
51
+ "'strict' — returns HTTP 400 if the request contains any field not defined in the schema. " +
52
+ "'passthrough' — allows all fields through without filtering (validation only, no sanitization). " +
53
+ "coerceQueryParams: when true (default), auto-converts query string values to their declared schema types.",
49
54
  inputSchema: {
50
55
  type: "object",
51
56
  properties: {
@@ -57,11 +62,12 @@ export const runtimeToolDefinitions: Tool[] = [
57
62
  type: "string",
58
63
  enum: ["strip", "strict", "passthrough"],
59
64
  description:
60
- "Normalize mode: strip (Mass Assignment 방지), strict (에러 발생), passthrough (모두 허용)",
65
+ "Normalize mode: 'strip' (remove undefined fields, prevents Mass Assignment), " +
66
+ "'strict' (return 400 on undefined fields), 'passthrough' (allow all fields through)",
61
67
  },
62
68
  coerceQueryParams: {
63
69
  type: "boolean",
64
- description: "Whether to auto-convert query string types (default: true)",
70
+ description: "Auto-convert URL query string values to schema-declared types (default: true)",
65
71
  },
66
72
  },
67
73
  required: ["routeId"],
@@ -70,8 +76,10 @@ export const runtimeToolDefinitions: Tool[] = [
70
76
  {
71
77
  name: "mandu_list_logger_options",
72
78
  description:
73
- "List available logger configuration options and their descriptions. " +
74
- "Useful for understanding how to configure logging in Mandu.",
79
+ "List all available logger configuration options with types, defaults, and descriptions. " +
80
+ "Covers: log format, level, header/body logging (security risk warnings), " +
81
+ "sampling rate, slow request threshold, redaction fields, custom sink, and skip patterns. " +
82
+ "Use this as a reference before calling mandu_generate_logger_config.",
75
83
  inputSchema: {
76
84
  type: "object",
77
85
  properties: {},
@@ -81,33 +89,36 @@ export const runtimeToolDefinitions: Tool[] = [
81
89
  {
82
90
  name: "mandu_generate_logger_config",
83
91
  description:
84
- "Generate logger configuration code based on requirements. " +
85
- "Returns TypeScript code that can be added to the project.",
92
+ "Generate ready-to-use TypeScript logger configuration code for a specific environment. " +
93
+ "Returns an import statement and logger() call with environment-appropriate defaults: " +
94
+ "development: debug level, pretty format, higher verbosity; " +
95
+ "production: info level, JSON format, 10% sampling, no headers/body. " +
96
+ "Security note: includeHeaders and includeBody are forced to false in production regardless of input.",
86
97
  inputSchema: {
87
98
  type: "object",
88
99
  properties: {
89
100
  environment: {
90
101
  type: "string",
91
102
  enum: ["development", "production", "testing"],
92
- description: "Target environment (default: development)",
103
+ description: "Target environment — determines default log level, format, and sampling rate (default: development)",
93
104
  },
94
105
  includeHeaders: {
95
106
  type: "boolean",
96
- description: "Whether to log request headers (default: false for security)",
107
+ description: "Log request headers — security risk, only recommended in development (default: false)",
97
108
  },
98
109
  includeBody: {
99
110
  type: "boolean",
100
- description: "Whether to log request body (default: false for security)",
111
+ description: "Log request body — security risk, only recommended in development (default: false)",
101
112
  },
102
113
  format: {
103
114
  type: "string",
104
115
  enum: ["pretty", "json"],
105
- description: "Log output format (pretty for dev, json for prod)",
116
+ description: "Log output format: 'pretty' (colored, human-readable) or 'json' (structured, for log aggregators)",
106
117
  },
107
118
  customRedact: {
108
119
  type: "array",
109
120
  items: { type: "string" },
110
- description: "Additional fields to redact from logs",
121
+ description: "Additional header or field names to redact/mask from logs",
111
122
  },
112
123
  },
113
124
  required: [],
@@ -160,17 +171,17 @@ export function runtimeTools(projectRoot: string) {
160
171
  logger: {
161
172
  format: "Log output format: 'pretty' (colored, dev) or 'json' (structured, prod)",
162
173
  level: "Minimum log level: 'debug' | 'info' | 'warn' | 'error'",
163
- includeHeaders: "⚠️ Security risk if true - logs request headers",
164
- includeBody: "⚠️ Security risk if true - logs request body",
165
- maxBodyBytes: "Maximum body size to log (truncates larger bodies)",
166
- sampleRate: "Sampling rate 0-1 (1 = 100% logging)",
167
- slowThresholdMs: "Requests slower than this get detailed logging",
168
- redact: "Header/field names to mask in logs",
174
+ includeHeaders: "⚠️ Security risk if true logs all request headers including Authorization, Cookie",
175
+ includeBody: "⚠️ Security risk if true logs raw request body; may expose PII",
176
+ maxBodyBytes: "Maximum body bytes to log (truncates larger bodies to avoid log bloat)",
177
+ sampleRate: "Sampling rate 0.0–1.0 (1.0 = 100% of requests logged)",
178
+ slowThresholdMs: "Requests exceeding this threshold (ms) are logged at warn level with details",
179
+ redact: "Header/field names to mask in logs (replaces value with '[REDACTED]')",
169
180
  },
170
181
  normalize: {
171
- mode: "strip: remove undefined fields (Mass Assignment 방지), strict: error on undefined, passthrough: allow all",
172
- coerceQueryParams: "Auto-convert query string '123' → number 123",
173
- deep: "Apply normalization to nested objects",
182
+ mode: "strip: remove undefined fields (prevents Mass Assignment attacks), strict: return 400 on undefined fields, passthrough: allow all fields (validation only)",
183
+ coerceQueryParams: "Auto-convert URL query string '123' → number 123 based on schema type",
184
+ deep: "Apply normalization recursively to nested objects",
174
185
  },
175
186
  },
176
187
  usage: {
@@ -242,11 +253,11 @@ export default Mandu.contract({
242
253
  },
243
254
  explanation: {
244
255
  normalize: {
245
- strip: "정의되지 않은 필드 제거 (Mass Assignment 공격 방지)",
246
- strict: "정의되지 않은 필드 있으면 400 에러",
247
- passthrough: "모든 필드 허용 (검증만, 필터링 함)",
256
+ strip: "Removes any request fields not defined in the schema — prevents Mass Assignment attacks (recommended default)",
257
+ strict: "Returns HTTP 400 if the request contains any field not defined in the schema",
258
+ passthrough: "Allows all fields through without filtering — validation only, no sanitization",
248
259
  },
249
- coerceQueryParams: "URL query string은 항상 문자열이므로, 스키마 타입으로 자동 변환",
260
+ coerceQueryParams: "URL query strings are always plain strings; this option auto-converts them to the declared schema types (e.g., '42' → number, 'true' → boolean)",
250
261
  },
251
262
  };
252
263
  },
@@ -341,10 +352,10 @@ export default Mandu.contract({
341
352
  message: `Updated ${route.contractModule}`,
342
353
  securityNote:
343
354
  normalize === "passthrough"
344
- ? "⚠️ passthrough 모드는 Mass Assignment 공격에 취약할 있습니다. 신뢰할 있는 입력에만 사용하세요."
355
+ ? "⚠️ passthrough mode may be vulnerable to Mass Assignment attacks. Only use with trusted, fully-validated input."
345
356
  : normalize === "strict"
346
- ? "strict 모드는 클라이언트가 추가 필드를 보내면 400 에러를 반환합니다."
347
- : "strip 모드 (권장): 정의되지 않은 필드는 자동 제거됩니다.",
357
+ ? "strict mode returns HTTP 400 if the client sends any field not defined in the contract schema."
358
+ : "strip mode (recommended): fields not defined in the schema are automatically removed from the request.",
348
359
  };
349
360
  },
350
361
 
@@ -355,78 +366,78 @@ export default Mandu.contract({
355
366
  name: "format",
356
367
  type: '"pretty" | "json"',
357
368
  default: "pretty",
358
- description: "로그 출력 형식. pretty 개발용 컬러 출력, json 운영용 구조화 로그",
369
+ description: "Log output format: 'pretty' (colored, human-readable for dev) or 'json' (structured, for log aggregators in prod)",
359
370
  },
360
371
  {
361
372
  name: "level",
362
373
  type: '"debug" | "info" | "warn" | "error"',
363
374
  default: "info",
364
- description: "최소 로그 레벨. debug 모든 요청 상세, error 에러만",
375
+ description: "Minimum log level: 'debug' (all requests with details), 'info' (standard), 'warn' (slow/suspicious only), 'error' (errors only)",
365
376
  },
366
377
  {
367
378
  name: "includeHeaders",
368
379
  type: "boolean",
369
380
  default: false,
370
- description: "⚠️ 요청 헤더 로깅. 민감 정보 노출 위험",
381
+ description: "⚠️ Security risk logs all request headers including Authorization and Cookie. Only enable in development.",
371
382
  },
372
383
  {
373
384
  name: "includeBody",
374
385
  type: "boolean",
375
386
  default: false,
376
- description: "⚠️ 요청 바디 로깅. 민감 정보 노출 + 스트림 문제",
387
+ description: "⚠️ Security risk logs raw request body which may contain PII or credentials. Only enable in development.",
377
388
  },
378
389
  {
379
390
  name: "maxBodyBytes",
380
391
  type: "number",
381
392
  default: 1024,
382
- description: "바디 로깅 최대 크기 (초과분 truncate)",
393
+ description: "Maximum bytes of request body to log (larger bodies are truncated to avoid log bloat)",
383
394
  },
384
395
  {
385
396
  name: "redact",
386
397
  type: "string[]",
387
398
  default: '["authorization", "cookie", "password", ...]',
388
- description: "로그에서 마스킹할 헤더/필드명",
399
+ description: "Header or field names to mask in logs (values are replaced with '[REDACTED]')",
389
400
  },
390
401
  {
391
402
  name: "requestId",
392
403
  type: '"auto" | ((ctx) => string)',
393
404
  default: "auto",
394
- description: "요청 ID 생성 방식. auto UUID 또는 타임스탬프 기반",
405
+ description: "Request ID generation strategy: 'auto' uses UUID or timestamp-based ID, or provide a custom function",
395
406
  },
396
407
  {
397
408
  name: "sampleRate",
398
- type: "number (0-1)",
409
+ type: "number (0.0–1.0)",
399
410
  default: 1,
400
- description: "샘플링 비율. 운영에서 로그 조절 (0.1 = 10%)",
411
+ description: "Fraction of requests to log (1.0 = 100%, 0.1 = 10%). Reduce in production to control log volume.",
401
412
  },
402
413
  {
403
414
  name: "slowThresholdMs",
404
415
  type: "number",
405
416
  default: 1000,
406
- description: "느린 요청 임계값. 초과 warn 레벨로 상세 출력",
417
+ description: "Requests exceeding this duration (ms) are logged at warn level with full details",
407
418
  },
408
419
  {
409
420
  name: "includeTraceOnSlow",
410
421
  type: "boolean",
411
422
  default: true,
412
- description: "느린 요청에 Trace 리포트 포함",
423
+ description: "Include a timing trace report in the log entry for slow requests",
413
424
  },
414
425
  {
415
426
  name: "sink",
416
427
  type: "(entry: LogEntry) => void",
417
428
  default: "console",
418
- description: "커스텀 로그 출력 (Pino, CloudWatch 연동)",
429
+ description: "Custom log output handler — use for integrating with Pino, CloudWatch, Datadog, etc.",
419
430
  },
420
431
  {
421
432
  name: "skip",
422
433
  type: "(string | RegExp)[]",
423
434
  default: "[]",
424
- description: '로깅 제외 경로 패턴. 예: ["/health", /^\\/static\\//]',
435
+ description: 'URL path patterns to exclude from logging. Example: ["/health", /^\\/static\\//]',
425
436
  },
426
437
  ],
427
438
  presets: {
428
- devLogger: "개발용: debug 레벨, pretty 포맷, 헤더 포함",
429
- prodLogger: "운영용: info 레벨, json 포맷, 헤더/바디 미포함",
439
+ devLogger: "Development preset: debug level, pretty format, detailed output",
440
+ prodLogger: "Production preset: info level, JSON format, no headers/body logging",
430
441
  },
431
442
  };
432
443
  },
@@ -471,10 +482,10 @@ export const appLogger = logger(${JSON.stringify(config, null, 2)});
471
482
 
472
483
  const warnings: string[] = [];
473
484
  if (includeHeaders && isProd) {
474
- warnings.push("⚠️ includeHeaders: true 프로덕션에서 민감 정보 노출 위험이 있습니다.");
485
+ warnings.push("⚠️ includeHeaders: true in production may expose sensitive Authorization, Cookie, and API key headers in logs.");
475
486
  }
476
487
  if (includeBody && isProd) {
477
- warnings.push("⚠️ includeBody: true 프로덕션에서 민감 정보 노출 위험이 있습니다.");
488
+ warnings.push("⚠️ includeBody: true in production may expose PII, passwords, or credentials in logs.");
478
489
  }
479
490
 
480
491
  return {
@@ -483,9 +494,9 @@ export const appLogger = logger(${JSON.stringify(config, null, 2)});
483
494
  code,
484
495
  warnings: warnings.length > 0 ? warnings : undefined,
485
496
  tips: [
486
- "devLogger() 또는 prodLogger() 프리셋을 사용할 수도 있습니다.",
487
- "sink 옵션으로 Pino, CloudWatch 외부 시스템 연동 가능",
488
- "skip 옵션으로 /health, /metrics 등 제외 가능",
497
+ "You can also use the devLogger() or prodLogger() preset helpers for quick setup.",
498
+ "Use the 'sink' option to integrate with external systems like Pino, CloudWatch, or Datadog.",
499
+ "Use the 'skip' option to exclude health check and static asset paths (e.g., ['/health', '/metrics']).",
489
500
  ],
490
501
  };
491
502
  },
package/src/tools/slot.ts CHANGED
@@ -11,13 +11,19 @@ import path from "path";
11
11
  export const slotToolDefinitions: Tool[] = [
12
12
  {
13
13
  name: "mandu_read_slot",
14
- description: "Read the contents of a slot file for a specific route",
14
+ description:
15
+ "Read the TypeScript source of a route's slot file and validate its structure. " +
16
+ "In Mandu, a 'slot' is the server-side data loader for a route: " +
17
+ "it runs on every request before rendering and returns a typed object " +
18
+ "that is injected into the page component as props (for pages) or as handler context (for API routes). " +
19
+ "Slot files live at spec/slots/{routeId}.slot.ts and are auto-linked by generateManifest(). " +
20
+ "Returns the raw source, line count, and any structural validation issues.",
15
21
  inputSchema: {
16
22
  type: "object",
17
23
  properties: {
18
24
  routeId: {
19
25
  type: "string",
20
- description: "The route ID whose slot file to read",
26
+ description: "The route ID whose slot file to read (use mandu_list_routes to find IDs)",
21
27
  },
22
28
  },
23
29
  required: ["routeId"],
@@ -26,13 +32,21 @@ export const slotToolDefinitions: Tool[] = [
26
32
  {
27
33
  name: "mandu_validate_slot",
28
34
  description:
29
- "Validate slot content without writing, get issues and suggestions",
35
+ "Validate TypeScript slot content against Mandu's structural rules — without writing any files. " +
36
+ "A valid slot must export a default function (or use the slot() builder) that accepts a Request " +
37
+ "and returns a plain serializable object (becomes the typed props injected into the page). " +
38
+ "Returns: " +
39
+ "errors (must fix before use), " +
40
+ "warnings (best-practice suggestions), " +
41
+ "autoFixable issues (with corrected code preview), " +
42
+ "manualFixRequired items (issues needing human review). " +
43
+ "Use this before writing a slot file with the Edit tool to catch structural problems early.",
30
44
  inputSchema: {
31
45
  type: "object",
32
46
  properties: {
33
47
  content: {
34
48
  type: "string",
35
- description: "The TypeScript content to validate",
49
+ description: "The TypeScript slot source code to validate",
36
50
  },
37
51
  },
38
52
  required: ["content"],
@@ -84,7 +98,7 @@ export function slotTools(projectRoot: string) {
84
98
 
85
99
  const content = await file.text();
86
100
 
87
- // 기존 슬롯 내용도 검증
101
+ // Validate existing slot content structure
88
102
  const validation = validateSlotContent(content);
89
103
 
90
104
  return {
@@ -110,11 +124,11 @@ export function slotTools(projectRoot: string) {
110
124
 
111
125
  const validation = validateSlotContent(content);
112
126
 
113
- // 자동 수정 가능한 항목 분류
127
+ // Classify issues by whether they can be auto-fixed
114
128
  const autoFixable = validation.issues.filter((i) => i.autoFixable);
115
129
  const manualFix = validation.issues.filter((i) => !i.autoFixable);
116
130
 
117
- // 수정 미리보기 제공
131
+ // Generate correction preview for auto-fixable issues
118
132
  let correctionPreview = null;
119
133
  if (autoFixable.length > 0) {
120
134
  const correction = correctSlotContent(content, validation.issues);