vantage-peers-mcp 2.16.0 → 2.18.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 +30 -2
- package/README.md +30 -28
- package/dist/server-http.d.ts +6 -1
- package/dist/server-http.js +26 -5
- package/dist/server.js +1 -1
- package/dist/src/auth.d.ts +33 -4
- package/dist/src/auth.js +78 -14
- package/dist/src/authenticatedConvexClient.d.ts +60 -0
- package/dist/src/authenticatedConvexClient.js +94 -0
- package/dist/src/fresh-state-guard.d.ts +107 -0
- package/dist/src/fresh-state-guard.js +306 -0
- package/dist/src/list-tasks-gate.d.ts +1 -1
- package/dist/src/list-tasks-gate.js +1 -1
- package/dist/src/paging.d.ts +1 -1
- package/dist/src/registerTool.d.ts +99 -0
- package/dist/src/registerTool.js +104 -0
- package/dist/src/serviceAccountAuth.d.ts +87 -0
- package/dist/src/serviceAccountAuth.js +187 -0
- package/dist/src/state-tokens.d.ts +62 -0
- package/dist/src/state-tokens.js +191 -0
- package/dist/src/tools/exportOkfBundle.d.ts +2 -1
- package/dist/src/tools/exportOkfBundle.js +3 -2
- package/dist/src/tools/importOkfBundle.d.ts +2 -1
- package/dist/src/tools/importOkfBundle.js +4 -3
- package/dist/src/tools/kbIngest.d.ts +1 -1
- package/dist/src/tools/kbIngest.js +12 -3
- package/dist/src/tools/validateOkfBundle.js +5 -1
- package/dist/src/tools.d.ts +36 -14
- package/dist/src/tools.js +957 -705
- package/package.json +5 -2
- package/tool-exposure.json +70 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.18.0] — 2026-08-11
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **Expose only the CORE tool surface (mission `vp-mcp-alias-cleanup-v1`, S8).** A data-driven allowlist (`mcp-server/tool-exposure.json`, `{"core":[...]}`) masks every tool whose `T2_verdict` is not `CORE` in `analysis/vantagepeers/vp-restructuring/vp-by-tool-day158.csv`. Masking ≠ deletion: non-CORE tools stay registered + handler-wired and are `disable()`d so `tools/list` does not advertise them; reverting = removing a line from the data file. Exposed = CORE ∩ registered = **66** tools; masked = **43** (registered 109). The exposure list is derived from the CSV, never typed; a name in the file matching no registered tool makes the server refuse to start, naming it. Applied at the single shared `registerTools` surface, so both stdio and HTTP transports (online clients) inherit the reduced surface. 5 CORE names from the day158 CSV are #1169-removed aliases whose CORE survivors stay exposed (zero capability lost). Tests: `test/tool-exposure.test.ts` 2/2, full suite 1071 passed / 0 failed, tsc 0.
|
|
7
|
+
|
|
8
|
+
## [2.17.0] — 2026-08-11
|
|
9
|
+
|
|
10
|
+
### Removed
|
|
11
|
+
- 14 duplicate alias tools removed (mission `vp-mcp-alias-cleanup-v1`, S2, merged in #1169), keeping the fleet-used survivor of each pair — decided by call-site usage, never the code's `DEPRECATED ALIAS` label. Registered tool count 123→109. Context-token saving 1347 (11.1%) of the tool-list surface. Full list + arbitration in the root `CHANGELOG.md` and `elpi-corp/.../S1-arbitrated-pairs-day159.md`.
|
|
12
|
+
|
|
13
|
+
## [Unreleased] — final refus-total sweep (8 tools) → remaining: 0
|
|
14
|
+
|
|
15
|
+
### Security
|
|
16
|
+
|
|
17
|
+
- **Close the last 8 same-class refus-total instances on the client MCP surface.** `get_profile`, `list_peers`, `list_repo_mappings`, `get_repo_mapping`, `get_message` fed `scopeFilterList`/`scopeFilterGet` rows lacking `createdBy`/`namespace` → named remap of the real ownership field (`profiles.orchestratorId`, `githubRepoMapping.orchestrator`, `messages.from`) → `createdBy` before the filter, synthetic field stripped (the `list_broadcast_status` precedent). `list_issues`, `get_issue`, `issue_stats` have no client-owner field (fleet routing/aggregate data; sibling mutations already master-only) → `defineTool` scope `{ kind: "master" }`, structural removal from the client surface. Per tool RED→GREEN with four independent poles; 37 new tests (4 files) + updated `scope-aware-filter-wave-c2`/`c3` (stale fixtures that asserted the old leak as acceptable, aligned to the merged `list_errors` template). RED without fix: 18 owner-pole failures; full CI-scoped suite 2679/2679 green; tsc + scope-typecheck exit 0. **Final class sweep: `remaining: 0`** — every remaining `scopeFilterList`/`scopeFilterGet` site operates on a table with native `createdBy`/`namespace` or already remaps its non-native owner; `check_messages`/`mark_as_read`/`list_tasks`/`list_missions`/`list_diaries` use dedicated non-`scopeFilterList` gates (out of this defect class). Closes VP `k1759mg282aqy6t7c91gnk10598bn4sv`; completes mission `vp-multitenant-zero-hole-v1`.
|
|
18
|
+
|
|
19
|
+
## [Unreleased] — list_bus / list_mandates / list_errors refus-total close
|
|
20
|
+
|
|
21
|
+
### Security
|
|
22
|
+
|
|
23
|
+
- **Scope `list_bus`/`get_bu`, `list_mandates`/`get_mandate` to their owner; move `list_errors`/`get_error` to master-only.** These six handlers fed `scopeFilterList`/`scopeFilterGet` rows whose shape carried neither `createdBy` nor `namespace`, so the guard refused **every** non-master caller including the owner (the refus-total form measured in the S0 campaign). Per-table remedy, decided by measurement: `businessUnits` is owned by `orchestratorId` → named remap to `createdBy` before the filter (the `list_broadcast_status` precedent); `mandates` has two owners `requestedBy`/`fulfilledBy` → dual remap unioned by `_id` (a single remap would hide one party's own mandates); `errorLogs` has no client owner (fleet-ops monitoring) → its `defineTool` scope becomes `{ kind: "master" }`, a structural removal from the client surface (non-master now gets an explicit Forbidden instead of a silent empty list). Per tool: RED (owner refused) → GREEN with owner/deny/master poles moving independently; 28 new tests + updated `scope-aware-filter-wave-c3`; full CI-scoped suite green, tsc + scope-typecheck exit 0. Class sweep found 8 further same-class instances (`get_profile`, `list_peers`, `list_issues`, `get_issue`, `issue_stats`, `list_repo_mappings`, `get_repo_mapping`, `get_message`) — tracked as a follow-up, out of this PR's named scope. Closes VP `k177617dqg6z5c099p1rdp5rqn8b2rp0`.
|
|
24
|
+
|
|
25
|
+
## [Unreleased] — content-search multi-tenant scope (refus-total close)
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
- **Scope three content-rendering MCP tools to their caller.** `list_messages`, `search_messages_by_keyword`, and `search_tasks_by_keyword` fed `scopeFilterList` rows whose rendered shape carried neither `createdBy` nor `namespace`, so the naive guard wrap would have refused **every** caller including the owner (the refus-total defect PR #1122 deliberately avoided for the two search tools; `list_messages` already exhibited it). Fix: message-shaped tools (`from` field) get a named `from`→`createdBy` remap before `scopeFilterList` with the synthetic field stripped on output (the `list_broadcast_status` precedent); `search_tasks_by_keyword` requests `fields=full` internally, filters against the real `createdBy`, then reprojects to the public `lite` shape (avoids changing the public tool shape). Per-tool RED→GREEN with four independent poles (cross-tenant reproduction, owner-only, deny-only, master-all): RED 8-fail without fix → GREEN 17/17. tsc exit 0. Unremapped-shape audit: all 22 `scopeFilterList` sites checked, only the two message sites needed the remap. Closes VP `k175j2jems5deccegp4p0fy4x98b4ypn` + `k1780azk7n8fdb7bpnx5n91sx18b5vjf`.
|
|
30
|
+
|
|
3
31
|
## 2.14.2 — 2026-06-30
|
|
4
32
|
|
|
5
33
|
- fix(oauth): DCR `/register` rejects empty/missing/malformed `redirect_uris` (RFC 7591 §3.2.2 `invalid_redirect_uri`). Closes zombie-client class — clients with empty `redirectUris` arrays can no longer be created. Defense-in-depth: same guard at Convex `registerPublicClient`. TDD-strict RED-then-GREEN.
|
|
@@ -319,7 +347,7 @@ across audit, docs, hooks, security, and consistency dimensions. 15 PRs merged t
|
|
|
319
347
|
|
|
320
348
|
### Scope-aware filtering
|
|
321
349
|
- `list_tasks` `fromAllowList[]` + case-insensitive matching (PR #654, #661).
|
|
322
|
-
- 3 admin endpoints reinstated for
|
|
350
|
+
- 3 admin endpoints reinstated for Nadia cohort (prior session).
|
|
323
351
|
|
|
324
352
|
### Tenant trio
|
|
325
353
|
- Persistent test tenant trio (alpha/beta/gamma) seeded on prod with bearers, scope_profiles, and seed data for cross-orchestrator E2E.
|
|
@@ -337,7 +365,7 @@ No runtime / API / schema changes. Documentation + metadata only.
|
|
|
337
365
|
|
|
338
366
|
What changed since v2.4.12:
|
|
339
367
|
- `mcp-server/package.json`: author restructured to "VantageOS AI Orchestrator Team" with contributors block (Pi, Laurent Perello, ElPi Corp). Dependency `@vantageos/mosaic@^0.1.2` added for Phase 1 Mosaic groundwork (PR #605, server-side createMosaicResource API ready for Phase 2 primitive swap).
|
|
340
|
-
- `mcp-server/CHANGELOG.md`: version headers simplified to `X.Y.Z — YYYY-MM-DD` (Day N anchors dropped per Laurent verdict 2026-06-02 — dates are self-explanatory, day numbers added noise). Narrative client-name mentions (
|
|
368
|
+
- `mcp-server/CHANGELOG.md`: version headers simplified to `X.Y.Z — YYYY-MM-DD` (Day N anchors dropped per Laurent verdict 2026-06-02 — dates are self-explanatory, day numbers added noise). Narrative client-name mentions (Nadia/<client-org>/Cédric Delport) genericized to "early-access RH cohort" / "self-host incident" per RULE #7 pre-public scrub.
|
|
341
369
|
- Root README rework (PR #611 + PR #610 + PR #616 chain): TL;DR + Mermaid architecture diagram + 5 hero features + 22-features collapsed details + 84-tools 8-groups + Backend: Convex 3-paths + attribution Credits section. README /team 404 hotfix landed in PR #616.
|
|
342
370
|
|
|
343
371
|
Merged PRs in this republish window:
|
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
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
7
|
|
|
8
8
|
> **Package:** `vantage-peers-mcp` (plain — NOT `@vantageos/vantage-peers-mcp`)
|
|
9
|
-
> **Current version:** `2.
|
|
9
|
+
> **Current version:** `2.18.0` (S8 CORE tool-exposure filter release — server now advertises only 66 CORE tools of 109 registered; masking is data-driven (`tool-exposure.json`) and reversible, non-CORE tools stay registered/handler-wired but are not listed)
|
|
10
10
|
> **License:** FSL-1.1-Apache-2.0
|
|
11
11
|
> **Repo:** https://github.com/vantageos-agency/vantage-peers (full monorepo README at `/README.md`)
|
|
12
12
|
> **Docs:** https://vantagepeers.com/docs
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
MCP server for [VantagePeers](https://vantagepeers.com) — shared memory, messaging, and task coordination for AI agent teams.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
109+ 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.
|
|
18
18
|
|
|
19
19
|
## Quick start
|
|
20
20
|
|
|
@@ -94,6 +94,8 @@ The same loop works verbatim for every `list_*` tool (`list_memories`, `list_epi
|
|
|
94
94
|
|
|
95
95
|
**Never** a CSV string (`"todo,in_progress"`) — no handler parses it; the call will reject with `invalid_union`.
|
|
96
96
|
|
|
97
|
+
The task/mission enum includes a terminal **`cancelled`** status for retiring an erroneously-created row. Set it via `update_task` / `update_mission` with `status="cancelled"` + a mandatory `cancelReason` (creator-only). A cancelled row is **excluded** from the `open` and `active` aliases (present only under `"all"`) and is never counted as `done`; an already-`done` task or `complete` mission cannot be cancelled (`CANNOT_CANCEL_DONE`).
|
|
98
|
+
|
|
97
99
|
### Day-114 fixes (shipped in v2.13.1)
|
|
98
100
|
|
|
99
101
|
- **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.
|
|
@@ -184,7 +186,7 @@ VantagePeers ships a built-in OAuth 2.1 authorization server so Claude.ai web ca
|
|
|
184
186
|
| Layer | Token type | scopeProfile | Namespace access |
|
|
185
187
|
|-------|-----------|-------------|-----------------|
|
|
186
188
|
| 1 | `BEARER_SECRET_MASTER` static token | `master` | Full — all namespaces |
|
|
187
|
-
| 2 | Admin-provisioned OAuth access token (`oauth_access_tokens` table) | varies (e.g.
|
|
189
|
+
| 2 | Admin-provisioned OAuth access token (`oauth_access_tokens` table) | varies (e.g. `<client-profile>`) | Per-profile prefix list |
|
|
188
190
|
| 2.5 | **Clerk JWT** (org session, `org_id` claim present) | `team-member` | `team/<orgId>/*` only |
|
|
189
191
|
| 3 | DCR auto-registered client (`oauthTokens` table) | `client-generic` | Deny-by-default (empty prefixes) |
|
|
190
192
|
| 4 | Legacy internal bearer (`mcpTenants` table) | unscoped | Tenant deployment URL routing |
|
|
@@ -207,8 +209,8 @@ The full registered list ships in `mcp-server/src/tools.ts` and is enumerated be
|
|
|
207
209
|
|
|
208
210
|
### Memory (8)
|
|
209
211
|
- `store_memory` — write a memory to a namespace
|
|
210
|
-
- `
|
|
211
|
-
- `
|
|
212
|
+
- `recall` — semantic vector-search over memories; VP-Sources doctrine applies
|
|
213
|
+
- `text_search` — BM25 keyword search over memories
|
|
212
214
|
- `list_memories` — page through memories in a namespace
|
|
213
215
|
- `get_memory` — fetch a single memory by id
|
|
214
216
|
- `soft_delete_memory` — mark a memory deleted (recoverable)
|
|
@@ -230,13 +232,13 @@ The full registered list ships in `mcp-server/src/tools.ts` and is enumerated be
|
|
|
230
232
|
- `list_tasks` — page through tasks with filters + `excludeAutoGenerated`
|
|
231
233
|
- `list_tasks_by_mission` — page through tasks for a single mission
|
|
232
234
|
- `get_task` — fetch a single task by id
|
|
233
|
-
- `update_task` — patch task fields
|
|
235
|
+
- `update_task` — patch task fields (incl. cancel: `status="cancelled"` + `cancelReason`, creator-only)
|
|
234
236
|
- `start_task` — transition to `in_progress`
|
|
235
237
|
- `complete_task` — close with evidence-bound `completionNote`
|
|
236
238
|
- `checkout_task` — claim a task without starting
|
|
237
|
-
- `delete_task` — destructive delete (master-gated;
|
|
239
|
+
- `delete_task` — destructive delete (master-gated, blocked in prod; to retire an erroneous task use `update_task status="cancelled"` + `cancelReason`, not `complete_task`)
|
|
238
240
|
- `block_task` — mark blocked with reason
|
|
239
|
-
- `add_task_dependency`
|
|
241
|
+
- `add_task_dependency` — add a predecessor
|
|
240
242
|
- `bulk_complete_tasks` — dry-run-default bulk close (cron-spam cleanup)
|
|
241
243
|
- `validate_task_payload` — client-side payload validation
|
|
242
244
|
- `search_tasks_by_keyword` — BM25 keyword search over tasks
|
|
@@ -324,7 +326,7 @@ Returns `{ count, sampleIds, bulkRunId, executedAt? }`:
|
|
|
324
326
|
- `create_mission` — create a mission with `agents` + `createdBy` + `project` (all required)
|
|
325
327
|
- `list_missions` — page through missions; accepts `status` array OR alias
|
|
326
328
|
- `get_mission` — fetch a single mission by id
|
|
327
|
-
- `update_mission` — patch mission fields
|
|
329
|
+
- `update_mission` — patch mission fields (incl. cancel: `status="cancelled"` + `cancelReason`, creator-only)
|
|
328
330
|
- `update_mission_status` — transition mission state
|
|
329
331
|
- `get_mission_template` — read a mission template
|
|
330
332
|
|
|
@@ -343,10 +345,10 @@ Returns `{ count, sampleIds, bulkRunId, executedAt? }`:
|
|
|
343
345
|
- `search_messages_by_keyword` — BM25 keyword search over messages
|
|
344
346
|
|
|
345
347
|
### Diary (4)
|
|
346
|
-
- `write_diary`
|
|
348
|
+
- `write_diary` — append a diary entry
|
|
347
349
|
- `get_diary` — fetch a single diary entry
|
|
348
350
|
- `list_diaries` — page through diary entries
|
|
349
|
-
- `
|
|
351
|
+
- `set_summary` — update session summary
|
|
350
352
|
|
|
351
353
|
### Briefing Notes (5)
|
|
352
354
|
- `create_briefing_note` — write a structured briefing note
|
|
@@ -368,9 +370,9 @@ Exports `SEARCH_BRIEFING_NOTES_BY_KEYWORD_TOOL_DESCRIPTION` from `mcp-server/src
|
|
|
368
370
|
Same two advisory VP-Sources doctrine paragraphs appended after the existing description (identical strings, see `recall` in Search / RAG above).
|
|
369
371
|
|
|
370
372
|
### Search / RAG (4)
|
|
371
|
-
- `
|
|
372
|
-
- `
|
|
373
|
-
- `
|
|
373
|
+
- `search_fix_patterns` — semantic vector-search over fix patterns
|
|
374
|
+
- `text_search` — BM25 keyword search over memories; VP-Sources doctrine applies
|
|
375
|
+
- `search_components` — keyword search over components
|
|
374
376
|
- `hybrid_search` — RRF-fused vector + BM25 search; VP-Sources doctrine applies
|
|
375
377
|
|
|
376
378
|
Knowledge Base document upload is a two-step flow (see `docs/cloud/kb-ingest.md`):
|
|
@@ -381,7 +383,7 @@ Ingested documents are then fully retrievable through `recall`, `text_search`, a
|
|
|
381
383
|
|
|
382
384
|
#### `recall` — VP-Sources doctrine (PR-H)
|
|
383
385
|
|
|
384
|
-
|
|
386
|
+
Canonical semantic memory-search tool. Exports `RECALL_TOOL_DESCRIPTION` from `mcp-server/src/tools.ts`.
|
|
385
387
|
|
|
386
388
|
The description now embeds two advisory VP-Sources doctrine paragraphs appended after the existing text:
|
|
387
389
|
|
|
@@ -393,7 +395,7 @@ Doctrine is advisory-only — no hook blocks on absence. Client LLMs read the do
|
|
|
393
395
|
|
|
394
396
|
#### `text_search` — VP-Sources doctrine (PR-H)
|
|
395
397
|
|
|
396
|
-
|
|
398
|
+
Canonical BM25 keyword memory-search tool. Exports `TEXT_SEARCH_TOOL_DESCRIPTION` from `mcp-server/src/tools.ts`.
|
|
397
399
|
|
|
398
400
|
Same two advisory VP-Sources doctrine paragraphs appended after the existing description (identical strings, see `recall` above).
|
|
399
401
|
|
|
@@ -415,8 +417,8 @@ Same two advisory VP-Sources doctrine paragraphs appended after the existing des
|
|
|
415
417
|
- `create_fix_pattern` — write a validated fix pattern to the KB
|
|
416
418
|
- `list_fix_patterns` — page through fix patterns
|
|
417
419
|
- `get_fix_pattern` — fetch a single fix pattern
|
|
418
|
-
- `add_fix_attempt`
|
|
419
|
-
- `validate_fix`
|
|
420
|
+
- `add_fix_attempt` — log an attempt against a pattern
|
|
421
|
+
- `validate_fix` — promote a candidate fix to validated
|
|
420
422
|
- `link_issue_to_pattern` — link a VP issue id to a fix pattern
|
|
421
423
|
|
|
422
424
|
#### `create_fix_pattern`
|
|
@@ -522,11 +524,11 @@ Example:
|
|
|
522
524
|
- `get_error` — fetch a single error event
|
|
523
525
|
|
|
524
526
|
### Deployments & Repos (6)
|
|
525
|
-
- `add_deployment`
|
|
526
|
-
- `remove_deployment`
|
|
527
|
+
- `add_deployment` — register a deployment URL
|
|
528
|
+
- `remove_deployment` — deregister a deployment
|
|
527
529
|
- `list_repo_mappings` — page through orchestrator ↔ repo mappings
|
|
528
|
-
- `add_repo_mapping`
|
|
529
|
-
- `remove_repo_mapping`
|
|
530
|
+
- `add_repo_mapping` — register a repo mapping
|
|
531
|
+
- `remove_repo_mapping` — deregister a repo mapping
|
|
530
532
|
- `get_repo_mapping` — fetch a single repo mapping
|
|
531
533
|
|
|
532
534
|
#### `list_repo_mappings` — args schema + defaults (PR-C)
|
|
@@ -572,7 +574,7 @@ Returns `{ items: BusinessUnit[], nextCursor: string | null }`. `nextCursor` is
|
|
|
572
574
|
- `get_component` — fetch a single component
|
|
573
575
|
- `update_component` — patch component fields
|
|
574
576
|
- `delete_component` — destructive delete (master-gated)
|
|
575
|
-
- `
|
|
577
|
+
- `search_components` — keyword search over components
|
|
576
578
|
|
|
577
579
|
#### `list_components` — args schema + defaults (PR-B)
|
|
578
580
|
|
|
@@ -596,7 +598,7 @@ Returns `{ items: Component[], nextCursor: string | null }`. `nextCursor` is `nu
|
|
|
596
598
|
- `get_mandate` — fetch a single mandate
|
|
597
599
|
- `accept_mandate` — counterparty acceptance
|
|
598
600
|
- `update_mandate` — patch mandate fields
|
|
599
|
-
- `validate_mandate_spending`
|
|
601
|
+
- `validate_mandate_spending` — verify spend is within cap
|
|
600
602
|
- `settle_mandate` — close a mandate with settlement note
|
|
601
603
|
|
|
602
604
|
### Recurring Tasks (7)
|
|
@@ -617,7 +619,7 @@ Returns `{ items: Component[], nextCursor: string | null }`. `nextCursor` is `nu
|
|
|
617
619
|
- `whoami` — returns `suggested_orchestrator_id`, `scope_profile`, `namespace_read_prefixes` for skill auto-resolution
|
|
618
620
|
|
|
619
621
|
### Session (1)
|
|
620
|
-
- `set_summary`
|
|
622
|
+
- `set_summary` — write the session summary
|
|
621
623
|
|
|
622
624
|
### Observability (1)
|
|
623
625
|
- `improvisation_digest` — weekly advisory scan for fleet-state claims missing VP-Sources footers
|
|
@@ -742,13 +744,13 @@ A fix pattern is a validated learning extracted from a resolved bug — symptom,
|
|
|
742
744
|
|
|
743
745
|
The cycle runs as follows:
|
|
744
746
|
|
|
745
|
-
1. **Agent encounters a bug.** Before touching any code, call `
|
|
747
|
+
1. **Agent encounters a bug.** Before touching any code, call `search_fix_patterns` with a plain-language description of the symptom. The KB returns ranked matches using semantic vector search.
|
|
746
748
|
2. **KB hit.** If a validated pattern is returned, apply the known fix directly. Log the reuse via `add_fix_attempt` (`worked: true`) so confidence scores stay current.
|
|
747
749
|
3. **KB miss.** If no pattern matches, the agent fixes the bug manually using standard debugging. Once resolved, the learning is captured immediately via `create_fix_pattern` — symptom, root cause, severity, stack, and the working fix.
|
|
748
750
|
4. **Validation.** After the fix holds in production (or after a second independent confirmation), call `validate_fix` to promote the pattern to validated status. This is the signal that downstream agents can trust the pattern without verification.
|
|
749
751
|
5. **Issue linkage.** Call `link_issue_to_pattern` to attach the VantagePeers issue ID to the pattern. This creates a bidirectional reference: the issue record points to the pattern, and the pattern's `linkedIssueIds` list points back.
|
|
750
752
|
|
|
751
|
-
The four tools that power this cycle are: `create_fix_pattern`, `add_fix_attempt`, `validate_fix`, and `link_issue_to_pattern`. The fifth tool, `
|
|
753
|
+
The four tools that power this cycle are: `create_fix_pattern`, `add_fix_attempt`, `validate_fix`, and `link_issue_to_pattern`. The fifth tool, `search_fix_patterns`, is in the Search / RAG category and is the entry point agents should call first.
|
|
752
754
|
|
|
753
755
|
On the agent side, the `/capitalize-fix` skill and the `inject-fix-patterns` hook automate steps 3-5: the hook fires on task completion events and prompts the orchestrator to capture the learning before closing the task. The cycle is designed to be low-friction — one tool call per step, all via MCP, no `npx convex run` required.
|
|
754
756
|
|
package/dist/server-http.d.ts
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* · master bearer (admin shortcut, scopeProfile=master)
|
|
12
12
|
* · OAuth access_token (scoped, persisted in oauth_access_tokens)
|
|
13
13
|
* · legacy mcpTenants bearer (internal orchestrators on their own deployment)
|
|
14
|
-
* - Per-request ConvexHttpClient pointed at the resolved deployment
|
|
14
|
+
* - Per-request ConvexHttpClient pointed at the resolved deployment, with
|
|
15
|
+
* the CALLER'S identity attached (see selectConvexClientForRequest in
|
|
16
|
+
* src/authenticatedConvexClient.ts) — the caller's own Clerk JWT on the
|
|
17
|
+
* Clerk-team path, the MCP server's service-account identity otherwise.
|
|
18
|
+
* Never a plain, identity-less client (P0 fix 2026-08-07: Convex's
|
|
19
|
+
* withOrgScope fail-closed on null identity, RBAC_DENIED for everyone).
|
|
15
20
|
* - Stateless mode: fresh McpServer + transport per request (no session state)
|
|
16
21
|
*
|
|
17
22
|
* OAuth state (clients, codes, access/refresh tokens, scope profiles) is
|
package/dist/server-http.js
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
* · master bearer (admin shortcut, scopeProfile=master)
|
|
12
12
|
* · OAuth access_token (scoped, persisted in oauth_access_tokens)
|
|
13
13
|
* · legacy mcpTenants bearer (internal orchestrators on their own deployment)
|
|
14
|
-
* - Per-request ConvexHttpClient pointed at the resolved deployment
|
|
14
|
+
* - Per-request ConvexHttpClient pointed at the resolved deployment, with
|
|
15
|
+
* the CALLER'S identity attached (see selectConvexClientForRequest in
|
|
16
|
+
* src/authenticatedConvexClient.ts) — the caller's own Clerk JWT on the
|
|
17
|
+
* Clerk-team path, the MCP server's service-account identity otherwise.
|
|
18
|
+
* Never a plain, identity-less client (P0 fix 2026-08-07: Convex's
|
|
19
|
+
* withOrgScope fail-closed on null identity, RBAC_DENIED for everyone).
|
|
15
20
|
* - Stateless mode: fresh McpServer + transport per request (no session state)
|
|
16
21
|
*
|
|
17
22
|
* OAuth state (clients, codes, access/refresh tokens, scope profiles) is
|
|
@@ -28,10 +33,10 @@ import { readFileSync } from "node:fs";
|
|
|
28
33
|
import { McpServer, ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
29
34
|
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
30
35
|
import { timingSafeEqual } from "@vantageos/cloud-identity";
|
|
31
|
-
import { ConvexHttpClient } from "convex/browser";
|
|
32
36
|
import { Hono } from "hono";
|
|
33
37
|
import { cors } from "hono/cors";
|
|
34
38
|
import { bearerAuthMiddleware, internalClient, masterOnlyMiddleware, sha256Base64Url, sha256Hex, } from "./src/auth.js";
|
|
39
|
+
import { selectConvexClientForRequest } from "./src/authenticatedConvexClient.js";
|
|
35
40
|
import { registerTools } from "./src/tools.js";
|
|
36
41
|
import { listUiResources, readUiResource } from "./src/ui-resources/index.js";
|
|
37
42
|
let pkg;
|
|
@@ -655,13 +660,21 @@ app.get("/health", (c) => c.json({
|
|
|
655
660
|
status: "ok",
|
|
656
661
|
service: "vantage-peers-mcp-http",
|
|
657
662
|
version: pkg.version,
|
|
663
|
+
// Day 145: /health could not discriminate which commit was actually
|
|
664
|
+
// serving traffic during the Railway silent-failure incident (10
|
|
665
|
+
// deploys failed 2026-07-14..2026-07-21 while the old container kept
|
|
666
|
+
// answering). RAILWAY_GIT_COMMIT_SHA is set by Railway's build system
|
|
667
|
+
// at build time — never hand-typed. Fallback is an honest "unknown"
|
|
668
|
+
// string, never a value shaped like a SHA (would be indistinguishable
|
|
669
|
+
// from a real commit and defeat the whole point of this field).
|
|
670
|
+
commit: process.env.RAILWAY_GIT_COMMIT_SHA ?? "unknown",
|
|
658
671
|
transport: "streamable-http",
|
|
659
672
|
oauth: "supported",
|
|
660
673
|
scopes: ["mcp:full"],
|
|
661
674
|
}));
|
|
662
675
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
663
676
|
// Admin endpoints — master token only
|
|
664
|
-
// Used by Pi to provision OAuth clients for external users (
|
|
677
|
+
// Used by Pi to provision OAuth clients for external users (Nadia, VIP).
|
|
665
678
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
666
679
|
const admin = new Hono();
|
|
667
680
|
admin.use("*", masterOnlyMiddleware());
|
|
@@ -1129,8 +1142,16 @@ app.route("/admin", admin);
|
|
|
1129
1142
|
app.all("/mcp", bearerAuthMiddleware(), async (c) => {
|
|
1130
1143
|
const tenant = c.get("tenant");
|
|
1131
1144
|
const oauthCtx = c.get("oauthContext");
|
|
1132
|
-
// Per-request Convex client bound to the resolved deployment
|
|
1133
|
-
|
|
1145
|
+
// Per-request Convex client bound to the resolved deployment, carrying
|
|
1146
|
+
// the IDENTITY matching how this caller authenticated — never a plain,
|
|
1147
|
+
// identity-less client. Fixes the P0 regression where every /mcp call
|
|
1148
|
+
// (fleet AND legitimate external clients) reached Convex with
|
|
1149
|
+
// ctx.auth.getUserIdentity() === null after #1156 removed the
|
|
1150
|
+
// allowNoIdentityMaster fail-open carve-out (which had been the actual
|
|
1151
|
+
// cross-tenant security hole). See selectConvexClientForRequest's doc
|
|
1152
|
+
// comment (authenticatedConvexClient.ts) for the full identity-selection
|
|
1153
|
+
// rationale per auth path.
|
|
1154
|
+
const convex = selectConvexClientForRequest(tenant.convexUrl, oauthCtx);
|
|
1134
1155
|
// Fresh McpServer per request — stateless mode, no session leakage
|
|
1135
1156
|
const server = new McpServer({
|
|
1136
1157
|
name: "vantage-peers",
|
package/dist/server.js
CHANGED
|
@@ -57,7 +57,7 @@ const convexUrl = loadConvexUrl();
|
|
|
57
57
|
const convex = new ConvexHttpClient(convexUrl);
|
|
58
58
|
const server = new McpServer({
|
|
59
59
|
name: "vantage-peers",
|
|
60
|
-
version: "2.
|
|
60
|
+
version: "2.18.0",
|
|
61
61
|
});
|
|
62
62
|
// stdio transport has no OAuth identity → pass oauthCtx=undefined to opt into
|
|
63
63
|
// the legacy bearer / system-scope code path inside registerTools (same path
|
package/dist/src/auth.d.ts
CHANGED
|
@@ -12,12 +12,16 @@
|
|
|
12
12
|
* middleware also sets the tenant to the internal deployment because
|
|
13
13
|
* OAuth tokens always target the VantagePeers core deployment.
|
|
14
14
|
* 3. Legacy bearer — falls through to mcpTenants table lookup (Pi/Tau/Phi
|
|
15
|
-
* internal orchestrators on their own Convex deployments).
|
|
15
|
+
* internal orchestrators on their own Convex deployments). Resolves a
|
|
16
|
+
* deny-by-default oauthContext (scopeProfile="legacy-tenant-generic",
|
|
17
|
+
* empty allowlist/prefixes) — the mcpTenants table has no per-tenant
|
|
18
|
+
* scope config, so this is fail-closed until a tenant is provisioned
|
|
19
|
+
* through the OAuth scoped-token path with explicit prefixes.
|
|
16
20
|
*
|
|
17
21
|
* 401 is returned with a WWW-Authenticate header per RFC 6750 §3 so Claude.ai's
|
|
18
22
|
* OAuth connector can bootstrap discovery.
|
|
19
23
|
*/
|
|
20
|
-
import { ConvexHttpClient } from "convex/browser";
|
|
24
|
+
import type { ConvexHttpClient } from "convex/browser";
|
|
21
25
|
import type { MiddlewareHandler } from "hono";
|
|
22
26
|
export type TenantContext = {
|
|
23
27
|
tenantName: string;
|
|
@@ -34,6 +38,18 @@ export type OAuthContext = {
|
|
|
34
38
|
expiresAt: number;
|
|
35
39
|
/** True when this request came in on the master bearer token (admin path). */
|
|
36
40
|
isMaster: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The raw, already-verified Clerk session JWT presented by the caller —
|
|
43
|
+
* set ONLY on the Clerk-team path (2.5, tryVerifyClerkJwt succeeded).
|
|
44
|
+
* server-http.ts forwards this exact token to Convex via
|
|
45
|
+
* ConvexHttpClient.setAuth() so `ctx.auth.getUserIdentity()` resolves to
|
|
46
|
+
* THIS caller's own org, never the MCP server's service-account identity.
|
|
47
|
+
* Every other path (master / OAuth / DCR / legacy) leaves this undefined
|
|
48
|
+
* and gets the service-account (master) Convex identity instead — see
|
|
49
|
+
* server-http.ts's per-request client selection and the P0 fix note
|
|
50
|
+
* there for why that is fail-closed-safe.
|
|
51
|
+
*/
|
|
52
|
+
clerkJwt?: string;
|
|
37
53
|
};
|
|
38
54
|
declare module "hono" {
|
|
39
55
|
interface ContextVariableMap {
|
|
@@ -52,14 +68,27 @@ export declare function sha256Base64Url(input: string): Promise<string>;
|
|
|
52
68
|
/**
|
|
53
69
|
* Returns true when the scope profile grants full, wildcard access. Master
|
|
54
70
|
* admin sessions skip every downstream enforcement check.
|
|
71
|
+
*
|
|
72
|
+
* Delegates the actual master/wildcard decision to
|
|
73
|
+
* `@vantageos/cloud-identity`'s `isMasterScope` (0.3.0+) via `toPackageOAuthCtx`
|
|
74
|
+
* above — this repo no longer reimplements that check locally. `undefined` is
|
|
75
|
+
* handled here (returns false) because several call sites in tools.ts pass an
|
|
76
|
+
* optional `OAuthContext` (legacy bearer path has no oauthContext at all);
|
|
77
|
+
* the package's own guard requires a non-null `OAuthCtx` and throws otherwise
|
|
78
|
+
* (0.3.0's "never grant by absence" contract), so the undefined check must
|
|
79
|
+
* happen before delegating, not be silently absorbed by the package call.
|
|
55
80
|
*/
|
|
56
81
|
export declare function isMasterScope(ctx: OAuthContext | undefined): boolean;
|
|
57
82
|
/**
|
|
58
83
|
* Checks that `from` is allowed by the current OAuth context.
|
|
59
84
|
* Returns null when allowed, an error message string otherwise.
|
|
60
85
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
86
|
+
* `ctx` is only undefined in tests that call these predicates directly
|
|
87
|
+
* without going through bearerAuthMiddleware — every real auth path
|
|
88
|
+
* (master, OAuth, Clerk, DCR, legacy mcpTenants bearer) sets an oauthContext.
|
|
89
|
+
* The legacy mcpTenants bearer path resolves to a deny-by-default
|
|
90
|
+
* "legacy-tenant-generic" scope (empty allowlist/prefixes) — see auth.ts
|
|
91
|
+
* path (4).
|
|
63
92
|
*/
|
|
64
93
|
export declare function checkFromAllowed(ctx: OAuthContext | undefined, from: string): string | null;
|
|
65
94
|
/**
|
package/dist/src/auth.js
CHANGED
|
@@ -12,14 +12,18 @@
|
|
|
12
12
|
* middleware also sets the tenant to the internal deployment because
|
|
13
13
|
* OAuth tokens always target the VantagePeers core deployment.
|
|
14
14
|
* 3. Legacy bearer — falls through to mcpTenants table lookup (Pi/Tau/Phi
|
|
15
|
-
* internal orchestrators on their own Convex deployments).
|
|
15
|
+
* internal orchestrators on their own Convex deployments). Resolves a
|
|
16
|
+
* deny-by-default oauthContext (scopeProfile="legacy-tenant-generic",
|
|
17
|
+
* empty allowlist/prefixes) — the mcpTenants table has no per-tenant
|
|
18
|
+
* scope config, so this is fail-closed until a tenant is provisioned
|
|
19
|
+
* through the OAuth scoped-token path with explicit prefixes.
|
|
16
20
|
*
|
|
17
21
|
* 401 is returned with a WWW-Authenticate header per RFC 6750 §3 so Claude.ai's
|
|
18
22
|
* OAuth connector can bootstrap discovery.
|
|
19
23
|
*/
|
|
20
|
-
import { validateMasterBearer } from "@vantageos/cloud-identity";
|
|
21
|
-
import { ConvexHttpClient } from "convex/browser";
|
|
24
|
+
import { isMasterScope as packageIsMasterScope, validateMasterBearer, } from "@vantageos/cloud-identity";
|
|
22
25
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
26
|
+
import { createServiceAccountConvexClient } from "./authenticatedConvexClient.js";
|
|
23
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
28
|
// Internal Convex client (reads mcpTenants + oauth_* tables)
|
|
25
29
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -29,7 +33,13 @@ function buildInternalClient() {
|
|
|
29
33
|
throw new Error("CONVEX_URL_INTERNAL is required for HTTP transport. " +
|
|
30
34
|
"Set it to your internal VantagePeers Convex deployment URL.");
|
|
31
35
|
}
|
|
32
|
-
|
|
36
|
+
// Every outgoing call from this client carries the MCP server's
|
|
37
|
+
// service-account Clerk identity (see authenticatedConvexClient.ts /
|
|
38
|
+
// serviceAccountAuth.ts). It never proceeds unauthenticated: if the
|
|
39
|
+
// identity cannot be minted, the call throws instead of silently
|
|
40
|
+
// falling back to an anonymous request that Convex's withOrgScope()
|
|
41
|
+
// would treat as master/unfiltered.
|
|
42
|
+
return createServiceAccountConvexClient(url);
|
|
33
43
|
}
|
|
34
44
|
// Lazily instantiated so the module can be imported without env vars in tests
|
|
35
45
|
let _internalClient = null;
|
|
@@ -72,37 +82,66 @@ export async function sha256Base64Url(input) {
|
|
|
72
82
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
73
83
|
// Scope enforcement helpers — used by MCP tool guards
|
|
74
84
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
85
|
+
/**
|
|
86
|
+
* Adapts this module's local `OAuthContext` shape onto the package's
|
|
87
|
+
* framework-agnostic `OAuthCtx` (`@vantageos/cloud-identity`). This is a
|
|
88
|
+
* field-renaming adapter only — no master/wildcard DECISION is made here,
|
|
89
|
+
* that decision is entirely delegated to the package's `isMasterScope`. Local
|
|
90
|
+
* `OAuthContext` carries an extra `isMaster` boolean (set explicitly by the
|
|
91
|
+
* master-bearer-token middleware branch) alongside `scopeProfile`; the
|
|
92
|
+
* package's `OAuthCtx.scope` field consolidates both prior conditions
|
|
93
|
+
* (`ctx.isMaster || ctx.scopeProfile === "master"`) into the single `scope`
|
|
94
|
+
* field the package checks — the union of conditions is preserved exactly,
|
|
95
|
+
* only the representation changes.
|
|
96
|
+
*/
|
|
97
|
+
function toPackageOAuthCtx(ctx) {
|
|
98
|
+
return {
|
|
99
|
+
scope: ctx.isMaster ? "master" : ctx.scopeProfile,
|
|
100
|
+
fromAllowList: ctx.fromAllowList,
|
|
101
|
+
namespaceReadPrefixes: ctx.namespaceReadPrefixes,
|
|
102
|
+
namespaceWritePrefixes: ctx.namespaceWritePrefixes,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
75
105
|
/**
|
|
76
106
|
* Returns true when the scope profile grants full, wildcard access. Master
|
|
77
107
|
* admin sessions skip every downstream enforcement check.
|
|
108
|
+
*
|
|
109
|
+
* Delegates the actual master/wildcard decision to
|
|
110
|
+
* `@vantageos/cloud-identity`'s `isMasterScope` (0.3.0+) via `toPackageOAuthCtx`
|
|
111
|
+
* above — this repo no longer reimplements that check locally. `undefined` is
|
|
112
|
+
* handled here (returns false) because several call sites in tools.ts pass an
|
|
113
|
+
* optional `OAuthContext` (legacy bearer path has no oauthContext at all);
|
|
114
|
+
* the package's own guard requires a non-null `OAuthCtx` and throws otherwise
|
|
115
|
+
* (0.3.0's "never grant by absence" contract), so the undefined check must
|
|
116
|
+
* happen before delegating, not be silently absorbed by the package call.
|
|
78
117
|
*/
|
|
79
118
|
export function isMasterScope(ctx) {
|
|
80
119
|
if (!ctx)
|
|
81
120
|
return false;
|
|
82
|
-
|
|
83
|
-
return true;
|
|
84
|
-
if (ctx.scopeProfile === "master")
|
|
85
|
-
return true;
|
|
86
|
-
return ctx.fromAllowList.includes("*");
|
|
121
|
+
return packageIsMasterScope(toPackageOAuthCtx(ctx));
|
|
87
122
|
}
|
|
88
123
|
/**
|
|
89
124
|
* Checks that `from` is allowed by the current OAuth context.
|
|
90
125
|
* Returns null when allowed, an error message string otherwise.
|
|
91
126
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
127
|
+
* `ctx` is only undefined in tests that call these predicates directly
|
|
128
|
+
* without going through bearerAuthMiddleware — every real auth path
|
|
129
|
+
* (master, OAuth, Clerk, DCR, legacy mcpTenants bearer) sets an oauthContext.
|
|
130
|
+
* The legacy mcpTenants bearer path resolves to a deny-by-default
|
|
131
|
+
* "legacy-tenant-generic" scope (empty allowlist/prefixes) — see auth.ts
|
|
132
|
+
* path (4).
|
|
94
133
|
*/
|
|
95
134
|
export function checkFromAllowed(ctx, from) {
|
|
96
135
|
if (!ctx)
|
|
97
|
-
return null; //
|
|
136
|
+
return null; // no context (direct predicate call, e.g. unit tests)
|
|
98
137
|
if (isMasterScope(ctx))
|
|
99
138
|
return null;
|
|
100
139
|
if (ctx.fromAllowList.includes(from))
|
|
101
140
|
return null;
|
|
102
141
|
// Day 88 friction capitalize: surface the allowed values so the LLM caller
|
|
103
142
|
// can self-correct on the next attempt instead of guessing identifiers.
|
|
104
|
-
//
|
|
105
|
-
// the actual allowlist was ["
|
|
143
|
+
// Nadia onboarding case (2026-06-01): Claude.ai guessed "Greek letter" when
|
|
144
|
+
// the actual allowlist was ["nadia"].
|
|
106
145
|
const allowed = ctx.fromAllowList.length === 0
|
|
107
146
|
? "(none — this client has no allowed 'from' identities)"
|
|
108
147
|
: ctx.fromAllowList.join(", ");
|
|
@@ -327,6 +366,11 @@ export function bearerAuthMiddleware() {
|
|
|
327
366
|
namespaceWritePrefixes: [`team/${orgId}`],
|
|
328
367
|
expiresAt: clerkResult.exp * 1000,
|
|
329
368
|
isMaster: false,
|
|
369
|
+
// Forward the caller's own verified Clerk JWT to Convex — see
|
|
370
|
+
// OAuthContext.clerkJwt doc comment. This is the P0 fix: without
|
|
371
|
+
// this, server-http.ts had no way to attach any identity to the
|
|
372
|
+
// per-request Convex client for this path.
|
|
373
|
+
clerkJwt: token,
|
|
330
374
|
});
|
|
331
375
|
await next();
|
|
332
376
|
return;
|
|
@@ -411,6 +455,26 @@ export function bearerAuthMiddleware() {
|
|
|
411
455
|
tenantName: tenant.tenantName,
|
|
412
456
|
convexUrl: tenant.convexUrl,
|
|
413
457
|
});
|
|
458
|
+
// SECURITY FIX (k17dt8pq4zkafsvt162z9qzgsn8abs0r): legacy bearer tokens
|
|
459
|
+
// used to leave oauthContext unset, which made every guard in tools.ts
|
|
460
|
+
// (guardRead/guardWrite/guardMasterOnly) and every checkNamespace*/
|
|
461
|
+
// checkFromAllowed predicate here treat the request as unscoped/allowed.
|
|
462
|
+
// A legacy bearer could therefore read/write any namespace and call any
|
|
463
|
+
// master-only tool. The mcpTenants table carries no per-tenant scope
|
|
464
|
+
// config (no namespacePrefixes field), so there is nothing to honor —
|
|
465
|
+
// deny-by-default (empty prefixes/allowlist) is the only defensible
|
|
466
|
+
// scope until tenants are re-provisioned with explicit prefixes.
|
|
467
|
+
c.set("oauthContext", {
|
|
468
|
+
clientId: `legacy:${tenant.tenantName}`,
|
|
469
|
+
userId: `legacy:${tenant.tenantName}`,
|
|
470
|
+
scopes: [],
|
|
471
|
+
scopeProfile: "legacy-tenant-generic",
|
|
472
|
+
fromAllowList: [],
|
|
473
|
+
namespaceReadPrefixes: [],
|
|
474
|
+
namespaceWritePrefixes: [],
|
|
475
|
+
expiresAt: Date.now() + 3600 * 1000,
|
|
476
|
+
isMaster: false,
|
|
477
|
+
});
|
|
414
478
|
// Fire-and-forget lastUsedAt update (non-blocking)
|
|
415
479
|
internalClient()
|
|
416
480
|
// biome-ignore lint/suspicious/noExplicitAny: Convex string API
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConvexHttpClient factory that attaches the MCP server's Clerk
|
|
3
|
+
* service-account identity to every request.
|
|
4
|
+
*
|
|
5
|
+
* ConvexHttpClient.setAuth() takes a static token string (unlike
|
|
6
|
+
* ConvexReactClient, it has no fetchToken callback), so a client that lives
|
|
7
|
+
* across multiple tool calls (the stdio server's single long-lived client,
|
|
8
|
+
* server.ts) would otherwise present a stale/expired JWT after ~60 seconds.
|
|
9
|
+
* This wrapper intercepts .query()/.mutation()/.action() and re-attaches a
|
|
10
|
+
* freshly-minted (or still-cached, non-expired) token immediately before
|
|
11
|
+
* every call, so callers of registerTools() never need to think about token
|
|
12
|
+
* refresh.
|
|
13
|
+
*
|
|
14
|
+
* Fail-closed: if the service-account credential is not configured
|
|
15
|
+
* (getServiceAccountToken returns null) or minting fails for any reason, the
|
|
16
|
+
* call is aborted BEFORE it reaches the wire — it never falls back to
|
|
17
|
+
* .clearAuth() / an unauthenticated request. "I could not authenticate" and
|
|
18
|
+
* "I am the system" must never produce the same outgoing call: the former
|
|
19
|
+
* throws loudly, the latter is simply not a code path this client has.
|
|
20
|
+
*/
|
|
21
|
+
import type { ConvexHttpClient } from "convex/browser";
|
|
22
|
+
/**
|
|
23
|
+
* Selects the per-request Convex client identity to attach to a /mcp call.
|
|
24
|
+
*
|
|
25
|
+
* P0 fix (2026-08-07): before this, server-http.ts's /mcp handler built a
|
|
26
|
+
* PLAIN `new ConvexHttpClient(tenant.convexUrl)` for every request — no
|
|
27
|
+
* identity was ever attached, so `ctx.auth.getUserIdentity()` was always
|
|
28
|
+
* null on the Convex side and convex/lib/auth.ts's withOrgScope fail-closed
|
|
29
|
+
* branch (post-#1156, correctly) rejected every caller, fleet-internal and
|
|
30
|
+
* external alike, with RBAC_DENIED.
|
|
31
|
+
*
|
|
32
|
+
* Two outcomes, selected purely by whether `oauthCtx.clerkJwt` is present
|
|
33
|
+
* (set ONLY by the Clerk-team org-scoped auth path, auth.ts case 2.5):
|
|
34
|
+
*
|
|
35
|
+
* - `clerkJwt` present → forward the CALLER'S OWN verified Clerk JWT via
|
|
36
|
+
* `.setAuth()`. Convex resolves the caller's own org — genuine,
|
|
37
|
+
* Convex-layer multi-tenant isolation, never cross-tenant.
|
|
38
|
+
* - `clerkJwt` absent (master / OAuth-scoped / DCR / legacy mcpTenants) →
|
|
39
|
+
* the MCP server's own Clerk service-account identity
|
|
40
|
+
* (`createServiceAccountConvexClient`). Convex's withOrgScope
|
|
41
|
+
* service-account carve-out (CLERK_SERVICE_ACCOUNT_USER_ID) grants
|
|
42
|
+
* isMaster=true for this identity. Isolation for the non-master-bearer
|
|
43
|
+
* variants among these (OAuth scoped tokens, DCR clients, legacy
|
|
44
|
+
* tenants) is enforced at the MCP tool layer instead
|
|
45
|
+
* (guardRead/guardWrite/checkNamespaceRead/checkNamespaceWrite in
|
|
46
|
+
* tools.ts, run BEFORE any Convex call) — exactly the enforcement layer
|
|
47
|
+
* these paths already relied on before this fix (Convex itself
|
|
48
|
+
* previously granted them master via the now-removed
|
|
49
|
+
* `allowNoIdentityMaster` fail-open carve-out). This fix does not widen
|
|
50
|
+
* access for any of these paths; it restores the master-identity access
|
|
51
|
+
* they already had at the Convex layer while leaving the MCP-layer
|
|
52
|
+
* guard as the (unchanged) isolation boundary for them.
|
|
53
|
+
*/
|
|
54
|
+
export declare function selectConvexClientForRequest(convexUrl: string, oauthCtx: {
|
|
55
|
+
clerkJwt?: string;
|
|
56
|
+
} | undefined, deps?: {
|
|
57
|
+
createServiceAccountClient?: (url: string) => ConvexHttpClient;
|
|
58
|
+
createPlainClient?: (url: string) => ConvexHttpClient;
|
|
59
|
+
}): ConvexHttpClient;
|
|
60
|
+
export declare function createServiceAccountConvexClient(url: string): ConvexHttpClient;
|