@mandujs/mcp 0.10.0 → 0.12.1

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.
@@ -0,0 +1,298 @@
1
+ /**
2
+ * MCP Tool Registry
3
+ *
4
+ * DNA-001 플러그인 시스템 기반 MCP 도구 레지스트리
5
+ * - 동적 도구 등록/해제
6
+ * - 카테고리별 관리
7
+ * - MCP SDK 형식 변환
8
+ */
9
+
10
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
11
+ import type { McpToolPlugin } from "@mandujs/core";
12
+ import { pluginToTool } from "../adapters/tool-adapter.js";
13
+
14
+ /**
15
+ * 도구 등록 정보
16
+ */
17
+ export interface ToolRegistration {
18
+ plugin: McpToolPlugin;
19
+ category?: string;
20
+ registeredAt: Date;
21
+ enabled: boolean;
22
+ }
23
+
24
+ /**
25
+ * MCP 도구 레지스트리
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * // 도구 등록
30
+ * mcpToolRegistry.register({
31
+ * name: "custom_tool",
32
+ * description: "My custom tool",
33
+ * inputSchema: { type: "object", properties: {} },
34
+ * execute: async (args) => ({ success: true }),
35
+ * }, "custom");
36
+ *
37
+ * // MCP SDK 형식으로 변환
38
+ * const tools = mcpToolRegistry.toToolDefinitions();
39
+ * ```
40
+ */
41
+ export class McpToolRegistry {
42
+ private tools = new Map<string, ToolRegistration>();
43
+ private categories = new Map<string, Set<string>>();
44
+ private listeners = new Set<(event: RegistryEvent) => void>();
45
+
46
+ /**
47
+ * 도구 등록
48
+ *
49
+ * @param plugin - McpToolPlugin 인스턴스
50
+ * @param category - 도구 카테고리 (선택)
51
+ * @returns 등록 해제 함수
52
+ */
53
+ register(plugin: McpToolPlugin, category?: string): () => void {
54
+ const registration: ToolRegistration = {
55
+ plugin,
56
+ category,
57
+ registeredAt: new Date(),
58
+ enabled: true,
59
+ };
60
+
61
+ this.tools.set(plugin.name, registration);
62
+
63
+ if (category) {
64
+ if (!this.categories.has(category)) {
65
+ this.categories.set(category, new Set());
66
+ }
67
+ this.categories.get(category)!.add(plugin.name);
68
+ }
69
+
70
+ this.emit({ type: "register", toolName: plugin.name, category });
71
+
72
+ return () => this.unregister(plugin.name);
73
+ }
74
+
75
+ /**
76
+ * 여러 도구 일괄 등록
77
+ */
78
+ registerAll(plugins: McpToolPlugin[], category?: string): void {
79
+ for (const plugin of plugins) {
80
+ this.register(plugin, category);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 도구 해제
86
+ */
87
+ unregister(name: string): boolean {
88
+ const registration = this.tools.get(name);
89
+ if (!registration) return false;
90
+
91
+ this.tools.delete(name);
92
+
93
+ // 카테고리에서도 제거
94
+ if (registration.category) {
95
+ const categorySet = this.categories.get(registration.category);
96
+ categorySet?.delete(name);
97
+ if (categorySet?.size === 0) {
98
+ this.categories.delete(registration.category);
99
+ }
100
+ }
101
+
102
+ this.emit({ type: "unregister", toolName: name });
103
+ return true;
104
+ }
105
+
106
+ /**
107
+ * 카테고리 전체 해제
108
+ */
109
+ unregisterCategory(category: string): number {
110
+ const names = this.categories.get(category);
111
+ if (!names) return 0;
112
+
113
+ let count = 0;
114
+ for (const name of Array.from(names)) {
115
+ if (this.unregister(name)) count++;
116
+ }
117
+ return count;
118
+ }
119
+
120
+ /**
121
+ * 도구 조회
122
+ */
123
+ get(name: string): McpToolPlugin | undefined {
124
+ return this.tools.get(name)?.plugin;
125
+ }
126
+
127
+ /**
128
+ * 도구 존재 여부
129
+ */
130
+ has(name: string): boolean {
131
+ return this.tools.has(name);
132
+ }
133
+
134
+ /**
135
+ * 도구 활성화/비활성화
136
+ */
137
+ setEnabled(name: string, enabled: boolean): void {
138
+ const registration = this.tools.get(name);
139
+ if (registration) {
140
+ registration.enabled = enabled;
141
+ this.emit({ type: enabled ? "enable" : "disable", toolName: name });
142
+ }
143
+ }
144
+
145
+ /**
146
+ * MCP SDK Tool 형식으로 변환
147
+ *
148
+ * 활성화된 도구만 반환
149
+ */
150
+ toToolDefinitions(): Tool[] {
151
+ const tools: Tool[] = [];
152
+
153
+ for (const registration of this.tools.values()) {
154
+ if (registration.enabled) {
155
+ tools.push(pluginToTool(registration.plugin));
156
+ }
157
+ }
158
+
159
+ return tools;
160
+ }
161
+
162
+ /**
163
+ * 핸들러 맵 반환
164
+ *
165
+ * 활성화된 도구의 핸들러만 반환
166
+ */
167
+ toHandlers(): Record<string, (args: Record<string, unknown>) => Promise<unknown>> {
168
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {};
169
+
170
+ for (const [name, registration] of this.tools) {
171
+ if (registration.enabled) {
172
+ handlers[name] = async (args) => registration.plugin.execute(args);
173
+ }
174
+ }
175
+
176
+ return handlers;
177
+ }
178
+
179
+ /**
180
+ * 카테고리별 도구 목록
181
+ */
182
+ getByCategory(category: string): McpToolPlugin[] {
183
+ const names = this.categories.get(category);
184
+ if (!names) return [];
185
+
186
+ return Array.from(names)
187
+ .map((name) => this.tools.get(name)?.plugin)
188
+ .filter((p): p is McpToolPlugin => p !== undefined);
189
+ }
190
+
191
+ /**
192
+ * 모든 카테고리 목록
193
+ */
194
+ getCategories(): string[] {
195
+ return Array.from(this.categories.keys());
196
+ }
197
+
198
+ /**
199
+ * 등록된 도구 수
200
+ */
201
+ get size(): number {
202
+ return this.tools.size;
203
+ }
204
+
205
+ /**
206
+ * 활성화된 도구 수
207
+ */
208
+ get enabledCount(): number {
209
+ let count = 0;
210
+ for (const registration of this.tools.values()) {
211
+ if (registration.enabled) count++;
212
+ }
213
+ return count;
214
+ }
215
+
216
+ /**
217
+ * 모든 도구 이름
218
+ */
219
+ get names(): string[] {
220
+ return Array.from(this.tools.keys());
221
+ }
222
+
223
+ /**
224
+ * 모든 도구 초기화
225
+ */
226
+ clear(): void {
227
+ this.tools.clear();
228
+ this.categories.clear();
229
+ this.emit({ type: "clear" });
230
+ }
231
+
232
+ /**
233
+ * 이벤트 리스너 등록
234
+ */
235
+ on(listener: (event: RegistryEvent) => void): () => void {
236
+ this.listeners.add(listener);
237
+ return () => this.listeners.delete(listener);
238
+ }
239
+
240
+ /**
241
+ * 이벤트 발생
242
+ */
243
+ private emit(event: RegistryEvent): void {
244
+ for (const listener of this.listeners) {
245
+ try {
246
+ listener(event);
247
+ } catch (err) {
248
+ console.error("[McpToolRegistry] Listener error:", err);
249
+ }
250
+ }
251
+ }
252
+
253
+ /**
254
+ * 디버그용 상태 덤프
255
+ */
256
+ dump(): RegistryDump {
257
+ const tools: Record<string, { category?: string; enabled: boolean; registeredAt: string }> = {};
258
+
259
+ for (const [name, reg] of this.tools) {
260
+ tools[name] = {
261
+ category: reg.category,
262
+ enabled: reg.enabled,
263
+ registeredAt: reg.registeredAt.toISOString(),
264
+ };
265
+ }
266
+
267
+ return {
268
+ totalTools: this.size,
269
+ enabledTools: this.enabledCount,
270
+ categories: this.getCategories(),
271
+ tools,
272
+ };
273
+ }
274
+ }
275
+
276
+ /**
277
+ * 레지스트리 이벤트
278
+ */
279
+ export interface RegistryEvent {
280
+ type: "register" | "unregister" | "enable" | "disable" | "clear";
281
+ toolName?: string;
282
+ category?: string;
283
+ }
284
+
285
+ /**
286
+ * 레지스트리 상태 덤프
287
+ */
288
+ export interface RegistryDump {
289
+ totalTools: number;
290
+ enabledTools: number;
291
+ categories: string[];
292
+ tools: Record<string, { category?: string; enabled: boolean; registeredAt: string }>;
293
+ }
294
+
295
+ /**
296
+ * 전역 MCP 도구 레지스트리 인스턴스
297
+ */
298
+ export const mcpToolRegistry = new McpToolRegistry();
package/src/server.ts CHANGED
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Mandu MCP Server v2
3
+ *
4
+ * DNA 기능 통합:
5
+ * - DNA-001: 플러그인 기반 도구 등록
6
+ * - DNA-006: 설정 핫 리로드
7
+ * - DNA-007: 에러 추출 및 분류
8
+ * - DNA-008: 구조화된 로깅
9
+ * - DNA-016: Pre/Post 도구 훅
10
+ */
11
+
1
12
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
14
  import {
@@ -5,40 +16,60 @@ import {
5
16
  ListToolsRequestSchema,
6
17
  ListResourcesRequestSchema,
7
18
  ReadResourceRequestSchema,
8
- type Tool,
9
- type Resource,
10
19
  } from "@modelcontextprotocol/sdk/types.js";
11
20
 
12
- import { specTools, specToolDefinitions } from "./tools/spec.js";
13
- import { generateTools, generateToolDefinitions } from "./tools/generate.js";
14
- import { transactionTools, transactionToolDefinitions } from "./tools/transaction.js";
15
- import { historyTools, historyToolDefinitions } from "./tools/history.js";
16
- import { guardTools, guardToolDefinitions } from "./tools/guard.js";
17
- import { slotTools, slotToolDefinitions } from "./tools/slot.js";
18
- import { hydrationTools, hydrationToolDefinitions } from "./tools/hydration.js";
19
- import { contractTools, contractToolDefinitions } from "./tools/contract.js";
20
- import { brainTools, brainToolDefinitions } from "./tools/brain.js";
21
- import { runtimeTools, runtimeToolDefinitions } from "./tools/runtime.js";
22
- import { seoTools, seoToolDefinitions } from "./tools/seo.js";
23
- import { projectTools, projectToolDefinitions } from "./tools/project.js";
21
+ import { loadManduConfig, startWatcher, type ManduConfig } from "@mandujs/core";
22
+
23
+ // DNA-001: 플러그인 기반 도구 레지스트리
24
+ import { mcpToolRegistry } from "./registry/mcp-tool-registry.js";
25
+ import { registerBuiltinTools, getToolsSummary } from "./tools/index.js";
26
+
27
+ // DNA-007: 에러 처리
28
+ import { createToolResponse, logToolError } from "./executor/error-handler.js";
29
+ import { ToolExecutor, createToolExecutor } from "./executor/tool-executor.js";
30
+
31
+ // DNA-008: 로깅 통합
32
+ import { setupMcpLogging, teardownMcpLogging } from "./logging/mcp-transport.js";
33
+
34
+ // DNA-016: 훅 시스템
35
+ import { mcpHookRegistry, registerDefaultMcpHooks, type McpToolContext } from "./hooks/mcp-hooks.js";
36
+
37
+ // DNA-006: 설정 핫 리로드
38
+ import { startMcpConfigWatcher, type McpConfigWatcher } from "./hooks/config-watcher.js";
39
+
40
+ // 기존 컴포넌트
24
41
  import { resourceHandlers, resourceDefinitions } from "./resources/handlers.js";
25
42
  import { findProjectRoot } from "./utils/project.js";
26
43
  import { applyWarningInjection } from "./utils/withWarnings.js";
27
44
  import { ActivityMonitor } from "./activity-monitor.js";
28
- import { startWatcher } from "../../core/src/index.js";
29
45
 
46
+ /**
47
+ * MCP 서버 버전
48
+ */
49
+ const MCP_VERSION = "0.12.0";
50
+
51
+ /**
52
+ * ManduMcpServer v2
53
+ *
54
+ * DNA 기능들을 통합한 MCP 서버
55
+ */
30
56
  export class ManduMcpServer {
31
57
  private server: Server;
32
58
  private projectRoot: string;
33
59
  private monitor: ActivityMonitor;
60
+ private config?: ManduConfig;
61
+ private configWatcher?: McpConfigWatcher;
62
+ private toolExecutor: ToolExecutor;
34
63
 
35
64
  constructor(projectRoot: string) {
36
65
  this.projectRoot = projectRoot;
37
66
  this.monitor = new ActivityMonitor(projectRoot);
67
+
68
+ // MCP Server 초기화
38
69
  this.server = new Server(
39
70
  {
40
71
  name: "mandu-mcp",
41
- version: "0.1.0",
72
+ version: MCP_VERSION,
42
73
  },
43
74
  {
44
75
  capabilities: {
@@ -49,100 +80,52 @@ export class ManduMcpServer {
49
80
  }
50
81
  );
51
82
 
83
+ // DNA-001: 플러그인 기반 도구 등록
84
+ registerBuiltinTools(projectRoot, this.server, this.monitor);
85
+
86
+ // DNA-008: 로깅 통합
87
+ setupMcpLogging({ consoleOutput: false });
88
+
89
+ // DNA-016: 기본 훅 등록
90
+ registerDefaultMcpHooks();
91
+
92
+ // Tool Executor 생성
93
+ this.toolExecutor = createToolExecutor({
94
+ projectRoot,
95
+ logTool: (name, args, result, error) => this.monitor.logTool(name, args, result, error),
96
+ logResult: (name, result) => this.monitor.logResult(name, result),
97
+ });
98
+
99
+ // 핸들러 등록
52
100
  this.registerToolHandlers();
53
101
  this.registerResourceHandlers();
54
102
  }
55
103
 
56
- private getAllToolDefinitions(): Tool[] {
57
- return [
58
- ...specToolDefinitions,
59
- ...generateToolDefinitions,
60
- ...transactionToolDefinitions,
61
- ...historyToolDefinitions,
62
- ...guardToolDefinitions,
63
- ...slotToolDefinitions,
64
- ...hydrationToolDefinitions,
65
- ...contractToolDefinitions,
66
- ...brainToolDefinitions,
67
- ...runtimeToolDefinitions,
68
- ...seoToolDefinitions,
69
- ...projectToolDefinitions,
70
- ];
71
- }
72
-
73
- private getAllToolHandlers(): Record<string, (args: Record<string, unknown>) => Promise<unknown>> {
74
- const handlers = {
75
- ...specTools(this.projectRoot),
76
- ...generateTools(this.projectRoot),
77
- ...transactionTools(this.projectRoot),
78
- ...historyTools(this.projectRoot),
79
- ...guardTools(this.projectRoot),
80
- ...slotTools(this.projectRoot),
81
- ...hydrationTools(this.projectRoot),
82
- ...contractTools(this.projectRoot),
83
- ...brainTools(this.projectRoot, this.server, this.monitor),
84
- ...runtimeTools(this.projectRoot),
85
- ...seoTools(this.projectRoot),
86
- ...projectTools(this.projectRoot, this.server, this.monitor),
87
- };
88
-
89
- return applyWarningInjection(handlers);
90
- }
91
-
104
+ /**
105
+ * 도구 핸들러 등록 (DNA-001 레지스트리 사용)
106
+ */
92
107
  private registerToolHandlers(): void {
93
- const toolHandlers = this.getAllToolHandlers();
94
-
108
+ // 도구 목록 요청
95
109
  this.server.setRequestHandler(ListToolsRequestSchema, async () => {
96
110
  return {
97
- tools: this.getAllToolDefinitions(),
111
+ tools: mcpToolRegistry.toToolDefinitions(),
98
112
  };
99
113
  });
100
114
 
115
+ // 도구 실행 요청
101
116
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
102
117
  const { name, arguments: args } = request.params;
103
118
 
104
- const handler = toolHandlers[name];
105
- if (!handler) {
106
- this.monitor.logTool(name, args, null, "Unknown tool");
107
- return {
108
- content: [
109
- {
110
- type: "text",
111
- text: JSON.stringify({ error: `Unknown tool: ${name}` }),
112
- },
113
- ],
114
- isError: true,
115
- };
116
- }
119
+ // DNA-007 + DNA-016: Tool Executor로 실행
120
+ const result = await this.toolExecutor.execute(name, args || {});
117
121
 
118
- try {
119
- this.monitor.logTool(name, args);
120
- const result = await handler(args || {});
121
- this.monitor.logResult(name, result);
122
- return {
123
- content: [
124
- {
125
- type: "text",
126
- text: JSON.stringify(result, null, 2),
127
- },
128
- ],
129
- };
130
- } catch (error) {
131
- const msg = error instanceof Error ? error.message : String(error);
132
- this.monitor.logTool(name, args, null, msg);
133
- return {
134
- content: [
135
- {
136
- type: "text",
137
- text: JSON.stringify({ error: msg }),
138
- },
139
- ],
140
- isError: true,
141
- };
142
- }
122
+ return result.response;
143
123
  });
144
124
  }
145
125
 
126
+ /**
127
+ * 리소스 핸들러 등록 (기존 유지)
128
+ */
146
129
  private registerResourceHandlers(): void {
147
130
  const handlers = resourceHandlers(this.projectRoot);
148
131
 
@@ -157,7 +140,7 @@ export class ManduMcpServer {
157
140
 
158
141
  const handler = handlers[uri];
159
142
  if (!handler) {
160
- // Try pattern matching for dynamic resources
143
+ // 동적 리소스 패턴 매칭
161
144
  for (const [pattern, h] of Object.entries(handlers)) {
162
145
  if (pattern.includes("{") && matchResourcePattern(pattern, uri)) {
163
146
  const params = extractResourceParams(pattern, uri);
@@ -212,12 +195,44 @@ export class ManduMcpServer {
212
195
  });
213
196
  }
214
197
 
198
+ /**
199
+ * 서버 실행
200
+ */
215
201
  async run(): Promise<void> {
202
+ // 설정 로드
203
+ try {
204
+ this.config = await loadManduConfig(this.projectRoot);
205
+ this.toolExecutor.updateConfig(this.config);
206
+ } catch {
207
+ // 설정 로드 실패 시 기본값 사용
208
+ console.error("[MCP] Config load failed, using defaults");
209
+ }
210
+
211
+ // DNA-006: 설정 핫 리로드 시작
212
+ try {
213
+ this.configWatcher = await startMcpConfigWatcher(this.projectRoot, {
214
+ server: this.server,
215
+ onReload: (newConfig) => {
216
+ this.config = newConfig;
217
+ this.toolExecutor.updateConfig(newConfig);
218
+ },
219
+ onMcpConfigChange: async () => {
220
+ // MCP 설정 변경 시 도구 재등록 가능
221
+ // 현재는 알림만 전송
222
+ },
223
+ });
224
+ } catch {
225
+ console.error("[MCP] Config watcher start failed (non-critical)");
226
+ }
227
+
228
+ // 서버 연결
216
229
  const transport = new StdioServerTransport();
217
230
  await this.server.connect(transport);
231
+
232
+ // 모니터 시작
218
233
  this.monitor.start();
219
234
 
220
- // Auto-start watcher with activity monitor integration
235
+ // 와처 자동 시작
221
236
  try {
222
237
  const watcher = await startWatcher({ rootDir: this.projectRoot });
223
238
  watcher.onWarning((warning) => {
@@ -225,9 +240,10 @@ export class ManduMcpServer {
225
240
  warning.level || "warn",
226
241
  warning.ruleId,
227
242
  warning.file,
228
- warning.message,
243
+ warning.message
229
244
  );
230
- // Also notify Claude Code via MCP
245
+
246
+ // Claude Code에 알림
231
247
  this.server.sendLoggingMessage({
232
248
  level: "warning",
233
249
  logger: "mandu-watch",
@@ -243,17 +259,67 @@ export class ManduMcpServer {
243
259
  },
244
260
  }).catch(() => {});
245
261
  });
262
+
246
263
  this.monitor.logEvent("SYSTEM", "Watcher auto-started");
247
264
  } catch {
248
265
  this.monitor.logEvent("SYSTEM", "Watcher auto-start failed (non-critical)");
249
266
  }
250
267
 
251
- console.error(`Mandu MCP Server running for project: ${this.projectRoot}`);
268
+ // 시작 로그
269
+ const summary = getToolsSummary();
270
+ console.error(`Mandu MCP Server v${MCP_VERSION} running`);
271
+ console.error(` Project: ${this.projectRoot}`);
272
+ console.error(` Tools: ${summary.total} (${summary.categories.join(", ")})`);
273
+ }
274
+
275
+ /**
276
+ * 서버 종료
277
+ */
278
+ async stop(): Promise<void> {
279
+ // 설정 감시 중지
280
+ this.configWatcher?.stop();
281
+
282
+ // 로깅 해제
283
+ teardownMcpLogging();
284
+
285
+ // 모니터 종료
286
+ this.monitor.stop();
287
+
288
+ // 훅 정리
289
+ mcpHookRegistry.clear();
290
+
291
+ // 도구 레지스트리 정리
292
+ mcpToolRegistry.clear();
293
+ }
294
+
295
+ /**
296
+ * 현재 설정 반환
297
+ */
298
+ getConfig(): ManduConfig | undefined {
299
+ return this.config;
300
+ }
301
+
302
+ /**
303
+ * 도구 레지스트리 접근
304
+ */
305
+ getToolRegistry(): typeof mcpToolRegistry {
306
+ return mcpToolRegistry;
307
+ }
308
+
309
+ /**
310
+ * 훅 레지스트리 접근
311
+ */
312
+ getHookRegistry(): typeof mcpHookRegistry {
313
+ return mcpHookRegistry;
252
314
  }
253
315
  }
254
316
 
317
+ // ============================================
318
+ // 유틸리티 함수
319
+ // ============================================
320
+
255
321
  /**
256
- * Match a resource pattern like "mandu://slots/{routeId}" against a URI
322
+ * 리소스 패턴 매칭
257
323
  */
258
324
  function matchResourcePattern(pattern: string, uri: string): boolean {
259
325
  const regexPattern = pattern.replace(/\{[^}]+\}/g, "([^/]+)");
@@ -262,7 +328,7 @@ function matchResourcePattern(pattern: string, uri: string): boolean {
262
328
  }
263
329
 
264
330
  /**
265
- * Extract parameters from a URI based on a pattern
331
+ * 리소스 파라미터 추출
266
332
  */
267
333
  function extractResourceParams(pattern: string, uri: string): Record<string, string> {
268
334
  const paramNames: string[] = [];
@@ -285,10 +351,15 @@ function extractResourceParams(pattern: string, uri: string): Record<string, str
285
351
  }
286
352
 
287
353
  /**
288
- * Create and start the MCP server
354
+ * MCP 서버 시작
289
355
  */
290
356
  export async function startServer(projectRoot?: string): Promise<void> {
291
357
  const root = projectRoot || (await findProjectRoot()) || process.cwd();
292
358
  const server = new ManduMcpServer(root);
293
359
  await server.run();
294
360
  }
361
+
362
+ // Re-exports
363
+ export { mcpToolRegistry } from "./registry/mcp-tool-registry.js";
364
+ export { mcpHookRegistry } from "./hooks/mcp-hooks.js";
365
+ export { registerBuiltinTools } from "./tools/index.js";