@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 +2 -2
- package/src/activity-monitor.ts +231 -0
- package/src/resources/handlers.ts +52 -0
- package/src/server.ts +42 -4
- package/src/tools/brain.ts +86 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.9.
|
|
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.
|
|
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
|
}
|
package/src/tools/brain.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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 {
|