@agent-plan/mcp 0.3.0 → 0.3.1

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;AAs2FD,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 } 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
  },
@@ -3079,7 +3118,7 @@ server.registerTool("planner-web", {
3079
3118
  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
3119
  });
3081
3120
  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).",
3121
+ 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
3122
  inputSchema: {
3084
3123
  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
3124
  },
@@ -3104,50 +3143,29 @@ server.registerTool("planner-load", {
3104
3143
  const web = await ensureWebStarted();
3105
3144
  const recap = await buildRecap(st, web, { harness: "mcp" });
3106
3145
  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
3146
  // Explicit planner load must deliver the complete project-level context
3117
3147
  // (P102(F005)/T404), attested through the same lossless contract used
3118
3148
  // 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
3149
  const boundedMaxChars = maxChars ?? DEFAULT_PROJECT_CONTEXT_CHUNK_CHARS;
3122
3150
  const projectContextDelivery = await st.loadProjectContextDelivery(boundedMaxChars);
3123
3151
  const projectContextReply = buildProjectContextLoadReply(projectContextDelivery, boundedMaxChars);
3124
3152
  if (projectContextReply.structured.contextComplete) {
3125
3153
  await st.recordProjectContextRead({ sessionId: plannerSessionId, chunks: projectContextDelivery.chunks });
3126
3154
  }
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
- };
3155
+ // One text, each piece once (shared with Pi via buildPlannerLoadReply).
3156
+ // The guide body is not resent: the /planner command already carries it,
3157
+ // and a bare planner-load call is pointed at the project-local copy.
3158
+ const reply = buildPlannerLoadReply({
3159
+ recap: preparation.changed ? `${preparation.legacyProjectContext.summary}\n\n${recap}` : recap,
3160
+ projectContext: projectContextReply,
3161
+ scopedDecisions: buildScopedDecisionContext(plan),
3162
+ plannerGuide: { mode: "pointer", path: ".planner/SKILL.md", customized: plannerSkill.customized, message: plannerSkill.message },
3163
+ });
3164
+ return text(reply.text, {
3165
+ ...reply.structured,
3166
+ webUi: plannerWebMetadata(web),
3167
+ preparation: preparation.legacyProjectContext,
3168
+ });
3151
3169
  });
3152
3170
  server.registerTool("planner-disable", {
3153
3171
  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.1",
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.1",
47
+ "@agent-plan/server": "0.3.1"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsc -p tsconfig.json",