@mandujs/mcp 0.18.8 → 0.18.10
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 +0 -1
- package/package.json +42 -42
- package/src/activity-monitor.ts +9 -8
- package/src/adapters/tool-adapter.ts +2 -0
- package/src/executor/error-handler.ts +268 -250
- package/src/index.ts +8 -0
- package/src/new-resources.ts +119 -0
- package/src/profiles.ts +34 -0
- package/src/prompts.ts +104 -0
- package/src/resources/handlers.ts +0 -23
- package/src/server.ts +78 -5
- package/src/tools/ate.ts +28 -0
- package/src/tools/brain.ts +56 -24
- package/src/tools/component.ts +194 -185
- package/src/tools/composite.ts +440 -0
- package/src/tools/contract.ts +58 -58
- package/src/tools/decisions.ts +270 -0
- package/src/tools/generate.ts +23 -21
- package/src/tools/guard.ts +32 -708
- package/src/tools/history.ts +24 -7
- package/src/tools/hydration.ts +40 -13
- package/src/tools/index.ts +28 -2
- package/src/tools/kitchen.ts +107 -0
- package/src/tools/negotiate.ts +263 -0
- package/src/tools/project.ts +464 -382
- package/src/tools/resource.ts +19 -2
- package/src/tools/runtime.ts +533 -508
- package/src/tools/seo.ts +446 -417
- package/src/tools/slot-validation.ts +200 -0
- package/src/tools/slot.ts +20 -21
- package/src/tools/spec.ts +45 -43
- package/src/tools/transaction.ts +55 -13
- package/src/tx-lock.ts +73 -0
- package/src/utils/project.ts +48 -9
- package/src/utils/runtime-control.ts +52 -0
- package/src/utils/withWarnings.ts +2 -1
package/src/tools/runtime.ts
CHANGED
|
@@ -1,508 +1,533 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu MCP Runtime Tools
|
|
3
|
-
* Query and manage runtime configuration: logger settings and contract normalize options.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
-
import { getProjectPaths, readJsonFile } from "../utils/project.js";
|
|
8
|
-
import { loadManifest } from "@mandujs/core";
|
|
9
|
-
import path from "path";
|
|
10
|
-
import fs from "fs/promises";
|
|
11
|
-
|
|
12
|
-
export const runtimeToolDefinitions: Tool[] = [
|
|
13
|
-
{
|
|
14
|
-
name: "
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
if (!
|
|
236
|
-
return {
|
|
237
|
-
routeId,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Mandu MCP Runtime Tools
|
|
3
|
+
* Query and manage runtime configuration: logger settings and contract normalize options.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import { getProjectPaths, readJsonFile } from "../utils/project.js";
|
|
8
|
+
import { loadManifest } from "@mandujs/core";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import fs from "fs/promises";
|
|
11
|
+
|
|
12
|
+
export const runtimeToolDefinitions: Tool[] = [
|
|
13
|
+
{
|
|
14
|
+
name: "mandu.runtime.config",
|
|
15
|
+
annotations: {
|
|
16
|
+
readOnlyHint: true,
|
|
17
|
+
},
|
|
18
|
+
description:
|
|
19
|
+
"Get the Mandu runtime configuration defaults for logger and normalize settings. " +
|
|
20
|
+
"Shows default values for every configurable option along with usage examples. " +
|
|
21
|
+
"Use this to understand the runtime before calling mandu.runtime.setNormalize " +
|
|
22
|
+
"or mandu.runtime.loggerConfig.",
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: "object",
|
|
25
|
+
properties: {},
|
|
26
|
+
required: [],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: "mandu.runtime.contractOptions",
|
|
31
|
+
annotations: {
|
|
32
|
+
readOnlyHint: true,
|
|
33
|
+
},
|
|
34
|
+
description:
|
|
35
|
+
"Read the normalize and coerceQueryParams options currently set in a specific contract file. " +
|
|
36
|
+
"These options control how incoming request data is validated and sanitized: " +
|
|
37
|
+
"'normalize' removes or blocks undefined fields (Mass Assignment protection), " +
|
|
38
|
+
"'coerceQueryParams' auto-converts URL query string values to their declared schema types (e.g., '123' → number). " +
|
|
39
|
+
"Returns the parsed values and their effect, or defaults if no explicit options are set.",
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
routeId: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "The route ID to get contract options for",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
required: ["routeId"],
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "mandu.runtime.setNormalize",
|
|
53
|
+
annotations: {
|
|
54
|
+
readOnlyHint: false,
|
|
55
|
+
},
|
|
56
|
+
description:
|
|
57
|
+
"Set the normalize mode (and optionally coerceQueryParams) in a route's contract file. " +
|
|
58
|
+
"Normalize modes: " +
|
|
59
|
+
"'strip' (default, recommended) — removes any request fields not defined in the schema, preventing Mass Assignment attacks. " +
|
|
60
|
+
"'strict' — returns HTTP 400 if the request contains any field not defined in the schema. " +
|
|
61
|
+
"'passthrough' — allows all fields through without filtering (validation only, no sanitization). " +
|
|
62
|
+
"coerceQueryParams: when true (default), auto-converts query string values to their declared schema types.",
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {
|
|
66
|
+
routeId: {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: "The route ID to update",
|
|
69
|
+
},
|
|
70
|
+
normalize: {
|
|
71
|
+
type: "string",
|
|
72
|
+
enum: ["strip", "strict", "passthrough"],
|
|
73
|
+
description:
|
|
74
|
+
"Normalize mode: 'strip' (remove undefined fields, prevents Mass Assignment), " +
|
|
75
|
+
"'strict' (return 400 on undefined fields), 'passthrough' (allow all fields through)",
|
|
76
|
+
},
|
|
77
|
+
coerceQueryParams: {
|
|
78
|
+
type: "boolean",
|
|
79
|
+
description: "Auto-convert URL query string values to schema-declared types (default: true)",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ["routeId"],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "mandu.runtime.loggerOptions",
|
|
87
|
+
annotations: {
|
|
88
|
+
readOnlyHint: true,
|
|
89
|
+
},
|
|
90
|
+
description:
|
|
91
|
+
"List all available logger configuration options with types, defaults, and descriptions. " +
|
|
92
|
+
"Covers: log format, level, header/body logging (security risk warnings), " +
|
|
93
|
+
"sampling rate, slow request threshold, redaction fields, custom sink, and skip patterns. " +
|
|
94
|
+
"Use this as a reference before calling mandu.runtime.loggerConfig.",
|
|
95
|
+
inputSchema: {
|
|
96
|
+
type: "object",
|
|
97
|
+
properties: {},
|
|
98
|
+
required: [],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "mandu.runtime.loggerConfig",
|
|
103
|
+
annotations: {
|
|
104
|
+
readOnlyHint: true,
|
|
105
|
+
idempotentHint: true,
|
|
106
|
+
},
|
|
107
|
+
description:
|
|
108
|
+
"Generate ready-to-use TypeScript logger configuration code for a specific environment. " +
|
|
109
|
+
"Returns an import statement and logger() call with environment-appropriate defaults: " +
|
|
110
|
+
"development: debug level, pretty format, higher verbosity; " +
|
|
111
|
+
"production: info level, JSON format, 10% sampling, no headers/body. " +
|
|
112
|
+
"Security note: includeHeaders and includeBody are forced to false in production regardless of input.",
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: "object",
|
|
115
|
+
properties: {
|
|
116
|
+
environment: {
|
|
117
|
+
type: "string",
|
|
118
|
+
enum: ["development", "production", "testing"],
|
|
119
|
+
description: "Target environment — determines default log level, format, and sampling rate (default: development)",
|
|
120
|
+
},
|
|
121
|
+
includeHeaders: {
|
|
122
|
+
type: "boolean",
|
|
123
|
+
description: "Log request headers — security risk, only recommended in development (default: false)",
|
|
124
|
+
},
|
|
125
|
+
includeBody: {
|
|
126
|
+
type: "boolean",
|
|
127
|
+
description: "Log request body — security risk, only recommended in development (default: false)",
|
|
128
|
+
},
|
|
129
|
+
format: {
|
|
130
|
+
type: "string",
|
|
131
|
+
enum: ["pretty", "json"],
|
|
132
|
+
description: "Log output format: 'pretty' (colored, human-readable) or 'json' (structured, for log aggregators)",
|
|
133
|
+
},
|
|
134
|
+
customRedact: {
|
|
135
|
+
type: "array",
|
|
136
|
+
items: { type: "string" },
|
|
137
|
+
description: "Additional header or field names to redact/mask from logs",
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
required: [],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
async function readFileContent(filePath: string): Promise<string | null> {
|
|
146
|
+
try {
|
|
147
|
+
return await Bun.file(filePath).text();
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function runtimeTools(projectRoot: string) {
|
|
154
|
+
const paths = getProjectPaths(projectRoot);
|
|
155
|
+
|
|
156
|
+
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
157
|
+
"mandu.runtime.config": async () => {
|
|
158
|
+
return {
|
|
159
|
+
defaults: {
|
|
160
|
+
logger: {
|
|
161
|
+
format: "pretty",
|
|
162
|
+
level: "info",
|
|
163
|
+
includeHeaders: false,
|
|
164
|
+
includeBody: false,
|
|
165
|
+
maxBodyBytes: 1024,
|
|
166
|
+
sampleRate: 1,
|
|
167
|
+
slowThresholdMs: 1000,
|
|
168
|
+
redact: [
|
|
169
|
+
"authorization",
|
|
170
|
+
"cookie",
|
|
171
|
+
"set-cookie",
|
|
172
|
+
"x-api-key",
|
|
173
|
+
"password",
|
|
174
|
+
"token",
|
|
175
|
+
"secret",
|
|
176
|
+
"bearer",
|
|
177
|
+
"credential",
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
normalize: {
|
|
181
|
+
mode: "strip",
|
|
182
|
+
coerceQueryParams: true,
|
|
183
|
+
deep: true,
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
description: {
|
|
187
|
+
logger: {
|
|
188
|
+
format: "Log output format: 'pretty' (colored, dev) or 'json' (structured, prod)",
|
|
189
|
+
level: "Minimum log level: 'debug' | 'info' | 'warn' | 'error'",
|
|
190
|
+
includeHeaders: "⚠️ Security risk if true — logs all request headers including Authorization, Cookie",
|
|
191
|
+
includeBody: "⚠️ Security risk if true — logs raw request body; may expose PII",
|
|
192
|
+
maxBodyBytes: "Maximum body bytes to log (truncates larger bodies to avoid log bloat)",
|
|
193
|
+
sampleRate: "Sampling rate 0.0–1.0 (1.0 = 100% of requests logged)",
|
|
194
|
+
slowThresholdMs: "Requests exceeding this threshold (ms) are logged at warn level with details",
|
|
195
|
+
redact: "Header/field names to mask in logs (replaces value with '[REDACTED]')",
|
|
196
|
+
},
|
|
197
|
+
normalize: {
|
|
198
|
+
mode: "strip: remove undefined fields (prevents Mass Assignment attacks), strict: return 400 on undefined fields, passthrough: allow all fields (validation only)",
|
|
199
|
+
coerceQueryParams: "Auto-convert URL query string '123' → number 123 based on schema type",
|
|
200
|
+
deep: "Apply normalization recursively to nested objects",
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
usage: {
|
|
204
|
+
logger: `import { logger, devLogger, prodLogger } from "@mandujs/core";
|
|
205
|
+
|
|
206
|
+
// Development
|
|
207
|
+
app.use(devLogger());
|
|
208
|
+
|
|
209
|
+
// Production
|
|
210
|
+
app.use(prodLogger({ sampleRate: 0.1 }));`,
|
|
211
|
+
normalize: `// In contract definition
|
|
212
|
+
export default Mandu.contract({
|
|
213
|
+
normalize: "strip", // or "strict" | "passthrough"
|
|
214
|
+
coerceQueryParams: true,
|
|
215
|
+
request: { ... },
|
|
216
|
+
response: { ... },
|
|
217
|
+
});`,
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
"mandu.runtime.contractOptions": async (args: Record<string, unknown>) => {
|
|
223
|
+
const { routeId } = args as { routeId: string };
|
|
224
|
+
|
|
225
|
+
const result = await loadManifest(paths.manifestPath);
|
|
226
|
+
if (!result.success || !result.data) {
|
|
227
|
+
return { error: result.errors };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const route = result.data.routes.find((r) => r.id === routeId);
|
|
231
|
+
if (!route) {
|
|
232
|
+
return { error: `Route not found: ${routeId}` };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!route.contractModule) {
|
|
236
|
+
return {
|
|
237
|
+
routeId,
|
|
238
|
+
hasContract: false,
|
|
239
|
+
defaults: {
|
|
240
|
+
normalize: "strip",
|
|
241
|
+
coerceQueryParams: true,
|
|
242
|
+
},
|
|
243
|
+
suggestion: `Create a contract with: mandu.contract.create({ routeId: "${routeId}" })`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Read contract file and extract options
|
|
248
|
+
const contractPath = path.join(projectRoot, route.contractModule);
|
|
249
|
+
const contractContent = await readFileContent(contractPath);
|
|
250
|
+
|
|
251
|
+
if (!contractContent) {
|
|
252
|
+
return {
|
|
253
|
+
routeId,
|
|
254
|
+
contractModule: route.contractModule,
|
|
255
|
+
error: "Contract file not found",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Parse normalize and coerceQueryParams from content
|
|
260
|
+
const normalizeMatch = contractContent.match(/normalize\s*:\s*["'](\w+)["']/);
|
|
261
|
+
const coerceMatch = contractContent.match(/coerceQueryParams\s*:\s*(true|false)/);
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
routeId,
|
|
265
|
+
contractModule: route.contractModule,
|
|
266
|
+
options: {
|
|
267
|
+
normalize: normalizeMatch?.[1] || "strip (default)",
|
|
268
|
+
coerceQueryParams: coerceMatch ? coerceMatch[1] === "true" : "true (default)",
|
|
269
|
+
},
|
|
270
|
+
explanation: {
|
|
271
|
+
normalize: {
|
|
272
|
+
strip: "Removes any request fields not defined in the schema — prevents Mass Assignment attacks (recommended default)",
|
|
273
|
+
strict: "Returns HTTP 400 if the request contains any field not defined in the schema",
|
|
274
|
+
passthrough: "Allows all fields through without filtering — validation only, no sanitization",
|
|
275
|
+
},
|
|
276
|
+
coerceQueryParams: "URL query strings are always plain strings; this option auto-converts them to the declared schema types (e.g., '42' → number, 'true' → boolean)",
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
"mandu.runtime.setNormalize": async (args: Record<string, unknown>) => {
|
|
282
|
+
const { routeId, normalize, coerceQueryParams } = args as {
|
|
283
|
+
routeId: string;
|
|
284
|
+
normalize?: "strip" | "strict" | "passthrough";
|
|
285
|
+
coerceQueryParams?: boolean;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const result = await loadManifest(paths.manifestPath);
|
|
289
|
+
if (!result.success || !result.data) {
|
|
290
|
+
return { error: result.errors };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const route = result.data.routes.find((r) => r.id === routeId);
|
|
294
|
+
if (!route) {
|
|
295
|
+
return { error: `Route not found: ${routeId}` };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (!route.contractModule) {
|
|
299
|
+
return {
|
|
300
|
+
error: "Route has no contract module",
|
|
301
|
+
suggestion: `Create a contract first: mandu.contract.create({ routeId: "${routeId}" })`,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const contractPath = path.join(projectRoot, route.contractModule);
|
|
306
|
+
let content = await readFileContent(contractPath);
|
|
307
|
+
|
|
308
|
+
if (!content) {
|
|
309
|
+
return { error: `Contract file not found: ${route.contractModule}` };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const changes: string[] = [];
|
|
313
|
+
|
|
314
|
+
// Update normalize option
|
|
315
|
+
if (normalize) {
|
|
316
|
+
if (content.includes("normalize:")) {
|
|
317
|
+
content = content.replace(
|
|
318
|
+
/normalize\s*:\s*["']\w+["']/,
|
|
319
|
+
`normalize: "${normalize}"`
|
|
320
|
+
);
|
|
321
|
+
changes.push(`normalize: "${normalize}"`);
|
|
322
|
+
} else {
|
|
323
|
+
// Add normalize option after description or tags
|
|
324
|
+
const insertPoint =
|
|
325
|
+
content.indexOf("request:") ||
|
|
326
|
+
content.indexOf("response:");
|
|
327
|
+
if (insertPoint > 0) {
|
|
328
|
+
const before = content.slice(0, insertPoint);
|
|
329
|
+
const after = content.slice(insertPoint);
|
|
330
|
+
content = before + `normalize: "${normalize}",\n ` + after;
|
|
331
|
+
changes.push(`normalize: "${normalize}" (added)`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Update coerceQueryParams option
|
|
337
|
+
if (coerceQueryParams !== undefined) {
|
|
338
|
+
if (content.includes("coerceQueryParams:")) {
|
|
339
|
+
content = content.replace(
|
|
340
|
+
/coerceQueryParams\s*:\s*(true|false)/,
|
|
341
|
+
`coerceQueryParams: ${coerceQueryParams}`
|
|
342
|
+
);
|
|
343
|
+
changes.push(`coerceQueryParams: ${coerceQueryParams}`);
|
|
344
|
+
} else if (insertAfter(content, "normalize:")) {
|
|
345
|
+
content = content.replace(
|
|
346
|
+
/(normalize\s*:\s*["']\w+["']),?/,
|
|
347
|
+
`$1,\n coerceQueryParams: ${coerceQueryParams},`
|
|
348
|
+
);
|
|
349
|
+
changes.push(`coerceQueryParams: ${coerceQueryParams} (added)`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (changes.length === 0) {
|
|
354
|
+
return {
|
|
355
|
+
success: false,
|
|
356
|
+
message: "No changes to apply",
|
|
357
|
+
currentContent: content.slice(0, 500) + "...",
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Write updated content
|
|
362
|
+
await Bun.write(contractPath, content);
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
success: true,
|
|
366
|
+
contractModule: route.contractModule,
|
|
367
|
+
changes,
|
|
368
|
+
message: `Updated ${route.contractModule}`,
|
|
369
|
+
securityNote:
|
|
370
|
+
normalize === "passthrough"
|
|
371
|
+
? "⚠️ passthrough mode may be vulnerable to Mass Assignment attacks. Only use with trusted, fully-validated input."
|
|
372
|
+
: normalize === "strict"
|
|
373
|
+
? "strict mode returns HTTP 400 if the client sends any field not defined in the contract schema."
|
|
374
|
+
: "strip mode (recommended): fields not defined in the schema are automatically removed from the request.",
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
"mandu.runtime.loggerOptions": async () => {
|
|
379
|
+
return {
|
|
380
|
+
options: [
|
|
381
|
+
{
|
|
382
|
+
name: "format",
|
|
383
|
+
type: '"pretty" | "json"',
|
|
384
|
+
default: "pretty",
|
|
385
|
+
description: "Log output format: 'pretty' (colored, human-readable for dev) or 'json' (structured, for log aggregators in prod)",
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: "level",
|
|
389
|
+
type: '"debug" | "info" | "warn" | "error"',
|
|
390
|
+
default: "info",
|
|
391
|
+
description: "Minimum log level: 'debug' (all requests with details), 'info' (standard), 'warn' (slow/suspicious only), 'error' (errors only)",
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
name: "includeHeaders",
|
|
395
|
+
type: "boolean",
|
|
396
|
+
default: false,
|
|
397
|
+
description: "⚠️ Security risk — logs all request headers including Authorization and Cookie. Only enable in development.",
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "includeBody",
|
|
401
|
+
type: "boolean",
|
|
402
|
+
default: false,
|
|
403
|
+
description: "⚠️ Security risk — logs raw request body which may contain PII or credentials. Only enable in development.",
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "maxBodyBytes",
|
|
407
|
+
type: "number",
|
|
408
|
+
default: 1024,
|
|
409
|
+
description: "Maximum bytes of request body to log (larger bodies are truncated to avoid log bloat)",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "redact",
|
|
413
|
+
type: "string[]",
|
|
414
|
+
default: '["authorization", "cookie", "password", ...]',
|
|
415
|
+
description: "Header or field names to mask in logs (values are replaced with '[REDACTED]')",
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: "requestId",
|
|
419
|
+
type: '"auto" | ((ctx) => string)',
|
|
420
|
+
default: "auto",
|
|
421
|
+
description: "Request ID generation strategy: 'auto' uses UUID or timestamp-based ID, or provide a custom function",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
name: "sampleRate",
|
|
425
|
+
type: "number (0.0–1.0)",
|
|
426
|
+
default: 1,
|
|
427
|
+
description: "Fraction of requests to log (1.0 = 100%, 0.1 = 10%). Reduce in production to control log volume.",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
name: "slowThresholdMs",
|
|
431
|
+
type: "number",
|
|
432
|
+
default: 1000,
|
|
433
|
+
description: "Requests exceeding this duration (ms) are logged at warn level with full details",
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "includeTraceOnSlow",
|
|
437
|
+
type: "boolean",
|
|
438
|
+
default: true,
|
|
439
|
+
description: "Include a timing trace report in the log entry for slow requests",
|
|
440
|
+
},
|
|
441
|
+
{
|
|
442
|
+
name: "sink",
|
|
443
|
+
type: "(entry: LogEntry) => void",
|
|
444
|
+
default: "console",
|
|
445
|
+
description: "Custom log output handler — use for integrating with Pino, CloudWatch, Datadog, etc.",
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
name: "skip",
|
|
449
|
+
type: "(string | RegExp)[]",
|
|
450
|
+
default: "[]",
|
|
451
|
+
description: 'URL path patterns to exclude from logging. Example: ["/health", /^\\/static\\//]',
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
presets: {
|
|
455
|
+
devLogger: "Development preset: debug level, pretty format, detailed output",
|
|
456
|
+
prodLogger: "Production preset: info level, JSON format, no headers/body logging",
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
},
|
|
460
|
+
|
|
461
|
+
"mandu.runtime.loggerConfig": async (args: Record<string, unknown>) => {
|
|
462
|
+
const {
|
|
463
|
+
environment = "development",
|
|
464
|
+
includeHeaders = false,
|
|
465
|
+
includeBody = false,
|
|
466
|
+
format,
|
|
467
|
+
customRedact = [],
|
|
468
|
+
} = args as {
|
|
469
|
+
environment?: "development" | "production" | "testing";
|
|
470
|
+
includeHeaders?: boolean;
|
|
471
|
+
includeBody?: boolean;
|
|
472
|
+
format?: "pretty" | "json";
|
|
473
|
+
customRedact?: string[];
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
const isDev = environment === "development";
|
|
477
|
+
const isProd = environment === "production";
|
|
478
|
+
|
|
479
|
+
const config = {
|
|
480
|
+
format: format || (isDev ? "pretty" : "json"),
|
|
481
|
+
level: isDev ? "debug" : "info",
|
|
482
|
+
includeHeaders: isDev ? includeHeaders : false,
|
|
483
|
+
includeBody: isDev ? includeBody : false,
|
|
484
|
+
maxBodyBytes: 1024,
|
|
485
|
+
sampleRate: isProd ? 0.1 : 1,
|
|
486
|
+
slowThresholdMs: isDev ? 500 : 1000,
|
|
487
|
+
...(customRedact.length > 0 && { redact: customRedact }),
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const code = `import { logger } from "@mandujs/core";
|
|
491
|
+
|
|
492
|
+
// ${environment} environment logger configuration
|
|
493
|
+
export const appLogger = logger(${JSON.stringify(config, null, 2)});
|
|
494
|
+
|
|
495
|
+
// Usage in your app:
|
|
496
|
+
// app.use(appLogger);
|
|
497
|
+
`;
|
|
498
|
+
|
|
499
|
+
const warnings: string[] = [];
|
|
500
|
+
if (includeHeaders && isProd) {
|
|
501
|
+
warnings.push("⚠️ includeHeaders: true in production may expose sensitive Authorization, Cookie, and API key headers in logs.");
|
|
502
|
+
}
|
|
503
|
+
if (includeBody && isProd) {
|
|
504
|
+
warnings.push("⚠️ includeBody: true in production may expose PII, passwords, or credentials in logs.");
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
environment,
|
|
509
|
+
config,
|
|
510
|
+
code,
|
|
511
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
512
|
+
tips: [
|
|
513
|
+
"You can also use the devLogger() or prodLogger() preset helpers for quick setup.",
|
|
514
|
+
"Use the 'sink' option to integrate with external systems like Pino, CloudWatch, or Datadog.",
|
|
515
|
+
"Use the 'skip' option to exclude health check and static asset paths (e.g., ['/health', '/metrics']).",
|
|
516
|
+
],
|
|
517
|
+
};
|
|
518
|
+
},
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
// Backward-compatible aliases (deprecated)
|
|
522
|
+
handlers["mandu_get_runtime_config"] = handlers["mandu.runtime.config"];
|
|
523
|
+
handlers["mandu_set_contract_normalize"] = handlers["mandu.runtime.setNormalize"];
|
|
524
|
+
handlers["mandu_get_contract_options"] = handlers["mandu.runtime.contractOptions"];
|
|
525
|
+
handlers["mandu_list_logger_options"] = handlers["mandu.runtime.loggerOptions"];
|
|
526
|
+
handlers["mandu_generate_logger_config"] = handlers["mandu.runtime.loggerConfig"];
|
|
527
|
+
|
|
528
|
+
return handlers;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function insertAfter(content: string, search: string): boolean {
|
|
532
|
+
return content.includes(search);
|
|
533
|
+
}
|