vantage-peers-mcp 2.8.0 → 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 +32 -0
- package/README.md +1 -1
- package/dist/server.js +1 -1
- package/dist/src/tools.js +219 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
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
|
+
|
|
3
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)
|
|
4
36
|
|
|
5
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.
|
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.
|
|
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.
|
|
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
|
@@ -1240,6 +1240,225 @@ export function registerTools(server, convex, oauthCtx) {
|
|
|
1240
1240
|
return mcpConvexError(error);
|
|
1241
1241
|
}
|
|
1242
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
|
+
});
|
|
1243
1462
|
// ── get_profile ─────────────────────────────────────────────────────────────
|
|
1244
1463
|
server.tool("get_profile", "Fetch an orchestrator profile with static identity and dynamic session state fields. " +
|
|
1245
1464
|
"WHEN: use to check peer status, capabilities, or current task before assigning work. " +
|
package/package.json
CHANGED