@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.
- package/package.json +1 -1
- package/src/activity-monitor.ts +11 -14
- package/src/server.ts +14 -5
- package/src/tools/ate.ts +132 -51
- package/src/tools/brain.ts +1 -1
- package/src/tools/contract.ts +56 -17
- package/src/tools/generate.ts +18 -4
- package/src/tools/project.ts +92 -44
- package/src/tools/runtime.ts +69 -58
- package/src/tools/slot.ts +21 -7
- package/src/tools/spec.ts +39 -10
- package/src/utils/project.ts +4 -2
package/src/tools/project.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
284
|
-
|
|
331
|
+
proc.exited.then(() => {
|
|
332
|
+
if (devServerState?.process === proc) {
|
|
333
|
+
devServerState = null;
|
|
334
|
+
}
|
|
335
|
+
}).catch(() => {});
|
|
285
336
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
devServerState = null;
|
|
337
|
+
if (monitor) {
|
|
338
|
+
monitor.logEvent("dev", `Dev server started (${targetDir})`);
|
|
289
339
|
}
|
|
290
|
-
}).catch(() => {});
|
|
291
340
|
|
|
292
|
-
|
|
293
|
-
|
|
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 () => {
|
package/src/tools/runtime.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mandu MCP Runtime Tools
|
|
3
|
-
*
|
|
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
|
|
21
|
-
"Shows default values
|
|
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
|
-
"
|
|
32
|
-
"These options control how request data is
|
|
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
|
|
48
|
-
"
|
|
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 (
|
|
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: "
|
|
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
|
|
74
|
-
"
|
|
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
|
|
85
|
-
"Returns
|
|
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: "
|
|
107
|
+
description: "Log request headers — security risk, only recommended in development (default: false)",
|
|
97
108
|
},
|
|
98
109
|
includeBody: {
|
|
99
110
|
type: "boolean",
|
|
100
|
-
description: "
|
|
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
|
|
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
|
|
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
|
|
164
|
-
includeBody: "⚠️ Security risk if true
|
|
165
|
-
maxBodyBytes: "Maximum body
|
|
166
|
-
sampleRate: "Sampling rate 0
|
|
167
|
-
slowThresholdMs: "Requests
|
|
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
|
|
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: "
|
|
246
|
-
strict: "
|
|
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
|
|
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
|
|
355
|
+
? "⚠️ passthrough mode may be vulnerable to Mass Assignment attacks. Only use with trusted, fully-validated input."
|
|
345
356
|
: normalize === "strict"
|
|
346
|
-
? "strict
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
409
|
+
type: "number (0.0–1.0)",
|
|
399
410
|
default: 1,
|
|
400
|
-
description: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: '
|
|
435
|
+
description: 'URL path patterns to exclude from logging. Example: ["/health", /^\\/static\\//]',
|
|
425
436
|
},
|
|
426
437
|
],
|
|
427
438
|
presets: {
|
|
428
|
-
devLogger: "
|
|
429
|
-
prodLogger: "
|
|
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()
|
|
487
|
-
"sink
|
|
488
|
-
"skip
|
|
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:
|
|
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
|
|
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
|
|
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);
|