vantage-peers-mcp 2.3.1 → 2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## v2.3.2 — 2026-05-28
4
+
5
+ **Hotfix** — Expose `fields="lite"` + `status` array/aliases in MCP tool schemas (Day 82 sprint gap).
6
+
7
+ Backend support for these params shipped in v2.3.1 but the MCP wrapper Zod schemas never exposed them, so MCP clients couldn't pass them. Fixed for 4 list tools:
8
+
9
+ - `list_tasks`: + `fields`, status now accepts aliases (`"open"`, `"active"`, `"all"`) and arrays
10
+ - `list_tasks_by_mission`: same
11
+ - `list_missions`: + `fields`, status accepts aliases and arrays
12
+ - `list_briefing_notes`: + `fields`
13
+
14
+ Aliases NOT permitted inside arrays (matches backend rejection contract).
15
+
16
+ Tests: 14 new cases (`src/__tests__/list-queries-schema-v2.3.2.test.ts`), 0 regression on 295+ existing.
17
+
18
+ Fix-pattern (fleet-wide): When backend query supports a new param, ALWAYS update the MCP wrapper tool schema in the SAME PR.
19
+
20
+ VP task: `k17e09ng1tf217n93z9m4tr0mx87hfe0`.
21
+
3
22
  ## 2.3.1 — 2026-05-26
4
23
 
5
24
  ### Fixed (Eta PR #530 delta-review)
@@ -46,4 +46,95 @@ export declare const updateBriefingNoteSchema: z.ZodObject<{
46
46
  decisions: z.ZodOptional<z.ZodArray<z.ZodString>>;
47
47
  linkedMemoryIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
48
48
  }, z.core.$strip>;
49
+ export declare const taskStatusSchema: z.ZodEnum<{
50
+ todo: "todo";
51
+ in_progress: "in_progress";
52
+ review: "review";
53
+ blocked: "blocked";
54
+ done: "done";
55
+ }>;
56
+ export declare const missionStatusSchema: z.ZodEnum<{
57
+ brainstorm: "brainstorm";
58
+ plan: "plan";
59
+ execute: "execute";
60
+ validate: "validate";
61
+ complete: "complete";
62
+ }>;
63
+ export declare const taskStatusFilterSchema: z.ZodUnion<readonly [z.ZodEnum<{
64
+ todo: "todo";
65
+ in_progress: "in_progress";
66
+ review: "review";
67
+ blocked: "blocked";
68
+ done: "done";
69
+ active: "active";
70
+ open: "open";
71
+ all: "all";
72
+ }>, z.ZodArray<z.ZodEnum<{
73
+ todo: "todo";
74
+ in_progress: "in_progress";
75
+ review: "review";
76
+ blocked: "blocked";
77
+ done: "done";
78
+ }>>]>;
79
+ export declare const missionStatusFilterSchema: z.ZodUnion<readonly [z.ZodEnum<{
80
+ brainstorm: "brainstorm";
81
+ plan: "plan";
82
+ execute: "execute";
83
+ validate: "validate";
84
+ complete: "complete";
85
+ active: "active";
86
+ open: "open";
87
+ all: "all";
88
+ }>, z.ZodArray<z.ZodEnum<{
89
+ brainstorm: "brainstorm";
90
+ plan: "plan";
91
+ execute: "execute";
92
+ validate: "validate";
93
+ complete: "complete";
94
+ }>>]>;
95
+ export declare const fieldsSchema: z.ZodEnum<{
96
+ lite: "lite";
97
+ full: "full";
98
+ }>;
99
+ export interface ParsedConvexError {
100
+ code: string;
101
+ message: string;
102
+ path: string | null;
103
+ hint: string | null;
104
+ }
105
+ /**
106
+ * Parse a Convex error message string into a structured object.
107
+ *
108
+ * Input example (from ConvexHttpClient):
109
+ * "[CONVEX M(briefingNotes:create)] ArgumentValidationError: Found ID
110
+ * \"js72ewf0m...\" from table briefingNotes, which does not match the table
111
+ * name in validator v.id(\"memories\"). Path: .linkedMemoryIds[4]"
112
+ *
113
+ * Returns { code, message, path, hint } where:
114
+ * - code = "ArgumentValidationError" (or the parsed error type)
115
+ * - message = the full human-readable error description after the code prefix
116
+ * - path = e.g. ".linkedMemoryIds[4]" extracted from "Path: ..." suffix
117
+ * - hint = a concise guidance string derived from the error, or null
118
+ *
119
+ * For unrecognised error strings, code = "ServerError" and path/hint = null.
120
+ *
121
+ * Exported for unit testing.
122
+ */
123
+ export declare function parseConvexError(rawMessage: string): ParsedConvexError;
124
+ /**
125
+ * Produce a structured MCP error response for any error thrown by a Convex
126
+ * operation. For ConvexError / ArgumentValidationError the response body
127
+ * contains a JSON object with { code, message, path, hint } so the MCP client
128
+ * can display actionable diagnostics instead of a bare "Server Error" string.
129
+ *
130
+ * For unrecognised errors the response falls back to the plain text format
131
+ * used by `mcpError`.
132
+ */
133
+ export declare function mcpConvexError(error: unknown): {
134
+ content: Array<{
135
+ type: "text";
136
+ text: string;
137
+ }>;
138
+ isError: true;
139
+ };
49
140
  export declare function registerTools(server: McpServer, convex: ConvexHttpClient, oauthCtx?: OAuthContext): void;
package/dist/src/tools.js CHANGED
@@ -101,7 +101,9 @@ export const updateBriefingNoteSchema = z.object({
101
101
  linkedMemoryIds: z
102
102
  .array(memoryIdSchema)
103
103
  .optional()
104
- .describe("Optional new linkedMemoryIds array — full replace, not append. Each ID must point to memories table, NOT briefingNotes or any other table."),
104
+ .describe("Optional new linkedMemoryIds array — full replace, not append. " +
105
+ "DISCLAIMER: Memory IDs only — NOT briefingNotes IDs or any other table. " +
106
+ "Passing a briefingNotes ID causes ArgumentValidationError at path .linkedMemoryIds[N]."),
105
107
  });
106
108
  const assigneeSchema = z
107
109
  .string()
@@ -113,12 +115,53 @@ const prioritySchema = z
113
115
  const componentTypeSchema = z
114
116
  .enum(["agent", "skill", "hook", "plugin"])
115
117
  .describe("Component type");
116
- const taskStatusSchema = z
117
- .enum(["todo", "in_progress", "review", "blocked", "done"])
118
- .describe("Task status");
119
- const missionStatusSchema = z
120
- .enum(["brainstorm", "plan", "execute", "validate", "complete"])
118
+ const taskStatusValues = [
119
+ "todo",
120
+ "in_progress",
121
+ "review",
122
+ "blocked",
123
+ "done",
124
+ ];
125
+ const taskStatusAliases = ["open", "active", "all"];
126
+ export const taskStatusSchema = z.enum(taskStatusValues).describe("Task status");
127
+ const missionStatusValues = [
128
+ "brainstorm",
129
+ "plan",
130
+ "execute",
131
+ "validate",
132
+ "complete",
133
+ ];
134
+ const missionStatusAliases = ["open", "active", "all"];
135
+ export const missionStatusSchema = z
136
+ .enum(missionStatusValues)
121
137
  .describe("Mission lifecycle status");
138
+ // v2.3.2 — filter-only schemas: expose status aliases ("open"/"active"/"all")
139
+ // AND multi-status arrays to the MCP client. Backend (convex/tasks.ts +
140
+ // convex/missions.ts) handles alias expansion + array validation.
141
+ // Aliases NOT allowed inside arrays (matches backend rejection).
142
+ export const taskStatusFilterSchema = z
143
+ .union([
144
+ z.enum([...taskStatusValues, ...taskStatusAliases]),
145
+ z.array(z.enum(taskStatusValues)).min(1),
146
+ ])
147
+ .describe('Task status filter. Single status ("todo"|"in_progress"|"review"|"blocked"|"done"), ' +
148
+ 'alias ("open" = todo+in_progress+review+blocked, "active" = todo+in_progress, "all" = no filter), ' +
149
+ 'or array of direct statuses (no aliases inside array).');
150
+ export const missionStatusFilterSchema = z
151
+ .union([
152
+ z.enum([...missionStatusValues, ...missionStatusAliases]),
153
+ z.array(z.enum(missionStatusValues)).min(1),
154
+ ])
155
+ .describe('Mission status filter. Single status ("brainstorm"|"plan"|"execute"|"validate"|"complete"), ' +
156
+ 'alias ("open" = brainstorm+plan+execute+validate, "active" = plan+execute, "all" = no filter), ' +
157
+ 'or array of direct statuses (no aliases inside array).');
158
+ // v2.3.2 — fields projection toggle. "lite" returns compact projection
159
+ // (5-10× smaller payload), "full" (default) returns full doc.
160
+ export const fieldsSchema = z
161
+ .enum(["lite", "full"])
162
+ .describe('Field projection — "lite" returns compact fields only ' +
163
+ '(typical 5-10× smaller payload for large list scans), ' +
164
+ '"full" (default) returns the full document.');
122
165
  const mandateStatusSchema = z
123
166
  .enum(["requested", "accepted", "in_progress", "delivered", "settled"])
124
167
  .describe("Mandate lifecycle status");
@@ -180,6 +223,103 @@ function mcpError(message) {
180
223
  isError: true,
181
224
  };
182
225
  }
226
+ /**
227
+ * Parse a Convex error message string into a structured object.
228
+ *
229
+ * Input example (from ConvexHttpClient):
230
+ * "[CONVEX M(briefingNotes:create)] ArgumentValidationError: Found ID
231
+ * \"js72ewf0m...\" from table briefingNotes, which does not match the table
232
+ * name in validator v.id(\"memories\"). Path: .linkedMemoryIds[4]"
233
+ *
234
+ * Returns { code, message, path, hint } where:
235
+ * - code = "ArgumentValidationError" (or the parsed error type)
236
+ * - message = the full human-readable error description after the code prefix
237
+ * - path = e.g. ".linkedMemoryIds[4]" extracted from "Path: ..." suffix
238
+ * - hint = a concise guidance string derived from the error, or null
239
+ *
240
+ * For unrecognised error strings, code = "ServerError" and path/hint = null.
241
+ *
242
+ * Exported for unit testing.
243
+ */
244
+ export function parseConvexError(rawMessage) {
245
+ // Known Convex validation/runtime error codes surfaced as plaintext
246
+ const knownCodes = [
247
+ "ArgumentValidationError",
248
+ "AuthorizationError",
249
+ "ConvexError",
250
+ "SchemaValidationError",
251
+ "QueryError",
252
+ "MutationError",
253
+ "ActionError",
254
+ ];
255
+ // Strip the [CONVEX M(path)] / [CONVEX Q(path)] prefix if present
256
+ const stripped = rawMessage.replace(/^\[CONVEX [A-Z]+\([^\]]*\)\]\s*/, "");
257
+ // Detect the error code
258
+ let code = "ServerError";
259
+ let remainder = stripped;
260
+ for (const candidate of knownCodes) {
261
+ if (stripped.startsWith(candidate + ":") || stripped.startsWith(candidate + " ")) {
262
+ code = candidate;
263
+ remainder = stripped.slice(candidate.length).replace(/^[:\s]+/, "");
264
+ break;
265
+ }
266
+ }
267
+ // Extract "Path: .<fieldPath>" from the tail of the message
268
+ // Convex appends this as the last sentence: "Path: .linkedMemoryIds[4]"
269
+ let path = null;
270
+ const pathMatch = remainder.match(/\bPath:\s*([\w.\[\]"']+)\s*$/);
271
+ if (pathMatch) {
272
+ path = pathMatch[1];
273
+ remainder = remainder.slice(0, pathMatch.index).trim().replace(/\.\s*$/, "");
274
+ }
275
+ // Build a concise hint for common patterns
276
+ let hint = null;
277
+ if (code === "ArgumentValidationError") {
278
+ const tableMatch = remainder.match(/from table (\w+),.*validator v\.id\("(\w+)"\)/);
279
+ if (tableMatch) {
280
+ hint = `ID belongs to table "${tableMatch[1]}" but validator expects v.id("${tableMatch[2]}"). Check that you are passing the correct document ID from the "${tableMatch[2]}" table.`;
281
+ }
282
+ }
283
+ return { code, message: remainder || rawMessage, path, hint };
284
+ }
285
+ /**
286
+ * Produce a structured MCP error response for any error thrown by a Convex
287
+ * operation. For ConvexError / ArgumentValidationError the response body
288
+ * contains a JSON object with { code, message, path, hint } so the MCP client
289
+ * can display actionable diagnostics instead of a bare "Server Error" string.
290
+ *
291
+ * For unrecognised errors the response falls back to the plain text format
292
+ * used by `mcpError`.
293
+ */
294
+ export function mcpConvexError(error) {
295
+ const rawMessage = error instanceof Error ? error.message : String(error);
296
+ const parsed = parseConvexError(rawMessage);
297
+ // For ArgumentValidationError and other known Convex codes, return structured JSON
298
+ if (parsed.code !== "ServerError") {
299
+ const payload = {
300
+ code: parsed.code,
301
+ message: parsed.message,
302
+ };
303
+ if (parsed.path !== null)
304
+ payload.path = parsed.path;
305
+ if (parsed.hint !== null)
306
+ payload.hint = parsed.hint;
307
+ return {
308
+ content: [
309
+ {
310
+ type: "text",
311
+ text: JSON.stringify(payload, null, 2),
312
+ },
313
+ ],
314
+ isError: true,
315
+ };
316
+ }
317
+ // Fallback: generic error, preserve existing plain-text format
318
+ return {
319
+ content: [{ type: "text", text: `Error: ${rawMessage}` }],
320
+ isError: true,
321
+ };
322
+ }
183
323
  // ─────────────────────────────────────────────────────────────────────────────
184
324
  // Main export: register all tools against a server + convex client pair
185
325
  // ─────────────────────────────────────────────────────────────────────────────
@@ -991,7 +1131,9 @@ export function registerTools(server, convex, oauthCtx) {
991
1131
  .string()
992
1132
  .optional()
993
1133
  .describe("Filter by instance — e.g. 'pi-vps'. Returns only tasks assigned to that instance."),
994
- status: taskStatusSchema.optional().describe("Filter by status"),
1134
+ status: taskStatusFilterSchema
1135
+ .optional()
1136
+ .describe("Filter by status (single, alias, or array)"),
995
1137
  project: z.string().optional().describe("Filter by project name"),
996
1138
  limit: z
997
1139
  .number()
@@ -1001,7 +1143,10 @@ export function registerTools(server, convex, oauthCtx) {
1001
1143
  .optional()
1002
1144
  .default(50)
1003
1145
  .describe("Maximum number of tasks to return (default 50)"),
1004
- }, async ({ assignedTo, assignedToInstance, status, project, limit }) => {
1146
+ fields: fieldsSchema
1147
+ .optional()
1148
+ .describe('Field projection ("lite"|"full")'),
1149
+ }, async ({ assignedTo, assignedToInstance, status, project, limit, fields, }) => {
1005
1150
  try {
1006
1151
  const tasks = await convex.query("tasks:list", {
1007
1152
  assignedTo,
@@ -1009,6 +1154,7 @@ export function registerTools(server, convex, oauthCtx) {
1009
1154
  status,
1010
1155
  project,
1011
1156
  limit: limit ?? 50,
1157
+ fields,
1012
1158
  });
1013
1159
  return {
1014
1160
  content: [
@@ -1315,7 +1461,9 @@ export function registerTools(server, convex, oauthCtx) {
1315
1461
  // ── list_tasks_by_mission ───────────────────────────────────────────────────
1316
1462
  server.tool("list_tasks_by_mission", "List all tasks linked to a specific mission. Optionally filter by status.", {
1317
1463
  missionId: z.string().describe("Convex document ID of the mission"),
1318
- status: taskStatusSchema.optional().describe("Filter by task status"),
1464
+ status: taskStatusFilterSchema
1465
+ .optional()
1466
+ .describe("Filter by task status (single, alias, or array)"),
1319
1467
  limit: z
1320
1468
  .number()
1321
1469
  .int()
@@ -1324,12 +1472,16 @@ export function registerTools(server, convex, oauthCtx) {
1324
1472
  .optional()
1325
1473
  .default(50)
1326
1474
  .describe("Maximum number of tasks to return (default 50)"),
1327
- }, async ({ missionId, status, limit }) => {
1475
+ fields: fieldsSchema
1476
+ .optional()
1477
+ .describe('Field projection ("lite"|"full")'),
1478
+ }, async ({ missionId, status, limit, fields }) => {
1328
1479
  try {
1329
1480
  const tasks = await convex.query("tasks:listByMission", {
1330
1481
  missionId: missionId,
1331
1482
  status,
1332
1483
  limit: limit ?? 50,
1484
+ fields,
1333
1485
  });
1334
1486
  return {
1335
1487
  content: [
@@ -1404,7 +1556,9 @@ export function registerTools(server, convex, oauthCtx) {
1404
1556
  "Filter by project, pilot, and/or status. Returns newest first.", {
1405
1557
  project: z.string().optional().describe("Filter by project name"),
1406
1558
  pilot: creatorSchema.optional().describe("Filter by pilot orchestrator"),
1407
- status: missionStatusSchema.optional().describe("Filter by status"),
1559
+ status: missionStatusFilterSchema
1560
+ .optional()
1561
+ .describe("Filter by status (single, alias, or array)"),
1408
1562
  limit: z
1409
1563
  .number()
1410
1564
  .int()
@@ -1413,13 +1567,17 @@ export function registerTools(server, convex, oauthCtx) {
1413
1567
  .optional()
1414
1568
  .default(50)
1415
1569
  .describe("Maximum number of missions to return (default 50)"),
1416
- }, async ({ project, pilot, status, limit }) => {
1570
+ fields: fieldsSchema
1571
+ .optional()
1572
+ .describe('Field projection ("lite"|"full")'),
1573
+ }, async ({ project, pilot, status, limit, fields }) => {
1417
1574
  try {
1418
1575
  const missions = await convex.query("missions:list", {
1419
1576
  project,
1420
1577
  pilot,
1421
1578
  status,
1422
1579
  limit: limit ?? 50,
1580
+ fields,
1423
1581
  });
1424
1582
  return {
1425
1583
  content: [
@@ -1630,7 +1788,9 @@ export function registerTools(server, convex, oauthCtx) {
1630
1788
  // ── create_briefing_note ────────────────────────────────────────────────────
1631
1789
  server.tool("create_briefing_note", "Create a briefing note — a structured record of a topic discussion, with participants, " +
1632
1790
  "content, optional decisions, and optional links to existing memories. " +
1633
- "linkedMemoryIds MUST contain IDs from the memories table only — NOT briefingNotes IDs or IDs from any other table.", {
1791
+ "linkedMemoryIds MUST contain IDs from the memories table only — NOT briefingNotes IDs or IDs from any other table. " +
1792
+ "IMPORTANT: If you need to cross-link briefing notes together, use linkedBriefingIds (not yet shipped) — " +
1793
+ "passing a briefingNotes document ID into linkedMemoryIds will produce an ArgumentValidationError at the Convex validator boundary.", {
1634
1794
  title: z.string().describe("Briefing note title"),
1635
1795
  topic: z
1636
1796
  .string()
@@ -1643,7 +1803,10 @@ export function registerTools(server, convex, oauthCtx) {
1643
1803
  linkedMemoryIds: z
1644
1804
  .array(memoryIdSchema)
1645
1805
  .optional()
1646
- .describe("Convex document IDs of related memories — each must be a 32-char ID from the memories table, NOT briefingNotes or any other table"),
1806
+ .describe("Convex document IDs of related memories — each must be a 32-char ID from the memories table, NOT briefingNotes or any other table. " +
1807
+ "DISCLAIMER: Memory IDs only. Do NOT pass briefingNotes IDs here — they share the same 32-char alphanumeric format but belong to a different table. " +
1808
+ "Passing a briefingNotes ID will fail with ArgumentValidationError at path .linkedMemoryIds[N]. " +
1809
+ "If cross-linking briefings is needed, request the linkedBriefingIds feature instead."),
1647
1810
  createdBy: creatorSchema,
1648
1811
  }, async ({ title, topic, participants, content, decisions, linkedMemoryIds, createdBy, }) => {
1649
1812
  let contentBytes = 0;
@@ -1680,7 +1843,7 @@ export function registerTools(server, convex, oauthCtx) {
1680
1843
  title,
1681
1844
  errorMessage: error?.message ?? String(error),
1682
1845
  });
1683
- return mcpError(error.message ?? String(error));
1846
+ return mcpConvexError(error);
1684
1847
  }
1685
1848
  });
1686
1849
  // ── update_briefing_note ────────────────────────────────────────────────────
@@ -1721,7 +1884,7 @@ export function registerTools(server, convex, oauthCtx) {
1721
1884
  noteId,
1722
1885
  errorMessage: error?.message ?? String(error),
1723
1886
  });
1724
- return mcpError(error.message ?? String(error));
1887
+ return mcpConvexError(error);
1725
1888
  }
1726
1889
  });
1727
1890
  // ── list_briefing_notes ─────────────────────────────────────────────────────
@@ -1738,11 +1901,15 @@ export function registerTools(server, convex, oauthCtx) {
1738
1901
  .optional()
1739
1902
  .default(20)
1740
1903
  .describe("Maximum notes to return (default 20)"),
1741
- }, async ({ topic, limit }) => {
1904
+ fields: fieldsSchema
1905
+ .optional()
1906
+ .describe('Field projection ("lite"|"full")'),
1907
+ }, async ({ topic, limit, fields }) => {
1742
1908
  try {
1743
1909
  const notes = await convex.query("briefingNotes:list", {
1744
1910
  topic,
1745
1911
  limit: limit ?? 20,
1912
+ fields,
1746
1913
  });
1747
1914
  return {
1748
1915
  content: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vantage-peers-mcp",
3
- "version": "2.3.1",
3
+ "version": "2.3.2",
4
4
  "description": "MCP server for VantagePeers — shared memory, messaging, and task coordination for AI agent teams",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",