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 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 Marie cohort (prior session).
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 (Marie/Iris RH/Cédric Delport) genericized to "early-access RH cohort" / "self-host incident" per RULE #7 pre-public scrub.
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
  [![npm version](https://img.shields.io/npm/v/vantage-peers-mcp)](https://www.npmjs.com/package/vantage-peers-mcp)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/vantage-peers-mcp)](https://www.npmjs.com/package/vantage-peers-mcp)
5
5
  [![License: FSL-1.1-Apache-2.0](https://img.shields.io/badge/license-FSL--1.1--Apache--2.0-blue)](https://github.com/vantageos-agency/vantage-peers/blob/main/LICENSE)
6
- [![MCP tools: 116+](https://img.shields.io/badge/MCP_tools-116+-green)]()
6
+ [![MCP tools: 109+](https://img.shields.io/badge/MCP_tools-109+-green)]()
7
7
 
8
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)
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
- 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.
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. `marie-iris-rh`) | Per-profile prefix list |
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
- - `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
+ - `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; prefer `complete_task`)
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` (alias `create_task_dependency`) — add a predecessor
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` (alias `create_diary`) — append a diary entry
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
- - `update_summary` (alias of `set_summary`) — update session summary
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
- - `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
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
- Alias of `search_memories_by_semantic`. Exports `RECALL_TOOL_DESCRIPTION` from `mcp-server/src/tools.ts`.
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
- Alias of `search_memories_by_keyword`. Exports `TEXT_SEARCH_TOOL_DESCRIPTION` from `mcp-server/src/tools.ts`.
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` (alias `create_fix_attempt`) — log an attempt against a pattern
419
- - `validate_fix` (alias `check_fix`) — promote a candidate fix to validated
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` (alias `register_deployment`) — register a deployment URL
526
- - `remove_deployment` (alias `delete_deployment`) — deregister a 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` (alias `register_repo_mapping`) — register a repo mapping
529
- - `remove_repo_mapping` (alias `delete_repo_mapping`) — deregister a 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
- - `search_components_by_keyword` (alias `search_components`) — keyword search
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` (alias `check_mandate_spending`) — verify spend is within cap
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` (alias `update_summary`) — write the session 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 `search_fix_patterns_by_semantic` (alias `search_fix_patterns`) with a plain-language description of the symptom. The KB returns ranked matches using semantic vector search.
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, `search_fix_patterns_by_semantic` (alias `search_fix_patterns`), is in the Search / RAG category and is the entry point agents should call first.
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
 
@@ -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
@@ -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 (Marie, VIP).
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
- const convex = new ConvexHttpClient(tenant.convexUrl);
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.12.0",
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
@@ -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
- * If no oauthContext is set (legacy bearer from mcpTenants), all `from` values
62
- * are allowed — legacy path is unscoped.
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
- return new ConvexHttpClient(url);
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
- if (ctx.isMaster)
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
- * If no oauthContext is set (legacy bearer from mcpTenants), all `from` values
93
- * are allowed — legacy path is unscoped.
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; // legacy bearer — unscoped
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
- // Marie onboarding case (2026-06-01): Claude.ai guessed "Greek letter" when
105
- // the actual allowlist was ["marie"].
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;