vantage-peers-mcp 2.13.0 → 2.14.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,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ### Added
6
+
7
+ - **B5 KB ingest MCP tools** — `store_document_chunked` and `soft_delete_document` registered via `mcp-server/src/tools/kbIngest.ts`. `store_document_chunked` accepts `storageId` + `mimeType` + `filename` (+ optional `docId`) and proxies to `kb:storeDocumentChunked`; returns `{ docId, chunkCount, storageId }`. `soft_delete_document` accepts `docId` and proxies to `kb:softDeleteDocument`; returns `{ docId, markedCount }`. Both tools require Clerk JWT with `org_id` — no anonymous access. Exported: `STORE_DOCUMENT_CHUNKED_TOOL_DESCRIPTION`, `storeDocumentChunkedArgsSchema`, `SOFT_DELETE_DOCUMENT_TOOL_DESCRIPTION`, `softDeleteDocumentArgsSchema`. Mission `k5779qbxhwrfjmj02t31yvehns8911jp`, task `k17bdmhr2hffhz2t96p65j70nh891wcp`.
8
+
9
+ ## [Unreleased] — B4 RAG namespace team/<orgId> tenant enforcement
10
+
11
+ ### Added
12
+
13
+ - **`team-member` scope profile** in `bearerAuthMiddleware` (layer 2.5): Clerk JWTs carrying an `org_id` claim are now verified against the Clerk JWKS (`CLERK_DOMAIN/.well-known/jwks.json`, cached 10 min) and resolve to `scopeProfile="team-member"` with `namespaceReadPrefixes=["team/<orgId>"]` and `namespaceWritePrefixes=["team/<orgId>"]`. Cross-tenant reads/writes are rejected by the existing `checkNamespaceRead` / `checkNamespaceWrite` predicates before any Convex call.
14
+ - **`convex/memoriesScoped.ts`**: new `listMemoriesScoped` (query) and `storeMemoryScoped` (mutation) Convex functions that enforce `team/<orgId>` namespace isolation at the Convex layer using `ctx.auth.getUserIdentity()`. Clerk callers with `org_A` cannot read or write `team/org_B/*`.
15
+ - **`jose`** (transitive, already present via `@modelcontextprotocol/sdk`) used for JWKS fetch and JWT verification. No new npm dep.
16
+ - **`CLERK_DOMAIN` env var**: override the Clerk instance domain (default `https://sharp-sponge-67.clerk.accounts.dev`).
17
+
18
+ ### Security
19
+
20
+ - Architectural choice: **Option A** (direct Clerk JWT verification in bearer middleware) over Option B (DCR→Clerk join via Convex query). Rationale: Clerk JWTs are self-contained — no extra Convex round-trip needed. Option B would be required only if DCR clients were the sole entry point.
21
+ - Cross-tenant deny preserved for DCR `client-generic` (empty prefixes, unchanged) and unregistered orgs (fail-closed: `AUTH_NAMESPACE_DENIED`).
22
+
23
+ ### Tests
24
+
25
+ - `mcp-server/test/team-namespace-cross-tenant.test.ts` — 16 predicate tests on `checkNamespaceRead/Write/isMasterScope` with team-member, master, and DCR-generic fixtures.
26
+ - `convex/__tests__/auth-namespace-deny.test.ts` — 9 convex-test tests verifying `AUTH_NAMESPACE_DENIED` for cross-tenant access at the Convex layer.
27
+
3
28
  ## [2.12.0] — 2026-06-14
4
29
 
5
30
  ### Changed
package/README.md CHANGED
@@ -3,11 +3,18 @@
3
3
  [![npm version](https://img.shields.io/npm/v/vantage-peers-mcp)](https://www.npmjs.com/package/vantage-peers-mcp)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/vantage-peers-mcp)](https://www.npmjs.com/package/vantage-peers-mcp)
5
5
  [![License: FSL-1.1-Apache-2.0](https://img.shields.io/badge/license-FSL--1.1--Apache--2.0-blue)](https://github.com/vantageos-agency/vantage-peers/blob/main/LICENSE)
6
- [![Tests: 97/97](https://img.shields.io/badge/MCP_tools-97_registered-green)]()
6
+ [![MCP tools: 116+](https://img.shields.io/badge/MCP_tools-116+-green)]()
7
+
8
+ > **Package:** `vantage-peers-mcp` (plain — NOT `@vantageos/vantage-peers-mcp`)
9
+ > **Current version:** `2.13.1` (Day-114 release — `list_memories` + `list_episodes` silent `items:[]` fix)
10
+ > **License:** FSL-1.1-Apache-2.0
11
+ > **Repo:** https://github.com/vantageos-agency/vantage-peers (full monorepo README at `/README.md`)
12
+ > **Docs:** https://vantagepeers.com/docs
13
+ > **Install:** `npm install -g vantage-peers-mcp` — or `npx vantage-peers-mcp`
7
14
 
8
15
  MCP server for [VantagePeers](https://vantagepeers.com) — shared memory, messaging, and task coordination for AI agent teams.
9
16
 
10
- 97 tools across 18 categories: memory, profiles, tasks, missions, mission templates, messages, diary, briefing notes, search (RAG), issues, fix patterns, error monitoring, deployments, business units, components, mandates, recurring tasks, and session. All tools ship with ChatGPT Apps SDK annotations (`readOnlyHint`, `openWorldHint`, `destructiveHint`) for native UX in ChatGPT custom connectors.
17
+ 116+ tools across 20 categories: memory, episodes, profiles, tasks, missions, mission templates, messages, diary, briefing notes, search (RAG), issues, fix patterns, error monitoring, deployments, business units, components, mandates, recurring tasks, OKF bundles, observability, and session. All tools ship with ChatGPT Apps SDK annotations (`readOnlyHint`, `openWorldHint`, `destructiveHint`) for native UX in ChatGPT custom connectors.
11
18
 
12
19
  ## Quick start
13
20
 
@@ -32,6 +39,97 @@ Day 92 VP MCP quality overhaul (mission `k57a36y8w5t085bqr23dsmvb2d882506`, PR #
32
39
 
33
40
  See `mcp-server/CHANGELOG.md` for the full per-PR list.
34
41
 
42
+ ## Pagination & envelope safety (Day-114 doctrine)
43
+
44
+ Every `list_*` tool in `vantage-peers-mcp` follows a single fleet-canonical pagination contract — the **MCP Tools Standard pagination doctrine v1** (source: `projects/vantage-peers/mcp-tools-standard-doctrine-v1.md` in this repo, VR runbook `mcp-tools-standard-pagination-doctrine` id `kd750j7z7tqre6hxqmfsa8s9ed89erng`).
45
+
46
+ ### Why envelope safety matters
47
+
48
+ Claude Code, Claude.ai web, ChatGPT custom connectors and Codex all enforce a **~60 KB hard cap on tool-response payloads**. A list call that returns 200+ rows of `fields=full` documents will silently truncate, throw, or be rejected by the client runtime. The MCP server defends against this with three layered controls:
49
+
50
+ 1. `limit` default **20**, hard cap **200** (server-side `clampLimit`).
51
+ 2. `fields="lite"` projection — at most 6 fields per row (e.g. `{_id, _creationTime, title, status, ...}`).
52
+ 3. `enforceEnvelopeCap` — soft byte target of 50,000 bytes; halves the page if exceeded.
53
+
54
+ When in doubt: pass `fields="lite"` and a small `limit`.
55
+
56
+ ### Canonical envelope shape
57
+
58
+ Every `list_*` handler returns exactly this envelope and nothing else:
59
+
60
+ ```typescript
61
+ interface ListEnvelope<T> {
62
+ items: T[]; // projected rows (lite or full)
63
+ nextCursor?: string; // present IFF more pages; ABSENT (not null) when done
64
+ }
65
+ ```
66
+
67
+ ### Cursor semantics
68
+
69
+ - `cursor` is an **opaque** base64url-encoded token. Do not parse it. Do not construct it.
70
+ - Pass the value of `nextCursor` from page N as the `cursor` argument on page N+1.
71
+ - When `nextCursor` is **absent** from the response, you have reached the last page — stop iterating.
72
+ - A `cursor` token from a previous deploy may decode-fail silently; treat that as "start over".
73
+
74
+ ### Copy-paste cursor loop (TypeScript)
75
+
76
+ ```ts
77
+ let cursor: string | undefined = undefined;
78
+ do {
79
+ const { items, nextCursor } = await mcp.list_tasks({ cursor, limit: 50 });
80
+ // process items
81
+ cursor = nextCursor;
82
+ } while (cursor);
83
+ ```
84
+
85
+ The same loop works verbatim for every `list_*` tool (`list_memories`, `list_episodes`, `list_missions`, `list_briefing_notes`, `list_bus`, `list_components`, `list_repo_mappings`, `list_messages`, `list_issues`, `list_fix_patterns`, `list_errors`, `list_mandates`, `list_recurring_tasks`, `list_diaries`, `list_peers`, `list_tasks_by_mission`, `list_broadcast_status`).
86
+
87
+ ### Status aliases (read this before flattening `status` to a CSV)
88
+
89
+ `list_tasks`, `list_tasks_by_mission`, and `list_missions` accept `status` as:
90
+
91
+ - a single enum value (`"todo"`),
92
+ - an array (`["todo","in_progress"]`),
93
+ - or an alias (`"open"`, `"active"`, `"all"`).
94
+
95
+ **Never** a CSV string (`"todo,in_progress"`) — no handler parses it; the call will reject with `invalid_union`.
96
+
97
+ ### Day-114 fixes (shipped in v2.13.1)
98
+
99
+ - **CRITICAL — `list_memories` + `list_episodes` were silently returning `items: []` on every call.** Both handlers read `memories?.page` from the Convex `listMemories` paginate-shape `{value, continueCursor, isDone}`. `.page` is `undefined`, so the envelope shipped empty regardless of seeded data — and `nextCursor` was never emitted. **Pre-2.13.1 callers consuming these two tools MUST upgrade**: any logic that branched on "no memories found" was wrong.
100
+ - **Patch:** anti-pattern `memories?.page` replaced with the canonical `memories.value` + `nextCursor` emitted via `encodeCursor({ backendCursor })`. Mirrors the PR-A/B/C/E precedent (shared `mcp-server/src/paging.ts`).
101
+ - **Tests:** new `mcp-server/src/__tests__/list_memories_episodes_pagination.test.ts` (11/11 PASS, seeded-data assertion pattern — `items.length === N` on inserted data, not just wrapper shape).
102
+ - **Reference:** PR #978 (squash `0db28d5`), audit `projects/vantage-peers/mcp-pagination-audit-day114.md`.
103
+ - **2.13.0 → 2.13.1 chore release** — commit `55366f1`.
104
+ - **MCP Tools Standard doctrine v1** published as the cross-fleet `list_*` pagination canonical (PR #980 squash `d09fc5b`). All future MCP servers in the VantageOS fleet (`vantage-registry-mcp`, etc.) mirror this contract.
105
+
106
+ ### Anti-pattern catalogue (banned)
107
+
108
+ The doctrine document enumerates 7 banned patterns. Three relevant to library consumers:
109
+
110
+ - **`memories?.page` shape misread** — read `.value`, not `.page` (Day-114 incident class).
111
+ - **Flat-array return** — every `list_*` MUST wrap rows in `{ items, nextCursor? }`. A bare array breaks pagination chains.
112
+ - **Raw Convex `continueCursor` exposed** — `nextCursor` must always pass through `encodeCursor` for opacity. The Convex cursor format is internal and may change.
113
+
114
+ ## Versioning
115
+
116
+ `vantage-peers-mcp` follows **semver**:
117
+
118
+ - **MAJOR** (e.g. `2.x.x → 3.0.0`) — breaking changes to tool input/output shape, removed tools, or removed args.
119
+ - **MINOR** (e.g. `2.12.x → 2.13.0`) — additive tools, new optional args, new aliases, new annotations.
120
+ - **PATCH** (e.g. `2.13.0 → 2.13.1`) — bug fixes, no-API-change envelope corrections, doc-only releases that ship in the tarball.
121
+
122
+ Current line: **2.x.x**. Full per-version history: [CHANGELOG.md](https://github.com/vantageos-agency/vantage-peers/blob/main/CHANGELOG.md) and the per-package [mcp-server/CHANGELOG.md](https://github.com/vantageos-agency/vantage-peers/blob/main/mcp-server/CHANGELOG.md).
123
+
124
+ ## Cross-links
125
+
126
+ - **npm:** https://www.npmjs.com/package/vantage-peers-mcp
127
+ - **Main repo README:** https://github.com/vantageos-agency/vantage-peers/blob/main/README.md
128
+ - **Docs site:** https://vantagepeers.com/docs
129
+ - **MCP Tools Standard doctrine v1:** https://github.com/vantageos-agency/vantage-peers/blob/main/projects/vantage-peers/mcp-tools-standard-doctrine-v1.md
130
+ - **VR runbook id:** `kd750j7z7tqre6hxqmfsa8s9ed89erng` (`mcp-tools-standard-pagination-doctrine`)
131
+ - **Day-114 audit:** https://github.com/vantageos-agency/vantage-peers/blob/main/projects/vantage-peers/mcp-pagination-audit-day114.md
132
+
35
133
  ## Install
36
134
 
37
135
  ### Option 1: npx (no install)
@@ -81,6 +179,20 @@ VantagePeers ships a built-in OAuth 2.1 authorization server so Claude.ai web ca
81
179
 
82
180
  **Backward compatibility:** the `BEARER_SECRET_MASTER` env var still works unchanged. Claude Code and Claude Desktop users do not need to change anything — static bearer auth remains the default for those clients. OAuth 2.1 DCR is used exclusively when a client initiates the discovery flow (e.g. Claude.ai web).
83
181
 
182
+ ### Bearer auth layers (evaluation order)
183
+
184
+ | Layer | Token type | scopeProfile | Namespace access |
185
+ |-------|-----------|-------------|-----------------|
186
+ | 1 | `BEARER_SECRET_MASTER` static token | `master` | Full — all namespaces |
187
+ | 2 | Admin-provisioned OAuth access token (`oauth_access_tokens` table) | varies (e.g. `marie-iris-rh`) | Per-profile prefix list |
188
+ | 2.5 | **Clerk JWT** (org session, `org_id` claim present) | `team-member` | `team/<orgId>/*` only |
189
+ | 3 | DCR auto-registered client (`oauthTokens` table) | `client-generic` | Deny-by-default (empty prefixes) |
190
+ | 4 | Legacy internal bearer (`mcpTenants` table) | unscoped | Tenant deployment URL routing |
191
+
192
+ **Layer 2.5 (Clerk JWT / `team-member`):** Claude.ai clients that authenticate via the Clerk OIDC flow receive a `scopeProfile="team-member"` context. Their `namespaceReadPrefixes` and `namespaceWritePrefixes` are locked to `["team/<orgId>"]` — cross-tenant access is rejected at the middleware layer before any Convex call is made. The JWKS is fetched from `CLERK_DOMAIN/.well-known/jwks.json` (default: `https://sharp-sponge-67.clerk.accounts.dev`) and cached in-process with a 10-minute TTL.
193
+
194
+ **`CLERK_DOMAIN` env var:** Override the default Clerk domain if you use a custom Clerk instance.
195
+
84
196
  ## Environment variables
85
197
 
86
198
  | Variable | Required | Description |
@@ -89,16 +201,45 @@ VantagePeers ships a built-in OAuth 2.1 authorization server so Claude.ai web ca
89
201
 
90
202
  The server also reads `CONVEX_URL` from `.env.local` in the parent directory if not set via environment.
91
203
 
92
- ## Tools (97)
204
+ ## Tools
93
205
 
94
- ### Memory (6)
95
- `store_memory`, `search_memories_by_semantic` (alias `recall`), `list_memories`, `soft_delete_memory`, `get_memory`, `store_episode`
206
+ The full registered list ships in `mcp-server/src/tools.ts` and is enumerated below by domain. Counts may shift as additive PRs land; the doctrine guarantee is that **every `list_*` tool follows the envelope contract above** and **every tool exports a Zod input schema + ChatGPT Apps SDK annotations**.
96
207
 
97
- ### Profiles (3)
98
- `get_profile`, `update_profile`, `list_peers`
208
+ ### Memory (8)
209
+ - `store_memory` — write a memory to a namespace
210
+ - `search_memories_by_semantic` (alias `recall`) — vector-search memories; VP-Sources doctrine applies
211
+ - `search_memories_by_keyword` (alias `text_search`) — BM25 keyword search over memories
212
+ - `list_memories` — page through memories in a namespace
213
+ - `get_memory` — fetch a single memory by id
214
+ - `soft_delete_memory` — mark a memory deleted (recoverable)
215
+ - `store_episode` — write a structured episode record (multi-turn coherent event)
216
+ - `get_episode` — fetch a single episode by id
217
+
218
+ ### Episodes (3)
219
+ - `list_episodes` — page through episodes by namespace / orchestrator
220
+ - `search_episodes_by_keyword` — BM25 keyword search over episodes
221
+ - `search_episodes_by_semantic` — vector-search episodes
99
222
 
100
- ### Tasks (11)
101
- `create_task`, `list_tasks`, `list_tasks_by_mission`, `update_task`, `start_task`, `complete_task`, `checkout_task`, `delete_task`, `block_task`, `add_task_dependency`, `bulk_complete_tasks`
223
+ ### Profiles (3)
224
+ - `get_profile` — read an orchestrator profile
225
+ - `update_profile` — mutate an orchestrator profile (master-gated)
226
+ - `list_peers` — page through registered peers
227
+
228
+ ### Tasks (14)
229
+ - `create_task` — create a new task with VERIFICATION + TESTS blocks
230
+ - `list_tasks` — page through tasks with filters + `excludeAutoGenerated`
231
+ - `list_tasks_by_mission` — page through tasks for a single mission
232
+ - `get_task` — fetch a single task by id
233
+ - `update_task` — patch task fields
234
+ - `start_task` — transition to `in_progress`
235
+ - `complete_task` — close with evidence-bound `completionNote`
236
+ - `checkout_task` — claim a task without starting
237
+ - `delete_task` — destructive delete (master-gated; prefer `complete_task`)
238
+ - `block_task` — mark blocked with reason
239
+ - `add_task_dependency` (alias `create_task_dependency`) — add a predecessor
240
+ - `bulk_complete_tasks` — dry-run-default bulk close (cron-spam cleanup)
241
+ - `validate_task_payload` — client-side payload validation
242
+ - `search_tasks_by_keyword` — BM25 keyword search over tasks
102
243
 
103
244
  #### `list_tasks` — args schema + `excludeAutoGenerated` filter (PR-E)
104
245
 
@@ -180,19 +321,39 @@ Returns `{ count, sampleIds, bulkRunId, executedAt? }`:
180
321
  - `dryRun=false` — `{ count, sampleIds, bulkRunId, executedAt }` — `bulkRunId` is the Day-76 evidence token; `executedAt` is the mutation epoch ms.
181
322
 
182
323
  ### Missions (6)
183
- `create_mission`, `list_missions`, `update_mission`, `update_mission_status`, `get_mission_template`, `get_mission`
184
-
185
- ### Mission Templates (1)
186
- `update_mission_template`
187
-
188
- ### Messages (6)
189
- `send_message`, `check_messages`, `mark_as_read`, `list_messages`, `delete_message`, `list_broadcast_status`
190
-
191
- ### Diary (3)
192
- `write_diary`, `get_diary`, `list_diaries`
193
-
194
- ### Briefing Notes (2)
195
- `create_briefing_note`, `list_briefing_notes`
324
+ - `create_mission` — create a mission with `agents` + `createdBy` + `project` (all required)
325
+ - `list_missions` — page through missions; accepts `status` array OR alias
326
+ - `get_mission` — fetch a single mission by id
327
+ - `update_mission` — patch mission fields
328
+ - `update_mission_status` — transition mission state
329
+ - `get_mission_template` — read a mission template
330
+
331
+ ### Mission Templates (2)
332
+ - `update_mission_template` — patch a mission template
333
+ - `instantiate_template_into_mission` — bootstrap a mission from a template
334
+
335
+ ### Messages (8)
336
+ - `send_message` — send to `channel=` (NEVER `recipient=`); see schema via `ToolSearch`
337
+ - `check_messages` — pull inbox for a recipient
338
+ - `mark_as_read` — ack messages by `receiptIds`
339
+ - `list_messages` — page through messages with filters
340
+ - `delete_message` — destructive delete (master-gated)
341
+ - `list_broadcast_status` — fan-out status for a broadcast envelope
342
+ - `get_message` — fetch a single message by id
343
+ - `search_messages_by_keyword` — BM25 keyword search over messages
344
+
345
+ ### Diary (4)
346
+ - `write_diary` (alias `create_diary`) — append a diary entry
347
+ - `get_diary` — fetch a single diary entry
348
+ - `list_diaries` — page through diary entries
349
+ - `update_summary` (alias of `set_summary`) — update session summary
350
+
351
+ ### Briefing Notes (5)
352
+ - `create_briefing_note` — write a structured briefing note
353
+ - `update_briefing_note` — patch a briefing note
354
+ - `list_briefing_notes` — page through briefing notes; VP-Sources doctrine applies
355
+ - `get_briefing_note` — fetch a single briefing note
356
+ - `search_briefing_notes_by_keyword` — BM25 keyword search; VP-Sources doctrine applies
196
357
 
197
358
  #### `list_briefing_notes` — VP-Sources doctrine (PR-H)
198
359
 
@@ -206,8 +367,11 @@ Exports `SEARCH_BRIEFING_NOTES_BY_KEYWORD_TOOL_DESCRIPTION` from `mcp-server/src
206
367
 
207
368
  Same two advisory VP-Sources doctrine paragraphs appended after the existing description (identical strings, see `recall` in Search / RAG above).
208
369
 
209
- ### Search / RAG (3)
210
- `search_fix_patterns_by_semantic` (alias `search_fix_patterns`), `search_memories_by_keyword` (alias `text_search`), `hybrid_search`
370
+ ### Search / RAG (4)
371
+ - `search_fix_patterns_by_semantic` (alias `search_fix_patterns`) — vector-search fix patterns
372
+ - `search_memories_by_keyword` (alias `text_search`) — BM25 keyword search; VP-Sources doctrine applies
373
+ - `search_components_by_keyword` (alias `search_components`) — keyword search over components
374
+ - `hybrid_search` — RRF-fused vector + BM25 search; VP-Sources doctrine applies
211
375
 
212
376
  #### `recall` — VP-Sources doctrine (PR-H)
213
377
 
@@ -234,10 +398,20 @@ Exports `HYBRID_SEARCH_TOOL_DESCRIPTION` from `mcp-server/src/tools.ts`.
234
398
  Same two advisory VP-Sources doctrine paragraphs appended after the existing description (identical strings, see `recall` above).
235
399
 
236
400
  ### Issues (6)
237
- `get_issue`, `list_issues`, `update_issue_status`, `verify_issue`, `issue_stats`, `link_commit_to_issue`
238
-
239
- ### Fix Patterns (5)
240
- `create_fix_pattern`, `list_fix_patterns`, `add_fix_attempt`, `validate_fix`, `link_issue_to_pattern`
401
+ - `get_issue` — fetch a single issue
402
+ - `list_issues` — page through issues with filters
403
+ - `update_issue_status` — patch issue status
404
+ - `verify_issue` — independently confirm an issue resolution
405
+ - `issue_stats` — aggregate stats by status / orchestrator / project
406
+ - `link_commit_to_issue` — link a commit SHA to an issue
407
+
408
+ ### Fix Patterns (6)
409
+ - `create_fix_pattern` — write a validated fix pattern to the KB
410
+ - `list_fix_patterns` — page through fix patterns
411
+ - `get_fix_pattern` — fetch a single fix pattern
412
+ - `add_fix_attempt` (alias `create_fix_attempt`) — log an attempt against a pattern
413
+ - `validate_fix` (alias `check_fix`) — promote a candidate fix to validated
414
+ - `link_issue_to_pattern` — link a VP issue id to a fix pattern
241
415
 
242
416
  #### `create_fix_pattern`
243
417
  Create a new fix pattern in the knowledge base. Documents a bug symptom, root cause, and optional validated fix. Agents search the KB before fixing to avoid repeating known mistakes.
@@ -338,10 +512,16 @@ Example:
338
512
  ```
339
513
 
340
514
  ### Error Monitoring (2)
341
- `list_errors`, `get_error`
515
+ - `list_errors` — page through monitored errors
516
+ - `get_error` — fetch a single error event
342
517
 
343
- ### Deployments & Repos (5)
344
- `add_deployment`, `remove_deployment`, `list_repo_mappings`, `add_repo_mapping`, `remove_repo_mapping`
518
+ ### Deployments & Repos (6)
519
+ - `add_deployment` (alias `register_deployment`) — register a deployment URL
520
+ - `remove_deployment` (alias `delete_deployment`) — deregister a deployment
521
+ - `list_repo_mappings` — page through orchestrator ↔ repo mappings
522
+ - `add_repo_mapping` (alias `register_repo_mapping`) — register a repo mapping
523
+ - `remove_repo_mapping` (alias `delete_repo_mapping`) — deregister a repo mapping
524
+ - `get_repo_mapping` — fetch a single repo mapping
345
525
 
346
526
  #### `list_repo_mappings` — args schema + defaults (PR-C)
347
527
 
@@ -358,7 +538,11 @@ list_repo_mappings(limit?, cursor?, fields?)
358
538
  Returns `{ items: RepoMapping[], nextCursor: string | null }`. `nextCursor` is `null` on the last page.
359
539
 
360
540
  ### Business Units (5)
361
- `create_bu`, `list_bus`, `get_bu`, `update_bu`, `delete_bu`
541
+ - `create_bu` — create a business unit
542
+ - `list_bus` — page through business units; filter by `orchestratorId` / `status`
543
+ - `get_bu` — fetch a single BU by id
544
+ - `update_bu` — patch BU fields
545
+ - `delete_bu` — destructive delete (master-gated)
362
546
 
363
547
  #### `list_bus` — args schema + defaults (PR-A)
364
548
 
@@ -377,7 +561,12 @@ list_bus(orchestratorId?, status?, limit?, cursor?, fields?)
377
561
  Returns `{ items: BusinessUnit[], nextCursor: string | null }`. `nextCursor` is `null` on the last page.
378
562
 
379
563
  ### Components (6)
380
- `register_component`, `list_components`, `get_component`, `update_component`, `delete_component`, `search_components_by_keyword` (alias `search_components`)
564
+ - `register_component` — register an agent / skill / hook / plugin
565
+ - `list_components` — page through components; filter by `type` / `team`
566
+ - `get_component` — fetch a single component
567
+ - `update_component` — patch component fields
568
+ - `delete_component` — destructive delete (master-gated)
569
+ - `search_components_by_keyword` (alias `search_components`) — keyword search
381
570
 
382
571
  #### `list_components` — args schema + defaults (PR-B)
383
572
 
@@ -395,17 +584,37 @@ list_components(type?, team?, limit?, cursor?, fields?)
395
584
 
396
585
  Returns `{ items: Component[], nextCursor: string | null }`. `nextCursor` is `null` on the last page.
397
586
 
398
- ### Mandates (6)
399
- `create_mandate`, `list_mandates`, `accept_mandate`, `update_mandate`, `validate_mandate_spending`, `settle_mandate`
400
-
401
- ### Recurring Tasks (6)
402
- `create_recurring_task`, `list_recurring_tasks`, `pause_recurring_task`, `resume_recurring_task`, `delete_recurring_task`, `update_recurring_task`
587
+ ### Mandates (7)
588
+ - `create_mandate` — create a delegated-spend mandate
589
+ - `list_mandates` — page through mandates
590
+ - `get_mandate` — fetch a single mandate
591
+ - `accept_mandate` — counterparty acceptance
592
+ - `update_mandate` — patch mandate fields
593
+ - `validate_mandate_spending` (alias `check_mandate_spending`) — verify spend is within cap
594
+ - `settle_mandate` — close a mandate with settlement note
595
+
596
+ ### Recurring Tasks (7)
597
+ - `create_recurring_task` — create a recurring task spec
598
+ - `list_recurring_tasks` — page through recurring tasks
599
+ - `get_recurring_task` — fetch a single recurring task
600
+ - `pause_recurring_task` — pause without deletion
601
+ - `resume_recurring_task` — resume a paused recurring task
602
+ - `update_recurring_task` — patch fields
603
+ - `delete_recurring_task` — destructive delete
604
+
605
+ ### OKF Bundles (3)
606
+ - `validate_okf_bundle` — read-only bundle validation (RFC §3.5)
607
+ - `import_okf_bundle` — dry-run / merge / replace import with idempotency key
608
+ - `export_okf_bundle` — export a namespace bundle (multi-tenant; fail-closed identity guard)
609
+
610
+ ### Identity (1)
611
+ - `whoami` — returns `suggested_orchestrator_id`, `scope_profile`, `namespace_read_prefixes` for skill auto-resolution
403
612
 
404
613
  ### Session (1)
405
- `set_summary`
614
+ - `set_summary` (alias `update_summary`) — write the session summary
406
615
 
407
616
  ### Observability (1)
408
- `improvisation_digest`
617
+ - `improvisation_digest` — weekly advisory scan for fleet-state claims missing VP-Sources footers
409
618
 
410
619
  #### `improvisation_digest` — weekly advisory digest (PR-I)
411
620
 
package/dist/server.js CHANGED
File without changes
package/dist/src/auth.js CHANGED
@@ -19,6 +19,7 @@
19
19
  */
20
20
  import { validateMasterBearer } from "@vantageos/cloud-identity";
21
21
  import { ConvexHttpClient } from "convex/browser";
22
+ import { createRemoteJWKSet, jwtVerify } from "jose";
22
23
  // ─────────────────────────────────────────────────────────────────────────────
23
24
  // Internal Convex client (reads mcpTenants + oauth_* tables)
24
25
  // ─────────────────────────────────────────────────────────────────────────────
@@ -155,6 +156,52 @@ export function checkNamespaceWrite(ctx, namespace) {
155
156
  return `Forbidden: namespace='${namespace}' is not writable by scope_profile=${ctx.scopeProfile}.`;
156
157
  }
157
158
  // ─────────────────────────────────────────────────────────────────────────────
159
+ // Clerk JWT verification — JWKS cache with 10-min TTL
160
+ //
161
+ // Architectural choice: Option A (direct Clerk JWT verification here) over
162
+ // Option B (DCR→Clerk join via Convex query). Rationale: Clerk JWTs are
163
+ // self-contained — no extra Convex round-trip needed. Option B would only be
164
+ // required if DCR clients were the sole entry point, which they are not.
165
+ // ─────────────────────────────────────────────────────────────────────────────
166
+ const CLERK_DOMAIN = process.env.CLERK_DOMAIN ?? "https://sharp-sponge-67.clerk.accounts.dev";
167
+ const CLERK_JWKS_URL = `${CLERK_DOMAIN}/.well-known/jwks.json`;
168
+ // Lazy singleton — createRemoteJWKSet caches JWKS in-process (10-min TTL).
169
+ let _clerkJwks = null;
170
+ function clerkJwks() {
171
+ _clerkJwks ??= createRemoteJWKSet(new URL(CLERK_JWKS_URL), {
172
+ cacheMaxAge: 10 * 60 * 1000,
173
+ });
174
+ return _clerkJwks;
175
+ }
176
+ /**
177
+ * Attempts to verify `token` as a Clerk JWT.
178
+ * Returns the relevant claims on success, or null if the token is not a Clerk
179
+ * JWT (wrong issuer, bad signature, expired, missing org_id).
180
+ * Never throws — failures are treated as "not a Clerk token, try next layer".
181
+ */
182
+ async function tryVerifyClerkJwt(token) {
183
+ try {
184
+ const { payload } = await jwtVerify(token, clerkJwks(), {
185
+ issuer: CLERK_DOMAIN,
186
+ });
187
+ // Org-session JWTs carry org_id; personal-session JWTs do not.
188
+ const orgId = payload.org_id;
189
+ if (!orgId)
190
+ return null;
191
+ const sub = payload.sub;
192
+ if (!sub)
193
+ return null;
194
+ const exp = payload.exp;
195
+ if (!exp)
196
+ return null;
197
+ return { sub, org_id: orgId, exp };
198
+ }
199
+ catch {
200
+ // Not a valid Clerk JWT — fall through to next auth layer
201
+ return null;
202
+ }
203
+ }
204
+ // ─────────────────────────────────────────────────────────────────────────────
158
205
  // Auth middleware
159
206
  // ─────────────────────────────────────────────────────────────────────────────
160
207
  export function bearerAuthMiddleware() {
@@ -254,6 +301,36 @@ export function bearerAuthMiddleware() {
254
301
  await next();
255
302
  return;
256
303
  }
304
+ // ── (2.5) Clerk JWT — team/<orgId> scoped access ────────────────────────
305
+ // Verify against Clerk JWKS. On success, extract org_id and set
306
+ // scopeProfile="team-member" with namespace prefixes locked to team/<orgId>.
307
+ // Falls through silently if the token is not a valid Clerk JWT.
308
+ const clerkResult = await tryVerifyClerkJwt(token);
309
+ if (clerkResult !== null) {
310
+ const internalUrl = process.env.CONVEX_URL_INTERNAL;
311
+ if (!internalUrl) {
312
+ console.error("[auth] CONVEX_URL_INTERNAL not set — cannot route Clerk JWT");
313
+ return c.json({ error: "Server misconfigured: internal deployment URL missing" }, 500);
314
+ }
315
+ const orgId = clerkResult.org_id;
316
+ c.set("tenant", {
317
+ tenantName: `clerk:${orgId}`,
318
+ convexUrl: internalUrl,
319
+ });
320
+ c.set("oauthContext", {
321
+ clientId: `dcr-clerk-${orgId}`,
322
+ userId: clerkResult.sub,
323
+ scopes: ["mcp:full"],
324
+ scopeProfile: "team-member",
325
+ fromAllowList: [],
326
+ namespaceReadPrefixes: [`team/${orgId}`],
327
+ namespaceWritePrefixes: [`team/${orgId}`],
328
+ expiresAt: clerkResult.exp * 1000,
329
+ isMaster: false,
330
+ });
331
+ await next();
332
+ return;
333
+ }
257
334
  // ── (3) DCR OAuth token — check oauthTokens via oauthDcr:validateAccessToken
258
335
  // Uses raw token (not hashed) — the DCR table stores tokens in plaintext.
259
336
  // This path handles Claude.ai clients registered via POST /register.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * MCP tools: store_document_chunked + soft_delete_document (B5 — KB ingest).
3
+ *
4
+ * Thin proxies around the Convex `kb:storeDocumentChunked` and
5
+ * `kb:softDeleteDocument` actions. Exposes the B5 Knowledge Base ingest
6
+ * pipeline to any MCP client (Claude.ai, ChatGPT, Claude Code, Codex, IDE…).
7
+ *
8
+ * store_document_chunked:
9
+ * - Accepts a Convex storage ID (blob already uploaded) + mimeType + filename.
10
+ * - Server-side: text extraction → paragraph-aware chunking (~512 tok/chunk)
11
+ * → inserts chunks as memories at namespace team/<orgId>/<docId>.
12
+ * - Requires Clerk JWT with org_id claim. No-org bearers are rejected.
13
+ * - Returns { docId, chunkCount, storageId }.
14
+ *
15
+ * soft_delete_document:
16
+ * - Marks all isLatest=true chunks for docId as isLatest=false.
17
+ * - Soft-delete only — chunks remain in the DB for audit; recall excludes them.
18
+ * - Returns { docId, markedCount }.
19
+ *
20
+ * **VantagePeers Cloud, multi-tenant** — NOT Self-host.
21
+ *
22
+ * mimeType support matrix:
23
+ * application/pdf → pdf-parse extraction (stub if extraction unavailable)
24
+ * text/markdown → raw UTF-8 decode
25
+ * text/plain → raw UTF-8 decode
26
+ *
27
+ * Mission: k5779qbxhwrfjmj02t31yvehns8911jp (VP Cloud Dashboard OKF Phase 2).
28
+ * Task: k17bdmhr2hffhz2t96p65j70nh891wcp (B5 KB ingest).
29
+ * B4 dep: PR #915 squash 64ca2ba (Clerk JWT layer 2.5 live in prod).
30
+ *
31
+ * Orchestrator: Sigma — VantagePeers | 2026-06-27
32
+ */
33
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
34
+ import type { ConvexHttpClient } from "convex/browser";
35
+ import { z } from "zod";
36
+ import type { OAuthContext } from "../auth.js";
37
+ export interface StoreDocumentChunkedResult {
38
+ docId: string;
39
+ chunkCount: number;
40
+ storageId: string;
41
+ }
42
+ export interface SoftDeleteDocumentResult {
43
+ docId: string;
44
+ markedCount: number;
45
+ }
46
+ export declare const STORE_DOCUMENT_CHUNKED_TOOL_DESCRIPTION: string;
47
+ export declare const storeDocumentChunkedArgsSchema: z.ZodObject<{
48
+ storageId: z.ZodString;
49
+ mimeType: z.ZodEnum<{
50
+ "application/pdf": "application/pdf";
51
+ "text/markdown": "text/markdown";
52
+ "text/plain": "text/plain";
53
+ }>;
54
+ filename: z.ZodString;
55
+ docId: z.ZodOptional<z.ZodString>;
56
+ }, z.core.$strip>;
57
+ export declare const SOFT_DELETE_DOCUMENT_TOOL_DESCRIPTION: string;
58
+ export declare const softDeleteDocumentArgsSchema: z.ZodObject<{
59
+ docId: z.ZodString;
60
+ }, z.core.$strip>;
61
+ /**
62
+ * Register store_document_chunked + soft_delete_document MCP tools.
63
+ *
64
+ * oauthCtx must be provided for tenant-scoped callers. The Clerk JWT layer 2.5
65
+ * (auth.ts:443-444) mints oauthCtx.namespaceWritePrefixes = ["team/<orgId>"].
66
+ * We extract orgId + namespace from that prefix and pass them as explicit args
67
+ * to the Convex action — NO ctx.auth call inside the action.
68
+ *
69
+ * Why: ConvexHttpClient (server-http.ts:1437) is constructed without setAuth,
70
+ * so ctx.auth.getUserIdentity() is always null over HTTP. The previous
71
+ * resolveOrgIdStrict pattern was green-in-test (convex-test withIdentity) but
72
+ * dead-in-production. This aligns with the B4 #915 oauthCtx→args pattern.
73
+ */
74
+ export declare function registerKbIngestTools(server: McpServer, convex: ConvexHttpClient, oauthCtx: OAuthContext | undefined): void;
@@ -0,0 +1,180 @@
1
+ /**
2
+ * MCP tools: store_document_chunked + soft_delete_document (B5 — KB ingest).
3
+ *
4
+ * Thin proxies around the Convex `kb:storeDocumentChunked` and
5
+ * `kb:softDeleteDocument` actions. Exposes the B5 Knowledge Base ingest
6
+ * pipeline to any MCP client (Claude.ai, ChatGPT, Claude Code, Codex, IDE…).
7
+ *
8
+ * store_document_chunked:
9
+ * - Accepts a Convex storage ID (blob already uploaded) + mimeType + filename.
10
+ * - Server-side: text extraction → paragraph-aware chunking (~512 tok/chunk)
11
+ * → inserts chunks as memories at namespace team/<orgId>/<docId>.
12
+ * - Requires Clerk JWT with org_id claim. No-org bearers are rejected.
13
+ * - Returns { docId, chunkCount, storageId }.
14
+ *
15
+ * soft_delete_document:
16
+ * - Marks all isLatest=true chunks for docId as isLatest=false.
17
+ * - Soft-delete only — chunks remain in the DB for audit; recall excludes them.
18
+ * - Returns { docId, markedCount }.
19
+ *
20
+ * **VantagePeers Cloud, multi-tenant** — NOT Self-host.
21
+ *
22
+ * mimeType support matrix:
23
+ * application/pdf → pdf-parse extraction (stub if extraction unavailable)
24
+ * text/markdown → raw UTF-8 decode
25
+ * text/plain → raw UTF-8 decode
26
+ *
27
+ * Mission: k5779qbxhwrfjmj02t31yvehns8911jp (VP Cloud Dashboard OKF Phase 2).
28
+ * Task: k17bdmhr2hffhz2t96p65j70nh891wcp (B5 KB ingest).
29
+ * B4 dep: PR #915 squash 64ca2ba (Clerk JWT layer 2.5 live in prod).
30
+ *
31
+ * Orchestrator: Sigma — VantagePeers | 2026-06-27
32
+ */
33
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
34
+ import { z } from "zod";
35
+ // ─────────────────────────────────────────────────────────────────────────────
36
+ // Exported Zod schemas (for snapshot/canonical tests per PR-J doctrine)
37
+ // ─────────────────────────────────────────────────────────────────────────────
38
+ export const STORE_DOCUMENT_CHUNKED_TOOL_DESCRIPTION = "Ingest a document binary (PDF, Markdown, plain text) into the Knowledge Base. " +
39
+ "Upload the file to Convex storage first, then call this tool with the storageId. " +
40
+ "Server-side: text extraction → paragraph-aware chunking (~512 tokens/chunk with 50-char overlap) " +
41
+ "→ stores chunks as memories at namespace team/<orgId>/<docId>. " +
42
+ "Requires Clerk JWT with org_id — no-org bearers are rejected. " +
43
+ "Re-ingest with same docId supersedes prior version (isLatest flip, idempotent). " +
44
+ "mimeType support: application/pdf (pdf-parse), text/markdown, text/plain. " +
45
+ "Default limit: 1 doc per call. cap: 1 doc. " +
46
+ "Returns { docId, chunkCount, storageId }. " +
47
+ "EXAMPLE: store_document_chunked storageId='kg2anjqa…' mimeType='text/markdown' filename='spec.md'.";
48
+ export const storeDocumentChunkedArgsSchema = z.object({
49
+ storageId: z
50
+ .string()
51
+ .describe("Convex storage ID (_storage id) of the already-uploaded binary blob. " +
52
+ "Upload the file via generateUploadUrl → POST → get storageId first."),
53
+ mimeType: z
54
+ .enum(["application/pdf", "text/markdown", "text/plain"])
55
+ .describe("MIME type of the document. Drives extraction strategy: " +
56
+ "application/pdf → pdf-parse, text/markdown|text/plain → raw UTF-8."),
57
+ filename: z
58
+ .string()
59
+ .describe("Original filename (e.g. 'spec.md', 'report.pdf'). Stored in chunk metadata."),
60
+ docId: z
61
+ .string()
62
+ .optional()
63
+ .describe("Optional stable document ID. If omitted, a UUID is generated. " +
64
+ "Supplying the same docId on re-ingest supersedes the prior version."),
65
+ });
66
+ export const SOFT_DELETE_DOCUMENT_TOOL_DESCRIPTION = "Soft-delete all Knowledge Base chunks for a document. " +
67
+ "Marks every isLatest=true chunk for docId as isLatest=false — " +
68
+ "chunks remain in the DB for audit but are excluded from recall and search. " +
69
+ "Requires Clerk JWT with org_id (same org that ingested the document). " +
70
+ "Default limit: 1 doc per call. cap: 1 doc. " +
71
+ "Returns { docId, markedCount }. " +
72
+ "EXAMPLE: soft_delete_document docId='abc-123-uuid'.";
73
+ export const softDeleteDocumentArgsSchema = z.object({
74
+ docId: z
75
+ .string()
76
+ .describe("Document ID returned by store_document_chunked. " +
77
+ "All chunks at namespace team/<orgId>/<docId> will be soft-deleted."),
78
+ });
79
+ // ─────────────────────────────────────────────────────────────────────────────
80
+ // Registration
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ /**
83
+ * Register store_document_chunked + soft_delete_document MCP tools.
84
+ *
85
+ * oauthCtx must be provided for tenant-scoped callers. The Clerk JWT layer 2.5
86
+ * (auth.ts:443-444) mints oauthCtx.namespaceWritePrefixes = ["team/<orgId>"].
87
+ * We extract orgId + namespace from that prefix and pass them as explicit args
88
+ * to the Convex action — NO ctx.auth call inside the action.
89
+ *
90
+ * Why: ConvexHttpClient (server-http.ts:1437) is constructed without setAuth,
91
+ * so ctx.auth.getUserIdentity() is always null over HTTP. The previous
92
+ * resolveOrgIdStrict pattern was green-in-test (convex-test withIdentity) but
93
+ * dead-in-production. This aligns with the B4 #915 oauthCtx→args pattern.
94
+ */
95
+ export function registerKbIngestTools(server, convex, oauthCtx) {
96
+ // ── Resolve orgId + namespace prefix from oauthCtx (B4 #915 pattern) ───────
97
+ // Validate here, once, before registering handlers. Both tools share the
98
+ // same org scope for the lifetime of this request.
99
+ const resolveOrgContext = () => {
100
+ if (!oauthCtx || oauthCtx.isMaster) {
101
+ // Master-scope or legacy bearer: no team namespace — KB ingest forbidden.
102
+ throw new McpError(ErrorCode.InvalidRequest, "AUTH_NO_ORG_ID: store_document_chunked requires a Clerk JWT with org_id claim (team-scoped bearer). Master-scope and legacy bearers cannot write to team/* namespace.");
103
+ }
104
+ const prefix = oauthCtx.namespaceWritePrefixes[0];
105
+ if (!prefix || !/^team\/[^/]+$/.test(prefix)) {
106
+ throw new McpError(ErrorCode.InvalidRequest, `AUTH_NO_ORG_ID: oauthCtx.namespaceWritePrefixes[0] = '${prefix ?? ""}' does not match ^team\\/[^/]+$ — cannot derive orgId for KB ingest.`);
107
+ }
108
+ const orgId = prefix.slice("team/".length);
109
+ return { orgId, namespacePrefix: prefix };
110
+ };
111
+ // ── store_document_chunked ──────────────────────────────────────────────────
112
+ server.tool("store_document_chunked", STORE_DOCUMENT_CHUNKED_TOOL_DESCRIPTION, storeDocumentChunkedArgsSchema.shape, {
113
+ readOnlyHint: false,
114
+ openWorldHint: false,
115
+ destructiveHint: false,
116
+ title: "Ingest document into Knowledge Base",
117
+ }, async ({ storageId, mimeType, filename, docId }) => {
118
+ try {
119
+ const { orgId, namespacePrefix } = resolveOrgContext();
120
+ const result = (await convex.action("kb:storeDocumentChunked", {
121
+ storageId,
122
+ mimeType,
123
+ filename,
124
+ docId: docId ?? undefined,
125
+ orgId,
126
+ namespace: namespacePrefix,
127
+ }));
128
+ return {
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: JSON.stringify(result, null, 2),
133
+ },
134
+ ],
135
+ };
136
+ }
137
+ catch (error) {
138
+ if (error instanceof McpError)
139
+ throw error;
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ console.error("[store_document_chunked] action failed", {
142
+ storageId,
143
+ mimeType,
144
+ filename,
145
+ errorMessage: message,
146
+ });
147
+ throw new McpError(ErrorCode.InternalError, message);
148
+ }
149
+ });
150
+ // ── soft_delete_document ───────────────────────────────────────────────────
151
+ server.tool("soft_delete_document", SOFT_DELETE_DOCUMENT_TOOL_DESCRIPTION, softDeleteDocumentArgsSchema.shape, {
152
+ readOnlyHint: false,
153
+ openWorldHint: false,
154
+ destructiveHint: false,
155
+ title: "Soft-delete Knowledge Base document",
156
+ }, async ({ docId }) => {
157
+ try {
158
+ const { orgId, namespacePrefix } = resolveOrgContext();
159
+ const result = (await convex.action("kb:softDeleteDocument", { docId, orgId, namespace: namespacePrefix }));
160
+ return {
161
+ content: [
162
+ {
163
+ type: "text",
164
+ text: JSON.stringify(result, null, 2),
165
+ },
166
+ ],
167
+ };
168
+ }
169
+ catch (error) {
170
+ if (error instanceof McpError)
171
+ throw error;
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ console.error("[soft_delete_document] action failed", {
174
+ docId,
175
+ errorMessage: message,
176
+ });
177
+ throw new McpError(ErrorCode.InternalError, message);
178
+ }
179
+ });
180
+ }
package/dist/src/tools.js CHANGED
@@ -16,6 +16,7 @@ import { normalizeOrchestratorId } from "./normalizeOrchestratorId.js";
16
16
  import { clampLimit, decodeCursor, encodeCursor } from "./paging.js";
17
17
  import { registerExportOkfBundle } from "./tools/exportOkfBundle.js";
18
18
  import { registerImportOkfBundle } from "./tools/importOkfBundle.js";
19
+ import { registerKbIngestTools } from "./tools/kbIngest.js";
19
20
  import { registerValidateOkfBundle } from "./tools/validateOkfBundle.js";
20
21
  import { wrapToolResult } from "./ui-resources/stream-marker.js";
21
22
  import { validateTaskPayload } from "./validate-task-payload.js";
@@ -1724,16 +1725,23 @@ export function registerTools(server, convex, oauthCtx) {
1724
1725
  };
1725
1726
  }
1726
1727
  const memories = await convex.query("memories:listMemories", queryArgs);
1727
- const rawList = Array.isArray(memories)
1728
- ? memories
1729
- : Array.isArray(memories?.page)
1730
- ? memories.page
1731
- : [];
1728
+ // S3.3 B8 — extract from Convex paginationOpts shape {value, continueCursor, isDone}
1729
+ // Pre-fix bug: handler read memories?.page (undefined) → rawList = [] always.
1730
+ const rawList = Array.isArray(memories?.value)
1731
+ ? memories.value
1732
+ : [];
1732
1733
  const filteredList = scopeFilterList(oauthCtx, rawList);
1733
- const filteredEnvelope = Array.isArray(memories)
1734
- ? filteredList
1735
- : { ...memories, page: filteredList };
1736
- const text = capListResponseBytes(filteredEnvelope, JSON.stringify(filteredEnvelope, null, 2), "list_episodes");
1734
+ // Encode continueCursor → opaque nextCursor token for the MCP caller.
1735
+ const backendNextCursor = memories?.continueCursor ?? null;
1736
+ const isDone = memories?.isDone ?? true;
1737
+ const nextCursor = !isDone && backendNextCursor !== null
1738
+ ? encodeCursor({ backendCursor: backendNextCursor })
1739
+ : undefined;
1740
+ const envelope = {
1741
+ items: filteredList,
1742
+ ...(nextCursor !== undefined ? { nextCursor } : {}),
1743
+ };
1744
+ const text = capListResponseBytes(filteredList, JSON.stringify(envelope, null, 2), "list_episodes");
1737
1745
  return {
1738
1746
  content: [{ type: "text", text }],
1739
1747
  };
@@ -2002,22 +2010,27 @@ export function registerTools(server, convex, oauthCtx) {
2002
2010
  };
2003
2011
  }
2004
2012
  const memories = await convex.query("memories:listMemories", queryArgs);
2005
- const rawList = Array.isArray(memories)
2006
- ? memories
2007
- : Array.isArray(memories?.page)
2008
- ? memories.page
2009
- : [];
2013
+ // S3.3 B8 — extract from Convex paginationOpts shape {value, continueCursor, isDone}
2014
+ // Pre-fix bug: handler read memories?.page (undefined) → rawList = [] always.
2015
+ const rawList = Array.isArray(memories?.value)
2016
+ ? memories.value
2017
+ : [];
2010
2018
  // S3.1.A Wave A — row-level scope filter on the post-query list.
2011
2019
  // Master + legacy bearer pass through unchanged. Non-master clients
2012
2020
  // see only rows whose createdBy ∈ fromAllowList OR whose namespace
2013
2021
  // matches one of namespaceReadPrefixes (exact or '/' boundary).
2014
2022
  const filteredList = scopeFilterList(oauthCtx, rawList);
2015
- // Preserve the original response shape (array vs {page} envelope)
2016
- // so downstream consumers don't need to special-case Wave A.
2017
- const filteredEnvelope = Array.isArray(memories)
2018
- ? filteredList
2019
- : { ...memories, page: filteredList };
2020
- const baseText = capListResponseBytes(filteredEnvelope, JSON.stringify(filteredEnvelope, null, 2), "list_memories");
2023
+ // Encode continueCursor → opaque nextCursor token for the MCP caller.
2024
+ const backendNextCursor = memories?.continueCursor ?? null;
2025
+ const isDone = memories?.isDone ?? true;
2026
+ const nextCursor = !isDone && backendNextCursor !== null
2027
+ ? encodeCursor({ backendCursor: backendNextCursor })
2028
+ : undefined;
2029
+ const filteredEnvelope = {
2030
+ items: filteredList,
2031
+ ...(nextCursor !== undefined ? { nextCursor } : {}),
2032
+ };
2033
+ const baseText = capListResponseBytes(filteredList, JSON.stringify(filteredEnvelope, null, 2), "list_memories");
2021
2034
  const text = appendMarkerIfEnabled(baseText, () => ({
2022
2035
  kind: "memory-quote",
2023
2036
  items: filteredList.map((m) => ({
@@ -7013,6 +7026,12 @@ export function registerTools(server, convex, oauthCtx) {
7013
7026
  // imports memories+briefings+tasks into target namespace with dedup-by-content.
7014
7027
  // Mission k5779qbxhwrfjmj02t31yvehns8911jp, task k17fja9v7pgnf25yvzkwrj5ch5891bb3.
7015
7028
  registerImportOkfBundle(server, convex);
7029
+ // ── store_document_chunked + soft_delete_document (B5 — KB ingest) ─────────
7030
+ // Thin proxies to convex actions `kb:storeDocumentChunked` and
7031
+ // `kb:softDeleteDocument`. Ingest pipeline: upload binary → text extract →
7032
+ // chunk → store at namespace team/<orgId>/<docId>. Requires Clerk JWT org_id.
7033
+ // Mission k5779qbxhwrfjmj02t31yvehns8911jp, task k17bdmhr2hffhz2t96p65j70nh891wcp.
7034
+ registerKbIngestTools(server, convex, oauthCtx);
7016
7035
  // ── improvisation_digest (PR-I — Bloc A T-GREEN) ──────────────────────────
7017
7036
  // Advisory scan of VP tasks+messages+memories for fleet/state claims without
7018
7037
  // VP-Sources footer (Eta heuristic, Pi-approved Option C).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vantage-peers-mcp",
3
- "version": "2.13.0",
3
+ "version": "2.14.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",