@mandujs/mcp 0.9.46 → 0.12.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.
- package/README.md +48 -1
- package/package.json +1 -1
- package/src/adapters/index.ts +20 -0
- package/src/adapters/monitor-adapter.ts +100 -0
- package/src/adapters/tool-adapter.ts +88 -0
- package/src/executor/error-handler.ts +250 -0
- package/src/executor/index.ts +22 -0
- package/src/executor/tool-executor.ts +148 -0
- package/src/hooks/config-watcher.ts +174 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/mcp-hooks.ts +227 -0
- package/src/index.ts +106 -9
- package/src/logging/index.ts +15 -0
- package/src/logging/mcp-transport.ts +134 -0
- package/src/registry/index.ts +13 -0
- package/src/registry/mcp-tool-registry.ts +298 -0
- package/src/server.ts +172 -98
- package/src/tools/guard.ts +896 -1
- package/src/tools/index.ts +133 -0
- package/src/tools/project.ts +334 -0
- package/src/utils/project.ts +32 -0
package/README.md
CHANGED
|
@@ -48,9 +48,23 @@ cd /path/to/project
|
|
|
48
48
|
bunx @mandujs/mcp
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### Global Mode
|
|
52
|
+
|
|
53
|
+
Run MCP without project auto-detection (use current directory):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bunx @mandujs/mcp --global
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Optional: target a specific root directory:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
bunx @mandujs/mcp --root /path/to/project
|
|
63
|
+
```
|
|
64
|
+
|
|
51
65
|
---
|
|
52
66
|
|
|
53
|
-
## Tools (
|
|
67
|
+
## Tools (35+)
|
|
54
68
|
|
|
55
69
|
### Spec Management
|
|
56
70
|
|
|
@@ -91,10 +105,35 @@ bunx @mandujs/mcp
|
|
|
91
105
|
| Tool | Description |
|
|
92
106
|
|------|-------------|
|
|
93
107
|
| `mandu_guard_check` | Run all guard checks |
|
|
108
|
+
| `mandu_guard_heal` | Self-Healing Guard - detect + auto-fix suggestions |
|
|
109
|
+
| `mandu_explain_rule` | Explain architecture rule with examples |
|
|
94
110
|
| `mandu_check_location` | Validate file location before creating |
|
|
95
111
|
| `mandu_check_import` | Validate imports against architecture rules |
|
|
96
112
|
| `mandu_get_architecture` | Get project architecture rules |
|
|
97
113
|
|
|
114
|
+
### Decision Memory (RFC-001) 🆕
|
|
115
|
+
|
|
116
|
+
| Tool | Description |
|
|
117
|
+
|------|-------------|
|
|
118
|
+
| `mandu_search_decisions` | Search ADRs by tags or status |
|
|
119
|
+
| `mandu_save_decision` | Save new architecture decision |
|
|
120
|
+
| `mandu_check_consistency` | Check decision-implementation consistency |
|
|
121
|
+
|
|
122
|
+
### Semantic Slots (RFC-001) 🆕
|
|
123
|
+
|
|
124
|
+
| Tool | Description |
|
|
125
|
+
|------|-------------|
|
|
126
|
+
| `mandu_validate_slot` | Validate slot against constraints |
|
|
127
|
+
| `mandu_validate_slots` | Batch validate multiple slots |
|
|
128
|
+
|
|
129
|
+
### Architecture Negotiation (RFC-001) 🆕
|
|
130
|
+
|
|
131
|
+
| Tool | Description |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `mandu_negotiate` | AI-Framework negotiation dialog |
|
|
134
|
+
| `mandu_generate_scaffold` | Generate structure scaffold |
|
|
135
|
+
| `mandu_analyze_structure` | Analyze existing project structure |
|
|
136
|
+
|
|
98
137
|
### Brain & Monitoring
|
|
99
138
|
|
|
100
139
|
| Tool | Description |
|
|
@@ -104,6 +143,14 @@ bunx @mandujs/mcp
|
|
|
104
143
|
| `mandu_watch_status` | Get watcher status |
|
|
105
144
|
| `mandu_watch_stop` | Stop file watcher |
|
|
106
145
|
|
|
146
|
+
### Project & Dev
|
|
147
|
+
|
|
148
|
+
| Tool | Description |
|
|
149
|
+
|------|-------------|
|
|
150
|
+
| `mandu_init` | Initialize new Mandu project (init + optional install) |
|
|
151
|
+
| `mandu_dev_start` | Start dev server (bun run dev) |
|
|
152
|
+
| `mandu_dev_stop` | Stop dev server |
|
|
153
|
+
|
|
107
154
|
### Hydration & Build
|
|
108
155
|
|
|
109
156
|
| Tool | Description |
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Adapters
|
|
3
|
+
*
|
|
4
|
+
* DNA 기능과 MCP 간의 변환 어댑터
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
toolToPlugin,
|
|
9
|
+
pluginToTool,
|
|
10
|
+
moduleToPlugins,
|
|
11
|
+
pluginsToTools,
|
|
12
|
+
pluginsToHandlers,
|
|
13
|
+
} from "./tool-adapter.js";
|
|
14
|
+
|
|
15
|
+
export {
|
|
16
|
+
monitorEventToRecord,
|
|
17
|
+
recordToMonitorEvent,
|
|
18
|
+
severityToLevel,
|
|
19
|
+
levelToSeverity,
|
|
20
|
+
} from "./monitor-adapter.js";
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monitor Event Adapter
|
|
3
|
+
*
|
|
4
|
+
* ActivityMonitor 이벤트와 DNA-008 LogTransportRecord 간 변환
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { LogTransportRecord } from "@mandujs/core";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Monitor 이벤트 타입 (ActivityMonitor에서 사용)
|
|
11
|
+
*/
|
|
12
|
+
export interface MonitorEvent {
|
|
13
|
+
ts: string;
|
|
14
|
+
type: string;
|
|
15
|
+
severity: MonitorSeverity;
|
|
16
|
+
source: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
data?: Record<string, unknown>;
|
|
19
|
+
actionRequired?: boolean;
|
|
20
|
+
fingerprint?: string;
|
|
21
|
+
count?: number;
|
|
22
|
+
schemaVersion?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type MonitorSeverity = "info" | "warn" | "error";
|
|
26
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* MonitorSeverity → LogLevel 변환
|
|
30
|
+
*/
|
|
31
|
+
export function severityToLevel(severity: MonitorSeverity): LogLevel {
|
|
32
|
+
switch (severity) {
|
|
33
|
+
case "error":
|
|
34
|
+
return "error";
|
|
35
|
+
case "warn":
|
|
36
|
+
return "warn";
|
|
37
|
+
case "info":
|
|
38
|
+
default:
|
|
39
|
+
return "info";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* LogLevel → MonitorSeverity 변환
|
|
45
|
+
*/
|
|
46
|
+
export function levelToSeverity(level: LogLevel): MonitorSeverity {
|
|
47
|
+
switch (level) {
|
|
48
|
+
case "error":
|
|
49
|
+
return "error";
|
|
50
|
+
case "warn":
|
|
51
|
+
return "warn";
|
|
52
|
+
case "debug":
|
|
53
|
+
case "info":
|
|
54
|
+
default:
|
|
55
|
+
return "info";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* MonitorEvent → LogTransportRecord 변환
|
|
61
|
+
*
|
|
62
|
+
* ActivityMonitor 이벤트를 DNA-008 로깅 시스템으로 전송할 때 사용
|
|
63
|
+
*/
|
|
64
|
+
export function monitorEventToRecord(event: MonitorEvent): LogTransportRecord {
|
|
65
|
+
return {
|
|
66
|
+
timestamp: event.ts,
|
|
67
|
+
level: severityToLevel(event.severity),
|
|
68
|
+
meta: {
|
|
69
|
+
type: event.type,
|
|
70
|
+
source: event.source,
|
|
71
|
+
fingerprint: event.fingerprint,
|
|
72
|
+
count: event.count,
|
|
73
|
+
actionRequired: event.actionRequired,
|
|
74
|
+
schemaVersion: event.schemaVersion,
|
|
75
|
+
...event.data,
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* LogTransportRecord → MonitorEvent 변환 (역방향)
|
|
82
|
+
*
|
|
83
|
+
* DNA-008 로그를 ActivityMonitor에서 표시할 때 사용
|
|
84
|
+
*/
|
|
85
|
+
export function recordToMonitorEvent(record: LogTransportRecord): MonitorEvent {
|
|
86
|
+
const meta = record.meta ?? {};
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
ts: record.timestamp,
|
|
90
|
+
type: (meta.type as string) ?? "log",
|
|
91
|
+
severity: levelToSeverity(record.level),
|
|
92
|
+
source: (meta.source as string) ?? "unknown",
|
|
93
|
+
message: record.error?.message,
|
|
94
|
+
data: meta,
|
|
95
|
+
actionRequired: (meta.actionRequired as boolean) ?? false,
|
|
96
|
+
fingerprint: meta.fingerprint as string | undefined,
|
|
97
|
+
count: meta.count as number | undefined,
|
|
98
|
+
schemaVersion: meta.schemaVersion as string | undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Adapter
|
|
3
|
+
*
|
|
4
|
+
* MCP SDK Tool을 DNA-001 McpToolPlugin으로 변환
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import type { McpToolPlugin } from "@mandujs/core";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* MCP SDK Tool을 McpToolPlugin으로 변환
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const plugin = toolToPlugin(
|
|
16
|
+
* { name: "my_tool", description: "...", inputSchema: {} },
|
|
17
|
+
* async (args) => ({ success: true })
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function toolToPlugin(
|
|
22
|
+
definition: Tool,
|
|
23
|
+
handler: (args: Record<string, unknown>) => Promise<unknown>
|
|
24
|
+
): McpToolPlugin {
|
|
25
|
+
return {
|
|
26
|
+
name: definition.name,
|
|
27
|
+
description: definition.description ?? "",
|
|
28
|
+
inputSchema: definition.inputSchema as Record<string, unknown>,
|
|
29
|
+
execute: handler,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* McpToolPlugin을 MCP SDK Tool로 변환 (역방향)
|
|
35
|
+
*/
|
|
36
|
+
export function pluginToTool(plugin: McpToolPlugin): Tool {
|
|
37
|
+
return {
|
|
38
|
+
name: plugin.name,
|
|
39
|
+
description: plugin.description,
|
|
40
|
+
inputSchema: plugin.inputSchema,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 기존 도구 모듈(definitions + handlers)을 플러그인 배열로 변환
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```ts
|
|
49
|
+
* const plugins = moduleToPlugins(
|
|
50
|
+
* specToolDefinitions,
|
|
51
|
+
* specTools(projectRoot)
|
|
52
|
+
* );
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function moduleToPlugins(
|
|
56
|
+
definitions: Tool[],
|
|
57
|
+
handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>>
|
|
58
|
+
): McpToolPlugin[] {
|
|
59
|
+
return definitions.map((def) => {
|
|
60
|
+
const handler = handlers[def.name];
|
|
61
|
+
if (!handler) {
|
|
62
|
+
throw new Error(`Handler not found for tool: ${def.name}`);
|
|
63
|
+
}
|
|
64
|
+
return toolToPlugin(def, handler);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 플러그인 배열을 MCP SDK Tool 배열로 변환
|
|
70
|
+
*/
|
|
71
|
+
export function pluginsToTools(plugins: McpToolPlugin[]): Tool[] {
|
|
72
|
+
return plugins.map(pluginToTool);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 플러그인 배열을 핸들러 맵으로 변환
|
|
77
|
+
*/
|
|
78
|
+
export function pluginsToHandlers(
|
|
79
|
+
plugins: McpToolPlugin[]
|
|
80
|
+
): Record<string, (args: Record<string, unknown>) => Promise<unknown>> {
|
|
81
|
+
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {};
|
|
82
|
+
|
|
83
|
+
for (const plugin of plugins) {
|
|
84
|
+
handlers[plugin.name] = async (args) => plugin.execute(args);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return handlers;
|
|
88
|
+
}
|
|
@@ -0,0 +1,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") {
|
|
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
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Executor
|
|
3
|
+
*
|
|
4
|
+
* 도구 실행 및 에러 처리
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
formatMcpError,
|
|
9
|
+
createToolResponse,
|
|
10
|
+
isErrorResponse,
|
|
11
|
+
extractErrorFromResponse,
|
|
12
|
+
logToolError,
|
|
13
|
+
type McpErrorResponse,
|
|
14
|
+
type McpToolResponse,
|
|
15
|
+
} from "./error-handler.js";
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
ToolExecutor,
|
|
19
|
+
createToolExecutor,
|
|
20
|
+
type ToolExecutorOptions,
|
|
21
|
+
type ExecutionResult,
|
|
22
|
+
} from "./tool-executor.js";
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Executor
|
|
3
|
+
*
|
|
4
|
+
* 도구 실행 + 훅 + 에러 처리 통합
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { McpToolPlugin, ManduConfig } from "@mandujs/core";
|
|
8
|
+
import { mcpToolRegistry } from "../registry/mcp-tool-registry.js";
|
|
9
|
+
import { mcpHookRegistry, type McpToolContext } from "../hooks/mcp-hooks.js";
|
|
10
|
+
import { createToolResponse, logToolError, type McpToolResponse } from "./error-handler.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Tool Executor 옵션
|
|
14
|
+
*/
|
|
15
|
+
export interface ToolExecutorOptions {
|
|
16
|
+
/** 프로젝트 루트 */
|
|
17
|
+
projectRoot: string;
|
|
18
|
+
/** Mandu 설정 */
|
|
19
|
+
config?: ManduConfig;
|
|
20
|
+
/** 활동 모니터 로깅 함수 */
|
|
21
|
+
logTool?: (name: string, args?: Record<string, unknown>, result?: unknown, error?: string) => void;
|
|
22
|
+
/** 결과 로깅 함수 */
|
|
23
|
+
logResult?: (name: string, result: unknown) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 도구 실행 결과
|
|
28
|
+
*/
|
|
29
|
+
export interface ExecutionResult {
|
|
30
|
+
success: boolean;
|
|
31
|
+
response: McpToolResponse;
|
|
32
|
+
duration: number;
|
|
33
|
+
toolName: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 도구 실행기
|
|
38
|
+
*
|
|
39
|
+
* DNA 기능들(플러그인, 훅, 에러 처리)을 통합한 도구 실행
|
|
40
|
+
*/
|
|
41
|
+
export class ToolExecutor {
|
|
42
|
+
private options: ToolExecutorOptions;
|
|
43
|
+
|
|
44
|
+
constructor(options: ToolExecutorOptions) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 도구 실행
|
|
50
|
+
*
|
|
51
|
+
* @param name - 도구 이름
|
|
52
|
+
* @param args - 도구 인자
|
|
53
|
+
* @returns 실행 결과
|
|
54
|
+
*/
|
|
55
|
+
async execute(name: string, args: Record<string, unknown> = {}): Promise<ExecutionResult> {
|
|
56
|
+
const startTime = Date.now();
|
|
57
|
+
|
|
58
|
+
// 도구 조회
|
|
59
|
+
const tool = mcpToolRegistry.get(name);
|
|
60
|
+
if (!tool) {
|
|
61
|
+
const response = createToolResponse(name, null, new Error(`Unknown tool: ${name}`));
|
|
62
|
+
return {
|
|
63
|
+
success: false,
|
|
64
|
+
response,
|
|
65
|
+
duration: Date.now() - startTime,
|
|
66
|
+
toolName: name,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 실행 컨텍스트 생성
|
|
71
|
+
const ctx: McpToolContext = {
|
|
72
|
+
toolName: name,
|
|
73
|
+
args,
|
|
74
|
+
projectRoot: this.options.projectRoot,
|
|
75
|
+
config: this.options.config,
|
|
76
|
+
startTime,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Pre-Tool 훅 실행
|
|
81
|
+
await mcpHookRegistry.runPreHooks(ctx);
|
|
82
|
+
|
|
83
|
+
// 활동 로깅 (호출)
|
|
84
|
+
this.options.logTool?.(name, args);
|
|
85
|
+
|
|
86
|
+
// 도구 실행
|
|
87
|
+
const result = await tool.execute(args);
|
|
88
|
+
|
|
89
|
+
// 활동 로깅 (결과)
|
|
90
|
+
this.options.logResult?.(name, result);
|
|
91
|
+
|
|
92
|
+
// Post-Tool 훅 실행
|
|
93
|
+
await mcpHookRegistry.runPostHooks(ctx, result);
|
|
94
|
+
|
|
95
|
+
const response = createToolResponse(name, result);
|
|
96
|
+
return {
|
|
97
|
+
success: true,
|
|
98
|
+
response,
|
|
99
|
+
duration: Date.now() - startTime,
|
|
100
|
+
toolName: name,
|
|
101
|
+
};
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// 에러 로깅
|
|
104
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
105
|
+
this.options.logTool?.(name, args, null, errorMsg);
|
|
106
|
+
logToolError(name, error, args);
|
|
107
|
+
|
|
108
|
+
// Post-Tool 훅 실행 (에러와 함께)
|
|
109
|
+
await mcpHookRegistry.runPostHooks(ctx, null, error);
|
|
110
|
+
|
|
111
|
+
const response = createToolResponse(name, null, error);
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
response,
|
|
115
|
+
duration: Date.now() - startTime,
|
|
116
|
+
toolName: name,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 설정 업데이트
|
|
123
|
+
*/
|
|
124
|
+
updateConfig(config: ManduConfig): void {
|
|
125
|
+
this.options.config = config;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 도구 존재 여부 확인
|
|
130
|
+
*/
|
|
131
|
+
hasTool(name: string): boolean {
|
|
132
|
+
return mcpToolRegistry.has(name);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 등록된 도구 목록
|
|
137
|
+
*/
|
|
138
|
+
getToolNames(): string[] {
|
|
139
|
+
return mcpToolRegistry.names;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Tool Executor 팩토리 함수
|
|
145
|
+
*/
|
|
146
|
+
export function createToolExecutor(options: ToolExecutorOptions): ToolExecutor {
|
|
147
|
+
return new ToolExecutor(options);
|
|
148
|
+
}
|