@agent-plan/mcp 0.2.9 → 0.2.11
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/dist/index.d.ts.map +1 -1
- package/dist/index.js +117 -13
- package/package.json +3 -2
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA21BA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,8 @@ import { join } from "node:path";
|
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import { pathToFileURL } from "node:url";
|
|
8
8
|
import { PlanStore, ExportService, withFeatureLock, needsMotivation } from "@agent-plan/core";
|
|
9
|
-
import {
|
|
9
|
+
import { serve } from "@agent-plan/server";
|
|
10
|
+
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug } from "@agent-plan/core/naming";
|
|
10
11
|
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
11
12
|
const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
|
|
12
13
|
function text(textValue, structuredContent) {
|
|
@@ -29,6 +30,67 @@ async function requireStore() {
|
|
|
29
30
|
throw new Error(`No .planner/ found at ${st.root}. Use planner-init first.`);
|
|
30
31
|
return st;
|
|
31
32
|
}
|
|
33
|
+
// In-process web server handle, managed by the planner-web tool and planner-load.
|
|
34
|
+
// Lives as long as this MCP stdio process (i.e. the host session). Null when not running.
|
|
35
|
+
let webHandle = null;
|
|
36
|
+
/** Start the web dashboard on LAN (0.0.0.0:0, OS-assigned port) if not already
|
|
37
|
+
* running. Returns the local/LAN URLs. No-op (empty url) when no .planner/ exists. */
|
|
38
|
+
async function ensureWebStarted() {
|
|
39
|
+
if (webHandle)
|
|
40
|
+
return { localUrl: webHandle.localUrl, lanUrl: webHandle.lanUrl, mode: webHandle.mode };
|
|
41
|
+
const root = planRoot();
|
|
42
|
+
const st = new PlanStore(root);
|
|
43
|
+
if (!(await st.exists()))
|
|
44
|
+
return { localUrl: "" };
|
|
45
|
+
try {
|
|
46
|
+
webHandle = await serve({ planRoot: root, host: "0.0.0.0", port: 0, quiet: true });
|
|
47
|
+
return { localUrl: webHandle.localUrl, lanUrl: webHandle.lanUrl, mode: webHandle.mode };
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return { localUrl: "" };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Build a consolidated planner recap (project state + active work + pending
|
|
54
|
+
* handoff + web URL). Harness-agnostic equivalent of Pi's buildStartupResumeSummary. */
|
|
55
|
+
async function buildRecapText(st, web) {
|
|
56
|
+
const [plan, resume, handoff] = await Promise.all([st.loadAll(), st.loadResume(), st.loadHandoff()]);
|
|
57
|
+
const allTasks = plan.phases.flatMap((p) => p.tasks.map((t) => ({ phase: p, task: t })));
|
|
58
|
+
const totalF = plan.features.features.length;
|
|
59
|
+
const doneF = plan.features.features.filter((f) => f.status === "done").length;
|
|
60
|
+
const activeF = plan.features.features.filter((f) => f.status === "in-progress").length;
|
|
61
|
+
const totalP = plan.phases.length;
|
|
62
|
+
const doneP = plan.phases.filter((p) => p.status === "done").length;
|
|
63
|
+
const activeP = plan.phases.filter((p) => p.status === "in-progress" || p.status === "discovery").length;
|
|
64
|
+
const totalT = allTasks.length;
|
|
65
|
+
const doneT = allTasks.filter(({ task }) => task.status === "done").length;
|
|
66
|
+
const activeT = allTasks.filter(({ task }) => task.status === "in-progress").length;
|
|
67
|
+
const inProgress = allTasks.filter(({ task }) => task.status === "in-progress");
|
|
68
|
+
const focus = inProgress[0];
|
|
69
|
+
const focusFeature = focus ? plan.features.features.find((f) => f.id === focus.phase.featureId) : undefined;
|
|
70
|
+
const lines = [];
|
|
71
|
+
lines.push("## Planner recap");
|
|
72
|
+
const name = plan.project.name || "(unnamed project)";
|
|
73
|
+
lines.push(`Project: ${name}${plan.project.goal ? " — " + plan.project.goal : ""}`);
|
|
74
|
+
lines.push(`Progress: Features ${doneF}/${totalF} done (${activeF} active) · Phases ${doneP}/${totalP} done (${activeP} active) · Tasks ${doneT}/${totalT} done (${activeT} active)`);
|
|
75
|
+
if (focus) {
|
|
76
|
+
lines.push(`Current focus: ${focusFeature?.name ?? "?"} / ${focus.phase.title} / ${focus.task.title} (in-progress)`);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
lines.push("Current focus: no active task — review the plan and pick the next concrete task");
|
|
80
|
+
}
|
|
81
|
+
if (resume?.nextSteps?.length)
|
|
82
|
+
lines.push(`Next step: ${resume.nextSteps[0]}`);
|
|
83
|
+
if (handoff) {
|
|
84
|
+
lines.push("", "## Pending handoff (read me first)", handoff.content || "(empty handoff)", "", "→ After processing the handoff above, call planner-handoff-clear to remove it.");
|
|
85
|
+
}
|
|
86
|
+
else if (activeT === 0) {
|
|
87
|
+
lines.push("", "No handoff pending and no task in-progress. Use planner-task-add / planner-task-start to begin work.");
|
|
88
|
+
}
|
|
89
|
+
if (web.localUrl) {
|
|
90
|
+
lines.push("", "## Web UI", `🌐 ${web.localUrl}${web.lanUrl ? " (LAN: " + web.lanUrl + ")" : ""}`);
|
|
91
|
+
}
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
32
94
|
function findFeatureByRef(features, ref) {
|
|
33
95
|
const normalized = ref.trim().toLowerCase();
|
|
34
96
|
return features.find((feature) => feature.id.toLowerCase() === normalized)
|
|
@@ -504,7 +566,7 @@ server.registerTool("planner-task-add", {
|
|
|
504
566
|
id: taskId,
|
|
505
567
|
phaseId: found.id,
|
|
506
568
|
number: found.tasks.length + 1,
|
|
507
|
-
shortName:
|
|
569
|
+
shortName: clampSlug(title, 30, `task-${Date.now().toString(36)}`),
|
|
508
570
|
title: title.trim(),
|
|
509
571
|
status: "planned",
|
|
510
572
|
description: description?.trim() ?? "",
|
|
@@ -640,12 +702,13 @@ server.registerTool("planner-task-start", {
|
|
|
640
702
|
inputSchema: { task: z.string().min(1) },
|
|
641
703
|
}, async ({ task: ref }) => {
|
|
642
704
|
const st = await requireStore();
|
|
643
|
-
// Hygiene
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
705
|
+
// Hygiene notice (non-blocking): surface an existing handoff but never block
|
|
706
|
+
// task_start — the handoff is a captured-context artifact, not a lock (e.g. a
|
|
707
|
+
// small modification after writing a handoff should start without forcing
|
|
708
|
+
// deletion, which would lose context).
|
|
709
|
+
const handoffNotice = existsSync(join(st.root, "HANDOFF.md"))
|
|
710
|
+
? "ℹ️ A handoff exists at .planner/HANDOFF.md — read it for context, then delete it with planner-handoff-delete when no longer needed. Proceeding with task start.\n"
|
|
711
|
+
: "";
|
|
649
712
|
const found = findTaskByRef(await st.loadAllPhases(), ref);
|
|
650
713
|
if (!found)
|
|
651
714
|
return text(`Task not found: ${ref}`);
|
|
@@ -673,7 +736,7 @@ server.registerTool("planner-task-start", {
|
|
|
673
736
|
return phase;
|
|
674
737
|
});
|
|
675
738
|
await st.syncTaskStatusRollup(found.phase.id);
|
|
676
|
-
return writeAndSummarize(st,
|
|
739
|
+
return writeAndSummarize(st, `${handoffNotice}✅ Task started: ${found.task.id}`, { task: updatedTask ?? found.task });
|
|
677
740
|
});
|
|
678
741
|
server.registerTool("planner-task-complete", {
|
|
679
742
|
description: "Set a task to done. Fails if checklist is incomplete unless force=true.",
|
|
@@ -754,14 +817,55 @@ server.registerTool("planner-handoff-clear", {
|
|
|
754
817
|
return text("Deleted .planner/HANDOFF.md");
|
|
755
818
|
});
|
|
756
819
|
server.registerTool("planner-web", {
|
|
757
|
-
description: "
|
|
820
|
+
description: "Manage the planner web dashboard (start/status/stop) from MCP stdio. Runs an in-process HTTP+WS server bound to LAN; it lives as long as this MCP process. plan-root comes from AGENT_PLAN_ROOT or cwd()/.planner.",
|
|
758
821
|
inputSchema: {
|
|
759
822
|
action: z.enum(["start", "stop", "status"]).default("status"),
|
|
760
823
|
},
|
|
761
|
-
}, async ({ action }) =>
|
|
824
|
+
}, async ({ action }) => {
|
|
825
|
+
if (action === "start") {
|
|
826
|
+
if (webHandle) {
|
|
827
|
+
return text(`planner-web already running: ${webHandle.localUrl}${webHandle.lanUrl ? ` — LAN: ${webHandle.lanUrl}` : ""} (mode: ${webHandle.mode})`);
|
|
828
|
+
}
|
|
829
|
+
const root = planRoot();
|
|
830
|
+
const storeCheck = new PlanStore(root);
|
|
831
|
+
if (!(await storeCheck.exists())) {
|
|
832
|
+
return text(`planner-web start: no .planner/ found at ${root}. Run planner-init first.`);
|
|
833
|
+
}
|
|
834
|
+
try {
|
|
835
|
+
// port: 0 → OS assigns a free port (no conflict with plan-server CLI / Pi).
|
|
836
|
+
// host: 0.0.0.0 → bind LAN (reachable from other devices), like Pi /planner load.
|
|
837
|
+
webHandle = await serve({ planRoot: root, host: "0.0.0.0", port: 0, quiet: true });
|
|
838
|
+
return text(`planner-web started: ${webHandle.localUrl}${webHandle.lanUrl ? ` — LAN: ${webHandle.lanUrl}` : ""} (mode: ${webHandle.mode})`);
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
webHandle = null;
|
|
842
|
+
return text(`planner-web start failed: ${err.message}`);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
if (action === "stop") {
|
|
846
|
+
if (!webHandle)
|
|
847
|
+
return text("planner-web not running.");
|
|
848
|
+
const stoppedUrl = webHandle.localUrl;
|
|
849
|
+
await webHandle.close().catch(() => { });
|
|
850
|
+
webHandle = null;
|
|
851
|
+
return text(`planner-web stopped (was ${stoppedUrl}).`);
|
|
852
|
+
}
|
|
853
|
+
// status
|
|
854
|
+
if (webHandle) {
|
|
855
|
+
return text(`planner-web running: ${webHandle.localUrl}${webHandle.lanUrl ? ` — LAN: ${webHandle.lanUrl}` : ""} (mode: ${webHandle.mode}, bindHost: ${webHandle.bindHost})`);
|
|
856
|
+
}
|
|
857
|
+
return text("planner-web not running. Use planner-web with action=start to start the dashboard.");
|
|
858
|
+
});
|
|
762
859
|
server.registerTool("planner-load", {
|
|
763
|
-
description: "MCP
|
|
764
|
-
}, async () =>
|
|
860
|
+
description: "Load/refresh the planner on explicit user request (NOT automatic): starts the web dashboard on LAN and returns a consolidated recap (project state, active task, pending handoff, web URL). This is the MCP equivalent of Pi /planner load. Call it ONLY when the user runs /planner load or /planner recap (or asks to load the planner). Present the recap to the user in that reply, including the web URL on a final prominent line. If a pending handoff is included, read it, summarize it to the user, then call planner-handoff-clear. Do NOT start the planner/web or show the web URL unless the user explicitly asks (load/recap/web status).",
|
|
861
|
+
}, async () => {
|
|
862
|
+
const st = store();
|
|
863
|
+
if (!(await st.exists()))
|
|
864
|
+
return text("No .planner/ found at " + planRoot() + ". Run planner-init first.");
|
|
865
|
+
const web = await ensureWebStarted();
|
|
866
|
+
const recap = await buildRecapText(st, web);
|
|
867
|
+
return text(recap);
|
|
868
|
+
});
|
|
765
869
|
server.registerTool("planner-disable", {
|
|
766
870
|
description: "MCP no-op equivalent of Pi /planner disable. Stop the MCP process from the host to disable it.",
|
|
767
871
|
}, async () => text("To disable planner MCP, remove/disable this MCP server from the host config or stop the process."));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
43
43
|
"zod": "^3.24.0",
|
|
44
|
-
"@agent-plan/core": "0.2.
|
|
44
|
+
"@agent-plan/core": "0.2.11",
|
|
45
|
+
"@agent-plan/server": "0.2.11"
|
|
45
46
|
},
|
|
46
47
|
"scripts": {
|
|
47
48
|
"build": "tsc -p tsconfig.json",
|