@mandujs/mcp 0.13.0 → 0.16.0

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.
Files changed (136) hide show
  1. package/README.md +6 -7
  2. package/package.json +3 -2
  3. package/src/adapters/index.ts +20 -20
  4. package/src/adapters/monitor-adapter.ts +100 -100
  5. package/src/adapters/tool-adapter.ts +88 -88
  6. package/src/executor/error-handler.ts +250 -250
  7. package/src/executor/index.ts +22 -22
  8. package/src/executor/tool-executor.ts +148 -148
  9. package/src/hooks/config-watcher.ts +174 -174
  10. package/src/hooks/index.ts +23 -23
  11. package/src/hooks/mcp-hooks.ts +227 -227
  12. package/src/logging/index.ts +15 -15
  13. package/src/logging/mcp-transport.ts +134 -134
  14. package/src/registry/index.ts +13 -13
  15. package/src/registry/mcp-tool-registry.ts +298 -298
  16. package/src/resources/skills/guides.ts +1136 -1136
  17. package/src/resources/skills/index.ts +12 -12
  18. package/src/resources/skills/loader.ts +218 -218
  19. package/src/resources/skills/mandu-composition/SKILL.md +91 -91
  20. package/src/resources/skills/mandu-composition/metadata.json +13 -13
  21. package/src/resources/skills/mandu-composition/rules/_sections.md +26 -26
  22. package/src/resources/skills/mandu-composition/rules/_template.md +77 -77
  23. package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -146
  24. package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -164
  25. package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -161
  26. package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -167
  27. package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -149
  28. package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -148
  29. package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -150
  30. package/src/resources/skills/mandu-deployment/SKILL.md +92 -92
  31. package/src/resources/skills/mandu-deployment/_sections.md +41 -41
  32. package/src/resources/skills/mandu-deployment/_template.md +38 -38
  33. package/src/resources/skills/mandu-deployment/metadata.json +13 -13
  34. package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -109
  35. package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -115
  36. package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -219
  37. package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -150
  38. package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -223
  39. package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -152
  40. package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -179
  41. package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +323 -323
  42. package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -140
  43. package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -82
  44. package/src/resources/skills/mandu-fs-routes/metadata.json +12 -12
  45. package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -36
  46. package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -69
  47. package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -65
  48. package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -93
  49. package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -55
  50. package/src/resources/skills/mandu-guard/SKILL.md +129 -129
  51. package/src/resources/skills/mandu-guard/metadata.json +12 -12
  52. package/src/resources/skills/mandu-guard/rules/_sections.md +36 -36
  53. package/src/resources/skills/mandu-guard/rules/_template.md +82 -82
  54. package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -100
  55. package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -76
  56. package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -81
  57. package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -80
  58. package/src/resources/skills/mandu-hydration/SKILL.md +91 -91
  59. package/src/resources/skills/mandu-hydration/metadata.json +12 -12
  60. package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -31
  61. package/src/resources/skills/mandu-hydration/rules/_template.md +72 -72
  62. package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -109
  63. package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -55
  64. package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -113
  65. package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -68
  66. package/src/resources/skills/mandu-performance/SKILL.md +85 -85
  67. package/src/resources/skills/mandu-performance/metadata.json +14 -14
  68. package/src/resources/skills/mandu-performance/rules/_sections.md +31 -31
  69. package/src/resources/skills/mandu-performance/rules/_template.md +64 -64
  70. package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -103
  71. package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -95
  72. package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -124
  73. package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -125
  74. package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -80
  75. package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -145
  76. package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -98
  77. package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -154
  78. package/src/resources/skills/mandu-security/SKILL.md +87 -87
  79. package/src/resources/skills/mandu-security/metadata.json +13 -13
  80. package/src/resources/skills/mandu-security/rules/_sections.md +31 -31
  81. package/src/resources/skills/mandu-security/rules/_template.md +74 -74
  82. package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -127
  83. package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -133
  84. package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -148
  85. package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -146
  86. package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -138
  87. package/src/resources/skills/mandu-slot/SKILL.md +85 -85
  88. package/src/resources/skills/mandu-slot/metadata.json +12 -12
  89. package/src/resources/skills/mandu-slot/rules/_sections.md +36 -36
  90. package/src/resources/skills/mandu-slot/rules/_template.md +63 -63
  91. package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -38
  92. package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -56
  93. package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -59
  94. package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -64
  95. package/src/resources/skills/mandu-styling/SKILL.md +154 -154
  96. package/src/resources/skills/mandu-styling/_sections.md +43 -43
  97. package/src/resources/skills/mandu-styling/_template.md +32 -32
  98. package/src/resources/skills/mandu-styling/metadata.json +15 -15
  99. package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -235
  100. package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -255
  101. package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -205
  102. package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -272
  103. package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -167
  104. package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -221
  105. package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -209
  106. package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -192
  107. package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -162
  108. package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -164
  109. package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +170 -170
  110. package/src/resources/skills/mandu-styling/rules/style-tailwind-v4-gotchas.md +179 -179
  111. package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -229
  112. package/src/resources/skills/mandu-testing/SKILL.md +99 -99
  113. package/src/resources/skills/mandu-testing/metadata.json +13 -13
  114. package/src/resources/skills/mandu-testing/rules/_sections.md +26 -26
  115. package/src/resources/skills/mandu-testing/rules/_template.md +65 -65
  116. package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -195
  117. package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -196
  118. package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -219
  119. package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -192
  120. package/src/resources/skills/mandu-ui/SKILL.md +117 -117
  121. package/src/resources/skills/mandu-ui/_sections.md +23 -23
  122. package/src/resources/skills/mandu-ui/_template.md +32 -32
  123. package/src/resources/skills/mandu-ui/metadata.json +13 -13
  124. package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -232
  125. package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -238
  126. package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -259
  127. package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -258
  128. package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -213
  129. package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -209
  130. package/src/resources/skills/recipes.ts +932 -932
  131. package/src/tools/ate.ts +129 -0
  132. package/src/tools/index.ts +4 -1
  133. package/src/tools/project.ts +334 -334
  134. package/src/tools/runtime.ts +497 -497
  135. package/src/tools/seo.ts +417 -417
  136. package/src/utils/withWarnings.ts +83 -83
@@ -1,497 +1,497 @@
1
- /**
2
- * Mandu MCP Runtime Tools
3
- * Runtime 설정 조회 및 관리 도구
4
- *
5
- * - Logger 설정 조회/변경
6
- * - Normalize 설정 조회
7
- * - Contract 옵션 확인
8
- */
9
-
10
- import type { Tool } from "@modelcontextprotocol/sdk/types.js";
11
- import { getProjectPaths, readJsonFile } from "../utils/project.js";
12
- import { loadManifest } from "@mandujs/core";
13
- import path from "path";
14
- import fs from "fs/promises";
15
-
16
- export const runtimeToolDefinitions: Tool[] = [
17
- {
18
- name: "mandu_get_runtime_config",
19
- description:
20
- "Get current runtime configuration including logger and normalize settings. " +
21
- "Shows default values and any overrides from contracts.",
22
- inputSchema: {
23
- type: "object",
24
- properties: {},
25
- required: [],
26
- },
27
- },
28
- {
29
- name: "mandu_get_contract_options",
30
- description:
31
- "Get normalize and coerce options for a specific contract. " +
32
- "These options control how request data is sanitized and type-converted.",
33
- inputSchema: {
34
- type: "object",
35
- properties: {
36
- routeId: {
37
- type: "string",
38
- description: "The route ID to get contract options for",
39
- },
40
- },
41
- required: ["routeId"],
42
- },
43
- },
44
- {
45
- name: "mandu_set_contract_normalize",
46
- description:
47
- "Set normalize mode for a contract. " +
48
- "Modes: 'strip' (remove undefined fields, default), 'strict' (error on undefined), 'passthrough' (allow all).",
49
- inputSchema: {
50
- type: "object",
51
- properties: {
52
- routeId: {
53
- type: "string",
54
- description: "The route ID to update",
55
- },
56
- normalize: {
57
- type: "string",
58
- enum: ["strip", "strict", "passthrough"],
59
- description:
60
- "Normalize mode: strip (Mass Assignment 방지), strict (에러 발생), passthrough (모두 허용)",
61
- },
62
- coerceQueryParams: {
63
- type: "boolean",
64
- description: "Whether to auto-convert query string types (default: true)",
65
- },
66
- },
67
- required: ["routeId"],
68
- },
69
- },
70
- {
71
- name: "mandu_list_logger_options",
72
- description:
73
- "List available logger configuration options and their descriptions. " +
74
- "Useful for understanding how to configure logging in Mandu.",
75
- inputSchema: {
76
- type: "object",
77
- properties: {},
78
- required: [],
79
- },
80
- },
81
- {
82
- name: "mandu_generate_logger_config",
83
- description:
84
- "Generate logger configuration code based on requirements. " +
85
- "Returns TypeScript code that can be added to the project.",
86
- inputSchema: {
87
- type: "object",
88
- properties: {
89
- environment: {
90
- type: "string",
91
- enum: ["development", "production", "testing"],
92
- description: "Target environment (default: development)",
93
- },
94
- includeHeaders: {
95
- type: "boolean",
96
- description: "Whether to log request headers (default: false for security)",
97
- },
98
- includeBody: {
99
- type: "boolean",
100
- description: "Whether to log request body (default: false for security)",
101
- },
102
- format: {
103
- type: "string",
104
- enum: ["pretty", "json"],
105
- description: "Log output format (pretty for dev, json for prod)",
106
- },
107
- customRedact: {
108
- type: "array",
109
- items: { type: "string" },
110
- description: "Additional fields to redact from logs",
111
- },
112
- },
113
- required: [],
114
- },
115
- },
116
- ];
117
-
118
- async function readFileContent(filePath: string): Promise<string | null> {
119
- try {
120
- return await Bun.file(filePath).text();
121
- } catch {
122
- return null;
123
- }
124
- }
125
-
126
- export function runtimeTools(projectRoot: string) {
127
- const paths = getProjectPaths(projectRoot);
128
-
129
- return {
130
- mandu_get_runtime_config: async () => {
131
- return {
132
- defaults: {
133
- logger: {
134
- format: "pretty",
135
- level: "info",
136
- includeHeaders: false,
137
- includeBody: false,
138
- maxBodyBytes: 1024,
139
- sampleRate: 1,
140
- slowThresholdMs: 1000,
141
- redact: [
142
- "authorization",
143
- "cookie",
144
- "set-cookie",
145
- "x-api-key",
146
- "password",
147
- "token",
148
- "secret",
149
- "bearer",
150
- "credential",
151
- ],
152
- },
153
- normalize: {
154
- mode: "strip",
155
- coerceQueryParams: true,
156
- deep: true,
157
- },
158
- },
159
- description: {
160
- logger: {
161
- format: "Log output format: 'pretty' (colored, dev) or 'json' (structured, prod)",
162
- 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",
169
- },
170
- 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",
174
- },
175
- },
176
- usage: {
177
- logger: `import { logger, devLogger, prodLogger } from "@mandujs/core";
178
-
179
- // Development
180
- app.use(devLogger());
181
-
182
- // Production
183
- app.use(prodLogger({ sampleRate: 0.1 }));`,
184
- normalize: `// In contract definition
185
- export default Mandu.contract({
186
- normalize: "strip", // or "strict" | "passthrough"
187
- coerceQueryParams: true,
188
- request: { ... },
189
- response: { ... },
190
- });`,
191
- },
192
- };
193
- },
194
-
195
- mandu_get_contract_options: async (args: Record<string, unknown>) => {
196
- const { routeId } = args as { routeId: string };
197
-
198
- const result = await loadManifest(paths.manifestPath);
199
- if (!result.success || !result.data) {
200
- return { error: result.errors };
201
- }
202
-
203
- const route = result.data.routes.find((r) => r.id === routeId);
204
- if (!route) {
205
- return { error: `Route not found: ${routeId}` };
206
- }
207
-
208
- if (!route.contractModule) {
209
- return {
210
- routeId,
211
- hasContract: false,
212
- defaults: {
213
- normalize: "strip",
214
- coerceQueryParams: true,
215
- },
216
- suggestion: `Create a contract with: mandu_create_contract({ routeId: "${routeId}" })`,
217
- };
218
- }
219
-
220
- // Read contract file and extract options
221
- const contractPath = path.join(projectRoot, route.contractModule);
222
- const contractContent = await readFileContent(contractPath);
223
-
224
- if (!contractContent) {
225
- return {
226
- routeId,
227
- contractModule: route.contractModule,
228
- error: "Contract file not found",
229
- };
230
- }
231
-
232
- // Parse normalize and coerceQueryParams from content
233
- const normalizeMatch = contractContent.match(/normalize\s*:\s*["'](\w+)["']/);
234
- const coerceMatch = contractContent.match(/coerceQueryParams\s*:\s*(true|false)/);
235
-
236
- return {
237
- routeId,
238
- contractModule: route.contractModule,
239
- options: {
240
- normalize: normalizeMatch?.[1] || "strip (default)",
241
- coerceQueryParams: coerceMatch ? coerceMatch[1] === "true" : "true (default)",
242
- },
243
- explanation: {
244
- normalize: {
245
- strip: "정의되지 않은 필드 제거 (Mass Assignment 공격 방지)",
246
- strict: "정의되지 않은 필드 있으면 400 에러",
247
- passthrough: "모든 필드 허용 (검증만, 필터링 안 함)",
248
- },
249
- coerceQueryParams: "URL query string은 항상 문자열이므로, 스키마 타입으로 자동 변환",
250
- },
251
- };
252
- },
253
-
254
- mandu_set_contract_normalize: async (args: Record<string, unknown>) => {
255
- const { routeId, normalize, coerceQueryParams } = args as {
256
- routeId: string;
257
- normalize?: "strip" | "strict" | "passthrough";
258
- coerceQueryParams?: boolean;
259
- };
260
-
261
- const result = await loadManifest(paths.manifestPath);
262
- if (!result.success || !result.data) {
263
- return { error: result.errors };
264
- }
265
-
266
- const route = result.data.routes.find((r) => r.id === routeId);
267
- if (!route) {
268
- return { error: `Route not found: ${routeId}` };
269
- }
270
-
271
- if (!route.contractModule) {
272
- return {
273
- error: "Route has no contract module",
274
- suggestion: `Create a contract first: mandu_create_contract({ routeId: "${routeId}" })`,
275
- };
276
- }
277
-
278
- const contractPath = path.join(projectRoot, route.contractModule);
279
- let content = await readFileContent(contractPath);
280
-
281
- if (!content) {
282
- return { error: `Contract file not found: ${route.contractModule}` };
283
- }
284
-
285
- const changes: string[] = [];
286
-
287
- // Update normalize option
288
- if (normalize) {
289
- if (content.includes("normalize:")) {
290
- content = content.replace(
291
- /normalize\s*:\s*["']\w+["']/,
292
- `normalize: "${normalize}"`
293
- );
294
- changes.push(`normalize: "${normalize}"`);
295
- } else {
296
- // Add normalize option after description or tags
297
- const insertPoint =
298
- content.indexOf("request:") ||
299
- content.indexOf("response:");
300
- if (insertPoint > 0) {
301
- const before = content.slice(0, insertPoint);
302
- const after = content.slice(insertPoint);
303
- content = before + `normalize: "${normalize}",\n ` + after;
304
- changes.push(`normalize: "${normalize}" (added)`);
305
- }
306
- }
307
- }
308
-
309
- // Update coerceQueryParams option
310
- if (coerceQueryParams !== undefined) {
311
- if (content.includes("coerceQueryParams:")) {
312
- content = content.replace(
313
- /coerceQueryParams\s*:\s*(true|false)/,
314
- `coerceQueryParams: ${coerceQueryParams}`
315
- );
316
- changes.push(`coerceQueryParams: ${coerceQueryParams}`);
317
- } else if (insertAfter(content, "normalize:")) {
318
- content = content.replace(
319
- /(normalize\s*:\s*["']\w+["']),?/,
320
- `$1,\n coerceQueryParams: ${coerceQueryParams},`
321
- );
322
- changes.push(`coerceQueryParams: ${coerceQueryParams} (added)`);
323
- }
324
- }
325
-
326
- if (changes.length === 0) {
327
- return {
328
- success: false,
329
- message: "No changes to apply",
330
- currentContent: content.slice(0, 500) + "...",
331
- };
332
- }
333
-
334
- // Write updated content
335
- await Bun.write(contractPath, content);
336
-
337
- return {
338
- success: true,
339
- contractModule: route.contractModule,
340
- changes,
341
- message: `Updated ${route.contractModule}`,
342
- securityNote:
343
- normalize === "passthrough"
344
- ? "⚠️ passthrough 모드는 Mass Assignment 공격에 취약할 수 있습니다. 신뢰할 수 있는 입력에만 사용하세요."
345
- : normalize === "strict"
346
- ? "strict 모드는 클라이언트가 추가 필드를 보내면 400 에러를 반환합니다."
347
- : "strip 모드 (권장): 정의되지 않은 필드는 자동 제거됩니다.",
348
- };
349
- },
350
-
351
- mandu_list_logger_options: async () => {
352
- return {
353
- options: [
354
- {
355
- name: "format",
356
- type: '"pretty" | "json"',
357
- default: "pretty",
358
- description: "로그 출력 형식. pretty는 개발용 컬러 출력, json은 운영용 구조화 로그",
359
- },
360
- {
361
- name: "level",
362
- type: '"debug" | "info" | "warn" | "error"',
363
- default: "info",
364
- description: "최소 로그 레벨. debug는 모든 요청 상세, error는 에러만",
365
- },
366
- {
367
- name: "includeHeaders",
368
- type: "boolean",
369
- default: false,
370
- description: "⚠️ 요청 헤더 로깅. 민감 정보 노출 위험",
371
- },
372
- {
373
- name: "includeBody",
374
- type: "boolean",
375
- default: false,
376
- description: "⚠️ 요청 바디 로깅. 민감 정보 노출 + 스트림 문제",
377
- },
378
- {
379
- name: "maxBodyBytes",
380
- type: "number",
381
- default: 1024,
382
- description: "바디 로깅 시 최대 크기 (초과분 truncate)",
383
- },
384
- {
385
- name: "redact",
386
- type: "string[]",
387
- default: '["authorization", "cookie", "password", ...]',
388
- description: "로그에서 마스킹할 헤더/필드명",
389
- },
390
- {
391
- name: "requestId",
392
- type: '"auto" | ((ctx) => string)',
393
- default: "auto",
394
- description: "요청 ID 생성 방식. auto는 UUID 또는 타임스탬프 기반",
395
- },
396
- {
397
- name: "sampleRate",
398
- type: "number (0-1)",
399
- default: 1,
400
- description: "샘플링 비율. 운영에서 로그 양 조절 (0.1 = 10%)",
401
- },
402
- {
403
- name: "slowThresholdMs",
404
- type: "number",
405
- default: 1000,
406
- description: "느린 요청 임계값. 초과 시 warn 레벨로 상세 출력",
407
- },
408
- {
409
- name: "includeTraceOnSlow",
410
- type: "boolean",
411
- default: true,
412
- description: "느린 요청에 Trace 리포트 포함",
413
- },
414
- {
415
- name: "sink",
416
- type: "(entry: LogEntry) => void",
417
- default: "console",
418
- description: "커스텀 로그 출력 (Pino, CloudWatch 등 연동)",
419
- },
420
- {
421
- name: "skip",
422
- type: "(string | RegExp)[]",
423
- default: "[]",
424
- description: '로깅 제외 경로 패턴. 예: ["/health", /^\\/static\\//]',
425
- },
426
- ],
427
- presets: {
428
- devLogger: "개발용: debug 레벨, pretty 포맷, 헤더 포함",
429
- prodLogger: "운영용: info 레벨, json 포맷, 헤더/바디 미포함",
430
- },
431
- };
432
- },
433
-
434
- mandu_generate_logger_config: async (args: Record<string, unknown>) => {
435
- const {
436
- environment = "development",
437
- includeHeaders = false,
438
- includeBody = false,
439
- format,
440
- customRedact = [],
441
- } = args as {
442
- environment?: "development" | "production" | "testing";
443
- includeHeaders?: boolean;
444
- includeBody?: boolean;
445
- format?: "pretty" | "json";
446
- customRedact?: string[];
447
- };
448
-
449
- const isDev = environment === "development";
450
- const isProd = environment === "production";
451
-
452
- const config = {
453
- format: format || (isDev ? "pretty" : "json"),
454
- level: isDev ? "debug" : "info",
455
- includeHeaders: isDev ? includeHeaders : false,
456
- includeBody: isDev ? includeBody : false,
457
- maxBodyBytes: 1024,
458
- sampleRate: isProd ? 0.1 : 1,
459
- slowThresholdMs: isDev ? 500 : 1000,
460
- ...(customRedact.length > 0 && { redact: customRedact }),
461
- };
462
-
463
- const code = `import { logger } from "@mandujs/core";
464
-
465
- // ${environment} environment logger configuration
466
- export const appLogger = logger(${JSON.stringify(config, null, 2)});
467
-
468
- // Usage in your app:
469
- // app.use(appLogger);
470
- `;
471
-
472
- const warnings: string[] = [];
473
- if (includeHeaders && isProd) {
474
- warnings.push("⚠️ includeHeaders: true는 프로덕션에서 민감 정보 노출 위험이 있습니다.");
475
- }
476
- if (includeBody && isProd) {
477
- warnings.push("⚠️ includeBody: true는 프로덕션에서 민감 정보 노출 위험이 있습니다.");
478
- }
479
-
480
- return {
481
- environment,
482
- config,
483
- code,
484
- warnings: warnings.length > 0 ? warnings : undefined,
485
- tips: [
486
- "devLogger() 또는 prodLogger() 프리셋을 사용할 수도 있습니다.",
487
- "sink 옵션으로 Pino, CloudWatch 등 외부 시스템 연동 가능",
488
- "skip 옵션으로 /health, /metrics 등 제외 가능",
489
- ],
490
- };
491
- },
492
- };
493
- }
494
-
495
- function insertAfter(content: string, search: string): boolean {
496
- return content.includes(search);
497
- }
1
+ /**
2
+ * Mandu MCP Runtime Tools
3
+ * Runtime 설정 조회 및 관리 도구
4
+ *
5
+ * - Logger 설정 조회/변경
6
+ * - Normalize 설정 조회
7
+ * - Contract 옵션 확인
8
+ */
9
+
10
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
11
+ import { getProjectPaths, readJsonFile } from "../utils/project.js";
12
+ import { loadManifest } from "@mandujs/core";
13
+ import path from "path";
14
+ import fs from "fs/promises";
15
+
16
+ export const runtimeToolDefinitions: Tool[] = [
17
+ {
18
+ name: "mandu_get_runtime_config",
19
+ description:
20
+ "Get current runtime configuration including logger and normalize settings. " +
21
+ "Shows default values and any overrides from contracts.",
22
+ inputSchema: {
23
+ type: "object",
24
+ properties: {},
25
+ required: [],
26
+ },
27
+ },
28
+ {
29
+ name: "mandu_get_contract_options",
30
+ description:
31
+ "Get normalize and coerce options for a specific contract. " +
32
+ "These options control how request data is sanitized and type-converted.",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ routeId: {
37
+ type: "string",
38
+ description: "The route ID to get contract options for",
39
+ },
40
+ },
41
+ required: ["routeId"],
42
+ },
43
+ },
44
+ {
45
+ name: "mandu_set_contract_normalize",
46
+ description:
47
+ "Set normalize mode for a contract. " +
48
+ "Modes: 'strip' (remove undefined fields, default), 'strict' (error on undefined), 'passthrough' (allow all).",
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {
52
+ routeId: {
53
+ type: "string",
54
+ description: "The route ID to update",
55
+ },
56
+ normalize: {
57
+ type: "string",
58
+ enum: ["strip", "strict", "passthrough"],
59
+ description:
60
+ "Normalize mode: strip (Mass Assignment 방지), strict (에러 발생), passthrough (모두 허용)",
61
+ },
62
+ coerceQueryParams: {
63
+ type: "boolean",
64
+ description: "Whether to auto-convert query string types (default: true)",
65
+ },
66
+ },
67
+ required: ["routeId"],
68
+ },
69
+ },
70
+ {
71
+ name: "mandu_list_logger_options",
72
+ description:
73
+ "List available logger configuration options and their descriptions. " +
74
+ "Useful for understanding how to configure logging in Mandu.",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {},
78
+ required: [],
79
+ },
80
+ },
81
+ {
82
+ name: "mandu_generate_logger_config",
83
+ description:
84
+ "Generate logger configuration code based on requirements. " +
85
+ "Returns TypeScript code that can be added to the project.",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ environment: {
90
+ type: "string",
91
+ enum: ["development", "production", "testing"],
92
+ description: "Target environment (default: development)",
93
+ },
94
+ includeHeaders: {
95
+ type: "boolean",
96
+ description: "Whether to log request headers (default: false for security)",
97
+ },
98
+ includeBody: {
99
+ type: "boolean",
100
+ description: "Whether to log request body (default: false for security)",
101
+ },
102
+ format: {
103
+ type: "string",
104
+ enum: ["pretty", "json"],
105
+ description: "Log output format (pretty for dev, json for prod)",
106
+ },
107
+ customRedact: {
108
+ type: "array",
109
+ items: { type: "string" },
110
+ description: "Additional fields to redact from logs",
111
+ },
112
+ },
113
+ required: [],
114
+ },
115
+ },
116
+ ];
117
+
118
+ async function readFileContent(filePath: string): Promise<string | null> {
119
+ try {
120
+ return await Bun.file(filePath).text();
121
+ } catch {
122
+ return null;
123
+ }
124
+ }
125
+
126
+ export function runtimeTools(projectRoot: string) {
127
+ const paths = getProjectPaths(projectRoot);
128
+
129
+ return {
130
+ mandu_get_runtime_config: async () => {
131
+ return {
132
+ defaults: {
133
+ logger: {
134
+ format: "pretty",
135
+ level: "info",
136
+ includeHeaders: false,
137
+ includeBody: false,
138
+ maxBodyBytes: 1024,
139
+ sampleRate: 1,
140
+ slowThresholdMs: 1000,
141
+ redact: [
142
+ "authorization",
143
+ "cookie",
144
+ "set-cookie",
145
+ "x-api-key",
146
+ "password",
147
+ "token",
148
+ "secret",
149
+ "bearer",
150
+ "credential",
151
+ ],
152
+ },
153
+ normalize: {
154
+ mode: "strip",
155
+ coerceQueryParams: true,
156
+ deep: true,
157
+ },
158
+ },
159
+ description: {
160
+ logger: {
161
+ format: "Log output format: 'pretty' (colored, dev) or 'json' (structured, prod)",
162
+ 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",
169
+ },
170
+ 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",
174
+ },
175
+ },
176
+ usage: {
177
+ logger: `import { logger, devLogger, prodLogger } from "@mandujs/core";
178
+
179
+ // Development
180
+ app.use(devLogger());
181
+
182
+ // Production
183
+ app.use(prodLogger({ sampleRate: 0.1 }));`,
184
+ normalize: `// In contract definition
185
+ export default Mandu.contract({
186
+ normalize: "strip", // or "strict" | "passthrough"
187
+ coerceQueryParams: true,
188
+ request: { ... },
189
+ response: { ... },
190
+ });`,
191
+ },
192
+ };
193
+ },
194
+
195
+ mandu_get_contract_options: async (args: Record<string, unknown>) => {
196
+ const { routeId } = args as { routeId: string };
197
+
198
+ const result = await loadManifest(paths.manifestPath);
199
+ if (!result.success || !result.data) {
200
+ return { error: result.errors };
201
+ }
202
+
203
+ const route = result.data.routes.find((r) => r.id === routeId);
204
+ if (!route) {
205
+ return { error: `Route not found: ${routeId}` };
206
+ }
207
+
208
+ if (!route.contractModule) {
209
+ return {
210
+ routeId,
211
+ hasContract: false,
212
+ defaults: {
213
+ normalize: "strip",
214
+ coerceQueryParams: true,
215
+ },
216
+ suggestion: `Create a contract with: mandu_create_contract({ routeId: "${routeId}" })`,
217
+ };
218
+ }
219
+
220
+ // Read contract file and extract options
221
+ const contractPath = path.join(projectRoot, route.contractModule);
222
+ const contractContent = await readFileContent(contractPath);
223
+
224
+ if (!contractContent) {
225
+ return {
226
+ routeId,
227
+ contractModule: route.contractModule,
228
+ error: "Contract file not found",
229
+ };
230
+ }
231
+
232
+ // Parse normalize and coerceQueryParams from content
233
+ const normalizeMatch = contractContent.match(/normalize\s*:\s*["'](\w+)["']/);
234
+ const coerceMatch = contractContent.match(/coerceQueryParams\s*:\s*(true|false)/);
235
+
236
+ return {
237
+ routeId,
238
+ contractModule: route.contractModule,
239
+ options: {
240
+ normalize: normalizeMatch?.[1] || "strip (default)",
241
+ coerceQueryParams: coerceMatch ? coerceMatch[1] === "true" : "true (default)",
242
+ },
243
+ explanation: {
244
+ normalize: {
245
+ strip: "정의되지 않은 필드 제거 (Mass Assignment 공격 방지)",
246
+ strict: "정의되지 않은 필드 있으면 400 에러",
247
+ passthrough: "모든 필드 허용 (검증만, 필터링 안 함)",
248
+ },
249
+ coerceQueryParams: "URL query string은 항상 문자열이므로, 스키마 타입으로 자동 변환",
250
+ },
251
+ };
252
+ },
253
+
254
+ mandu_set_contract_normalize: async (args: Record<string, unknown>) => {
255
+ const { routeId, normalize, coerceQueryParams } = args as {
256
+ routeId: string;
257
+ normalize?: "strip" | "strict" | "passthrough";
258
+ coerceQueryParams?: boolean;
259
+ };
260
+
261
+ const result = await loadManifest(paths.manifestPath);
262
+ if (!result.success || !result.data) {
263
+ return { error: result.errors };
264
+ }
265
+
266
+ const route = result.data.routes.find((r) => r.id === routeId);
267
+ if (!route) {
268
+ return { error: `Route not found: ${routeId}` };
269
+ }
270
+
271
+ if (!route.contractModule) {
272
+ return {
273
+ error: "Route has no contract module",
274
+ suggestion: `Create a contract first: mandu_create_contract({ routeId: "${routeId}" })`,
275
+ };
276
+ }
277
+
278
+ const contractPath = path.join(projectRoot, route.contractModule);
279
+ let content = await readFileContent(contractPath);
280
+
281
+ if (!content) {
282
+ return { error: `Contract file not found: ${route.contractModule}` };
283
+ }
284
+
285
+ const changes: string[] = [];
286
+
287
+ // Update normalize option
288
+ if (normalize) {
289
+ if (content.includes("normalize:")) {
290
+ content = content.replace(
291
+ /normalize\s*:\s*["']\w+["']/,
292
+ `normalize: "${normalize}"`
293
+ );
294
+ changes.push(`normalize: "${normalize}"`);
295
+ } else {
296
+ // Add normalize option after description or tags
297
+ const insertPoint =
298
+ content.indexOf("request:") ||
299
+ content.indexOf("response:");
300
+ if (insertPoint > 0) {
301
+ const before = content.slice(0, insertPoint);
302
+ const after = content.slice(insertPoint);
303
+ content = before + `normalize: "${normalize}",\n ` + after;
304
+ changes.push(`normalize: "${normalize}" (added)`);
305
+ }
306
+ }
307
+ }
308
+
309
+ // Update coerceQueryParams option
310
+ if (coerceQueryParams !== undefined) {
311
+ if (content.includes("coerceQueryParams:")) {
312
+ content = content.replace(
313
+ /coerceQueryParams\s*:\s*(true|false)/,
314
+ `coerceQueryParams: ${coerceQueryParams}`
315
+ );
316
+ changes.push(`coerceQueryParams: ${coerceQueryParams}`);
317
+ } else if (insertAfter(content, "normalize:")) {
318
+ content = content.replace(
319
+ /(normalize\s*:\s*["']\w+["']),?/,
320
+ `$1,\n coerceQueryParams: ${coerceQueryParams},`
321
+ );
322
+ changes.push(`coerceQueryParams: ${coerceQueryParams} (added)`);
323
+ }
324
+ }
325
+
326
+ if (changes.length === 0) {
327
+ return {
328
+ success: false,
329
+ message: "No changes to apply",
330
+ currentContent: content.slice(0, 500) + "...",
331
+ };
332
+ }
333
+
334
+ // Write updated content
335
+ await Bun.write(contractPath, content);
336
+
337
+ return {
338
+ success: true,
339
+ contractModule: route.contractModule,
340
+ changes,
341
+ message: `Updated ${route.contractModule}`,
342
+ securityNote:
343
+ normalize === "passthrough"
344
+ ? "⚠️ passthrough 모드는 Mass Assignment 공격에 취약할 수 있습니다. 신뢰할 수 있는 입력에만 사용하세요."
345
+ : normalize === "strict"
346
+ ? "strict 모드는 클라이언트가 추가 필드를 보내면 400 에러를 반환합니다."
347
+ : "strip 모드 (권장): 정의되지 않은 필드는 자동 제거됩니다.",
348
+ };
349
+ },
350
+
351
+ mandu_list_logger_options: async () => {
352
+ return {
353
+ options: [
354
+ {
355
+ name: "format",
356
+ type: '"pretty" | "json"',
357
+ default: "pretty",
358
+ description: "로그 출력 형식. pretty는 개발용 컬러 출력, json은 운영용 구조화 로그",
359
+ },
360
+ {
361
+ name: "level",
362
+ type: '"debug" | "info" | "warn" | "error"',
363
+ default: "info",
364
+ description: "최소 로그 레벨. debug는 모든 요청 상세, error는 에러만",
365
+ },
366
+ {
367
+ name: "includeHeaders",
368
+ type: "boolean",
369
+ default: false,
370
+ description: "⚠️ 요청 헤더 로깅. 민감 정보 노출 위험",
371
+ },
372
+ {
373
+ name: "includeBody",
374
+ type: "boolean",
375
+ default: false,
376
+ description: "⚠️ 요청 바디 로깅. 민감 정보 노출 + 스트림 문제",
377
+ },
378
+ {
379
+ name: "maxBodyBytes",
380
+ type: "number",
381
+ default: 1024,
382
+ description: "바디 로깅 시 최대 크기 (초과분 truncate)",
383
+ },
384
+ {
385
+ name: "redact",
386
+ type: "string[]",
387
+ default: '["authorization", "cookie", "password", ...]',
388
+ description: "로그에서 마스킹할 헤더/필드명",
389
+ },
390
+ {
391
+ name: "requestId",
392
+ type: '"auto" | ((ctx) => string)',
393
+ default: "auto",
394
+ description: "요청 ID 생성 방식. auto는 UUID 또는 타임스탬프 기반",
395
+ },
396
+ {
397
+ name: "sampleRate",
398
+ type: "number (0-1)",
399
+ default: 1,
400
+ description: "샘플링 비율. 운영에서 로그 양 조절 (0.1 = 10%)",
401
+ },
402
+ {
403
+ name: "slowThresholdMs",
404
+ type: "number",
405
+ default: 1000,
406
+ description: "느린 요청 임계값. 초과 시 warn 레벨로 상세 출력",
407
+ },
408
+ {
409
+ name: "includeTraceOnSlow",
410
+ type: "boolean",
411
+ default: true,
412
+ description: "느린 요청에 Trace 리포트 포함",
413
+ },
414
+ {
415
+ name: "sink",
416
+ type: "(entry: LogEntry) => void",
417
+ default: "console",
418
+ description: "커스텀 로그 출력 (Pino, CloudWatch 등 연동)",
419
+ },
420
+ {
421
+ name: "skip",
422
+ type: "(string | RegExp)[]",
423
+ default: "[]",
424
+ description: '로깅 제외 경로 패턴. 예: ["/health", /^\\/static\\//]',
425
+ },
426
+ ],
427
+ presets: {
428
+ devLogger: "개발용: debug 레벨, pretty 포맷, 헤더 포함",
429
+ prodLogger: "운영용: info 레벨, json 포맷, 헤더/바디 미포함",
430
+ },
431
+ };
432
+ },
433
+
434
+ mandu_generate_logger_config: async (args: Record<string, unknown>) => {
435
+ const {
436
+ environment = "development",
437
+ includeHeaders = false,
438
+ includeBody = false,
439
+ format,
440
+ customRedact = [],
441
+ } = args as {
442
+ environment?: "development" | "production" | "testing";
443
+ includeHeaders?: boolean;
444
+ includeBody?: boolean;
445
+ format?: "pretty" | "json";
446
+ customRedact?: string[];
447
+ };
448
+
449
+ const isDev = environment === "development";
450
+ const isProd = environment === "production";
451
+
452
+ const config = {
453
+ format: format || (isDev ? "pretty" : "json"),
454
+ level: isDev ? "debug" : "info",
455
+ includeHeaders: isDev ? includeHeaders : false,
456
+ includeBody: isDev ? includeBody : false,
457
+ maxBodyBytes: 1024,
458
+ sampleRate: isProd ? 0.1 : 1,
459
+ slowThresholdMs: isDev ? 500 : 1000,
460
+ ...(customRedact.length > 0 && { redact: customRedact }),
461
+ };
462
+
463
+ const code = `import { logger } from "@mandujs/core";
464
+
465
+ // ${environment} environment logger configuration
466
+ export const appLogger = logger(${JSON.stringify(config, null, 2)});
467
+
468
+ // Usage in your app:
469
+ // app.use(appLogger);
470
+ `;
471
+
472
+ const warnings: string[] = [];
473
+ if (includeHeaders && isProd) {
474
+ warnings.push("⚠️ includeHeaders: true는 프로덕션에서 민감 정보 노출 위험이 있습니다.");
475
+ }
476
+ if (includeBody && isProd) {
477
+ warnings.push("⚠️ includeBody: true는 프로덕션에서 민감 정보 노출 위험이 있습니다.");
478
+ }
479
+
480
+ return {
481
+ environment,
482
+ config,
483
+ code,
484
+ warnings: warnings.length > 0 ? warnings : undefined,
485
+ tips: [
486
+ "devLogger() 또는 prodLogger() 프리셋을 사용할 수도 있습니다.",
487
+ "sink 옵션으로 Pino, CloudWatch 등 외부 시스템 연동 가능",
488
+ "skip 옵션으로 /health, /metrics 등 제외 가능",
489
+ ],
490
+ };
491
+ },
492
+ };
493
+ }
494
+
495
+ function insertAfter(content: string, search: string): boolean {
496
+ return content.includes(search);
497
+ }