agent-planner-mcp 1.5.8 → 1.5.10

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-planner-mcp",
3
- "version": "1.5.8",
3
+ "version": "1.5.10",
4
4
  "description": "MCP server for AgentPlanner — AI agent orchestration with planning, dependencies, knowledge graphs, and human oversight",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -26,6 +26,22 @@ function safeArray(value) {
26
26
  return Array.isArray(value) ? value : [];
27
27
  }
28
28
 
29
+ /**
30
+ * Extract the most actionable message from an axios error. The AP backend's
31
+ * validation errors carry a field-level `message` (e.g. "urgency: must be one
32
+ * of ...; options.0: Unrecognized key") plus a `details` array — far more
33
+ * useful than the generic top-line `error` ("Validation failed"), which is all
34
+ * most handlers surfaced. Prefer message → error → err.message.
35
+ */
36
+ function apiErrorMessage(err) {
37
+ const data = err?.response?.data;
38
+ if (data) {
39
+ if (data.message && data.message !== data.error) return data.message;
40
+ if (typeof data.error === 'string') return data.error;
41
+ }
42
+ return err?.message || 'unknown error';
43
+ }
44
+
29
45
  /**
30
46
  * The web app origin (where /app/plans/:id lives), for building shareable plan
31
47
  * links agents can post (e.g. to Slack). Derived from API_URL — the web app
@@ -56,4 +72,4 @@ function isV1Unavailable(err) {
56
72
  return !(body && typeof body === 'object' && body.error);
57
73
  }
58
74
 
59
- module.exports = { asOf, formatResponse, errorResponse, safeArray, isV1Unavailable, webOrigin, planUrl };
75
+ module.exports = { asOf, formatResponse, errorResponse, safeArray, apiErrorMessage, isV1Unavailable, webOrigin, planUrl };
@@ -13,7 +13,7 @@
13
13
  * See ../../../docs/MCP_v1.0_FULL_SURFACE.md for design rationale.
14
14
  */
15
15
 
16
- const { asOf, formatResponse, errorResponse, isV1Unavailable, planUrl } = require('./_shared');
16
+ const { asOf, formatResponse, errorResponse, apiErrorMessage, isV1Unavailable, planUrl } = require('./_shared');
17
17
  const { version: PKG_VERSION } = require('../../../package.json');
18
18
 
19
19
  // Provenance tag stamped onto every plan this server creates, so a plan stays
@@ -108,14 +108,37 @@ async function queueDecisionHandler(args, apiClient) {
108
108
  return errorResponse('invalid_arg', 'queue_decision requires either plan_id or node_id');
109
109
  }
110
110
 
111
+ // The agent-facing urgency vocabulary (low/normal/high) differs from the
112
+ // backend decision schema (blocking/can_continue/informational) — map it, or
113
+ // every call fails strict validation.
114
+ const URGENCY_MAP = { low: 'informational', normal: 'can_continue', high: 'blocking' };
115
+ const mappedUrgency = URGENCY_MAP[urgency] || 'can_continue';
116
+
117
+ // The backend decisionOption shape is {option, pros?, cons?, recommendation?}
118
+ // and is .strict() — our {label, description} would be rejected. Fold
119
+ // description into the option text and mark the recommended one.
120
+ const recText = String(recommendation || '').toLowerCase();
121
+ const mappedOptions = Array.isArray(options)
122
+ ? options
123
+ .map((o) => {
124
+ const label = o.label || o.option || '';
125
+ const option = o.description ? `${label} — ${o.description}` : label;
126
+ const isRecommended = label && recText.includes(label.toLowerCase());
127
+ return option ? { option, ...(isRecommended ? { recommendation: true } : {}) } : null;
128
+ })
129
+ .filter(Boolean)
130
+ : [];
131
+
111
132
  const body = {
112
133
  title,
113
134
  context,
114
- options: options || [],
115
- recommendation: recommendation || null,
116
- urgency: urgency || 'normal',
135
+ options: mappedOptions,
136
+ urgency: mappedUrgency,
137
+ // Top-level `recommendation` is not in the strict schema — keep the agent's
138
+ // free-text recommendation and ask in metadata instead.
117
139
  metadata: {
118
140
  smallest_input_needed,
141
+ recommendation: recommendation || null,
119
142
  goal_id: goal_id || null,
120
143
  source: 'bdi.queue_decision',
121
144
  proposed_subtasks: Array.isArray(proposed_subtasks) ? proposed_subtasks : undefined,
@@ -136,10 +159,7 @@ async function queueDecisionHandler(args, apiClient) {
136
159
  title: created.title,
137
160
  });
138
161
  } catch (err) {
139
- return errorResponse(
140
- 'upstream_unavailable',
141
- `Failed to queue decision: ${err.response?.data?.error || err.message}`
142
- );
162
+ return errorResponse('upstream_unavailable', `Failed to queue decision: ${apiErrorMessage(err)}`);
143
163
  }
144
164
  }
145
165
 
@@ -187,20 +207,20 @@ async function resolveDecisionHandler(args, apiClient) {
187
207
  // Best-effort — if fetch fails, we still try to resolve.
188
208
  }
189
209
 
210
+ // The backend resolve schema is strict {decision, rationale}. Encode the
211
+ // action (+ chosen option) into `decision` and the note into `rationale` —
212
+ // the previous {resolution, message, selected_option} body was rejected.
213
+ const decisionText = selected_option ? `${action} — ${selected_option}` : action;
190
214
  let resolved;
191
215
  try {
192
216
  resolved = await apiClient.axiosInstance
193
217
  .post(`/plans/${plan_id}/decisions/${decision_id}/resolve`, {
194
- resolution: action,
195
- message: message || null,
196
- selected_option: selected_option || null,
218
+ decision: decisionText,
219
+ rationale: message || undefined,
197
220
  })
198
221
  .then((r) => r.data);
199
222
  } catch (err) {
200
- return errorResponse(
201
- 'upstream_unavailable',
202
- `Failed to resolve decision: ${err.response?.data?.error || err.message}`
203
- );
223
+ return errorResponse('upstream_unavailable', `Failed to resolve decision: ${apiErrorMessage(err)}`);
204
224
  }
205
225
 
206
226
  // On approve, materialize any proposed_subtasks atomically (best-effort per task).
@@ -738,6 +758,15 @@ function validateTreeShape(tree, depth = 0) {
738
758
  return null;
739
759
  }
740
760
 
761
+ // Count every node in a returned tree, recursing through `children`. The facade
762
+ // response's `tree` is the array of top-level nodes; `tree.length` alone counts
763
+ // only those (e.g. 2 phases) and contradicts structure.task_count, so report
764
+ // the true recursive total.
765
+ function countTreeNodes(tree) {
766
+ if (!Array.isArray(tree)) return 0;
767
+ return tree.reduce((sum, node) => sum + 1 + countTreeNodes(node?.children), 0);
768
+ }
769
+
741
770
  const formIntentionDefinition = {
742
771
  name: 'form_intention',
743
772
  description:
@@ -862,7 +891,7 @@ async function formIntentionHandler(args, apiClient) {
862
891
  goal_id,
863
892
  status: result.plan?.status || status,
864
893
  is_draft: (result.plan?.status || status) === 'draft',
865
- nodes_created: Array.isArray(result.tree) ? result.tree.length : undefined,
894
+ nodes_created: Array.isArray(result.tree) ? countTreeNodes(result.tree) : undefined,
866
895
  next_step: (result.plan?.status || status) === 'draft'
867
896
  ? "Plan created as draft. Will surface in dashboard pending for human review. Auto-promotes to active when first task moves to in_progress."
868
897
  : `Plan active. Claim a task with claim_next_task({plan_id}) to begin work. Shareable link: ${planUrl(facadePlanId)} (set visibility:'unlisted' for a rich Slack/social preview).`,