@mandujs/mcp 0.9.11 → 0.9.13

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/mcp",
3
- "version": "0.9.11",
3
+ "version": "0.9.13",
4
4
  "description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -32,7 +32,7 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@mandujs/core": "^0.9.11",
35
+ "@mandujs/core": "^0.9.13",
36
36
  "@modelcontextprotocol/sdk": "^1.25.3"
37
37
  },
38
38
  "engines": {
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Mandu Activity Monitor
3
+ *
4
+ * Real-time terminal dashboard for MCP server activity.
5
+ * Opens automatically when the MCP server starts.
6
+ * Shows all tool calls, watch events, errors, and agent behavior.
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { spawn, type ChildProcess } from "child_process";
12
+
13
+ const TOOL_ICONS: Record<string, string> = {
14
+ // Spec
15
+ mandu_list_routes: "SPEC",
16
+ mandu_get_route: "SPEC",
17
+ mandu_add_route: "SPEC+",
18
+ mandu_update_route: "SPEC~",
19
+ mandu_delete_route: "SPEC-",
20
+ mandu_validate_spec: "SPEC?",
21
+ // Generate
22
+ mandu_generate: "GEN",
23
+ mandu_generate_status: "GEN?",
24
+ // Guard
25
+ mandu_guard_check: "GUARD",
26
+ // Slot
27
+ mandu_read_slot: "SLOT",
28
+ mandu_write_slot: "SLOT~",
29
+ mandu_validate_slot: "SLOT?",
30
+ // Contract
31
+ mandu_list_contracts: "CONTRACT",
32
+ mandu_get_contract: "CONTRACT",
33
+ mandu_create_contract: "CONTRACT+",
34
+ mandu_validate_contracts: "CONTRACT?",
35
+ mandu_sync_contract_slot: "SYNC",
36
+ mandu_generate_openapi: "OPENAPI",
37
+ mandu_update_route_contract: "CONTRACT~",
38
+ // Transaction
39
+ mandu_begin: "TX-BEGIN",
40
+ mandu_commit: "TX-COMMIT",
41
+ mandu_rollback: "TX-ROLLBACK",
42
+ mandu_tx_status: "TX?",
43
+ // History
44
+ mandu_list_history: "HISTORY",
45
+ mandu_get_snapshot: "SNAPSHOT",
46
+ mandu_prune_history: "PRUNE",
47
+ // Brain
48
+ mandu_doctor: "DOCTOR",
49
+ mandu_watch_start: "WATCH+",
50
+ mandu_watch_status: "WATCH?",
51
+ mandu_watch_stop: "WATCH-",
52
+ mandu_check_location: "ARCH?",
53
+ mandu_check_import: "IMPORT?",
54
+ mandu_get_architecture: "ARCH",
55
+ // Build
56
+ mandu_build: "BUILD",
57
+ mandu_build_status: "BUILD?",
58
+ mandu_list_islands: "ISLAND",
59
+ mandu_set_hydration: "HYDRA~",
60
+ mandu_add_client_slot: "CLIENT+",
61
+ // Error
62
+ mandu_analyze_error: "ERROR",
63
+ };
64
+
65
+ function getTime(): string {
66
+ return new Date().toLocaleTimeString("ko-KR", { hour12: false });
67
+ }
68
+
69
+ function summarizeArgs(args: Record<string, unknown> | null | undefined): string {
70
+ if (!args || Object.keys(args).length === 0) return "";
71
+ const entries = Object.entries(args)
72
+ .filter(([_, v]) => v !== undefined && v !== null)
73
+ .map(([k, v]) => {
74
+ const val = typeof v === "string"
75
+ ? (v.length > 40 ? v.slice(0, 40) + "..." : v)
76
+ : JSON.stringify(v);
77
+ return `${k}=${val}`;
78
+ });
79
+ return entries.length > 0 ? ` (${entries.join(", ")})` : "";
80
+ }
81
+
82
+ function summarizeResult(result: unknown): string {
83
+ if (!result || typeof result !== "object") return "";
84
+ const obj = result as Record<string, unknown>;
85
+
86
+ // Common patterns
87
+ if (obj.error) return ` >> ERROR: ${obj.error}`;
88
+ if (obj.success === true) return obj.message ? ` >> ${obj.message}` : " >> OK";
89
+ if (obj.success === false) return ` >> FAILED: ${obj.message || "unknown"}`;
90
+ if (Array.isArray(obj.routes)) return ` >> ${obj.routes.length} routes`;
91
+ if (obj.passed === true) return obj.message ? ` >> ${obj.message}` : " >> PASSED";
92
+ if (obj.passed === false) return ` >> FAILED (${(obj.violations as unknown[])?.length || 0} violations)`;
93
+ if (obj.valid === true) return obj.message ? ` >> ${obj.message}` : " >> VALID";
94
+ if (obj.valid === false) return ` >> INVALID (${(obj.violations as unknown[])?.length || 0} violations)`;
95
+ if (obj.generated) return " >> Generated";
96
+ if (obj.status) return ` >> ${JSON.stringify(obj.status).slice(0, 60)}`;
97
+
98
+ return "";
99
+ }
100
+
101
+ export class ActivityMonitor {
102
+ private logFile: string;
103
+ private logStream: fs.WriteStream | null = null;
104
+ private tailProcess: ChildProcess | null = null;
105
+ private projectRoot: string;
106
+ private callCount = 0;
107
+
108
+ constructor(projectRoot: string) {
109
+ this.projectRoot = projectRoot;
110
+ const manduDir = path.join(projectRoot, ".mandu");
111
+ if (!fs.existsSync(manduDir)) {
112
+ fs.mkdirSync(manduDir, { recursive: true });
113
+ }
114
+ this.logFile = path.join(manduDir, "activity.log");
115
+ }
116
+
117
+ start(): void {
118
+ // Create/overwrite log file
119
+ this.logStream = fs.createWriteStream(this.logFile, { flags: "w" });
120
+
121
+ const time = getTime();
122
+ const header =
123
+ `\n` +
124
+ ` ╔══════════════════════════════════════════════╗\n` +
125
+ ` ║ MANDU MCP Activity Monitor ║\n` +
126
+ ` ║ ║\n` +
127
+ ` ║ ${time} ║\n` +
128
+ ` ║ ${this.projectRoot.slice(-40).padEnd(40)} ║\n` +
129
+ ` ╚══════════════════════════════════════════════╝\n\n`;
130
+
131
+ this.logStream.write(header);
132
+
133
+ // Auto-open terminal
134
+ this.openTerminal();
135
+ }
136
+
137
+ stop(): void {
138
+ if (this.tailProcess) {
139
+ this.tailProcess.kill();
140
+ this.tailProcess = null;
141
+ }
142
+ if (this.logStream) {
143
+ this.logStream.end();
144
+ this.logStream = null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Log a tool call (invocation)
150
+ */
151
+ logTool(
152
+ name: string,
153
+ args?: Record<string, unknown> | null,
154
+ _result?: unknown,
155
+ error?: string,
156
+ ): void {
157
+ this.callCount++;
158
+ const time = getTime();
159
+ const tag = TOOL_ICONS[name] || name.replace("mandu_", "").toUpperCase();
160
+ const argsStr = summarizeArgs(args);
161
+
162
+ let line: string;
163
+ if (error) {
164
+ line = `${time} ✗ [${tag}]${argsStr}\n ERROR: ${error}\n`;
165
+ } else {
166
+ line = `${time} → [${tag}]${argsStr}\n`;
167
+ }
168
+
169
+ this.write(line);
170
+ }
171
+
172
+ /**
173
+ * Log a tool result
174
+ */
175
+ logResult(name: string, result: unknown): void {
176
+ const time = getTime();
177
+ const tag = TOOL_ICONS[name] || name.replace("mandu_", "").toUpperCase();
178
+ const summary = summarizeResult(result);
179
+
180
+ if (summary) {
181
+ this.write(`${time} ✓ [${tag}]${summary}\n`);
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Log a watch event (called from watcher)
187
+ */
188
+ logWatch(level: string, ruleId: string, file: string, message: string): void {
189
+ const time = getTime();
190
+ const icon = level === "info" ? "ℹ" : "⚠";
191
+ this.write(`${time} ${icon} [WATCH:${ruleId}] ${file}\n ${message}\n`);
192
+ }
193
+
194
+ /**
195
+ * Log a custom event
196
+ */
197
+ logEvent(category: string, message: string): void {
198
+ const time = getTime();
199
+ this.write(`${time} [${category}] ${message}\n`);
200
+ }
201
+
202
+ private write(text: string): void {
203
+ if (this.logStream) {
204
+ this.logStream.write(text);
205
+ }
206
+ }
207
+
208
+ private openTerminal(): void {
209
+ try {
210
+ if (process.platform === "win32") {
211
+ this.tailProcess = spawn("cmd", [
212
+ "/c", "start",
213
+ "Mandu Activity Monitor",
214
+ "powershell", "-NoExit", "-Command",
215
+ `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; chcp 65001 | Out-Null; Get-Content '${this.logFile}' -Wait -Encoding UTF8`,
216
+ ], { cwd: this.projectRoot, detached: true, stdio: "ignore" });
217
+ } else if (process.platform === "darwin") {
218
+ this.tailProcess = spawn("osascript", [
219
+ "-e", `tell application "Terminal" to do script "tail -f '${this.logFile}'"`,
220
+ ], { detached: true, stdio: "ignore" });
221
+ } else {
222
+ this.tailProcess = spawn("x-terminal-emulator", [
223
+ "-e", `tail -f '${this.logFile}'`,
224
+ ], { cwd: this.projectRoot, detached: true, stdio: "ignore" });
225
+ }
226
+ this.tailProcess?.unref();
227
+ } catch {
228
+ // Terminal auto-open failed silently
229
+ }
230
+ }
231
+ }
@@ -2,6 +2,7 @@ import type { Resource } from "@modelcontextprotocol/sdk/types.js";
2
2
  import {
3
3
  loadManifest,
4
4
  getTransactionStatus,
5
+ getWatcher,
5
6
  type GeneratedMap,
6
7
  type SpecLock,
7
8
  } from "@mandujs/core";
@@ -39,6 +40,18 @@ export const resourceDefinitions: Resource[] = [
39
40
  description: "Slot file content for a specific route",
40
41
  mimeType: "text/typescript",
41
42
  },
43
+ {
44
+ uri: "mandu://watch/warnings",
45
+ name: "Watch Warnings",
46
+ description: "Recent file watcher warnings (architecture rule violations)",
47
+ mimeType: "application/json",
48
+ },
49
+ {
50
+ uri: "mandu://watch/status",
51
+ name: "Watch Status",
52
+ description: "File watcher status (active/inactive, uptime, rule count)",
53
+ mimeType: "application/json",
54
+ },
42
55
  ];
43
56
 
44
57
  type ResourceHandler = (params: Record<string, string>) => Promise<unknown>;
@@ -172,5 +185,44 @@ export function resourceHandlers(
172
185
  content,
173
186
  };
174
187
  },
188
+
189
+ "mandu://watch/warnings": async () => {
190
+ const watcher = getWatcher();
191
+ if (!watcher) {
192
+ return { active: false, warnings: [] };
193
+ }
194
+
195
+ const warnings = watcher.getRecentWarnings(50);
196
+ return {
197
+ active: true,
198
+ count: warnings.length,
199
+ warnings: warnings.map((w) => ({
200
+ ruleId: w.ruleId,
201
+ file: w.file,
202
+ message: w.message,
203
+ event: w.event,
204
+ timestamp: w.timestamp.toISOString(),
205
+ })),
206
+ };
207
+ },
208
+
209
+ "mandu://watch/status": async () => {
210
+ const watcher = getWatcher();
211
+ if (!watcher) {
212
+ return { active: false };
213
+ }
214
+
215
+ const status = watcher.getStatus();
216
+ return {
217
+ active: status.active,
218
+ rootDir: status.rootDir,
219
+ fileCount: status.fileCount,
220
+ warningCount: status.recentWarnings.length,
221
+ uptime: status.startedAt
222
+ ? Math.floor((Date.now() - status.startedAt.getTime()) / 1000)
223
+ : 0,
224
+ startedAt: status.startedAt?.toISOString() || null,
225
+ };
226
+ },
175
227
  };
176
228
  }
package/src/server.ts CHANGED
@@ -20,13 +20,17 @@ import { contractTools, contractToolDefinitions } from "./tools/contract.js";
20
20
  import { brainTools, brainToolDefinitions } from "./tools/brain.js";
21
21
  import { resourceHandlers, resourceDefinitions } from "./resources/handlers.js";
22
22
  import { findProjectRoot } from "./utils/project.js";
23
+ import { ActivityMonitor } from "./activity-monitor.js";
24
+ import { startWatcher } from "../../core/src/index.js";
23
25
 
24
26
  export class ManduMcpServer {
25
27
  private server: Server;
26
28
  private projectRoot: string;
29
+ private monitor: ActivityMonitor;
27
30
 
28
31
  constructor(projectRoot: string) {
29
32
  this.projectRoot = projectRoot;
33
+ this.monitor = new ActivityMonitor(projectRoot);
30
34
  this.server = new Server(
31
35
  {
32
36
  name: "mandu-mcp",
@@ -36,6 +40,7 @@ export class ManduMcpServer {
36
40
  capabilities: {
37
41
  tools: {},
38
42
  resources: {},
43
+ logging: {},
39
44
  },
40
45
  }
41
46
  );
@@ -68,7 +73,7 @@ export class ManduMcpServer {
68
73
  ...slotTools(this.projectRoot),
69
74
  ...hydrationTools(this.projectRoot),
70
75
  ...contractTools(this.projectRoot),
71
- ...brainTools(this.projectRoot),
76
+ ...brainTools(this.projectRoot, this.server, this.monitor),
72
77
  };
73
78
  }
74
79
 
@@ -86,6 +91,7 @@ export class ManduMcpServer {
86
91
 
87
92
  const handler = toolHandlers[name];
88
93
  if (!handler) {
94
+ this.monitor.logTool(name, args, null, "Unknown tool");
89
95
  return {
90
96
  content: [
91
97
  {
@@ -98,7 +104,9 @@ export class ManduMcpServer {
98
104
  }
99
105
 
100
106
  try {
107
+ this.monitor.logTool(name, args);
101
108
  const result = await handler(args || {});
109
+ this.monitor.logResult(name, result);
102
110
  return {
103
111
  content: [
104
112
  {
@@ -108,13 +116,13 @@ export class ManduMcpServer {
108
116
  ],
109
117
  };
110
118
  } catch (error) {
119
+ const msg = error instanceof Error ? error.message : String(error);
120
+ this.monitor.logTool(name, args, null, msg);
111
121
  return {
112
122
  content: [
113
123
  {
114
124
  type: "text",
115
- text: JSON.stringify({
116
- error: error instanceof Error ? error.message : String(error),
117
- }),
125
+ text: JSON.stringify({ error: msg }),
118
126
  },
119
127
  ],
120
128
  isError: true,
@@ -195,6 +203,36 @@ export class ManduMcpServer {
195
203
  async run(): Promise<void> {
196
204
  const transport = new StdioServerTransport();
197
205
  await this.server.connect(transport);
206
+ this.monitor.start();
207
+
208
+ // Auto-start watcher with activity monitor integration
209
+ try {
210
+ const watcher = await startWatcher({ rootDir: this.projectRoot });
211
+ watcher.onWarning((warning) => {
212
+ this.monitor.logWatch(
213
+ warning.level || "warn",
214
+ warning.ruleId,
215
+ warning.file,
216
+ warning.message,
217
+ );
218
+ // Also notify Claude Code via MCP
219
+ this.server.sendLoggingMessage({
220
+ level: "warning",
221
+ logger: "mandu-watch",
222
+ data: {
223
+ type: "watch_warning",
224
+ ruleId: warning.ruleId,
225
+ file: warning.file,
226
+ message: warning.message,
227
+ event: warning.event,
228
+ },
229
+ }).catch(() => {});
230
+ });
231
+ this.monitor.logEvent("SYSTEM", "Watcher auto-started");
232
+ } catch {
233
+ this.monitor.logEvent("SYSTEM", "Watcher auto-start failed (non-critical)");
234
+ }
235
+
198
236
  console.error(`Mandu MCP Server running for project: ${this.projectRoot}`);
199
237
  }
200
238
  }
@@ -11,6 +11,8 @@
11
11
  */
12
12
 
13
13
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
14
+ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
+ import type { ActivityMonitor } from "../activity-monitor.js";
14
16
  import {
15
17
  loadManifest,
16
18
  runGuardCheck,
@@ -69,6 +71,16 @@ export const brainToolDefinitions: Tool[] = [
69
71
  required: [],
70
72
  },
71
73
  },
74
+ {
75
+ name: "mandu_watch_stop",
76
+ description:
77
+ "Stop file watching and clean up MCP notification subscriptions.",
78
+ inputSchema: {
79
+ type: "object",
80
+ properties: {},
81
+ required: [],
82
+ },
83
+ },
72
84
  // Architecture tools (v0.2)
73
85
  {
74
86
  name: "mandu_check_location",
@@ -126,12 +138,15 @@ export const brainToolDefinitions: Tool[] = [
126
138
  },
127
139
  ];
128
140
 
129
- export function brainTools(projectRoot: string) {
141
+ /** Module-level unsubscribe handle for MCP warning notifications */
142
+ let mcpWarningUnsubscribe: (() => void) | null = null;
143
+
144
+ export function brainTools(projectRoot: string, server?: Server, monitor?: ActivityMonitor) {
130
145
  const paths = getProjectPaths(projectRoot);
131
146
 
132
147
  return {
133
148
  mandu_doctor: async (args: Record<string, unknown>) => {
134
- const { useLLM = true } = args as { useLLM?: boolean };
149
+ const { useLLM = false } = args as { useLLM?: boolean };
135
150
 
136
151
  try {
137
152
  // Initialize Brain
@@ -221,11 +236,55 @@ export function brainTools(projectRoot: string) {
221
236
  debounceMs,
222
237
  });
223
238
 
239
+ // Register MCP notification handler
240
+ let notifications = false;
241
+ if (server) {
242
+ // Clean up previous subscription
243
+ if (mcpWarningUnsubscribe) {
244
+ mcpWarningUnsubscribe();
245
+ mcpWarningUnsubscribe = null;
246
+ }
247
+
248
+ mcpWarningUnsubscribe = watcher.onWarning((warning) => {
249
+ // Log to activity monitor
250
+ if (monitor) {
251
+ monitor.logWatch(
252
+ warning.level || "warn",
253
+ warning.ruleId,
254
+ warning.file,
255
+ warning.message,
256
+ );
257
+ }
258
+
259
+ // Push logging message (Claude Code receives in real-time)
260
+ server.sendLoggingMessage({
261
+ level: "warning",
262
+ logger: "mandu-watch",
263
+ data: {
264
+ type: "watch_warning",
265
+ ruleId: warning.ruleId,
266
+ file: warning.file,
267
+ message: warning.message,
268
+ event: warning.event,
269
+ timestamp: warning.timestamp.toISOString(),
270
+ },
271
+ }).catch(() => {});
272
+
273
+ // Resource update notification
274
+ server.sendResourceUpdated({
275
+ uri: "mandu://watch/warnings",
276
+ }).catch(() => {});
277
+ });
278
+
279
+ notifications = true;
280
+ }
281
+
224
282
  const status = watcher.getStatus();
225
283
 
226
284
  return {
227
285
  success: true,
228
286
  message: "Watch started successfully",
287
+ notifications: notifications ? "enabled" : "disabled",
229
288
  status: {
230
289
  active: status.active,
231
290
  rootDir: status.rootDir,
@@ -238,8 +297,10 @@ export function brainTools(projectRoot: string) {
238
297
  "SLOT_NAMING - Slot 파일 네이밍 규칙",
239
298
  "CONTRACT_NAMING - Contract 파일 네이밍 규칙",
240
299
  "FORBIDDEN_IMPORT - Generated 파일의 금지된 import 감지",
300
+ "SLOT_MODIFIED - Slot 파일 수정 감지 (info)",
241
301
  ],
242
- tip: "Watch emits warnings only - it never blocks operations",
302
+ logFile: ".mandu/watch.log",
303
+ tip: "Run `tail -f .mandu/watch.log` in another terminal for real-time warnings.",
243
304
  };
244
305
  } catch (error) {
245
306
  return {
@@ -288,6 +349,28 @@ export function brainTools(projectRoot: string) {
288
349
  }
289
350
  },
290
351
 
352
+ mandu_watch_stop: async () => {
353
+ try {
354
+ // Clean up MCP notification subscription
355
+ if (mcpWarningUnsubscribe) {
356
+ mcpWarningUnsubscribe();
357
+ mcpWarningUnsubscribe = null;
358
+ }
359
+
360
+ stopWatcher();
361
+
362
+ return {
363
+ success: true,
364
+ message: "Watch stopped and notifications cleaned up",
365
+ };
366
+ } catch (error) {
367
+ return {
368
+ error: "Failed to stop watch",
369
+ details: error instanceof Error ? error.message : "Unknown error",
370
+ };
371
+ }
372
+ },
373
+
291
374
  // Architecture tools (v0.2)
292
375
  mandu_check_location: async (args: Record<string, unknown>) => {
293
376
  const { path: filePath, content } = args as {