@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 CHANGED
@@ -185,7 +185,6 @@ bunx @mandujs/mcp --root /path/to/project
185
185
  | URI | Description |
186
186
  |-----|-------------|
187
187
  | `mandu://spec/manifest` | Current routes.manifest.json |
188
- | `mandu://spec/lock` | Current spec.lock.json with hash |
189
188
  | `mandu://generated/map` | Generated files mapping |
190
189
  | `mandu://transaction/active` | Active transaction state |
191
190
  | `mandu://slots/{routeId}` | Slot file content by route ID |
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
- {
2
- "name": "@mandujs/mcp",
3
- "version": "0.18.8",
4
- "description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
5
- "type": "module",
6
- "main": "./src/index.ts",
7
- "bin": {
8
- "mandu-mcp": "./src/index.ts"
9
- },
10
- "exports": {
11
- ".": "./src/index.ts"
12
- },
13
- "files": [
14
- "src/**/*"
15
- ],
16
- "keywords": [
17
- "mandu",
18
- "mcp",
19
- "model-context-protocol",
20
- "ai",
21
- "agent",
22
- "code-generation"
23
- ],
24
- "repository": {
25
- "type": "git",
26
- "url": "https://github.com/konamgil/mandu.git",
27
- "directory": "packages/mcp"
28
- },
29
- "author": "konamgil",
30
- "license": "MPL-2.0",
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
- "dependencies": {
35
- "@mandujs/core": "^0.18.22",
36
- "@mandujs/ate": "^0.17.2",
37
- "@modelcontextprotocol/sdk": "^1.25.3"
38
- },
39
- "engines": {
40
- "bun": ">=1.0.0"
41
- }
42
- }
1
+ {
2
+ "name": "@mandujs/mcp",
3
+ "version": "0.18.10",
4
+ "description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "bin": {
8
+ "mandu-mcp": "./src/index.ts"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.ts"
12
+ },
13
+ "files": [
14
+ "src/**/*"
15
+ ],
16
+ "keywords": [
17
+ "mandu",
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "ai",
21
+ "agent",
22
+ "code-generation"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/konamgil/mandu.git",
27
+ "directory": "packages/mcp"
28
+ },
29
+ "author": "konamgil",
30
+ "license": "MPL-2.0",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@mandujs/core": "^0.19.1",
36
+ "@mandujs/ate": "^0.17.2",
37
+ "@modelcontextprotocol/sdk": "^1.25.3"
38
+ },
39
+ "engines": {
40
+ "bun": ">=1.0.0"
41
+ }
42
+ }
@@ -18,14 +18,14 @@ const TOOL_ICONS: Record<string, string> = {
18
18
  mandu_delete_route: "SPEC-",
19
19
  mandu_validate_manifest: "SPEC?",
20
20
  // Generate
21
- mandu_generate: "GEN",
22
- mandu_generate_status: "GEN?",
21
+ "mandu.generate": "GEN",
22
+ "mandu.generate.status": "GEN?",
23
23
  // Guard
24
24
  mandu_guard_check: "GUARD",
25
25
  // Slot
26
- mandu_read_slot: "SLOT",
26
+ "mandu.slot.read": "SLOT",
27
27
  mandu_write_slot: "SLOT~",
28
- mandu_validate_slot: "SLOT?",
28
+ "mandu.slot.validate": "SLOT?",
29
29
  // Contract
30
30
  mandu_list_contracts: "CONTRACT",
31
31
  mandu_get_contract: "CONTRACT",
@@ -281,7 +281,10 @@ export class ActivityMonitor {
281
281
  this.projectRoot = projectRoot;
282
282
  const userConfig = loadMonitorConfig(projectRoot);
283
283
  this.config = mergeConfig(DEFAULT_CONFIG, userConfig);
284
- this.outputFormat = resolveOutputFormat(this.config.output);
284
+ // When openTerminal is enabled, force pretty format for human-readable terminal output
285
+ this.outputFormat = this.config.openTerminal
286
+ ? "pretty"
287
+ : resolveOutputFormat(this.config.output);
285
288
  }
286
289
 
287
290
  start(): void {
@@ -327,9 +330,7 @@ export class ActivityMonitor {
327
330
  );
328
331
  }
329
332
 
330
- const openTerminal =
331
- this.config.openTerminal && this.outputFormat === "pretty";
332
- if (openTerminal) {
333
+ if (this.config.openTerminal) {
333
334
  this.openTerminal();
334
335
  }
335
336
  }
@@ -26,6 +26,7 @@ export function toolToPlugin(
26
26
  name: definition.name,
27
27
  description: definition.description ?? "",
28
28
  inputSchema: definition.inputSchema as unknown as Record<string, unknown>,
29
+ ...(definition.annotations && { annotations: definition.annotations as unknown as Record<string, unknown> }),
29
30
  execute: handler as (input: unknown) => Promise<unknown>,
30
31
  };
31
32
  }
@@ -38,6 +39,7 @@ export function pluginToTool(plugin: McpToolPlugin): Tool {
38
39
  name: plugin.name,
39
40
  description: plugin.description,
40
41
  inputSchema: plugin.inputSchema as Tool["inputSchema"],
42
+ ...(plugin.annotations && { annotations: plugin.annotations as Tool["annotations"] }),
41
43
  };
42
44
  }
43
45
 
@@ -1,250 +1,268 @@
1
- /**
2
- * MCP Error Handler
3
- *
4
- * DNA-007 ์—๋Ÿฌ ์ถ”์ถœ ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ MCP ์—๋Ÿฌ ์ฒ˜๋ฆฌ
5
- */
6
-
7
- import {
8
- extractErrorInfo,
9
- classifyError,
10
- serializeError,
11
- isRetryableError,
12
- type ErrorCategory,
13
- type ExtractedErrorInfo,
14
- } from "@mandujs/core";
15
-
16
- /**
17
- * MCP ์—๋Ÿฌ ์‘๋‹ต ํƒ€์ž…
18
- */
19
- export interface McpErrorResponse {
20
- /** ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */
21
- error: string;
22
- /** ์—๋Ÿฌ ์ฝ”๋“œ (์žˆ๋Š” ๊ฒฝ์šฐ) */
23
- code?: string;
24
- /** ์—๋Ÿฌ ์นดํ…Œ๊ณ ๋ฆฌ (DNA-007) */
25
- category: ErrorCategory;
26
- /** ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ */
27
- retryable: boolean;
28
- /** ์ถ”๊ฐ€ ์ปจํ…์ŠคํŠธ */
29
- context?: Record<string, unknown>;
30
- /** ๋ณต๊ตฌ ์ œ์•ˆ */
31
- suggestion?: string;
32
- }
33
-
34
- /**
35
- * MCP ๋„๊ตฌ ์‘๋‹ต ํƒ€์ž…
36
- */
37
- export interface McpToolResponse {
38
- content: Array<{ type: string; text: string }>;
39
- isError?: boolean;
40
- }
41
-
42
- /**
43
- * ์—๋Ÿฌ๋ฅผ MCP ์‘๋‹ต ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
44
- *
45
- * @example
46
- * ```ts
47
- * try {
48
- * await tool.execute(args);
49
- * } catch (err) {
50
- * const response = formatMcpError(err, "mandu_guard_check");
51
- * // { error: "...", code: "...", category: "validation", ... }
52
- * }
53
- * ```
54
- */
55
- export function formatMcpError(err: unknown, toolName?: string): McpErrorResponse {
56
- const info = extractErrorInfo(err);
57
-
58
- return {
59
- error: info.message,
60
- code: info.code,
61
- category: info.category,
62
- retryable: isRetryableError(err),
63
- context: {
64
- ...info.context,
65
- toolName,
66
- errorName: info.name,
67
- },
68
- suggestion: generateSuggestion(info, toolName),
69
- };
70
- }
71
-
72
- /**
73
- * ์—๋Ÿฌ ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ ๋„๊ตฌ๋ณ„ ๋ณต๊ตฌ ์ œ์•ˆ ์ƒ์„ฑ
74
- */
75
- function generateSuggestion(info: ExtractedErrorInfo, toolName?: string): string | undefined {
76
- // ๋„๊ตฌ๋ณ„ ํŠนํ™” ์ œ์•ˆ
77
- if (toolName) {
78
- const toolSuggestion = getToolSpecificSuggestion(toolName, info);
79
- if (toolSuggestion) return toolSuggestion;
80
- }
81
-
82
- // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ผ๋ฐ˜ ์ œ์•ˆ
83
- switch (info.category) {
84
- case "network":
85
- return "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜๊ณ  ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
86
-
87
- case "timeout":
88
- return "์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
89
-
90
- case "auth":
91
- return "์ธ์ฆ ์ •๋ณด๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
92
-
93
- case "validation":
94
- return "์ž…๋ ฅ ๊ฐ’์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ๊ฑฐ๋‚˜ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.";
95
-
96
- case "config":
97
- return "์„ค์ • ํŒŒ์ผ(mandu.config.ts)์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
98
-
99
- case "system":
100
- if (info.code === "ENOENT") {
101
- const path = info.context?.path ?? "unknown";
102
- return `ํŒŒ์ผ ๋˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${path}`;
103
- }
104
- if (info.code === "EACCES" || info.code === "EPERM") {
105
- return "ํŒŒ์ผ ์ ‘๊ทผ ๊ถŒํ•œ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
106
- }
107
- return "์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
108
-
109
- case "external":
110
- return "์™ธ๋ถ€ ์„œ๋น„์Šค์— ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
111
-
112
- case "internal":
113
- return "๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์ œ๊ฐ€ ์ง€์†๋˜๋ฉด ์ด์Šˆ๋ฅผ ๋ณด๊ณ ํ•ด์ฃผ์„ธ์š”.";
114
-
115
- default:
116
- return undefined;
117
- }
118
- }
119
-
120
- /**
121
- * ๋„๊ตฌ๋ณ„ ํŠนํ™”๋œ ์—๋Ÿฌ ์ œ์•ˆ
122
- */
123
- function getToolSpecificSuggestion(toolName: string, info: ExtractedErrorInfo): string | undefined {
124
- // spec ๊ด€๋ จ ๋„๊ตฌ
125
- if (toolName.startsWith("mandu_") && toolName.includes("route")) {
126
- if (info.code === "ENOENT") {
127
- return "routes.manifest.json ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. `mandu init`์„ ๋จผ์ € ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”.";
128
- }
129
- if (info.message.includes("not found")) {
130
- return "ํ•ด๋‹น ๋ผ์šฐํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. `mandu_list_routes`๋กœ ์กด์žฌํ•˜๋Š” ๋ผ์šฐํŠธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
131
- }
132
- }
133
-
134
- // guard ๊ด€๋ จ ๋„๊ตฌ
135
- if (toolName === "mandu_guard_check") {
136
- if (info.category === "config") {
137
- return "Guard ์„ค์ •์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”. mandu.config.ts์˜ guard ์„น์…˜์„ ๊ฒ€ํ† ํ•ด์ฃผ์„ธ์š”.";
138
- }
139
- }
140
-
141
- // contract ๊ด€๋ จ ๋„๊ตฌ
142
- if (toolName.includes("contract")) {
143
- if (info.category === "validation") {
144
- return "Contract ์Šคํ‚ค๋งˆ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”. Zod ์Šคํ‚ค๋งˆ ๋ฌธ๋ฒ•์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
145
- }
146
- }
147
-
148
- // generate ๊ด€๋ จ ๋„๊ตฌ
149
- if (toolName === "mandu_generate") {
150
- if (info.code === "EEXIST") {
151
- return "ํŒŒ์ผ์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋ฎ์–ด์“ฐ๋ ค๋ฉด force ์˜ต์…˜์„ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”.";
152
- }
153
- }
154
-
155
- // transaction ๊ด€๋ จ ๋„๊ตฌ
156
- if (toolName.includes("tx") || toolName.includes("transaction")) {
157
- if (info.message.includes("no active")) {
158
- return "ํ™œ์„ฑํ™”๋œ ํŠธ๋žœ์žญ์…˜์ด ์—†์Šต๋‹ˆ๋‹ค. `mandu_begin`์œผ๋กœ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•ด์ฃผ์„ธ์š”.";
159
- }
160
- }
161
-
162
- return undefined;
163
- }
164
-
165
- /**
166
- * ๋„๊ตฌ ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ MCP ์‘๋‹ต์œผ๋กœ ๋ณ€ํ™˜
167
- *
168
- * @example
169
- * ```ts
170
- * // ์„ฑ๊ณต ์‘๋‹ต
171
- * const response = createToolResponse("mandu_list_routes", { routes: [...] });
172
- *
173
- * // ์—๋Ÿฌ ์‘๋‹ต
174
- * const response = createToolResponse("mandu_list_routes", null, new Error("..."));
175
- * ```
176
- */
177
- export function createToolResponse(
178
- toolName: string,
179
- result: unknown,
180
- error?: unknown
181
- ): McpToolResponse {
182
- if (error) {
183
- const errorResponse = formatMcpError(error, toolName);
184
- return {
185
- content: [
186
- {
187
- type: "text",
188
- text: JSON.stringify(errorResponse, null, 2),
189
- },
190
- ],
191
- isError: true,
192
- };
193
- }
194
-
195
- return {
196
- content: [
197
- {
198
- type: "text",
199
- text: JSON.stringify(result, null, 2),
200
- },
201
- ],
202
- };
203
- }
204
-
205
- /**
206
- * ์—๋Ÿฌ ์‘๋‹ต์ธ์ง€ ํ™•์ธ
207
- */
208
- export function isErrorResponse(response: McpToolResponse): boolean {
209
- return response.isError === true;
210
- }
211
-
212
- /**
213
- * ์—๋Ÿฌ ์‘๋‹ต์—์„œ McpErrorResponse ์ถ”์ถœ
214
- */
215
- export function extractErrorFromResponse(response: McpToolResponse): McpErrorResponse | null {
216
- if (!response.isError) return null;
217
-
218
- try {
219
- const text = response.content[0]?.text;
220
- if (!text) return null;
221
- return JSON.parse(text) as McpErrorResponse;
222
- } catch {
223
- return null;
224
- }
225
- }
226
-
227
- /**
228
- * ์—๋Ÿฌ ๋กœ๊น… ํ—ฌํผ
229
- */
230
- export function logToolError(
231
- toolName: string,
232
- error: unknown,
233
- args?: Record<string, unknown>
234
- ): void {
235
- const info = extractErrorInfo(error);
236
-
237
- console.error(`[MCP:${toolName}] ${info.category.toUpperCase()}: ${info.message}`);
238
-
239
- if (info.code) {
240
- console.error(` Code: ${info.code}`);
241
- }
242
-
243
- if (args && Object.keys(args).length > 0) {
244
- console.error(` Args:`, JSON.stringify(args, null, 2));
245
- }
246
-
247
- if (info.context && Object.keys(info.context).length > 0) {
248
- console.error(` Context:`, info.context);
249
- }
250
- }
1
+ /**
2
+ * MCP Error Handler
3
+ *
4
+ * DNA-007 ์—๋Ÿฌ ์ถ”์ถœ ์‹œ์Šคํ…œ ๊ธฐ๋ฐ˜ MCP ์—๋Ÿฌ ์ฒ˜๋ฆฌ
5
+ */
6
+
7
+ import {
8
+ extractErrorInfo,
9
+ classifyError,
10
+ serializeError,
11
+ isRetryableError,
12
+ type ErrorCategory,
13
+ type ExtractedErrorInfo,
14
+ } from "@mandujs/core";
15
+
16
+ /**
17
+ * MCP ์—๋Ÿฌ ์‘๋‹ต ํƒ€์ž…
18
+ */
19
+ export interface McpErrorResponse {
20
+ /** ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ */
21
+ error: string;
22
+ /** ์—๋Ÿฌ ์ฝ”๋“œ (์žˆ๋Š” ๊ฒฝ์šฐ) */
23
+ code?: string;
24
+ /** ์—๋Ÿฌ ์นดํ…Œ๊ณ ๋ฆฌ (DNA-007) */
25
+ category: ErrorCategory;
26
+ /** ์žฌ์‹œ๋„ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ */
27
+ retryable: boolean;
28
+ /** ์ถ”๊ฐ€ ์ปจํ…์ŠคํŠธ */
29
+ context?: Record<string, unknown>;
30
+ /** ๋ณต๊ตฌ ์ œ์•ˆ */
31
+ suggestion?: string;
32
+ }
33
+
34
+ /**
35
+ * MCP ๋„๊ตฌ ์‘๋‹ต ํƒ€์ž…
36
+ */
37
+ export interface McpToolResponse {
38
+ content: Array<{ type: string; text: string }>;
39
+ isError?: boolean;
40
+ }
41
+
42
+ /**
43
+ * ์—๋Ÿฌ๋ฅผ MCP ์‘๋‹ต ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * try {
48
+ * await tool.execute(args);
49
+ * } catch (err) {
50
+ * const response = formatMcpError(err, "mandu_guard_check");
51
+ * // { error: "...", code: "...", category: "validation", ... }
52
+ * }
53
+ * ```
54
+ */
55
+ export function formatMcpError(err: unknown, toolName?: string): McpErrorResponse {
56
+ const info = extractErrorInfo(err);
57
+
58
+ return {
59
+ error: info.message,
60
+ code: info.code,
61
+ category: info.category,
62
+ retryable: isRetryableError(err),
63
+ context: {
64
+ ...info.context,
65
+ toolName,
66
+ errorName: info.name,
67
+ },
68
+ suggestion: generateSuggestion(info, toolName),
69
+ };
70
+ }
71
+
72
+ /**
73
+ * ์—๋Ÿฌ ์นดํ…Œ๊ณ ๋ฆฌ ๋ฐ ๋„๊ตฌ๋ณ„ ๋ณต๊ตฌ ์ œ์•ˆ ์ƒ์„ฑ
74
+ */
75
+ function generateSuggestion(info: ExtractedErrorInfo, toolName?: string): string | undefined {
76
+ // ๋„๊ตฌ๋ณ„ ํŠนํ™” ์ œ์•ˆ
77
+ if (toolName) {
78
+ const toolSuggestion = getToolSpecificSuggestion(toolName, info);
79
+ if (toolSuggestion) return toolSuggestion;
80
+ }
81
+
82
+ // ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ผ๋ฐ˜ ์ œ์•ˆ
83
+ switch (info.category) {
84
+ case "network":
85
+ return "๋„คํŠธ์›Œํฌ ์—ฐ๊ฒฐ์„ ํ™•์ธํ•˜๊ณ  ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
86
+
87
+ case "timeout":
88
+ return "์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
89
+
90
+ case "auth":
91
+ return "์ธ์ฆ ์ •๋ณด๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
92
+
93
+ case "validation":
94
+ return "์ž…๋ ฅ ๊ฐ’์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”. ํ•„์ˆ˜ ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ๊ฑฐ๋‚˜ ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.";
95
+
96
+ case "config":
97
+ return "์„ค์ • ํŒŒ์ผ(mandu.config.ts)์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
98
+
99
+ case "system":
100
+ if (info.code === "ENOENT") {
101
+ const path = info.context?.path ?? "unknown";
102
+ return `ํŒŒ์ผ ๋˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${path}`;
103
+ }
104
+ if (info.code === "EACCES" || info.code === "EPERM") {
105
+ return "ํŒŒ์ผ ์ ‘๊ทผ ๊ถŒํ•œ์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
106
+ }
107
+ return "์‹œ์Šคํ…œ ๋ฆฌ์†Œ์Šค๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
108
+
109
+ case "external":
110
+ return "์™ธ๋ถ€ ์„œ๋น„์Šค์— ๋ฌธ์ œ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.";
111
+
112
+ case "internal":
113
+ return "๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋ฌธ์ œ๊ฐ€ ์ง€์†๋˜๋ฉด ์ด์Šˆ๋ฅผ ๋ณด๊ณ ํ•ด์ฃผ์„ธ์š”.";
114
+
115
+ default:
116
+ return undefined;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * ๋„๊ตฌ๋ณ„ ํŠนํ™”๋œ ์—๋Ÿฌ ์ œ์•ˆ
122
+ */
123
+ function getToolSpecificSuggestion(toolName: string, info: ExtractedErrorInfo): string | undefined {
124
+ // spec ๊ด€๋ จ ๋„๊ตฌ
125
+ if (toolName.startsWith("mandu_") && toolName.includes("route")) {
126
+ if (info.code === "ENOENT") {
127
+ return "routes.manifest.json ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค. `mandu init`์„ ๋จผ์ € ์‹คํ–‰ํ•ด์ฃผ์„ธ์š”.";
128
+ }
129
+ if (info.message.includes("not found")) {
130
+ return "ํ•ด๋‹น ๋ผ์šฐํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. `mandu_list_routes`๋กœ ์กด์žฌํ•˜๋Š” ๋ผ์šฐํŠธ๋ฅผ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
131
+ }
132
+ }
133
+
134
+ // guard ๊ด€๋ จ ๋„๊ตฌ
135
+ if (toolName === "mandu_guard_check") {
136
+ if (info.category === "config") {
137
+ return "Guard ์„ค์ •์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”. mandu.config.ts์˜ guard ์„น์…˜์„ ๊ฒ€ํ† ํ•ด์ฃผ์„ธ์š”.";
138
+ }
139
+ }
140
+
141
+ // contract ๊ด€๋ จ ๋„๊ตฌ
142
+ if (toolName.includes("contract")) {
143
+ if (info.category === "validation") {
144
+ return "Contract ์Šคํ‚ค๋งˆ๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”. Zod ์Šคํ‚ค๋งˆ ๋ฌธ๋ฒ•์„ ํ™•์ธํ•ด์ฃผ์„ธ์š”.";
145
+ }
146
+ }
147
+
148
+ // generate ๊ด€๋ จ ๋„๊ตฌ
149
+ if (toolName === "mandu.generate" || toolName === "mandu_generate") {
150
+ if (info.code === "EEXIST") {
151
+ return "ํŒŒ์ผ์ด ์ด๋ฏธ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค. ๋ฎ์–ด์“ฐ๋ ค๋ฉด force ์˜ต์…˜์„ ์‚ฌ์šฉํ•ด์ฃผ์„ธ์š”.";
152
+ }
153
+ }
154
+
155
+ // transaction ๊ด€๋ จ ๋„๊ตฌ
156
+ if (toolName.includes("tx") || toolName.includes("transaction")) {
157
+ if (info.message.includes("no active")) {
158
+ return "ํ™œ์„ฑํ™”๋œ ํŠธ๋žœ์žญ์…˜์ด ์—†์Šต๋‹ˆ๋‹ค. `mandu_begin`์œผ๋กœ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•ด์ฃผ์„ธ์š”.";
159
+ }
160
+ }
161
+
162
+ return undefined;
163
+ }
164
+
165
+ /**
166
+ * ๋„๊ตฌ ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ MCP ์‘๋‹ต์œผ๋กœ ๋ณ€ํ™˜
167
+ *
168
+ * @example
169
+ * ```ts
170
+ * // ์„ฑ๊ณต ์‘๋‹ต
171
+ * const response = createToolResponse("mandu_list_routes", { routes: [...] });
172
+ *
173
+ * // ์—๋Ÿฌ ์‘๋‹ต
174
+ * const response = createToolResponse("mandu_list_routes", null, new Error("..."));
175
+ * ```
176
+ */
177
+ /**
178
+ * Detect "soft error" results: handler returned { error: "..." } or { success: false, error: "..." }
179
+ * without throwing. These should still be flagged as isError for the MCP client.
180
+ */
181
+ function isSoftErrorResult(result: unknown): boolean {
182
+ if (result == null || typeof result !== "object") return false;
183
+ const obj = result as Record<string, unknown>;
184
+ // Has an explicit "error" field (string or array)
185
+ if ("error" in obj && obj.error != null) return true;
186
+ // Has success: false with no data payload
187
+ if (obj.success === false && "error" in obj) return true;
188
+ return false;
189
+ }
190
+
191
+ export function createToolResponse(
192
+ toolName: string,
193
+ result: unknown,
194
+ error?: unknown
195
+ ): McpToolResponse {
196
+ if (error) {
197
+ const errorResponse = formatMcpError(error, toolName);
198
+ return {
199
+ content: [
200
+ {
201
+ type: "text",
202
+ text: JSON.stringify(errorResponse, null, 2),
203
+ },
204
+ ],
205
+ isError: true,
206
+ };
207
+ }
208
+
209
+ // Detect soft errors returned by handlers (e.g. { error: "Route not found" })
210
+ const softError = isSoftErrorResult(result);
211
+
212
+ return {
213
+ content: [
214
+ {
215
+ type: "text",
216
+ text: JSON.stringify(result, null, 2),
217
+ },
218
+ ],
219
+ ...(softError && { isError: true }),
220
+ };
221
+ }
222
+
223
+ /**
224
+ * ์—๋Ÿฌ ์‘๋‹ต์ธ์ง€ ํ™•์ธ
225
+ */
226
+ export function isErrorResponse(response: McpToolResponse): boolean {
227
+ return response.isError === true;
228
+ }
229
+
230
+ /**
231
+ * ์—๋Ÿฌ ์‘๋‹ต์—์„œ McpErrorResponse ์ถ”์ถœ
232
+ */
233
+ export function extractErrorFromResponse(response: McpToolResponse): McpErrorResponse | null {
234
+ if (!response.isError) return null;
235
+
236
+ try {
237
+ const text = response.content[0]?.text;
238
+ if (!text) return null;
239
+ return JSON.parse(text) as McpErrorResponse;
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
244
+
245
+ /**
246
+ * ์—๋Ÿฌ ๋กœ๊น… ํ—ฌํผ
247
+ */
248
+ export function logToolError(
249
+ toolName: string,
250
+ error: unknown,
251
+ args?: Record<string, unknown>
252
+ ): void {
253
+ const info = extractErrorInfo(error);
254
+
255
+ console.error(`[MCP:${toolName}] ${info.category.toUpperCase()}: ${info.message}`);
256
+
257
+ if (info.code) {
258
+ console.error(` Code: ${info.code}`);
259
+ }
260
+
261
+ if (args && Object.keys(args).length > 0) {
262
+ console.error(` Args:`, JSON.stringify(args, null, 2));
263
+ }
264
+
265
+ if (info.context && Object.keys(info.context).length > 0) {
266
+ console.error(` Context:`, info.context);
267
+ }
268
+ }