@mandujs/mcp 0.28.2 → 0.30.0
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 +3 -3
- package/src/activity-monitor.ts +176 -0
- package/src/tools/ate-run.ts +404 -154
- package/src/tools/ate.ts +154 -5
- package/src/tools/brain.ts +37 -1
- package/src/tools/index.ts +33 -9
- package/src/tools/project.ts +128 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@mandujs/core": "^0.
|
|
38
|
-
"@mandujs/ate": "^0.
|
|
37
|
+
"@mandujs/core": "^0.42.0",
|
|
38
|
+
"@mandujs/ate": "^0.25.1",
|
|
39
39
|
"@mandujs/skills": "^0.18.0",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
41
41
|
},
|
package/src/activity-monitor.ts
CHANGED
|
@@ -9,6 +9,14 @@ import fs from "fs";
|
|
|
9
9
|
import path from "path";
|
|
10
10
|
import type { Subprocess } from "bun";
|
|
11
11
|
import { eventBus } from "@mandujs/core/observability";
|
|
12
|
+
import type { AteMonitorEvent } from "@mandujs/ate";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Local alias — reserved in case we need to accept slightly looser
|
|
16
|
+
* shapes at the subscription boundary (forward-compat with events
|
|
17
|
+
* emitted by newer ATE versions). Today it is a direct re-export.
|
|
18
|
+
*/
|
|
19
|
+
type AteMonitorEventShape = AteMonitorEvent;
|
|
12
20
|
|
|
13
21
|
const TOOL_ICONS: Record<string, string> = {
|
|
14
22
|
// Spec
|
|
@@ -60,6 +68,10 @@ const TOOL_ICONS: Record<string, string> = {
|
|
|
60
68
|
mandu_add_client_slot: "CLIENT+",
|
|
61
69
|
// Error
|
|
62
70
|
mandu_analyze_error: "ERROR",
|
|
71
|
+
// ATE — display tokens for per-run/per-spec lifecycle events
|
|
72
|
+
"ate.run": "ATE-RUN",
|
|
73
|
+
"ate.pass": "ATE-PASS",
|
|
74
|
+
"ate.fail": "ATE-FAIL",
|
|
63
75
|
};
|
|
64
76
|
|
|
65
77
|
type MonitorSeverity = "info" | "warn" | "error";
|
|
@@ -281,6 +293,12 @@ export class ActivityMonitor {
|
|
|
281
293
|
private toolStartTimes = new Map<string, number>();
|
|
282
294
|
// Phase 5-1: 에이전트 세션 식별 (MCP 클라이언트별 추적)
|
|
283
295
|
public sessionId: string = crypto.randomUUID();
|
|
296
|
+
// ATE monitor plumbing — subscription handle + per-run accumulator for
|
|
297
|
+
// artifacts (so run_end can summarize them) + per-spec failure kind
|
|
298
|
+
// cache (so spec_done can inline it).
|
|
299
|
+
private ateUnsubscribe: (() => void) | null = null;
|
|
300
|
+
private ateRunArtifacts = new Map<string, { count: number; dir?: string }>();
|
|
301
|
+
private ateSpecFailureKinds = new Map<string, string>();
|
|
284
302
|
|
|
285
303
|
constructor(projectRoot: string) {
|
|
286
304
|
this.projectRoot = projectRoot;
|
|
@@ -338,9 +356,25 @@ export class ActivityMonitor {
|
|
|
338
356
|
if (this.config.openTerminal) {
|
|
339
357
|
this.openTerminal();
|
|
340
358
|
}
|
|
359
|
+
|
|
360
|
+
// Subscribe to ATE runner events — structured per-run progress,
|
|
361
|
+
// per-spec pass/fail, failure.v1 captures, artifact writes.
|
|
362
|
+
this.ateUnsubscribe = eventBus.on("ate", (event) => {
|
|
363
|
+
try {
|
|
364
|
+
const payload = event.data as unknown as AteMonitorEventShape | undefined;
|
|
365
|
+
if (!payload || typeof payload.kind !== "string") return;
|
|
366
|
+
this.handleAteEvent(payload);
|
|
367
|
+
} catch {
|
|
368
|
+
// Never let a bad payload tear the monitor down.
|
|
369
|
+
}
|
|
370
|
+
});
|
|
341
371
|
}
|
|
342
372
|
|
|
343
373
|
stop(): void {
|
|
374
|
+
if (this.ateUnsubscribe) {
|
|
375
|
+
this.ateUnsubscribe();
|
|
376
|
+
this.ateUnsubscribe = null;
|
|
377
|
+
}
|
|
344
378
|
this.flush(true);
|
|
345
379
|
if (this.tailProcess) {
|
|
346
380
|
this.tailProcess.kill();
|
|
@@ -637,6 +671,148 @@ export class ActivityMonitor {
|
|
|
637
671
|
}
|
|
638
672
|
}
|
|
639
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Render an ATE monitor event (run_start / spec_progress / spec_done /
|
|
676
|
+
* failure_captured / artifact_saved / run_end). Writes through the
|
|
677
|
+
* shared output path so both pretty + JSON modes work uniformly.
|
|
678
|
+
*
|
|
679
|
+
* Pretty mode policies:
|
|
680
|
+
* - `spec_progress` suppressed unless MANDU_ATE_VERBOSE=1 or the
|
|
681
|
+
* phase is `capturing_artifacts` (signal useful for debugging).
|
|
682
|
+
* - `artifact_saved` collected silently and summarized in run_end.
|
|
683
|
+
* - `spec_done(fail)` inlines the `failure.v1` kind when a matching
|
|
684
|
+
* `failure_captured` fired within the same spec.
|
|
685
|
+
*/
|
|
686
|
+
private handleAteEvent(data: AteMonitorEventShape): void {
|
|
687
|
+
if (!this.logStream) return;
|
|
688
|
+
|
|
689
|
+
// JSON mode → verbatim line per event.
|
|
690
|
+
if (this.outputFormat === "json") {
|
|
691
|
+
const payload: MonitorEvent = {
|
|
692
|
+
ts: new Date().toISOString(),
|
|
693
|
+
type: `ate.${data.kind}`,
|
|
694
|
+
severity: this.ateSeverityFor(data),
|
|
695
|
+
source: "ate",
|
|
696
|
+
data: data as unknown as Record<string, unknown>,
|
|
697
|
+
};
|
|
698
|
+
const line = this.formatEvent(payload);
|
|
699
|
+
if (line) {
|
|
700
|
+
this.write(line);
|
|
701
|
+
this.updateSummary(payload);
|
|
702
|
+
}
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Pretty mode — route per-kind.
|
|
707
|
+
const verbose = process.env.MANDU_ATE_VERBOSE === "1";
|
|
708
|
+
const time = getTime();
|
|
709
|
+
|
|
710
|
+
switch (data.kind) {
|
|
711
|
+
case "run_start": {
|
|
712
|
+
this.ateRunArtifacts.set(data.runId, { count: 0 });
|
|
713
|
+
const runIdShort = data.runId.slice(-8);
|
|
714
|
+
const line = `${time} > [ATE-RUN] ${runIdShort} starting (${data.specPaths.length} specs)\n`;
|
|
715
|
+
this.write(line);
|
|
716
|
+
this.updateSummary({
|
|
717
|
+
ts: new Date().toISOString(),
|
|
718
|
+
type: "ate.run_start",
|
|
719
|
+
severity: "info",
|
|
720
|
+
source: "ate",
|
|
721
|
+
});
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
case "spec_progress": {
|
|
725
|
+
// Suppressed by default — too noisy. Render only when
|
|
726
|
+
// MANDU_ATE_VERBOSE=1 is set.
|
|
727
|
+
if (!verbose) return;
|
|
728
|
+
const line = `${time} [ATE] ${data.specPath} (${data.phase})\n`;
|
|
729
|
+
this.write(line);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
case "failure_captured": {
|
|
733
|
+
// Cache the failure kind so `spec_done` can inline it. Render
|
|
734
|
+
// nothing here — the line is attached to the spec_done row.
|
|
735
|
+
this.ateSpecFailureKinds.set(
|
|
736
|
+
`${data.runId}:${data.specPath}`,
|
|
737
|
+
data.failure.kind,
|
|
738
|
+
);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
case "spec_done": {
|
|
742
|
+
const secs = (data.durationMs / 1000).toFixed(1);
|
|
743
|
+
const file = data.specPath.split(/[\\/]/).pop() ?? data.specPath;
|
|
744
|
+
if (data.status === "pass") {
|
|
745
|
+
const line = `${time} + [ATE] ${file} (${secs}s)\n`;
|
|
746
|
+
this.write(line);
|
|
747
|
+
this.updateSummary({
|
|
748
|
+
ts: new Date().toISOString(),
|
|
749
|
+
type: "ate.spec_done",
|
|
750
|
+
severity: "info",
|
|
751
|
+
source: "ate",
|
|
752
|
+
});
|
|
753
|
+
} else if (data.status === "fail") {
|
|
754
|
+
const kindKey = `${data.runId}:${data.specPath}`;
|
|
755
|
+
const failureKind = this.ateSpecFailureKinds.get(kindKey);
|
|
756
|
+
this.ateSpecFailureKinds.delete(kindKey);
|
|
757
|
+
const suffix = failureKind ? ` [${failureKind}]` : "";
|
|
758
|
+
const line = `${time} x [ATE] ${file} (${secs}s)${suffix}\n`;
|
|
759
|
+
this.write(line);
|
|
760
|
+
this.updateSummary({
|
|
761
|
+
ts: new Date().toISOString(),
|
|
762
|
+
type: "ate.spec_done",
|
|
763
|
+
severity: "error",
|
|
764
|
+
source: "ate",
|
|
765
|
+
});
|
|
766
|
+
} else {
|
|
767
|
+
// skip
|
|
768
|
+
if (verbose) {
|
|
769
|
+
const line = `${time} [ATE] ${file} skipped\n`;
|
|
770
|
+
this.write(line);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
case "artifact_saved": {
|
|
776
|
+
// Accumulate silently; run_end summarizes.
|
|
777
|
+
const entry = this.ateRunArtifacts.get(data.runId) ?? { count: 0 };
|
|
778
|
+
entry.count += 1;
|
|
779
|
+
if (!entry.dir) {
|
|
780
|
+
const dir = path.dirname(data.path);
|
|
781
|
+
entry.dir = dir;
|
|
782
|
+
}
|
|
783
|
+
this.ateRunArtifacts.set(data.runId, entry);
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
case "run_end": {
|
|
787
|
+
const runIdShort = data.runId.slice(-8);
|
|
788
|
+
const secs = (data.durationMs / 1000).toFixed(1);
|
|
789
|
+
const artifactInfo = this.ateRunArtifacts.get(data.runId);
|
|
790
|
+
this.ateRunArtifacts.delete(data.runId);
|
|
791
|
+
const artifactSuffix = artifactInfo && artifactInfo.count > 0 && artifactInfo.dir
|
|
792
|
+
? `. artifacts: ${artifactInfo.dir}`
|
|
793
|
+
: "";
|
|
794
|
+
const line =
|
|
795
|
+
`${time} * [ATE-RUN] ${runIdShort} done — ` +
|
|
796
|
+
`${data.passed} pass, ${data.failed} fail, ${data.skipped} skip (${secs}s)${artifactSuffix}\n`;
|
|
797
|
+
this.write(line);
|
|
798
|
+
this.updateSummary({
|
|
799
|
+
ts: new Date().toISOString(),
|
|
800
|
+
type: "ate.run_end",
|
|
801
|
+
severity: data.failed > 0 ? "error" : "info",
|
|
802
|
+
source: "ate",
|
|
803
|
+
});
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
private ateSeverityFor(data: AteMonitorEventShape): MonitorSeverity {
|
|
810
|
+
if (data.kind === "failure_captured") return "error";
|
|
811
|
+
if (data.kind === "spec_done" && data.status === "fail") return "error";
|
|
812
|
+
if (data.kind === "run_end" && data.failed > 0) return "error";
|
|
813
|
+
return "info";
|
|
814
|
+
}
|
|
815
|
+
|
|
640
816
|
private enqueue(event: MonitorEvent): void {
|
|
641
817
|
if (!this.logStream) return;
|
|
642
818
|
const now = Date.now();
|