agent-planner-mcp 1.5.9 → 1.5.12

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.9",
3
+ "version": "1.5.12",
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 };
@@ -7,7 +7,7 @@
7
7
  * directly — no UI round-trip and no forced approval gate.
8
8
  */
9
9
 
10
- const { asOf, formatResponse, errorResponse, safeArray } = require('./_shared');
10
+ const { asOf, formatResponse, errorResponse, safeArray, apiErrorMessage } = require('./_shared');
11
11
 
12
12
  const listGoalsDefinition = {
13
13
  name: 'list_goals',
@@ -110,11 +110,20 @@ async function updateGoalHandler(args, apiClient) {
110
110
  const applied = [];
111
111
  const failures = [];
112
112
 
113
- // Direct field updates
113
+ // Direct field updates. title/description/priority/status share their name
114
+ // with the backend, but success_criteria must be camelCased to successCriteria
115
+ // — the goal update schema is .strict(), so the snake_case key was rejected
116
+ // with a 400 (the one multi-word field, hence "description writes but
117
+ // success_criteria fails"). Match the create shape: wrap a bare array as
118
+ // { criteria: [...] }.
114
119
  const directFields = {};
115
- for (const k of ['title', 'description', 'priority', 'status', 'success_criteria']) {
120
+ for (const k of ['title', 'description', 'priority', 'status']) {
116
121
  if (changes[k] !== undefined) directFields[k] = changes[k];
117
122
  }
123
+ if (changes.success_criteria !== undefined) {
124
+ const sc = changes.success_criteria;
125
+ directFields.successCriteria = Array.isArray(sc) ? { criteria: sc } : sc;
126
+ }
118
127
  // Map the public `committed` boolean onto the backend's commitment write
119
128
  // (the API still accepts the legacy goalType field and translates it to
120
129
  // promoted_at). committed:true ⇒ promoted, false ⇒ aspirational.
@@ -127,21 +136,21 @@ async function updateGoalHandler(args, apiClient) {
127
136
  await apiClient.goals.update(goal_id, directFields);
128
137
  applied.push('direct_fields');
129
138
  } catch (err) {
130
- failures.push({ step: 'direct_fields', error: err.message });
139
+ failures.push({ step: 'direct_fields', error: apiErrorMessage(err) });
131
140
  }
132
141
  }
133
142
 
134
143
  for (const planId of safeArray(changes.add_linked_plans)) {
135
144
  try { await apiClient.goals.linkPlan(goal_id, planId); applied.push(`link_plan:${planId}`); }
136
- catch (err) { failures.push({ step: `link_plan:${planId}`, error: err.message }); }
145
+ catch (err) { failures.push({ step: `link_plan:${planId}`, error: apiErrorMessage(err) }); }
137
146
  }
138
147
  for (const planId of safeArray(changes.remove_linked_plans)) {
139
148
  try { await apiClient.goals.unlinkPlan(goal_id, planId); applied.push(`unlink_plan:${planId}`); }
140
- catch (err) { failures.push({ step: `unlink_plan:${planId}`, error: err.message }); }
149
+ catch (err) { failures.push({ step: `unlink_plan:${planId}`, error: apiErrorMessage(err) }); }
141
150
  }
142
151
  for (const nodeId of safeArray(changes.add_achievers)) {
143
152
  try { await apiClient.goals.addAchiever(goal_id, nodeId); applied.push(`add_achiever:${nodeId}`); }
144
- catch (err) { failures.push({ step: `add_achiever:${nodeId}`, error: err.message }); }
153
+ catch (err) { failures.push({ step: `add_achiever:${nodeId}`, error: apiErrorMessage(err) }); }
145
154
  }
146
155
  for (const nodeId of safeArray(changes.remove_achievers)) {
147
156
  try {
@@ -152,7 +161,7 @@ async function updateGoalHandler(args, apiClient) {
152
161
  applied.push(`remove_achiever:${nodeId}`);
153
162
  }
154
163
  } catch (err) {
155
- failures.push({ step: `remove_achiever:${nodeId}`, error: err.message });
164
+ failures.push({ step: `remove_achiever:${nodeId}`, error: apiErrorMessage(err) });
156
165
  }
157
166
  }
158
167
 
@@ -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).
@@ -209,15 +229,21 @@ async function resolveDecisionHandler(args, apiClient) {
209
229
  if (action === 'approve' && decision?.metadata?.proposed_subtasks?.length) {
210
230
  for (const proposal of decision.metadata.proposed_subtasks) {
211
231
  try {
232
+ // createNode's schema is .strict() and has no acceptance_criteria field —
233
+ // sending it 400s the whole subtask. Fold it into the description so the
234
+ // criteria survive instead of being silently dropped on approval.
235
+ const description = [
236
+ proposal.description,
237
+ proposal.acceptance_criteria ? `Acceptance criteria: ${proposal.acceptance_criteria}` : null,
238
+ ].filter(Boolean).join('\n\n') || undefined;
212
239
  const node = await apiClient.nodes.createNode(plan_id, {
213
240
  parent_id: proposal.parent_id,
214
241
  node_type: proposal.node_type || 'task',
215
242
  title: proposal.title,
216
- description: proposal.description,
243
+ description,
217
244
  status: 'not_started',
218
245
  task_mode: proposal.task_mode || 'free',
219
246
  agent_instructions: proposal.agent_instructions,
220
- acceptance_criteria: proposal.acceptance_criteria,
221
247
  });
222
248
  created.push({ id: node.id || node.node?.id, title: proposal.title, parent_id: proposal.parent_id });
223
249
  } catch (err) {
@@ -254,6 +280,12 @@ const STATUS_TO_LOG_TYPE = {
254
280
  plan_ready: 'progress',
255
281
  };
256
282
 
283
+ // The backend log endpoint accepts comment/progress/reasoning/decision/challenge.
284
+ // Two friendly aliases the tool historically advertised are NOT valid there and
285
+ // 400'd the log step — map them to the closest valid type.
286
+ const LOG_TYPE_ALIASES = { blocker: 'challenge', completion: 'progress' };
287
+ const normalizeLogType = (lt) => (lt ? (LOG_TYPE_ALIASES[lt] || lt) : lt);
288
+
257
289
  const updateTaskDefinition = {
258
290
  name: 'update_task',
259
291
  description:
@@ -275,8 +307,9 @@ const updateTaskDefinition = {
275
307
  log_message: { type: 'string', description: 'Optional progress note' },
276
308
  log_type: {
277
309
  type: 'string',
278
- enum: ['progress', 'decision', 'blocker', 'completion', 'challenge'],
279
- description: "Defaults from status: blocked→challenge, others→progress.",
310
+ enum: ['progress', 'reasoning', 'decision', 'challenge', 'comment'],
311
+ description: "Defaults from status: blocked→challenge, others→progress. "
312
+ + "Legacy 'blocker'/'completion' are accepted and mapped to challenge/progress.",
280
313
  },
281
314
  release_claim: {
282
315
  type: 'boolean',
@@ -324,7 +357,7 @@ async function updateTaskHandler(args, apiClient) {
324
357
  const data = await apiClient.v1.updateTask(task_id, {
325
358
  status,
326
359
  log_message,
327
- log_type: args.log_type,
360
+ log_type: normalizeLogType(args.log_type),
328
361
  release_claim,
329
362
  add_learning,
330
363
  });
@@ -370,7 +403,7 @@ async function updateTaskHandler(args, apiClient) {
370
403
 
371
404
  // 2. Log entry
372
405
  if (log_message) {
373
- const logType = args.log_type || STATUS_TO_LOG_TYPE[status] || 'progress';
406
+ const logType = normalizeLogType(args.log_type) || STATUS_TO_LOG_TYPE[status] || 'progress';
374
407
  try {
375
408
  const log = await apiClient.logs.addLogEntry(planId, task_id, {
376
409
  content: log_message,
@@ -698,6 +731,13 @@ async function addLearningHandler(args, apiClient) {
698
731
  plan_id: scope.plan_id,
699
732
  node_id: scope.node_id,
700
733
  entity_type: entry_type,
734
+ // The episodes endpoint only persists {content,name,plan_id,node_id,metadata};
735
+ // the top-level entry_type/source_description were silently dropped. Carry
736
+ // the categorization in metadata so it survives on the stored episode.
737
+ metadata: {
738
+ entry_type,
739
+ source_description: source_description || 'BDI add_learning',
740
+ },
701
741
  });
702
742
  return formatResponse({
703
743
  as_of: asOf(),