vantage-peers-mcp 2.7.1 → 2.9.0

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,65 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.9.0] — 2026-06-14 — Day 102 CRUD baseline PR-B: episode entity 5-op surface (mission k575kc1ryps0n8br95jw3q7d0x88m2v9)
4
+
5
+ Mission `mcp-crud-baseline-standard` PR-B under T2. Second of 4 sub-PRs aligning the MCP surface with the Day 101 doctrine `j57dhrmkzjerjtssnr0z9ba57n88n7q7` ("5 ops per entity"). PR-B adds the missing read/list/search facades for the `episode` entity, completing the 5-op surface (the write side `store_episode` already existed since the 8-Sins doctrine).
6
+
7
+ Architectural note: episodes are NOT a separate Convex table — per hotfix `7f958d0`, episodes are stored as memories with `type='episode'` (context/goal/action/outcome/insight + severity). The 4 new tools are thin wrappers that force `type='episode'` on the existing `memories:*` / `search:*` actions, so callers get an ergonomic episode-scoped surface without introducing a new backend table or duplicating index logic.
8
+
9
+ ### Added (4 canonical episode tools)
10
+
11
+ - **`get_episode`** — fetch a single episode by memory ID. Calls `memories:getMemory` then asserts the returned row has `type='episode'`; otherwise returns a non-leaky "Episode not found" so wrong-type IDs do not leak existence of non-episode memories. Scope-aware via `scopeFilterGet`. Annotations: `readOnlyHint=true`, `openWorldHint=false`, `destructiveHint=false`.
12
+ - **`list_episodes`** — list episodes ordered newest first. Calls `memories:listMemories` with `type='episode'` forced. Accepts `namespace?`, `createdBy?`, `limit?`, `fields?`, `cursor?` — same paging semantics as `list_memories`. Scope-aware via `scopeFilterList`. Envelope-capped via `capListResponseBytes("list_episodes")`.
13
+ - **`search_episodes_by_keyword`** — BM25 search restricted to episodes. Calls `search:textSearch` with `type='episode'` forced. Same `guardRead` namespace gate, same 20-default / 200-cap limits.
14
+ - **`search_episodes_by_semantic`** — semantic vector cosine search restricted to episodes. Calls `search:recall` with `type='episode'` forced. Same gate, same limits.
15
+
16
+ All four are pure MCP-layer additions: no Convex schema change, no new index, no new action. tsc clean.
17
+
18
+ ### Why
19
+
20
+ Episode = the 8-Sins / orchestrator-introspection memory type (severity + context/goal/action/outcome/insight). Until 2.9.0, recalling past episodes required `recall query='...' type='episode'` — discoverable only to callers who already knew the memory-side filter trick. Surfacing dedicated wrappers makes the episode lifecycle (write via `store_episode`, read via `get_episode`, browse via `list_episodes`, recall via the two search ops) consistent with the doctrine and self-documenting in any MCP client's tool list.
21
+
22
+ `hybrid_search` remains entity-agnostic and is NOT mirrored for episodes (cross-cutting RRF tool per audit § 4).
23
+
24
+ ### Test fixture catch-up
25
+
26
+ - `READ_ONLY_TOOLS` set in `mcp-server/src/__tests__/chatgpt-tool-annotations.test.ts` extended with the 4 new tool names.
27
+
28
+ ### Refs
29
+
30
+ - Mission `k575kc1ryps0n8br95jw3q7d0x88m2v9` (MCP CRUD Baseline Standard, pilot Sigma + agents Sigma + Eta).
31
+ - Pi authorization msg `jn74v2pkfz08agex4nfm2yfvfd88nzdw` — "chain T2 PR-B episode entity en autonomie scope mission".
32
+ - Audit T1 deliverable: `analysis/mcp-crud-baseline-vp-audit-2026-06-14.md` row 7 (episode entity recommendation: add façade wrappers).
33
+ - Architecture: hotfix `7f958d0` — episodes are memories with metadata, not a separate table.
34
+
35
+ ## [2.8.0] — 2026-06-14 — Day 101 CRUD baseline PR-A: search_memories_by_keyword + search_memories_by_semantic (mission k575kc1ryps0n8br95jw3q7d0x88m2v9, task k1735qk9kx6agjjyt3e38rdvvh88mk0p)
36
+
37
+ Mission `mcp-crud-baseline-standard` PR-A under T2 `[CRUD-T2] Implémentation gaps VP MCP`. First of 4 sub-PRs aligning the MCP surface with the Day 101 doctrine `j57dhrmkzjerjtssnr0z9ba57n88n7q7` ("5 ops per entity: get / list / search_by_keyword / search_by_semantic / create-or-upsert"). PR-A handles the convention drift on the `memories` entity — the only entity that already had both keyword + semantic search wired, but under non-canonical names.
38
+
39
+ ### Added (canonical names)
40
+
41
+ - **`search_memories_by_keyword`** — BM25 full-text search over memories, identical wire to `text_search`. Calls `search:textSearch` Convex action 1:1, same params (`query`, `namespace?`, `type?`, `limit?`, `fields?`), same `guardRead` namespace gate, same 20-item default limit + 200 cap, same `mcpConvexError` error surface (2.7.1 sweep). Annotations: `readOnlyHint=true`, `openWorldHint=false`, `destructiveHint=false`, title `"Search memories by keyword (BM25)"`.
42
+ - **`search_memories_by_semantic`** — semantic vector cosine search over memories, identical wire to `recall`. Calls `search:recall` Convex action 1:1, same params, same gate, same defaults. Annotations: `readOnlyHint=true`, `openWorldHint=false`, `destructiveHint=false`, title `"Search memories by semantic (vector cosine)"`.
43
+
44
+ ### Deprecated (alias-only, one minor window)
45
+
46
+ - **`text_search`** — alias of `search_memories_by_keyword`. Description now leads with `DEPRECATED ALIAS`, source comment marks it for removal in `2.9.0`. Wire unchanged — existing callers continue to work for one minor.
47
+ - **`recall`** — alias of `search_memories_by_semantic`. Description now leads with `DEPRECATED ALIAS`, source comment marks it for removal in `2.9.0`. Wire unchanged.
48
+
49
+ ### Why
50
+
51
+ Per mission `k575kc1r` brief + Pi arbitrage `jn77rpx2msfy2v174sdqyjzp6n88m9bw` Q4 (RENAME CLEAN, optional one-minor dual-emit buffer if Sigma judges useful): the legacy `text_search` / `recall` pair predates the Day 101 CRUD baseline naming convention. Other entities will gain `search_<entity>s_by_keyword` / `search_<entity>s_by_semantic` in PR-C (cluster of 13) and PR-D (vectorIndex add for briefing_notes + diary). Aligning the `memories` entity FIRST means PR-C/D ship into a fleet that already understands the new naming, and skill / plugin / docs propagation (T-SKILLS, T-PLUGIN, T-DOC) has a single canonical truth to follow.
52
+
53
+ `hybrid_search` is intentionally NOT renamed — it remains a cross-cutting RRF-fusion tool, not entity-scoped, per the audit `analysis/mcp-crud-baseline-vp-audit-2026-06-14.md` § 4.
54
+
55
+ ### Refs
56
+
57
+ - Mission `k575kc1ryps0n8br95jw3q7d0x88m2v9` (MCP CRUD Baseline Standard, pilot Sigma + agents Sigma + Eta).
58
+ - Task T2 `k1735qk9kx6agjjyt3e38rdvvh88mk0p`.
59
+ - Audit T1 deliverable: `analysis/mcp-crud-baseline-vp-audit-2026-06-14.md` (140 lines, 53 table rows).
60
+ - Doctrine memory: `j57dhrmkzjerjtssnr0z9ba57n88n7q7` (5 ops per entity).
61
+ - Pi Q4 arbitrage: msg `jn77rpx2msfy2v174sdqyjzp6n88m9bw`.
62
+
3
63
  ## [2.7.1] — 2026-06-14 — Day 101 FIX-B mcpError() → mcpConvexError() sweep (task k1744wk2gfgqt2gdqh41d4r91h88n410)
4
64
 
5
65
  Patch-level fix to make every tool wrapper surface structured ConvexError
package/README.md CHANGED
@@ -242,7 +242,7 @@ Example:
242
242
  ### Session (1)
243
243
  `set_summary`
244
244
 
245
- ## Compact payloads and status aliases (v2.3.0)
245
+ ## Compact payloads and status aliases (v2.9.0 — feature since v2.3.0)
246
246
 
247
247
  ### `fields=lite` — reduced token payloads
248
248
 
package/dist/server.js CHANGED
@@ -102,7 +102,7 @@ const convexUrl = loadConvexUrl();
102
102
  const convex = new ConvexHttpClient(convexUrl);
103
103
  const server = new McpServer({
104
104
  name: "vantage-peers",
105
- version: "2.5.0",
105
+ version: "2.9.0",
106
106
  });
107
107
  // ─────────────────────────────────────────────────────────────────────────────
108
108
  // Helper: structured error response for MCP tool handlers
package/dist/src/tools.js CHANGED
@@ -916,8 +916,11 @@ export function registerTools(server, convex, oauthCtx) {
916
916
  }
917
917
  });
918
918
  // ── recall ──────────────────────────────────────────────────────────────────
919
- server.tool("recall", "Semantic vector search over VantagePeers memories, ranked by cosine similarity. " +
920
- "WHEN: use at session start or before decisions prefer over text_search for intent-based queries. " +
919
+ // DEPRECATED (Day 101 v2.8.0) alias of search_memories_by_semantic. Retained
920
+ // for one minor version as a back-compat shim. New callers should use
921
+ // `search_memories_by_semantic`. To be removed in 2.9.0.
922
+ server.tool("recall", "DEPRECATED ALIAS of search_memories_by_semantic — semantic vector search over VantagePeers memories, ranked by cosine similarity. " +
923
+ "WHEN: use at session start or before decisions — prefer over text_search for intent-based queries. New callers: use search_memories_by_semantic. " +
921
924
  "EXAMPLE: recall query='Pi feedback rules' namespace='global' type='feedback' limit=20.", {
922
925
  query: z
923
926
  .string()
@@ -971,8 +974,11 @@ export function registerTools(server, convex, oauthCtx) {
971
974
  }
972
975
  });
973
976
  // ── text_search ─────────────────────────────────────────────────────────────
974
- server.tool("text_search", "BM25 full-text keyword search over VantagePeers memories for exact term matching. " +
975
- "WHEN: use when recall returns too-broad results and you need a specific exact phrase or ID. " +
977
+ // DEPRECATED (Day 101 v2.8.0) alias of search_memories_by_keyword. Retained
978
+ // for one minor version as a back-compat shim. New callers should use
979
+ // `search_memories_by_keyword`. To be removed in 2.9.0.
980
+ server.tool("text_search", "DEPRECATED ALIAS of search_memories_by_keyword — BM25 full-text keyword search over VantagePeers memories for exact term matching. " +
981
+ "WHEN: use when search_memories_by_semantic returns too-broad results and you need a specific exact phrase or ID. New callers: use search_memories_by_keyword. " +
976
982
  "EXAMPLE: text_search query='Day 92 C3 descriptions' namespace='project/vantage-peers' limit=10.", {
977
983
  query: z.string().describe("Search query text"),
978
984
  namespace: z
@@ -1016,6 +1022,113 @@ export function registerTools(server, convex, oauthCtx) {
1016
1022
  return mcpConvexError(error);
1017
1023
  }
1018
1024
  });
1025
+ // ── search_memories_by_keyword ─────────────────────────────────────────────
1026
+ // Day 101 v2.8.0 — canonical name under MCP CRUD baseline doctrine.
1027
+ // Mirrors text_search 1:1 (same Convex action `search:textSearch`).
1028
+ // text_search is retained as a deprecated alias until 2.9.0.
1029
+ server.tool("search_memories_by_keyword", "BM25 full-text keyword search over VantagePeers memories for exact term matching. " +
1030
+ "WHEN: use when search_memories_by_semantic returns too-broad results and you need a specific exact phrase or ID. " +
1031
+ "EXAMPLE: search_memories_by_keyword query='Day 92 C3 descriptions' namespace='project/vantage-peers' limit=10.", {
1032
+ query: z.string().describe("Search query text"),
1033
+ namespace: z
1034
+ .string()
1035
+ .optional()
1036
+ .describe("Namespace filter (e.g. 'global', 'project/my-project')"),
1037
+ type: memoryTypeSchema.optional().describe("Filter by memory type"),
1038
+ limit: z
1039
+ .number()
1040
+ .int()
1041
+ .min(1)
1042
+ .max(200)
1043
+ .optional()
1044
+ .describe("Max items to return. Default 20 (envelope-safe). Cap 200."),
1045
+ fields: z
1046
+ .enum(["lite", "full"])
1047
+ .optional()
1048
+ .describe("'lite' returns compact payload (less tokens), 'full' is default. v2.4.9+."),
1049
+ }, {
1050
+ readOnlyHint: true,
1051
+ openWorldHint: false,
1052
+ destructiveHint: false,
1053
+ title: "Search memories by keyword (BM25)",
1054
+ }, async ({ query, namespace, type, limit, fields }) => {
1055
+ try {
1056
+ const nsDenied = guardRead(namespace);
1057
+ if (nsDenied)
1058
+ return nsDenied;
1059
+ const results = await convex.action("search:textSearch", {
1060
+ query,
1061
+ namespace,
1062
+ type,
1063
+ limit: limit ?? 20,
1064
+ fields: fields ?? "lite",
1065
+ });
1066
+ return {
1067
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
1068
+ };
1069
+ }
1070
+ catch (error) {
1071
+ return mcpConvexError(error);
1072
+ }
1073
+ });
1074
+ // ── search_memories_by_semantic ────────────────────────────────────────────
1075
+ // Day 101 v2.8.0 — canonical name under MCP CRUD baseline doctrine.
1076
+ // Mirrors recall 1:1 (same Convex action `search:recall`).
1077
+ // recall is retained as a deprecated alias until 2.9.0.
1078
+ server.tool("search_memories_by_semantic", "Semantic vector search over VantagePeers memories, ranked by cosine similarity. " +
1079
+ "WHEN: use at session start or before decisions — prefer over search_memories_by_keyword for intent-based queries. " +
1080
+ "EXAMPLE: search_memories_by_semantic query='Pi feedback rules' namespace='global' type='feedback' limit=20.", {
1081
+ query: z
1082
+ .string()
1083
+ .describe("Natural language query to search for relevant memories"),
1084
+ namespace: z
1085
+ .string()
1086
+ .optional()
1087
+ .describe("Filter to a specific namespace — omit to search all"),
1088
+ type: memoryTypeSchema
1089
+ .optional()
1090
+ .describe("Filter to a specific memory type — omit to search all"),
1091
+ limit: z
1092
+ .number()
1093
+ .int()
1094
+ .min(1)
1095
+ .max(200)
1096
+ .optional()
1097
+ .describe("Max items to return. Default 20 (envelope-safe). Cap 200."),
1098
+ fields: z
1099
+ .enum(["lite", "full"])
1100
+ .optional()
1101
+ .describe("'lite' returns compact payload (less tokens), 'full' is default. v2.4.9+."),
1102
+ }, {
1103
+ readOnlyHint: true,
1104
+ openWorldHint: false,
1105
+ destructiveHint: false,
1106
+ title: "Search memories by semantic (vector cosine)",
1107
+ }, async ({ query, namespace, type, limit, fields }) => {
1108
+ try {
1109
+ const nsDenied = guardRead(namespace);
1110
+ if (nsDenied)
1111
+ return nsDenied;
1112
+ const results = await convex.action("search:recall", {
1113
+ query,
1114
+ namespace,
1115
+ type,
1116
+ limit: limit ?? 20,
1117
+ fields: fields ?? "lite",
1118
+ });
1119
+ return {
1120
+ content: [
1121
+ {
1122
+ type: "text",
1123
+ text: JSON.stringify(results, null, 2),
1124
+ },
1125
+ ],
1126
+ };
1127
+ }
1128
+ catch (error) {
1129
+ return mcpConvexError(error);
1130
+ }
1131
+ });
1019
1132
  // ── hybrid_search ───────────────────────────────────────────────────────────
1020
1133
  server.tool("hybrid_search", "Combined vector + BM25 search via Reciprocal Rank Fusion for best semantic and keyword coverage. " +
1021
1134
  "WHEN: use when neither recall nor text_search alone yields good results — highest recall quality. " +
@@ -1127,6 +1240,225 @@ export function registerTools(server, convex, oauthCtx) {
1127
1240
  return mcpConvexError(error);
1128
1241
  }
1129
1242
  });
1243
+ // ── get_episode ────────────────────────────────────────────────────────────
1244
+ // Day 102 v2.9.0 — episode entity 5-op surface (PR-B).
1245
+ // Thin wrapper: episodes are stored as memories with type='episode'
1246
+ // (no separate table — see hotfix 7f958d0). This calls memories:getMemory
1247
+ // and asserts type='episode' so callers get a non-leaky 404 on wrong-type IDs.
1248
+ server.tool("get_episode", "Fetch a single episode by its memory document ID. Episodes are memories with type='episode' carrying context/goal/action/outcome/insight + severity. " +
1249
+ "WHEN: use when you have an episodeId from store_episode or a prior search and need the full record. " +
1250
+ "EXAMPLE: get_episode episodeId='j57dy3049btafda9m2f5d2ggk987ph3f'.", {
1251
+ episodeId: z.string().describe("Episode (memory) document ID"),
1252
+ }, {
1253
+ readOnlyHint: true,
1254
+ openWorldHint: false,
1255
+ destructiveHint: false,
1256
+ title: "Get episode",
1257
+ }, async ({ episodeId }) => {
1258
+ try {
1259
+ const memory = await convex.query("memories:getMemory", {
1260
+ memoryId: episodeId,
1261
+ });
1262
+ const filtered = scopeFilterGet(oauthCtx, memory);
1263
+ if (filtered === null) {
1264
+ return mcpError(`Episode not found: ${episodeId}`);
1265
+ }
1266
+ if (filtered?.type !== "episode") {
1267
+ return mcpError(`Episode not found: ${episodeId}`);
1268
+ }
1269
+ return {
1270
+ content: [{ type: "text", text: JSON.stringify(filtered, null, 2) }],
1271
+ };
1272
+ }
1273
+ catch (error) {
1274
+ return mcpConvexError(error);
1275
+ }
1276
+ });
1277
+ // ── list_episodes ──────────────────────────────────────────────────────────
1278
+ // Day 102 v2.9.0 — episode entity 5-op surface (PR-B).
1279
+ // Thin wrapper on memories:listMemories with type='episode' forced.
1280
+ server.tool("list_episodes", "List episodes (memories with type='episode') ordered newest first. " +
1281
+ "WHEN: use to enumerate episodes by namespace or creator before recall/audit. " +
1282
+ "EXAMPLE: list_episodes namespace='orchestrator/sigma' limit=20.", {
1283
+ namespace: z
1284
+ .string()
1285
+ .optional()
1286
+ .describe("Filter to a specific namespace — omit to list across all"),
1287
+ createdBy: z
1288
+ .string()
1289
+ .optional()
1290
+ .describe("Filter by creator role (e.g. 'sigma', 'pi')"),
1291
+ limit: z
1292
+ .number()
1293
+ .int()
1294
+ .min(1)
1295
+ .max(200)
1296
+ .optional()
1297
+ .describe("Max items to return. Default 20 (envelope-safe). Cap 200."),
1298
+ fields: z
1299
+ .enum(["lite", "full"])
1300
+ .optional()
1301
+ .describe("'lite' returns compact payload (less tokens), 'full' is default."),
1302
+ cursor: z
1303
+ .string()
1304
+ .optional()
1305
+ .describe("S3.3 B8 — opaque pagination cursor from a prior call's `nextCursor`."),
1306
+ }, {
1307
+ readOnlyHint: true,
1308
+ openWorldHint: false,
1309
+ destructiveHint: false,
1310
+ title: "List episodes",
1311
+ }, async ({ namespace, createdBy, limit, fields, cursor }) => {
1312
+ try {
1313
+ const nsDenied = guardRead(namespace);
1314
+ if (nsDenied)
1315
+ return nsDenied;
1316
+ let backendCursor;
1317
+ if (cursor !== undefined && cursor !== "") {
1318
+ try {
1319
+ const decoded = decodeCursor(cursor);
1320
+ if (decoded && "backendCursor" in decoded) {
1321
+ backendCursor = decoded.backendCursor;
1322
+ }
1323
+ }
1324
+ catch (err) {
1325
+ return mcpError(err?.message ?? "invalid cursor");
1326
+ }
1327
+ }
1328
+ const effectiveLimit = limit === undefined ? undefined : clampLimit(limit);
1329
+ const queryArgs = {
1330
+ namespace,
1331
+ type: "episode",
1332
+ createdBy,
1333
+ limit: effectiveLimit ?? 20,
1334
+ fields: fields ?? "lite",
1335
+ };
1336
+ if (backendCursor !== undefined) {
1337
+ queryArgs.paginationOpts = {
1338
+ numItems: effectiveLimit ?? 50,
1339
+ cursor: backendCursor,
1340
+ };
1341
+ }
1342
+ const memories = await convex.query("memories:listMemories", queryArgs);
1343
+ const rawList = Array.isArray(memories)
1344
+ ? memories
1345
+ : Array.isArray(memories?.page)
1346
+ ? memories.page
1347
+ : [];
1348
+ const filteredList = scopeFilterList(oauthCtx, rawList);
1349
+ const filteredEnvelope = Array.isArray(memories)
1350
+ ? filteredList
1351
+ : { ...memories, page: filteredList };
1352
+ const text = capListResponseBytes(filteredEnvelope, JSON.stringify(filteredEnvelope, null, 2), "list_episodes");
1353
+ return {
1354
+ content: [{ type: "text", text }],
1355
+ };
1356
+ }
1357
+ catch (error) {
1358
+ return mcpConvexError(error);
1359
+ }
1360
+ });
1361
+ // ── search_episodes_by_keyword ─────────────────────────────────────────────
1362
+ // Day 102 v2.9.0 — episode entity 5-op surface (PR-B).
1363
+ // Thin wrapper on search:textSearch with type='episode' forced.
1364
+ server.tool("search_episodes_by_keyword", "BM25 full-text keyword search restricted to episodes (memories with type='episode'). " +
1365
+ "WHEN: use when search_episodes_by_semantic returns too-broad results and you need an exact phrase or ID inside an episode field. " +
1366
+ "EXAMPLE: search_episodes_by_keyword query='convex deploy schema' namespace='orchestrator/sigma' limit=10.", {
1367
+ query: z.string().describe("Search query text"),
1368
+ namespace: z
1369
+ .string()
1370
+ .optional()
1371
+ .describe("Namespace filter (e.g. 'orchestrator/sigma')"),
1372
+ limit: z
1373
+ .number()
1374
+ .int()
1375
+ .min(1)
1376
+ .max(200)
1377
+ .optional()
1378
+ .describe("Max items to return. Default 20 (envelope-safe). Cap 200."),
1379
+ fields: z
1380
+ .enum(["lite", "full"])
1381
+ .optional()
1382
+ .describe("'lite' returns compact payload (less tokens), 'full' is default."),
1383
+ }, {
1384
+ readOnlyHint: true,
1385
+ openWorldHint: false,
1386
+ destructiveHint: false,
1387
+ title: "Search episodes by keyword (BM25)",
1388
+ }, async ({ query, namespace, limit, fields }) => {
1389
+ try {
1390
+ const nsDenied = guardRead(namespace);
1391
+ if (nsDenied)
1392
+ return nsDenied;
1393
+ const results = await convex.action("search:textSearch", {
1394
+ query,
1395
+ namespace,
1396
+ type: "episode",
1397
+ limit: limit ?? 20,
1398
+ fields: fields ?? "lite",
1399
+ });
1400
+ return {
1401
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
1402
+ };
1403
+ }
1404
+ catch (error) {
1405
+ return mcpConvexError(error);
1406
+ }
1407
+ });
1408
+ // ── search_episodes_by_semantic ────────────────────────────────────────────
1409
+ // Day 102 v2.9.0 — episode entity 5-op surface (PR-B).
1410
+ // Thin wrapper on search:recall with type='episode' forced.
1411
+ server.tool("search_episodes_by_semantic", "Semantic vector search restricted to episodes (memories with type='episode'), ranked by cosine similarity. " +
1412
+ "WHEN: use to recall structured past events by intent — failure modes, lessons, similar contexts. " +
1413
+ "EXAMPLE: search_episodes_by_semantic query='hook false positive blocked publish' namespace='orchestrator/sigma' limit=20.", {
1414
+ query: z
1415
+ .string()
1416
+ .describe("Natural language query to search for relevant episodes"),
1417
+ namespace: z
1418
+ .string()
1419
+ .optional()
1420
+ .describe("Filter to a specific namespace — omit to search all"),
1421
+ limit: z
1422
+ .number()
1423
+ .int()
1424
+ .min(1)
1425
+ .max(200)
1426
+ .optional()
1427
+ .describe("Max items to return. Default 20 (envelope-safe). Cap 200."),
1428
+ fields: z
1429
+ .enum(["lite", "full"])
1430
+ .optional()
1431
+ .describe("'lite' returns compact payload (less tokens), 'full' is default."),
1432
+ }, {
1433
+ readOnlyHint: true,
1434
+ openWorldHint: false,
1435
+ destructiveHint: false,
1436
+ title: "Search episodes by semantic (vector cosine)",
1437
+ }, async ({ query, namespace, limit, fields }) => {
1438
+ try {
1439
+ const nsDenied = guardRead(namespace);
1440
+ if (nsDenied)
1441
+ return nsDenied;
1442
+ const results = await convex.action("search:recall", {
1443
+ query,
1444
+ namespace,
1445
+ type: "episode",
1446
+ limit: limit ?? 20,
1447
+ fields: fields ?? "lite",
1448
+ });
1449
+ return {
1450
+ content: [
1451
+ {
1452
+ type: "text",
1453
+ text: JSON.stringify(results, null, 2),
1454
+ },
1455
+ ],
1456
+ };
1457
+ }
1458
+ catch (error) {
1459
+ return mcpConvexError(error);
1460
+ }
1461
+ });
1130
1462
  // ── get_profile ─────────────────────────────────────────────────────────────
1131
1463
  server.tool("get_profile", "Fetch an orchestrator profile with static identity and dynamic session state fields. " +
1132
1464
  "WHEN: use to check peer status, capabilities, or current task before assigning work. " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vantage-peers-mcp",
3
- "version": "2.7.1",
3
+ "version": "2.9.0",
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",