@agent-plan/mcp 0.2.24 → 0.2.25
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 +91 -31
- package/package.json +3 -3
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":";AAw0DA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
|
package/dist/index.js
CHANGED
|
@@ -10,11 +10,48 @@ import { serve } from "@agent-plan/server";
|
|
|
10
10
|
import { createChecklistItemId, createFeatureId, createPhaseId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, validateResolvedTarget } from "@agent-plan/core/naming";
|
|
11
11
|
const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
|
|
12
12
|
const PHASE_STATUS_VALUES = ["draft", "discovery", ...STATUS_VALUES];
|
|
13
|
-
const
|
|
14
|
-
startReadSession(
|
|
13
|
+
const fallbackPlannerSessionId = `mcp-process:${randomUUID()}`;
|
|
14
|
+
startReadSession(fallbackPlannerSessionId);
|
|
15
|
+
function plannerSessionIdFor(extra) {
|
|
16
|
+
const transportSessionId = extra?.sessionId?.trim();
|
|
17
|
+
const sessionId = transportSessionId ? `mcp:${transportSessionId}` : fallbackPlannerSessionId;
|
|
18
|
+
startReadSession(sessionId);
|
|
19
|
+
return sessionId;
|
|
20
|
+
}
|
|
15
21
|
function text(textValue, structuredContent) {
|
|
16
22
|
return structuredContent ? { content: [{ type: "text", text: textValue }], structuredContent } : { content: [{ type: "text", text: textValue }] };
|
|
17
23
|
}
|
|
24
|
+
function derivedStatusReadOnlyResult(entity, ref, attemptedStatus, effectiveStatus) {
|
|
25
|
+
const childKind = entity === "feature" ? "phases" : "tasks";
|
|
26
|
+
const retryAction = entity === "feature"
|
|
27
|
+
? `Retry planner-feature-update ${ref} without the status field if you need to update metadata.`
|
|
28
|
+
: `Retry planner-phase-update ${ref} without the status field if you need to update metadata.`;
|
|
29
|
+
const nextActions = [
|
|
30
|
+
`${entity[0].toUpperCase()}${entity.slice(1)} status is derived from child ${childKind}; update the child ${childKind} instead of setting ${entity}.status.`,
|
|
31
|
+
retryAction,
|
|
32
|
+
];
|
|
33
|
+
return {
|
|
34
|
+
isError: true,
|
|
35
|
+
content: [{ type: "text", text: [
|
|
36
|
+
`❌ ${entity.toUpperCase()} UPDATE FAILED [DERIVED_STATUS_READ_ONLY]`,
|
|
37
|
+
`${entity[0].toUpperCase()}${entity.slice(1)} status is derived from child ${childKind} and cannot be set directly.`,
|
|
38
|
+
`Attempted status: ${attemptedStatus}`,
|
|
39
|
+
`Effective derived status: ${effectiveStatus}`,
|
|
40
|
+
"No planner data was changed.",
|
|
41
|
+
"Next required actions:",
|
|
42
|
+
...nextActions.map((action, index) => `${index + 1}. ${action}`),
|
|
43
|
+
].join("\n") }],
|
|
44
|
+
structuredContent: {
|
|
45
|
+
updated: false,
|
|
46
|
+
errorCode: "DERIVED_STATUS_READ_ONLY",
|
|
47
|
+
entity,
|
|
48
|
+
ref,
|
|
49
|
+
attemptedStatus,
|
|
50
|
+
effectiveStatus,
|
|
51
|
+
nextActions,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
18
55
|
function taskStartError(outcome, structuredContent = {}) {
|
|
19
56
|
return {
|
|
20
57
|
isError: true,
|
|
@@ -29,6 +66,19 @@ function taskStartError(outcome, structuredContent = {}) {
|
|
|
29
66
|
structuredContent: { ...outcome, ...structuredContent },
|
|
30
67
|
};
|
|
31
68
|
}
|
|
69
|
+
function mcpContextReadActions(eligibility, refs, requirementReadRequired, retryAction) {
|
|
70
|
+
const actions = (eligibility.requiredReads ?? []).flatMap((read) => {
|
|
71
|
+
if (read.kind === "task")
|
|
72
|
+
return [`planner-task-show ${refs.task} with full=true`];
|
|
73
|
+
if (read.kind === "phase")
|
|
74
|
+
return [`planner-phase-show ${refs.phase} with full=true`];
|
|
75
|
+
return refs.feature ? [`planner-feature-show ${refs.feature} with full=true`] : [];
|
|
76
|
+
});
|
|
77
|
+
if (requirementReadRequired)
|
|
78
|
+
actions.push("planner-requirement-list");
|
|
79
|
+
actions.push(retryAction);
|
|
80
|
+
return actions;
|
|
81
|
+
}
|
|
32
82
|
const MCP_PACKAGE = packageVersionFromModule(import.meta.url, "@agent-plan/mcp");
|
|
33
83
|
const CORE_PACKAGE = resolvedPackageVersion("@agent-plan/core", import.meta.url);
|
|
34
84
|
function nowISO() {
|
|
@@ -461,7 +511,8 @@ server.registerTool("planner-feature-add", {
|
|
|
461
511
|
server.registerTool("planner-feature-show", {
|
|
462
512
|
description: "Show a feature by id or name.",
|
|
463
513
|
inputSchema: { feature: z.string().min(1).describe("Feature ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."), full: z.boolean().optional().describe("If true, include the feature description. Default: compact identity only (saves tokens).") },
|
|
464
|
-
}, async ({ feature: ref, full }) => {
|
|
514
|
+
}, async ({ feature: ref, full }, extra) => {
|
|
515
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
465
516
|
const st = await requireStore();
|
|
466
517
|
const features = (await st.loadFeatures()).features;
|
|
467
518
|
const resolvedFeature = resolveFeatureRefStrict(features, ref);
|
|
@@ -531,6 +582,9 @@ server.registerTool("planner-feature-update", {
|
|
|
531
582
|
if (!resolvedFeature.ok)
|
|
532
583
|
return text(resolvedFeature.error);
|
|
533
584
|
const feature = resolvedFeature.feature;
|
|
585
|
+
if (updates.status !== undefined) {
|
|
586
|
+
return derivedStatusReadOnlyResult("feature", formatFeatureRef(feature.number), updates.status, feature.status);
|
|
587
|
+
}
|
|
534
588
|
const updated = await st.updateFeatures((doc) => {
|
|
535
589
|
const target = doc.features.find((entry) => entry.id === feature.id);
|
|
536
590
|
if (!target)
|
|
@@ -539,8 +593,6 @@ server.registerTool("planner-feature-update", {
|
|
|
539
593
|
target.name = updates.name.trim();
|
|
540
594
|
if (updates.description !== undefined)
|
|
541
595
|
target.description = updates.description.trim();
|
|
542
|
-
if (updates.status !== undefined)
|
|
543
|
-
target.status = updates.status;
|
|
544
596
|
if (updates.workDone !== undefined)
|
|
545
597
|
target.workDone = updates.workDone.trim();
|
|
546
598
|
if (updates.workRemaining !== undefined)
|
|
@@ -672,7 +724,8 @@ server.registerTool("planner-phase-add", {
|
|
|
672
724
|
server.registerTool("planner-phase-show", {
|
|
673
725
|
description: "Show a phase by id or name, including derived linked requirements in the full view.",
|
|
674
726
|
inputSchema: { phase: z.string().min(1).describe("Phase ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."), full: z.boolean().optional().describe("If true, include the phase description. Default: compact identity only (saves tokens).") },
|
|
675
|
-
}, async ({ phase: ref, full }) => {
|
|
727
|
+
}, async ({ phase: ref, full }, extra) => {
|
|
728
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
676
729
|
const st = await requireStore();
|
|
677
730
|
const features = (await st.loadFeatures()).features;
|
|
678
731
|
const phase = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
@@ -757,11 +810,12 @@ server.registerTool("planner-phase-update", {
|
|
|
757
810
|
const found = findPhaseByRef(await st.loadAllPhases(), features, ref);
|
|
758
811
|
if (!found)
|
|
759
812
|
return text(`Phase not found: ${ref}`);
|
|
813
|
+
if (updates.status !== undefined) {
|
|
814
|
+
return derivedStatusReadOnlyResult("phase", formatPhaseRef(found.number, featureNumberOfPhase(found, features)), updates.status, found.status);
|
|
815
|
+
}
|
|
760
816
|
const phase = await st.updatePhase(found.id, (entry) => {
|
|
761
817
|
if (updates.title !== undefined)
|
|
762
818
|
entry.title = updates.title.trim();
|
|
763
|
-
if (updates.status !== undefined)
|
|
764
|
-
entry.status = updates.status;
|
|
765
819
|
if (updates.summary !== undefined)
|
|
766
820
|
entry.summary = updates.summary.trim();
|
|
767
821
|
if (updates.description !== undefined)
|
|
@@ -866,7 +920,8 @@ server.registerTool("planner-task-add", {
|
|
|
866
920
|
server.registerTool("planner-task-show", {
|
|
867
921
|
description: "Show a task by id or name.",
|
|
868
922
|
inputSchema: { task: z.string().min(1).describe("Task ref. Accepts F00x/P00x/T00x composite, bare P00x/T00x (global), 5-char shortId, UUID, or title."), full: z.boolean().optional().describe("If true, include the task description, resume checkpoint/advisory, and statusLog. Default: compact identity only (saves tokens).") },
|
|
869
|
-
}, async ({ task: ref, full }) => {
|
|
923
|
+
}, async ({ task: ref, full }, extra) => {
|
|
924
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
870
925
|
const st = await requireStore();
|
|
871
926
|
const found = findTaskByRef(await st.loadAllPhases(), (await st.loadFeatures()).features, ref);
|
|
872
927
|
if (!found)
|
|
@@ -1186,7 +1241,8 @@ server.registerTool("planner-task-pause", {
|
|
|
1186
1241
|
how_to_resume: z.string().min(1),
|
|
1187
1242
|
paused_by: z.string().optional(),
|
|
1188
1243
|
},
|
|
1189
|
-
}, async ({ task: ref, reason, what_was_being_done, resume_location, how_to_resume, paused_by }) => {
|
|
1244
|
+
}, async ({ task: ref, reason, what_was_being_done, resume_location, how_to_resume, paused_by }, extra) => {
|
|
1245
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
1190
1246
|
const st = await requireStore();
|
|
1191
1247
|
const features = (await st.loadFeatures()).features;
|
|
1192
1248
|
const found = findTaskByRef(await st.loadAllPhases(), features, ref);
|
|
@@ -1223,7 +1279,7 @@ server.registerTool("planner-task-pause", {
|
|
|
1223
1279
|
].join("\n"), { task: checkpointed, snapshot, ...(resume ? { resumeRequired: { taskId: resume.task.id } } : {}) });
|
|
1224
1280
|
});
|
|
1225
1281
|
server.registerTool("planner-task-switch", {
|
|
1226
|
-
description: "Atomically switch from active work to a temporary task, even outside normal priority order.
|
|
1282
|
+
description: "Atomically switch from active work to a temporary task, even outside normal priority order. Call this tool first so valid sessionInfo context can be reused; on denial, perform only the missing/stale reads listed in nextActions and retry. Pause the source with a mandatory snapshot, start the target, and push a durable LIFO return target.",
|
|
1227
1283
|
inputSchema: {
|
|
1228
1284
|
from_task: z.string().min(1),
|
|
1229
1285
|
to_task: z.string().min(1),
|
|
@@ -1233,7 +1289,8 @@ server.registerTool("planner-task-switch", {
|
|
|
1233
1289
|
how_to_resume: z.string().min(1),
|
|
1234
1290
|
switched_by: z.string().optional(),
|
|
1235
1291
|
},
|
|
1236
|
-
}, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }) => {
|
|
1292
|
+
}, async ({ from_task, to_task, reason, what_was_being_done, resume_location, how_to_resume, switched_by }, extra) => {
|
|
1293
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
1237
1294
|
const st = await requireStore();
|
|
1238
1295
|
const [featuresDoc, phases, project, focus] = await Promise.all([st.loadFeatures(), st.loadAllPhases(), st.loadProject(), st.loadResume()]);
|
|
1239
1296
|
const features = featuresDoc.features;
|
|
@@ -1266,10 +1323,16 @@ server.registerTool("planner-task-switch", {
|
|
|
1266
1323
|
requirementIds: linkedRequirementIds,
|
|
1267
1324
|
};
|
|
1268
1325
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1326
|
+
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
1327
|
+
const targetTaskRef = taskCompositeRef(target.task, target.phase, features);
|
|
1328
|
+
const targetPhaseRef = formatPhaseRef(target.phase.number, featureNumberOfPhase(target.phase, features));
|
|
1329
|
+
const targetFeatureRef = targetFeature ? formatFeatureRef(targetFeature.number) : "";
|
|
1330
|
+
if (!contextEligibility.eligible) {
|
|
1331
|
+
const nextActions = mcpContextReadActions(contextEligibility, { task: targetTaskRef, phase: targetPhaseRef, ...(targetFeatureRef ? { feature: targetFeatureRef } : {}) }, !requirementsReady, `Retry planner-task-switch to ${targetTaskRef}`);
|
|
1332
|
+
return text(`Task switch denied: context reads required. ${contextEligibility.reason}`, { ...contextEligibility, nextActions });
|
|
1333
|
+
}
|
|
1334
|
+
if (!requirementsReady) {
|
|
1335
|
+
return text("Task switch denied: read the requirements linked to the target phase and feature before switching.", { requirementIds: linkedRequirementIds, nextActions: ["planner-requirement-list", `Retry planner-task-switch to ${targetTaskRef}`] });
|
|
1273
1336
|
}
|
|
1274
1337
|
if (!hasValidSessionAttestation(contextInput)) {
|
|
1275
1338
|
await st.recordContextRead({ sessionId: plannerSessionId, phaseId: target.phase.id, taskId: target.task.id, ...(target.phase.featureId ? { featureId: target.phase.featureId } : {}), requirementIds: linkedRequirementIds });
|
|
@@ -1371,9 +1434,10 @@ server.registerTool("planner-task-switch", {
|
|
|
1371
1434
|
].join("\n"), { deviation: record, snapshot, resumeTaskId: source.task.id, temporaryTaskId: target.task.id });
|
|
1372
1435
|
});
|
|
1373
1436
|
server.registerTool("planner-task-start", {
|
|
1374
|
-
description: "Set a task to in-progress or resume checkpointed work.
|
|
1437
|
+
description: "Set a task to in-progress or resume checkpointed work. Call this tool before reading context so it can reuse valid sessionInfo attestations; if denied, perform only the missing/stale reads listed in nextActions, in order, and retry. Every denied start is an MCP isError result with started=false; NEVER claim work started unless structuredContent.started is true. Success is returned only after persisted in-progress state is verified.",
|
|
1375
1438
|
inputSchema: { task: z.string().min(1) },
|
|
1376
|
-
}, async ({ task: ref }) => {
|
|
1439
|
+
}, async ({ task: ref }, extra) => {
|
|
1440
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
1377
1441
|
let st;
|
|
1378
1442
|
try {
|
|
1379
1443
|
st = await requireStore();
|
|
@@ -1389,10 +1453,6 @@ server.registerTool("planner-task-start", {
|
|
|
1389
1453
|
const phaseRef = formatPhaseRef(found.phase.number, featureNumberOfPhase(found.phase, features));
|
|
1390
1454
|
const parentFeature = found.phase.featureId ? features.find((candidate) => candidate.id === found.phase.featureId) : undefined;
|
|
1391
1455
|
const featureRef = parentFeature ? formatFeatureRef(parentFeature.number) : "";
|
|
1392
|
-
if (found.task.status === "in-progress") {
|
|
1393
|
-
const outcome = taskStartSucceeded(found.task.id, true);
|
|
1394
|
-
return text(`✅ Task already started: ${taskRef} — ${found.task.title} (in-progress)\nstarted: true`, { ...outcome, task: found.task });
|
|
1395
|
-
}
|
|
1396
1456
|
if (found.task.status === "done")
|
|
1397
1457
|
return taskStartError(taskStartDenied("TASK_DONE", `Task ${taskRef} is done and was not reopened.`, [`Use planner-task-update with status=planned and a motivation to reopen ${taskRef}.`, `Repeat the required full context reads, then retry planner-task-start ${taskRef}.`], { taskId: found.task.id }));
|
|
1398
1458
|
const linkedRequirements = [
|
|
@@ -1412,20 +1472,19 @@ server.registerTool("planner-task-start", {
|
|
|
1412
1472
|
requirementIds: linkedRequirementIds,
|
|
1413
1473
|
};
|
|
1414
1474
|
const contextEligibility = contextReadEligibilityForSession(contextInput);
|
|
1475
|
+
const requirementsReady = hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements);
|
|
1415
1476
|
if (!contextEligibility.eligible)
|
|
1416
|
-
return taskStartError(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`,
|
|
1417
|
-
|
|
1418
|
-
`planner-phase-show ${phaseRef} with full=true`,
|
|
1419
|
-
...(featureRef ? [`planner-feature-show ${featureRef} with full=true`] : []),
|
|
1420
|
-
...(linkedRequirementIds.length > 0 ? ["planner-requirement-list"] : []),
|
|
1421
|
-
`Retry planner-task-start ${taskRef}`,
|
|
1422
|
-
], { taskId: found.task.id }), { contextEligibility });
|
|
1423
|
-
if (!hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements)) {
|
|
1477
|
+
return taskStartError(taskStartDenied("CONTEXT_READ_REQUIRED", `Required context reads are incomplete or stale. ${contextEligibility.reason}`, mcpContextReadActions(contextEligibility, { task: taskRef, phase: phaseRef, ...(featureRef ? { feature: featureRef } : {}) }, !requirementsReady, `Retry planner-task-start ${taskRef}`), { taskId: found.task.id }), { contextEligibility });
|
|
1478
|
+
if (!requirementsReady) {
|
|
1424
1479
|
return taskStartError(taskStartDenied("REQUIREMENTS_READ_REQUIRED", "Linked requirements were not read; the task remains unchanged.", ["planner-requirement-list", `Retry planner-task-start ${taskRef}`], { taskId: found.task.id, requirementIds: linkedRequirementIds }));
|
|
1425
1480
|
}
|
|
1426
1481
|
if (!hasValidSessionAttestation(contextInput)) {
|
|
1427
1482
|
await st.recordContextRead({ sessionId: plannerSessionId, phaseId: found.phase.id, taskId: found.task.id, ...(found.phase.featureId ? { featureId: found.phase.featureId } : {}), requirementIds: linkedRequirementIds });
|
|
1428
1483
|
}
|
|
1484
|
+
if (found.task.status === "in-progress") {
|
|
1485
|
+
const outcome = taskStartSucceeded(found.task.id, true);
|
|
1486
|
+
return text(`✅ Task already started: ${taskRef} — ${found.task.title} (in-progress)\nstarted: true`, { ...outcome, task: found.task });
|
|
1487
|
+
}
|
|
1429
1488
|
const [project, phases, focus] = await Promise.all([st.loadProject(), st.loadAllPhases(), st.loadResume()]);
|
|
1430
1489
|
const eligibility = checkExplicitTaskStart(features, phases, found.task.id, project.workDeviations);
|
|
1431
1490
|
if (!eligibility.eligible)
|
|
@@ -1624,7 +1683,8 @@ server.registerTool("planner-handoff-show", {
|
|
|
1624
1683
|
server.registerTool("planner-requirement-list", {
|
|
1625
1684
|
description: "List all top-level requirements in requirements.json. Reading this list records the requirements as read for the start/resume read-enforcement (explicit requirement read, required alongside feature/phase reads).",
|
|
1626
1685
|
inputSchema: {},
|
|
1627
|
-
}, async () => {
|
|
1686
|
+
}, async (_args, extra) => {
|
|
1687
|
+
const plannerSessionId = plannerSessionIdFor(extra);
|
|
1628
1688
|
const st = await requireStore();
|
|
1629
1689
|
const requirements = await st.loadRequirements();
|
|
1630
1690
|
requirements.requirements.forEach((req) => markRequirementReadForSessionId(plannerSessionId, req.id));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-plan/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.25",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "MCP server for Agent Plan, exposing planner-* tools over stdio.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@modelcontextprotocol/sdk": "^1.17.0",
|
|
45
45
|
"zod": "^3.24.0",
|
|
46
|
-
"@agent-plan/core": "0.2.
|
|
47
|
-
"@agent-plan/server": "0.2.
|
|
46
|
+
"@agent-plan/core": "0.2.25",
|
|
47
|
+
"@agent-plan/server": "0.2.25"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsc -p tsconfig.json",
|