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 +25 -0
- package/README.md +249 -40
- package/dist/server.js +0 -0
- package/dist/src/auth.js +77 -0
- package/dist/src/tools/kbIngest.d.ts +74 -0
- package/dist/src/tools/kbIngest.js +180 -0
- package/dist/src/tools.js +39 -20
- package/package.json +1 -1
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
|
[](https://www.npmjs.com/package/vantage-peers-mcp)
|
|
4
4
|
[](https://www.npmjs.com/package/vantage-peers-mcp)
|
|
5
5
|
[](https://github.com/vantageos-agency/vantage-peers/blob/main/LICENSE)
|
|
6
|
-
[]()
|
|
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
|
-
|
|
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
|
|
204
|
+
## Tools
|
|
93
205
|
|
|
94
|
-
|
|
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
|
-
###
|
|
98
|
-
|
|
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
|
-
###
|
|
101
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
`
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
###
|
|
195
|
-
`
|
|
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 (
|
|
210
|
-
`search_fix_patterns_by_semantic` (alias `search_fix_patterns`)
|
|
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
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|
515
|
+
- `list_errors` — page through monitored errors
|
|
516
|
+
- `get_error` — fetch a single error event
|
|
342
517
|
|
|
343
|
-
### Deployments & Repos (
|
|
344
|
-
`add_deployment
|
|
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
|
|
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
|
|
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 (
|
|
399
|
-
`create_mandate
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
const
|
|
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
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
-
//
|
|
2016
|
-
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
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