@mandujs/mcp 0.9.12 → 0.9.14
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 +2 -2
- package/src/activity-monitor.ts +231 -0
- package/src/server.ts +41 -4
- package/src/tools/brain.ts +14 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.14",
|
|
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.
|
|
35
|
+
"@mandujs/core": "^0.9.14",
|
|
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
|
}
|
package/src/tools/brain.ts
CHANGED
|
@@ -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 =
|
|
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.",
|