@agent-plan/mcp 0.3.0 → 0.3.2

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 CHANGED
@@ -1,3 +1,22 @@
1
1
  #!/usr/bin/env node
2
+ type ToolResult = {
3
+ content: Array<{
4
+ type: "text";
5
+ text: string;
6
+ }>;
7
+ structuredContent?: Record<string, unknown>;
8
+ isError?: boolean;
9
+ };
10
+ /**
11
+ * Claude Code gives the agent only `structuredContent` when a result has
12
+ * one, and drops `content[].text`. Anything a tool says in text — a handoff
13
+ * body, a phase work map, task-start context, an advisory — would never
14
+ * reach the agent. So every result with a structured part also carries its
15
+ * text there, as `text`. `content` keeps the text for clients that read
16
+ * only that. Applied once, to every tool, by the registerTool wrapper below;
17
+ * no handler needs to know.
18
+ */
19
+ export declare function foldTextIntoStructured<T extends ToolResult>(result: T): T;
2
20
  export declare function startStdioServer(): Promise<void>;
21
+ export {};
3
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA8jGA,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AA4BA,KAAK,UAAU,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AA2LrI;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAUzE;AA83FD,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAItD"}
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import * as z from "zod/v4";
5
5
  import { join } from "node:path";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { pathToFileURL } from "node:url";
8
- import { PlanStore, PlanStoreError, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, replaceChecklist, buildPhaseContextBlock, buildPhaseWorkMap, buildBoundedAcceptedDecisionContext, renderAcceptedDecisionsSection, checkExplicitTaskStart, recommendNextTask, recommendNextWork, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, runtimeCapabilities, runtimePackagesDiagnostic, markCanonicalFullReadForSessionId, contextReadEligibilityForSession, requirementReadEligibilityForSession, hasValidSessionAttestation, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded, taskStartGateState, taskStartGateStateLine, noMutableFieldsReceived, normalizeDescriptionRef, projectGuidelinesReadStateForSession, reconcileRequirementMacroTasks, RequirementMacroTaskError, HANDOFF_COMPLETENESS_AUDIT_VERSION, HANDOFF_COMPLETENESS_CATEGORIES, HANDOFF_COLD_START_INVENTORY_VERSION, HANDOFF_COLD_START_INVENTORY_CATEGORIES, HandoffContractError, buildHandoffShowReply, buildHandoffPrepareReply, buildProjectContextLoadReply, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS, projectContextStaleAdvisory, LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE, LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE, buildMutationReply, longTextFieldLimitNotice, buildRecommendationReply, buildTaskOrderContext, taskOrderContextLine, taskCreatedPriorityFragment, findHigherPriorityOpenPhase, higherPriorityOpenPhaseAdvisory, listOpenPhaseWork, openPhaseWorkLines, boundedOpenPhaseWork, deleteFeatureCascade, evaluateTaskCompletionGate, taskCompletionChecklistRefusal, taskCompletionForceMotivationRequired, taskCompletionOverrideNote, taskCompletionMismatchLine, boundedPage, nowISO, resolveFeatureRefStrict, resolveAcceptedDecisionTarget, applyTaskLifecycleDates, handoffContractFailureReply, derivedStatusReadOnlyReply, acceptedDecisionMutationFailureReply, legacyContextMigrationSummary } from "@agent-plan/core";
8
+ import { PlanStore, PlanStoreError, ExportService, withFeatureLock, needsMotivation, findPhaseByRef, findTaskByRef, findIdeaByRef, buildRecap, addChecklistItem, removeChecklistItem, toggleChecklistItem, replaceChecklist, buildPhaseContextBlock, buildPhaseWorkMap, buildBoundedAcceptedDecisionContext, renderAcceptedDecisionsSection, checkExplicitTaskStart, recommendNextTask, recommendNextWork, buildResumeRequiredProposal, packageVersionFromModule, resolvedPackageVersion, createStaleProcessCheck, runtimeCapabilities, runtimePackagesDiagnostic, markCanonicalFullReadForSessionId, contextReadEligibilityForSession, requirementReadEligibilityForSession, hasValidSessionAttestation, markRequirementReadForSessionId, startReadSession, invalidateReads, taskStartDenied, taskStartSucceeded, taskStartGateState, taskStartGateStateLine, noMutableFieldsReceived, normalizeDescriptionRef, projectGuidelinesReadStateForSession, reconcileRequirementMacroTasks, RequirementMacroTaskError, HANDOFF_COMPLETENESS_AUDIT_VERSION, HANDOFF_COMPLETENESS_CATEGORIES, HANDOFF_COLD_START_INVENTORY_VERSION, HANDOFF_COLD_START_INVENTORY_CATEGORIES, HandoffContractError, buildHandoffShowReply, buildHandoffPrepareReply, buildProjectContextLoadReply, buildPlannerLoadReply, buildScopedDecisionContext, DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS, projectContextStaleAdvisory, LEGACY_DECISIONS_ARRAY_READ_ONLY_MESSAGE, LEGACY_DECISIONS_ARRAY_READ_ONLY_ERROR_CODE, buildMutationReply, longTextFieldLimitNotice, buildRecommendationReply, buildTaskOrderContext, taskOrderContextLine, taskCreatedPriorityFragment, findHigherPriorityOpenPhase, higherPriorityOpenPhaseAdvisory, listOpenPhaseWork, openPhaseWorkLines, boundedOpenPhaseWork, deleteFeatureCascade, evaluateTaskCompletionGate, taskCompletionChecklistRefusal, taskCompletionForceMotivationRequired, taskCompletionOverrideNote, taskCompletionMismatchLine, boundedPage, nowISO, resolveFeatureRefStrict, resolveAcceptedDecisionTarget, applyTaskLifecycleDates, handoffContractFailureReply, derivedStatusReadOnlyReply, acceptedDecisionMutationFailureReply, legacyContextMigrationSummary, moveTasks, buildTaskMoveReply } from "@agent-plan/core";
9
9
  import { serve } from "@agent-plan/server";
10
10
  import { createChecklistItemId, createFeatureId, createPhaseId, createRequirementId, createTaskId, clampSlug, normalizeSlug, formatPhaseRef, formatFeatureRef, formatIdeaRef, featureNumberOfPhase, validateResolvedTarget } from "@agent-plan/core/naming";
11
11
  const STATUS_VALUES = ["planned", "in-progress", "done", "blocked", "canceled", "rejected", "deferred", "waiting"];
@@ -83,6 +83,7 @@ function mcpContextReadActions(eligibility, refs, projectGuidelinesReadRequired,
83
83
  }
84
84
  const MCP_PACKAGE = packageVersionFromModule(import.meta.url, "@agent-plan/mcp");
85
85
  const CORE_PACKAGE = resolvedPackageVersion("@agent-plan/core", import.meta.url);
86
+ const staleServerNotice = createStaleProcessCheck(MCP_PACKAGE);
86
87
  function planRoot() {
87
88
  return process.env.AGENT_PLAN_ROOT || join(process.cwd(), ".planner");
88
89
  }
@@ -169,6 +170,44 @@ const server = new McpServer({
169
170
  name: "agent-plan-planner",
170
171
  version: MCP_PACKAGE.version,
171
172
  });
173
+ /**
174
+ * Claude Code gives the agent only `structuredContent` when a result has
175
+ * one, and drops `content[].text`. Anything a tool says in text — a handoff
176
+ * body, a phase work map, task-start context, an advisory — would never
177
+ * reach the agent. So every result with a structured part also carries its
178
+ * text there, as `text`. `content` keeps the text for clients that read
179
+ * only that. Applied once, to every tool, by the registerTool wrapper below;
180
+ * no handler needs to know.
181
+ */
182
+ export function foldTextIntoStructured(result) {
183
+ if (!result?.structuredContent)
184
+ return result;
185
+ const replyText = result.content
186
+ .filter((part) => part.type === "text" && typeof part.text === "string")
187
+ .map((part) => part.text)
188
+ .join("\n\n");
189
+ // A reply that already uses `text` for something else is left as it is;
190
+ // the MCP tests fail on that, so it never ships.
191
+ if (!replyText || "text" in result.structuredContent)
192
+ return result;
193
+ return { ...result, structuredContent: { text: replyText, ...result.structuredContent } };
194
+ }
195
+ /** Add one line to a reply when this server is older than what is now
196
+ * installed (see createStaleProcessCheck). */
197
+ function withStaleServerNotice(result) {
198
+ const notice = staleServerNotice();
199
+ if (!notice || !result?.content)
200
+ return result;
201
+ return { ...result, content: [...result.content, { type: "text", text: notice }] };
202
+ }
203
+ {
204
+ const registerToolUnwrapped = server.registerTool.bind(server);
205
+ server.registerTool = (...args) => {
206
+ const handler = args[args.length - 1];
207
+ const wrapped = async (...handlerArgs) => foldTextIntoStructured(withStaleServerNotice(await handler(...handlerArgs)));
208
+ return registerToolUnwrapped(...args.slice(0, -1), wrapped);
209
+ };
210
+ }
172
211
  async function resolveRequirementPhaseRefs(st, refs) {
173
212
  if (!refs || refs.length === 0)
174
213
  return { ok: false, error: "linkedPhaseIds must contain at least one phase reference." };
@@ -235,7 +274,7 @@ server.registerTool("planner-export", {
235
274
  return text(`Project export generated. Summary results:\n\n${markdown.slice(0, 1000)}${markdown.length > 1000 ? "... (full report in .planner/EXPORT.md)" : ""}`);
236
275
  });
237
276
  server.registerTool("planner-authorize-bypass", {
238
- description: "Authorize a temporary guard bypass (default 15 minutes) so edit/write tools can proceed even when no task is in-progress. Use ONLY after the user explicitly authorizes proceeding without a task. Harness-agnostic: stored in resume.json so all adapters (Pi, Claude Code, Codex, ...) respect it.",
277
+ description: "Authorize a temporary guard bypass (default 15 minutes) that silences the no-task warning. Edits are never blocked; this only stops the agent being warned when it changes project code with no task in-progress. Use ONLY after the user explicitly authorizes working without a task. Harness-agnostic: stored in resume.json so all adapters (Pi, Claude Code, Codex, ...) respect it.",
239
278
  inputSchema: {
240
279
  durationMinutes: z.number().optional().describe("Bypass window in minutes. Default 15."),
241
280
  },
@@ -1610,7 +1649,7 @@ server.registerTool("planner-task-discuss", {
1610
1649
  return writeAndSummarize(st, taskDiscussReply.text, taskDiscussReply.structured);
1611
1650
  });
1612
1651
  server.registerTool("planner-task-dependency-add", {
1613
- description: "Add a validated task dependency atomically; rejects self-dependencies, foreign tasks, and cycles.",
1652
+ description: "Add a validated task dependency atomically. A task may depend on any task in any phase; the dependency is rejected only when the target does not exist, is the task itself, or would create a cycle.",
1614
1653
  inputSchema: { task: z.string().min(1), dependsOn: z.string().min(1) },
1615
1654
  }, async ({ task: ref, dependsOn }) => {
1616
1655
  const st = await requireStore();
@@ -1812,6 +1851,32 @@ server.registerTool("planner-task-update", {
1812
1851
  });
1813
1852
  return writeAndSummarize(st, taskUpdateReply.text, taskUpdateReply.structured);
1814
1853
  });
1854
+ server.registerTool("planner-task-move", {
1855
+ description: "Move one or more tasks to a different phase, atomically, without recreating them: id, number, shortId, status, statusLog, checklist, subtasks, acceptedDecisions, pause history, notes, description and dependsOn all survive — only the composite ref changes. Every other task's dependsOn keeps resolving (ids never change). An in-progress task may move. Refused into a terminal (done/canceled/rejected) target phase. Costs almost nothing regardless of task size: pass refs only, never task text; the reply echoes only old ref → new ref, id/shortId/title/status per task.",
1856
+ inputSchema: {
1857
+ tasks: z.array(z.string().min(1)).min(1).describe("Task refs to move. Accepts F00x/P00x/T00x composite, bare T00x (global), 5-char shortId, UUID, or title."),
1858
+ targetPhase: z.string().min(1).describe("Destination phase ref. Accepts F00x/P00x, bare P00x, 5-char shortId, UUID, or title."),
1859
+ },
1860
+ }, async ({ tasks: refs, targetPhase: targetRef }) => {
1861
+ const st = await requireStore();
1862
+ const phases = await st.loadAllPhases();
1863
+ const features = (await st.loadFeatures()).features;
1864
+ const target = findPhaseByRef(phases, features, targetRef);
1865
+ if (!target)
1866
+ return { ...text(`Target phase not found: ${targetRef}`, { moved: false, errorCode: "TARGET_PHASE_NOT_FOUND" }), isError: true };
1867
+ const foundTasks = [];
1868
+ for (const ref of refs) {
1869
+ const found = findTaskByRef(phases, features, ref);
1870
+ if (!found)
1871
+ return { ...text(`Task not found: ${ref}`, { moved: false, errorCode: "TASK_NOT_FOUND" }), isError: true };
1872
+ foundTasks.push(found);
1873
+ }
1874
+ const outcome = await moveTasks(st, foundTasks.map((entry) => entry.task.id), target.id);
1875
+ if (!outcome.ok)
1876
+ return { ...text(`❌ ${outcome.error}`, { moved: false, errorCode: outcome.errorCode }), isError: true };
1877
+ const reply = buildTaskMoveReply(outcome.result, `planner-task-show ${outcome.result.targetPhaseRef} full=true`);
1878
+ return writeAndSummarize(st, reply.text, reply.structured);
1879
+ });
1815
1880
  server.registerTool("planner-task-checklist-toggle", {
1816
1881
  description: "Tick/untick a task checklist item (a 'step') without rewriting the whole list. Accepts the item as C{n} (e.g. C2), the item id, or the title (case-insensitive, first match). Pass checked=true/false to set explicitly, or omit to toggle. Use this instead of writing DONE in step titles. Items are numbered C1/C2… per task.",
1817
1882
  inputSchema: {
@@ -3079,7 +3144,7 @@ server.registerTool("planner-web", {
3079
3144
  return text("planner-web not running. Use planner-web with action=start to start the dashboard.", { running: false, address: "", localUrl: "", lanUrl: "", host: "", port: 0, mode: "" });
3080
3145
  });
3081
3146
  server.registerTool("planner-load", {
3082
- description: "Load/refresh the planner on explicit user request (NOT automatic): starts the web dashboard on LAN and returns a consolidated human-facing recap, plus agent-only structuredContent carrying the complete project-level context (description, goal, scope, out-of-scope, technologies, tools, language preferences, Project Guidelines, every Requirement, and every project Accepted Decision, in structuredContent.projectContext and structuredContent.agentContext.projectContext) and bounded canonical Accepted Decision agentContext for feature/phase/task scopes. 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 ONLY structuredContent.recap.text (equivalently, the text content) verbatim to the user, including its final prominent Web UI line — never quote projectContext or agentContext in the human-facing recap. If structuredContent.projectContext.contextComplete is false, the project-level context was NOT delivered this turn — follow structuredContent.projectContext.nextActions and retry before relying on it. A pending handoff is read-only context: NEVER call planner-handoff-show or planner-handoff-clear as part of load/recap. Archive it only when every phase task is done/canceled, when replacing it with a new handoff, or after an explicit user handoff-clear request. Do NOT start the planner/web or show the web URL unless the user explicitly asks (load/recap/web status).",
3147
+ description: "Load/refresh the planner on explicit user request (NOT automatic): starts the web dashboard on LAN and returns one text with, in order: a recap under '## Recap — show this to the user verbatim', then agent-only context under '## Agent-only context' (the complete project-level context — description, goal, scope, out-of-scope, technologies, tools, language preferences, Project Guidelines, every Requirement, every project Accepted Decision — then Accepted Decisions of features, phases and tasks, then where the Agent Plan operating guide lives). Call it ONLY when the user runs /planner load or /planner recap (or asks to load the planner). Show the user ONLY the recap section, verbatim, including its final Web UI line; never quote the agent-only context. If structuredContent.projectContext.contextComplete is false, the project-level context was NOT delivered — follow structuredContent.projectContext.nextActions and retry before relying on it. A pending handoff is read-only context: NEVER call planner-handoff-show or planner-handoff-clear as part of load/recap. Archive it only when every phase task is done/canceled, when replacing it with a new handoff, or after an explicit user handoff-clear request. Do NOT start the planner/web or show the web URL unless the user explicitly asks (load/recap/web status).",
3083
3148
  inputSchema: {
3084
3149
  maxChars: z.number().int().positive().optional().describe(`Single-response bound (chars) for the project-context delivery. Default ${DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS}. Raise this and retry if a prior call returned projectContext.contextComplete=false.`),
3085
3150
  },
@@ -3104,50 +3169,29 @@ server.registerTool("planner-load", {
3104
3169
  const web = await ensureWebStarted();
3105
3170
  const recap = await buildRecap(st, web, { harness: "mcp" });
3106
3171
  const plan = await st.loadAll();
3107
- const acceptedDecisionContext = buildBoundedAcceptedDecisionContext([
3108
- { scope: "project", decisions: plan.project.acceptedDecisions },
3109
- ...plan.features.features.map((feature) => ({ scope: `feature ${formatFeatureRef(feature.number)}`, decisions: feature.acceptedDecisions })),
3110
- ...plan.phases.map((phase) => ({ scope: `phase ${formatPhaseRef(phase.number, featureNumberOfPhase(phase, plan.features.features))}`, decisions: phase.acceptedDecisions })),
3111
- ...plan.phases.flatMap((phase) => phase.tasks.map((task) => ({
3112
- scope: `task ${taskCompositeRef(task, phase, plan.features.features)}`,
3113
- decisions: task.acceptedDecisions,
3114
- }))),
3115
- ]);
3116
3172
  // Explicit planner load must deliver the complete project-level context
3117
3173
  // (P102(F005)/T404), attested through the same lossless contract used
3118
3174
  // everywhere else: loadProjectContextDelivery + recordProjectContextRead.
3119
- // buildProjectContextLoadReply is the single shared shaping for this reply
3120
- // (Pi's planner-load and /planner load use the identical builder).
3121
3175
  const boundedMaxChars = maxChars ?? DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS;
3122
3176
  const projectContextDelivery = await st.loadProjectContextDelivery(boundedMaxChars);
3123
3177
  const projectContextReply = buildProjectContextLoadReply(projectContextDelivery, boundedMaxChars);
3124
3178
  if (projectContextReply.structured.contextComplete) {
3125
3179
  await st.recordProjectContextRead({ sessionId: plannerSessionId, chunks: projectContextDelivery.chunks });
3126
3180
  }
3127
- const visibleRecap = preparation.changed ? `${preparation.legacyProjectContext.summary}\n\n${recap}` : recap;
3128
- return {
3129
- // The project-context text (like the planner skill and Accepted Decision
3130
- // context below) is agent-only: read and retain it, but never quote it
3131
- // or the Accepted Decisions it contains in the human-facing recap.
3132
- content: [{ type: "text", text: visibleRecap }],
3133
- structuredContent: {
3134
- loaded: true,
3135
- recap: { text: visibleRecap },
3136
- projectContext: projectContextReply.structured,
3137
- webUi: plannerWebMetadata(web),
3138
- preparation: preparation.legacyProjectContext,
3139
- agentContext: {
3140
- kind: "planner-usage-skill",
3141
- content: plannerSkill.content,
3142
- status: plannerSkill.status,
3143
- customized: plannerSkill.customized,
3144
- message: plannerSkill.message,
3145
- instruction: "Read and retain this project-local planner usage skill, project context, and Accepted Decision context. Do not quote it, the project context, or the Accepted Decision context in the human-facing recap.",
3146
- projectContext: { text: projectContextReply.text },
3147
- acceptedDecisions: acceptedDecisionContext,
3148
- },
3149
- },
3150
- };
3181
+ // One text, each piece once (shared with Pi via buildPlannerLoadReply).
3182
+ // The guide body is not resent: the /planner command already carries it,
3183
+ // and a bare planner-load call is pointed at the project-local copy.
3184
+ const reply = buildPlannerLoadReply({
3185
+ recap: preparation.changed ? `${preparation.legacyProjectContext.summary}\n\n${recap}` : recap,
3186
+ projectContext: projectContextReply,
3187
+ scopedDecisions: buildScopedDecisionContext(plan),
3188
+ plannerGuide: { mode: "pointer", path: ".planner/SKILL.md", customized: plannerSkill.customized, message: plannerSkill.message },
3189
+ });
3190
+ return text(reply.text, {
3191
+ ...reply.structured,
3192
+ webUi: plannerWebMetadata(web),
3193
+ preparation: preparation.legacyProjectContext,
3194
+ });
3151
3195
  });
3152
3196
  server.registerTool("planner-disable", {
3153
3197
  description: "MCP no-op equivalent of Pi /planner disable. Stop the MCP process from the host to disable it.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-plan/mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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.3.0",
47
- "@agent-plan/server": "0.3.0"
46
+ "@agent-plan/core": "0.3.2",
47
+ "@agent-plan/server": "0.3.2"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc -p tsconfig.json",