@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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAmvDA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
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 plannerSessionId = randomUUID();
14
- startReadSession(plannerSessionId);
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. Reuse the target's valid sessionInfo context or require the exact full tree when missing or stale. Pause the source with a mandatory snapshot, start the target, and push a durable LIFO return target.",
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
- if (!contextEligibility.eligible)
1270
- return text(`Task switch denied: context reads required. ${contextEligibility.reason}`, contextEligibility);
1271
- if (!hasReadRequirementsForSession(plannerSessionId, linkedRequirementIds, linkedRequirements)) {
1272
- return text("Task switch denied: read the requirements linked to the target phase and feature before switching.", { requirementIds: linkedRequirementIds });
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. Read the exact full task → phase → feature tree and linked requirements once per harness session; subsequent starts reuse persisted sessionInfo while entity updatedAt values remain unchanged. Every stale or denied start is an MCP isError result with started=false and exact retry actions; NEVER claim work started unless structuredContent.started is true. Success is returned only after persisted in-progress state is verified.",
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
- `planner-task-show ${taskRef} with full=true`,
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.24",
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.24",
47
- "@agent-plan/server": "0.2.24"
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",