@mandujs/mcp 0.9.12 → 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.12",
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.12",
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
+ }
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",
@@ -69,7 +73,7 @@ export class ManduMcpServer {
69
73
  ...slotTools(this.projectRoot),
70
74
  ...hydrationTools(this.projectRoot),
71
75
  ...contractTools(this.projectRoot),
72
- ...brainTools(this.projectRoot, this.server),
76
+ ...brainTools(this.projectRoot, this.server, this.monitor),
73
77
  };
74
78
  }
75
79
 
@@ -87,6 +91,7 @@ export class ManduMcpServer {
87
91
 
88
92
  const handler = toolHandlers[name];
89
93
  if (!handler) {
94
+ this.monitor.logTool(name, args, null, "Unknown tool");
90
95
  return {
91
96
  content: [
92
97
  {
@@ -99,7 +104,9 @@ export class ManduMcpServer {
99
104
  }
100
105
 
101
106
  try {
107
+ this.monitor.logTool(name, args);
102
108
  const result = await handler(args || {});
109
+ this.monitor.logResult(name, result);
103
110
  return {
104
111
  content: [
105
112
  {
@@ -109,13 +116,13 @@ export class ManduMcpServer {
109
116
  ],
110
117
  };
111
118
  } catch (error) {
119
+ const msg = error instanceof Error ? error.message : String(error);
120
+ this.monitor.logTool(name, args, null, msg);
112
121
  return {
113
122
  content: [
114
123
  {
115
124
  type: "text",
116
- text: JSON.stringify({
117
- error: error instanceof Error ? error.message : String(error),
118
- }),
125
+ text: JSON.stringify({ error: msg }),
119
126
  },
120
127
  ],
121
128
  isError: true,
@@ -196,6 +203,36 @@ export class ManduMcpServer {
196
203
  async run(): Promise<void> {
197
204
  const transport = new StdioServerTransport();
198
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
+
199
236
  console.error(`Mandu MCP Server running for project: ${this.projectRoot}`);
200
237
  }
201
238
  }
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
14
14
  import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
+ import type { ActivityMonitor } from "../activity-monitor.js";
15
16
  import {
16
17
  loadManifest,
17
18
  runGuardCheck,
@@ -140,12 +141,12 @@ export const brainToolDefinitions: Tool[] = [
140
141
  /** Module-level unsubscribe handle for MCP warning notifications */
141
142
  let mcpWarningUnsubscribe: (() => void) | null = null;
142
143
 
143
- export function brainTools(projectRoot: string, server?: Server) {
144
+ export function brainTools(projectRoot: string, server?: Server, monitor?: ActivityMonitor) {
144
145
  const paths = getProjectPaths(projectRoot);
145
146
 
146
147
  return {
147
148
  mandu_doctor: async (args: Record<string, unknown>) => {
148
- const { useLLM = true } = args as { useLLM?: boolean };
149
+ const { useLLM = false } = args as { useLLM?: boolean };
149
150
 
150
151
  try {
151
152
  // Initialize Brain
@@ -245,6 +246,16 @@ export function brainTools(projectRoot: string, server?: Server) {
245
246
  }
246
247
 
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
+
248
259
  // Push logging message (Claude Code receives in real-time)
249
260
  server.sendLoggingMessage({
250
261
  level: "warning",
@@ -286,6 +297,7 @@ export function brainTools(projectRoot: string, server?: Server) {
286
297
  "SLOT_NAMING - Slot 파일 네이밍 규칙",
287
298
  "CONTRACT_NAMING - Contract 파일 네이밍 규칙",
288
299
  "FORBIDDEN_IMPORT - Generated 파일의 금지된 import 감지",
300
+ "SLOT_MODIFIED - Slot 파일 수정 감지 (info)",
289
301
  ],
290
302
  logFile: ".mandu/watch.log",
291
303
  tip: "Run `tail -f .mandu/watch.log` in another terminal for real-time warnings.",