vantage-peers-mcp 2.4.14 → 2.7.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 +93 -0
- package/README.md +17 -3
- package/dist/server-http.d.ts +1 -0
- package/dist/server-http.js +417 -5
- package/dist/server.js +4 -4
- package/dist/src/auth.js +13 -6
- package/dist/src/list-tasks-gate.d.ts +34 -0
- package/dist/src/list-tasks-gate.js +57 -0
- package/dist/src/normalizeOrchestratorId.d.ts +25 -0
- package/dist/src/normalizeOrchestratorId.js +34 -0
- package/dist/src/paging.d.ts +81 -0
- package/dist/src/paging.js +131 -0
- package/dist/src/tools.d.ts +376 -0
- package/dist/src/tools.js +1907 -312
- package/dist/src/validate-task-payload-re.d.ts +15 -0
- package/dist/src/validate-task-payload-re.js +53 -0
- package/dist/src/validate-task-payload.d.ts +61 -0
- package/dist/src/validate-task-payload.js +355 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,98 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.7.0] — 2026-06-13 — Day 100 get_by_id surface Phase 2b (task k172735brsw6bc3j2dkkkfxqrx88kkjq)
|
|
4
|
+
|
|
5
|
+
Phase 2b wires 2 MCP wrappers calling the Phase 2a Convex queries deployed
|
|
6
|
+
in commit 2ebdaba (PR #735 + hotfix 7f958d0):
|
|
7
|
+
|
|
8
|
+
- **get_message** — `messages:getById` plumbing. Fetch full message row by
|
|
9
|
+
Convex doc ID (channel, sender, sessionDay, tenant scope) for read-receipt
|
|
10
|
+
audit, delete confirmation, or fix-pattern referencing.
|
|
11
|
+
- **get_recurring_task** — `recurringTasks:getById` plumbing. Fetch
|
|
12
|
+
recurring task definition (cron schedule, prompt, assignee, last-fire
|
|
13
|
+
metadata) before pause/update/delete.
|
|
14
|
+
|
|
15
|
+
Both follow the existing `get_memory` / `get_briefing_note` pattern:
|
|
16
|
+
`scopeFilterGet(oauthCtx, row)` for scope-aware cross-tenant collapse,
|
|
17
|
+
read-only annotations.
|
|
18
|
+
|
|
19
|
+
**get_episode** was DROPPED from Phase 2b scope: episodes are stored as
|
|
20
|
+
memories with episode metadata (no separate `episodes` table in
|
|
21
|
+
`convex/schema.ts`). Use existing `get_memory` to fetch episode rows by
|
|
22
|
+
their memory document ID. Hotfix 7f958d0 removed the invalid
|
|
23
|
+
`episodes:getById` query that broke `convex deploy` typecheck.
|
|
24
|
+
|
|
25
|
+
Phase 2a CHANGELOG entry deferred — Convex changes shipped in main repo
|
|
26
|
+
CHANGELOG, not mcp-server.
|
|
27
|
+
|
|
28
|
+
Total MCP `get_*` tools after Phase 2b: 16 (was 14 after Phase 1, 10 before
|
|
29
|
+
Day 100 task).
|
|
30
|
+
|
|
31
|
+
## [2.6.0] — 2026-06-13 — Day 100 get_by_id surface Phase 1 (task k172735brsw6bc3j2dkkkfxqrx88kkjq)
|
|
32
|
+
|
|
33
|
+
Pi reported get_<entity>_by_id MCP surface gaps observed during Day 100 fleet ops.
|
|
34
|
+
Phase 1 adds 4 plumbing-only read tools mapping to pre-existing Convex queries:
|
|
35
|
+
|
|
36
|
+
- **get_task** — `tasks:getById` plumbing. Fetch full task record (description, dependsOn,
|
|
37
|
+
missionId, completionNote, claimedByInstance, startedAt) by Convex doc ID.
|
|
38
|
+
- **get_fix_pattern** — `fixPatterns:get` plumbing. Fetch fix pattern with full linked
|
|
39
|
+
fix attempts history.
|
|
40
|
+
- **get_mandate** — `mandates:get` plumbing. Fetch spending mandate with limits, current
|
|
41
|
+
spend, approver chain for validateSpending/settleMandate pre-checks.
|
|
42
|
+
- **get_repo_mapping** — `githubRepoMapping:getByRepo` plumbing. Fetch GitHub repo→VP project
|
|
43
|
+
mapping by repo slug.
|
|
44
|
+
|
|
45
|
+
All 4 tools apply `scopeFilterGet(oauthCtx, row)` for scope-aware cross-tenant collapse,
|
|
46
|
+
mirroring the existing `get_memory` / `get_briefing_note` pattern (Day 92 S3.1 wave).
|
|
47
|
+
|
|
48
|
+
Annotations test (chatgpt-tool-annotations.test.ts) READ_ONLY_TOOLS set extended with
|
|
49
|
+
the 4 new tools. Pre-existing 84/97 count mismatch in same test is unrelated (separate
|
|
50
|
+
F-list track).
|
|
51
|
+
|
|
52
|
+
Phase 2 (follow-on PR) will add get_message / get_episode / get_recurring_task
|
|
53
|
+
(Convex query additions required).
|
|
54
|
+
Phase 3 (separate mission) will audit deployment entity (table+queries+tool) and the
|
|
55
|
+
cross-backend cloud proxy redeploy cadence.
|
|
56
|
+
|
|
57
|
+
## [2.5.0] — 2026-06-06 — Day 92 VP MCP quality overhaul (mission k57a36y8)
|
|
58
|
+
|
|
59
|
+
Day 92 mission `k57a36y8w5t085bqr23dsmvb2d882506` ships a fleet-wide VP MCP quality bump
|
|
60
|
+
across audit, docs, hooks, security, and consistency dimensions. 15 PRs merged to main.
|
|
61
|
+
|
|
62
|
+
### Phase A — Audit + new tools
|
|
63
|
+
- **A1** Day 92 VP MCP tools audit matrix (85 tools, 14 P0 zero-auth gaps) — `docs/test-reports/day92-vp-mcp-audit-matrix.md`.
|
|
64
|
+
- **A2** Consistency analysis report — `docs/test-reports/day92-vp-mcp-consistency-analysis.md`.
|
|
65
|
+
- **A3** New `whoami` LECTURE tool — first per-tool `outputSchema` export precedent.
|
|
66
|
+
- **A4** Consolidated gap matrix — `docs/test-reports/day92-vp-mcp-gap-matrix-consolidated.md`.
|
|
67
|
+
|
|
68
|
+
### Phase B — Documentation
|
|
69
|
+
- **B1** `docs/cloud/security-multi-tenant.md` §4 scope-aware filter framework rewrite.
|
|
70
|
+
- **B2** `docs/cloud/tools-quality-standard.md` (NEW) — 12-section bilingual quality standard.
|
|
71
|
+
- **B3** `docs/cloud/onboarding-customer.md` (NEW) — customer onboarding guide (bilingual FR+EN).
|
|
72
|
+
|
|
73
|
+
### Phase C — Consistency
|
|
74
|
+
- **C0** 14 P0 zero-auth write tools secured with `guardMasterOnly` (C0.1 → C0.6, 6 PRs).
|
|
75
|
+
- **C1** 87 Zod `outputSchema` exports per per-family envelope standard (B2 §3).
|
|
76
|
+
- **C2** Orchestrator-id NFC normalization + case-insensitive matching; idempotent prod migration `convex/migrations/c2-normalize-orchestrator-ids.ts` (7 tables).
|
|
77
|
+
- **C3** 97 tool descriptions standardized + 10 canonical aliases gated through `guardMasterOnly` (security regression fixed in iter 2) + alias-c0-gate-coverage test (15/15 PASS).
|
|
78
|
+
- **C4** Legacy `claude-peers` references removed repo-wide + `grep-gate` CI workflow.
|
|
79
|
+
|
|
80
|
+
### Phase F — Hooks + plugin
|
|
81
|
+
- **F1** New consolidated `validate_task_payload` MCP tool + TypeScript validator library (replaces 5 single-axis hooks).
|
|
82
|
+
- **F2** Plugin propagation runbook + `plugin-vs-workspace-hooks.md` doctrine.
|
|
83
|
+
|
|
84
|
+
### Scope-aware filtering
|
|
85
|
+
- `list_tasks` `fromAllowList[]` + case-insensitive matching (PR #654, #661).
|
|
86
|
+
- 3 admin endpoints reinstated for Marie cohort (prior session).
|
|
87
|
+
|
|
88
|
+
### Tenant trio
|
|
89
|
+
- Persistent test tenant trio (alpha/beta/gamma) seeded on prod with bearers, scope_profiles, and seed data for cross-orchestrator E2E.
|
|
90
|
+
|
|
91
|
+
### Deploy authorization
|
|
92
|
+
- `PI_AUTHORIZED_TASK_ID=k1751nfs27t9f9mpvg3ppd6xad884r59` (Day 82 doctrine).
|
|
93
|
+
- Mission: `k57a36y8w5t085bqr23dsmvb2d882506`.
|
|
94
|
+
- Branch: `release/v2.5.0` opened against `main` at HEAD `18a5530`.
|
|
95
|
+
|
|
3
96
|
## [2.4.13] — 2026-06-02 — Post-public republish: attribution + CHANGELOG day-numbers + RULE #7 narrative scrub
|
|
4
97
|
|
|
5
98
|
Repository visibility flip to PUBLIC on 2026-06-02 (mission D62 `k57e4t21sr55rhz8ng554eseb987wvh3`). This patch republishes the npm package so the published README + CHANGELOG + attribution match the now-public source.
|
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
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
|
MCP server for [VantagePeers](https://vantagepeers.com) — shared memory, messaging, and task coordination for AI agent teams.
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
97 tools across 18 categories: memory, profiles, tasks, missions, mission templates, messages, diary, briefing notes, search (RAG), issues, fix patterns, error monitoring, deployments, business units, components, mandates, recurring tasks, and session. All tools ship with ChatGPT Apps SDK annotations (`readOnlyHint`, `openWorldHint`, `destructiveHint`) for native UX in ChatGPT custom connectors.
|
|
11
11
|
|
|
12
12
|
## Quick start
|
|
13
13
|
|
|
@@ -17,6 +17,20 @@ npx vantage-peers-mcp
|
|
|
17
17
|
|
|
18
18
|
Requires `CONVEX_URL` pointing to your VantagePeers Convex deployment.
|
|
19
19
|
|
|
20
|
+
## What's new in v2.5.0
|
|
21
|
+
|
|
22
|
+
Day 92 VP MCP quality overhaul (mission `k57a36y8w5t085bqr23dsmvb2d882506`, PR #678):
|
|
23
|
+
|
|
24
|
+
- **C0 — 14 P0 zero-auth write tools secured** with master-only gates (`guardMasterOnly` / `checkFromAllowed`); all 14 tools identified in the A1 audit matrix (commit `d03d2d7`) now require an explicit scope gate before any mutation reaches Convex.
|
|
25
|
+
- **C1 — 87 Zod `outputSchema` exports** following the per-family envelope standard (`create_*` → `{id,...}`, `list_*` → `{items,cursor}`, `delete_*` → `{id,deleted:true}`, etc.) based on the `whoamiOutputSchema` precedent (commit `5231811`).
|
|
26
|
+
- **C2 — Unicode NFC normalization + case-insensitive orchestrator-ID matching** applied at all write paths and filter comparisons; closes the NFD/NFC silent mismatch class discovered in the Hélios/helios production regression.
|
|
27
|
+
- **C3 — 97 tool descriptions standardized** (1-line summary + WHEN clause + concrete EXAMPLE, 80–500 chars) + 10 canonical aliases aligned to the `verb_noun_snake` whitelist.
|
|
28
|
+
- **C4 — `claude-peers` legacy references removed** from source and docs + grep-gate CI check to prevent reintroduction.
|
|
29
|
+
- **A3 — `whoami` LECTURE tool** (PR #661, commit `5231811`) — returns `suggested_orchestrator_id`, `scope_profile`, and `namespace_read_prefixes` so skills auto-resolve identity without prompting the user.
|
|
30
|
+
- **F1 — `validate_task_payload` validator tool** (commit `cf6c961`) — client-side payload validation before any write reaches Convex.
|
|
31
|
+
|
|
32
|
+
See `mcp-server/CHANGELOG.md` for the full per-PR list.
|
|
33
|
+
|
|
20
34
|
## Install
|
|
21
35
|
|
|
22
36
|
### Option 1: npx (no install)
|
|
@@ -74,7 +88,7 @@ VantagePeers ships a built-in OAuth 2.1 authorization server so Claude.ai web ca
|
|
|
74
88
|
|
|
75
89
|
The server also reads `CONVEX_URL` from `.env.local` in the parent directory if not set via environment.
|
|
76
90
|
|
|
77
|
-
## Tools (
|
|
91
|
+
## Tools (97)
|
|
78
92
|
|
|
79
93
|
### Memory (6)
|
|
80
94
|
`store_memory`, `recall`, `list_memories`, `soft_delete_memory`, `get_memory`, `store_episode`
|
package/dist/server-http.d.ts
CHANGED
|
@@ -37,4 +37,5 @@ export declare function parseBasicAuthSecret(authHeader: string | undefined, bod
|
|
|
37
37
|
clientId: string | null;
|
|
38
38
|
clientSecret: string | null;
|
|
39
39
|
};
|
|
40
|
+
export declare function redirectUriMatches(registeredUri: string, presentedUri: string): boolean;
|
|
40
41
|
export declare const app: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
package/dist/server-http.js
CHANGED
|
@@ -31,7 +31,7 @@ import { ConvexHttpClient } from "convex/browser";
|
|
|
31
31
|
import { Hono } from "hono";
|
|
32
32
|
import { cors } from "hono/cors";
|
|
33
33
|
import { bearerAuthMiddleware, internalClient, masterOnlyMiddleware, sha256Base64Url, sha256Hex, } from "./src/auth.js";
|
|
34
|
-
import { timingSafeEqual } from "
|
|
34
|
+
import { timingSafeEqual } from "@vantageos/cloud-identity";
|
|
35
35
|
import { registerTools } from "./src/tools.js";
|
|
36
36
|
import { listUiResources, readUiResource } from "./src/ui-resources/index.js";
|
|
37
37
|
let pkg;
|
|
@@ -120,6 +120,54 @@ async function loadScopeProfile(profileId) {
|
|
|
120
120
|
"oauth:getScopeProfile", { profileId }));
|
|
121
121
|
}
|
|
122
122
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
// D7 wildcard redirect_uri matcher
|
|
124
|
+
//
|
|
125
|
+
// RFC 6749 §3.1.2.3/§3.1.2.4 mandates that the authorization server validate
|
|
126
|
+
// the inbound `redirect_uri` against the URIs registered for the client and
|
|
127
|
+
// reject anything that does not match. The default match is byte-exact.
|
|
128
|
+
//
|
|
129
|
+
// Some MCP clients (notably ChatGPT's custom connector flow as of Day 92,
|
|
130
|
+
// 2026-06-04) issue per-session callbacks under a stable path prefix with a
|
|
131
|
+
// dynamic trailing segment, e.g. `https://chatgpt.com/connector/oauth/<id>`
|
|
132
|
+
// where `<id>` rotates per connector instance. A pure exact-match policy
|
|
133
|
+
// blocks every such flow after the first registration.
|
|
134
|
+
//
|
|
135
|
+
// To allow these flows without re-opening the open-redirect attack surface
|
|
136
|
+
// that the exact-match rule was designed to close, a registered URI may
|
|
137
|
+
// embed exactly one `*` token. When present, the URI is treated as a glob:
|
|
138
|
+
// - every other character is matched literally (regex-escaped),
|
|
139
|
+
// - the `*` is expanded to `[a-zA-Z0-9_-]+` — at least one char, no slash,
|
|
140
|
+
// no dot, no path separator, no host-bracketing punctuation,
|
|
141
|
+
// - the result is anchored with `^` and `$`.
|
|
142
|
+
//
|
|
143
|
+
// Lookalike attacks are still rejected because:
|
|
144
|
+
// - the host portion is literal, so `chatgpt.com.evil.io` does not match
|
|
145
|
+
// `https://chatgpt.com/connector/oauth/*`,
|
|
146
|
+
// - the path prefix is literal, so `/connector/oauth/../admin` does not
|
|
147
|
+
// match (`.` and `/` are not in the dynamic char class),
|
|
148
|
+
// - the dynamic segment requires at least one allowed character, so a
|
|
149
|
+
// trailing-slash variant (`.../oauth/`) does not match either.
|
|
150
|
+
//
|
|
151
|
+
// URIs without a `*` keep the original exact-match semantics — this helper
|
|
152
|
+
// is a strict superset of the prior behavior.
|
|
153
|
+
export function redirectUriMatches(registeredUri, presentedUri) {
|
|
154
|
+
if (!registeredUri.includes("*")) {
|
|
155
|
+
return registeredUri === presentedUri;
|
|
156
|
+
}
|
|
157
|
+
// Escape regex metacharacters EXCEPT `*`, then expand `*`.
|
|
158
|
+
const escaped = registeredUri.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
159
|
+
const pattern = `^${escaped.replace(/\*/g, "[a-zA-Z0-9_-]+")}$`;
|
|
160
|
+
try {
|
|
161
|
+
return new RegExp(pattern).test(presentedUri);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function redirectUriMatchesAny(registeredUris, presentedUri) {
|
|
168
|
+
return registeredUris.some((u) => redirectUriMatches(u, presentedUri));
|
|
169
|
+
}
|
|
170
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
171
|
// App
|
|
124
172
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
173
|
export const app = new Hono();
|
|
@@ -300,7 +348,8 @@ app.get("/authorize", async (c) => {
|
|
|
300
348
|
// registered URI. Defense against open-redirect / token-exfiltration via
|
|
301
349
|
// attacker-controlled redirect. No partial / prefix / wildcard match.
|
|
302
350
|
const registeredUris = client.redirectUris ?? [];
|
|
303
|
-
if (registeredUris.length === 0 ||
|
|
351
|
+
if (registeredUris.length === 0 ||
|
|
352
|
+
!redirectUriMatchesAny(registeredUris, redirectUri)) {
|
|
304
353
|
return c.json({
|
|
305
354
|
error: "invalid_request",
|
|
306
355
|
error_description: "redirect_uri does not match a registered redirect URI for this client",
|
|
@@ -370,7 +419,7 @@ app.post("/token", async (c) => {
|
|
|
370
419
|
if (Date.now() > record.expiresAt) {
|
|
371
420
|
return c.json({ error: "invalid_grant", error_description: "code expired" }, 400);
|
|
372
421
|
}
|
|
373
|
-
if (redirectUri &&
|
|
422
|
+
if (redirectUri && !redirectUriMatches(record.redirectUri, redirectUri)) {
|
|
374
423
|
return c.json({
|
|
375
424
|
error: "invalid_grant",
|
|
376
425
|
error_description: "redirect_uri mismatch",
|
|
@@ -409,8 +458,9 @@ app.post("/token", async (c) => {
|
|
|
409
458
|
}, 401);
|
|
410
459
|
}
|
|
411
460
|
const presentedHash = await sha256Hex(clientSecret);
|
|
461
|
+
const _enc = new TextEncoder();
|
|
412
462
|
if (!client.clientSecretHash ||
|
|
413
|
-
!(await timingSafeEqual(presentedHash, client.clientSecretHash))) {
|
|
463
|
+
!(await timingSafeEqual(_enc.encode(presentedHash), _enc.encode(client.clientSecretHash)))) {
|
|
414
464
|
return c.json({
|
|
415
465
|
error: "invalid_client",
|
|
416
466
|
error_description: "client_secret mismatch",
|
|
@@ -497,8 +547,9 @@ app.post("/token", async (c) => {
|
|
|
497
547
|
}, 401);
|
|
498
548
|
}
|
|
499
549
|
const presentedHash = await sha256Hex(clientSecret);
|
|
550
|
+
const _enc = new TextEncoder();
|
|
500
551
|
if (!refreshClient.clientSecretHash ||
|
|
501
|
-
!(await timingSafeEqual(presentedHash, refreshClient.clientSecretHash))) {
|
|
552
|
+
!(await timingSafeEqual(_enc.encode(presentedHash), _enc.encode(refreshClient.clientSecretHash)))) {
|
|
502
553
|
return c.json({
|
|
503
554
|
error: "invalid_client",
|
|
504
555
|
error_description: "client_secret mismatch",
|
|
@@ -657,6 +708,367 @@ admin.post("/oauth/seed-profiles", async (c) => {
|
|
|
657
708
|
"oauth:seedDefaultProfiles", { callerToken: masterToken });
|
|
658
709
|
return c.json({ created });
|
|
659
710
|
});
|
|
711
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
712
|
+
// S2.2 D5 — PATCH /admin/scope-profiles/:id
|
|
713
|
+
//
|
|
714
|
+
// HTTP wrapper around Convex mutation `oauth:patchScopeProfileEmergency`
|
|
715
|
+
// (S1.2-mutation + S2.1 cascade + audit log).
|
|
716
|
+
//
|
|
717
|
+
// Auth: BEARER_SECRET_MASTER via masterOnlyMiddleware (already mounted on
|
|
718
|
+
// the `admin` Hono sub-app). The mutation itself does a second constant-time
|
|
719
|
+
// master-token check via `requireMasterAuth` at the Convex layer.
|
|
720
|
+
//
|
|
721
|
+
// Body schema:
|
|
722
|
+
// {
|
|
723
|
+
// rename?: string,
|
|
724
|
+
// fromAllowList?: string[],
|
|
725
|
+
// namespaceReadPrefixes?: string[],
|
|
726
|
+
// namespaceWritePrefixes?: string[],
|
|
727
|
+
// cascadeRevokeTokens: boolean, // REQUIRED
|
|
728
|
+
// reason: string, // REQUIRED, Convex enforces ≥40
|
|
729
|
+
// }
|
|
730
|
+
//
|
|
731
|
+
// Response (200):
|
|
732
|
+
// { patchedProfileId, cascadeRevokedCount, clientsRetargeted, auditLogId }
|
|
733
|
+
//
|
|
734
|
+
// Error mapping (Convex throw → HTTP status):
|
|
735
|
+
// "profile not found" → 404
|
|
736
|
+
// "D4 violation" → 400
|
|
737
|
+
// "reason must be" → 400 (reason length guard)
|
|
738
|
+
// anything else → 500
|
|
739
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
740
|
+
admin.patch("/scope-profiles/:id", async (c) => {
|
|
741
|
+
const masterToken = process.env.BEARER_SECRET_MASTER;
|
|
742
|
+
if (!masterToken)
|
|
743
|
+
return c.json({ error: "server_misconfigured" }, 500);
|
|
744
|
+
const profileId = c.req.param("id");
|
|
745
|
+
if (!profileId) {
|
|
746
|
+
return c.json({ error: "invalid_request", detail: "missing :id" }, 400);
|
|
747
|
+
}
|
|
748
|
+
let body = {};
|
|
749
|
+
try {
|
|
750
|
+
body = await c.req.json();
|
|
751
|
+
}
|
|
752
|
+
catch {
|
|
753
|
+
return c.json({ error: "invalid_request", detail: "body must be valid JSON" }, 400);
|
|
754
|
+
}
|
|
755
|
+
// Required: cascadeRevokeTokens (boolean), reason (string)
|
|
756
|
+
if (typeof body.cascadeRevokeTokens !== "boolean") {
|
|
757
|
+
return c.json({
|
|
758
|
+
error: "invalid_request",
|
|
759
|
+
detail: "cascadeRevokeTokens (boolean) is required",
|
|
760
|
+
}, 400);
|
|
761
|
+
}
|
|
762
|
+
if (typeof body.reason !== "string" || body.reason.length === 0) {
|
|
763
|
+
return c.json({ error: "invalid_request", detail: "reason (string) is required" }, 400);
|
|
764
|
+
}
|
|
765
|
+
// Optional fields — typed coercion / validation
|
|
766
|
+
const mutationArgs = {
|
|
767
|
+
callerToken: masterToken,
|
|
768
|
+
profileId,
|
|
769
|
+
cascadeRevokeTokens: body.cascadeRevokeTokens,
|
|
770
|
+
reason: body.reason,
|
|
771
|
+
};
|
|
772
|
+
if (body.rename !== undefined) {
|
|
773
|
+
if (typeof body.rename !== "string") {
|
|
774
|
+
return c.json({ error: "invalid_request", detail: "rename must be a string" }, 400);
|
|
775
|
+
}
|
|
776
|
+
mutationArgs.rename = body.rename;
|
|
777
|
+
}
|
|
778
|
+
for (const key of [
|
|
779
|
+
"fromAllowList",
|
|
780
|
+
"namespaceReadPrefixes",
|
|
781
|
+
"namespaceWritePrefixes",
|
|
782
|
+
]) {
|
|
783
|
+
const v = body[key];
|
|
784
|
+
if (v !== undefined) {
|
|
785
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
786
|
+
return c.json({ error: "invalid_request", detail: `${key} must be string[]` }, 400);
|
|
787
|
+
}
|
|
788
|
+
mutationArgs[key] = v;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
try {
|
|
792
|
+
const result = await internalClient().mutation(
|
|
793
|
+
// biome-ignore lint/suspicious/noExplicitAny: Convex string API
|
|
794
|
+
"oauth:patchScopeProfileEmergency", mutationArgs);
|
|
795
|
+
return c.json(result, 200);
|
|
796
|
+
}
|
|
797
|
+
catch (err) {
|
|
798
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
799
|
+
// Map known Convex throws to HTTP status
|
|
800
|
+
if (/profile not found/i.test(message)) {
|
|
801
|
+
return c.json({ error: "not_found", detail: message }, 404);
|
|
802
|
+
}
|
|
803
|
+
if (/D4 violation/i.test(message)) {
|
|
804
|
+
return c.json({ error: "D4 violation", detail: message }, 400);
|
|
805
|
+
}
|
|
806
|
+
if (/reason must be at least/i.test(message)) {
|
|
807
|
+
return c.json({ error: "invalid_request", detail: message }, 400);
|
|
808
|
+
}
|
|
809
|
+
console.error("[admin] patchScopeProfileEmergency failed:", message);
|
|
810
|
+
return c.json({ error: "server_error", detail: message }, 500);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
814
|
+
// POST /admin/oauth/access-tokens — direct mint (bypass full OAuth flow)
|
|
815
|
+
//
|
|
816
|
+
// Wraps Convex mutation `oauth:createAccessToken` so operators with the
|
|
817
|
+
// master bearer can mint a scoped access token in a single call without
|
|
818
|
+
// running the DCR → /authorize → /token dance.
|
|
819
|
+
//
|
|
820
|
+
// Use case: provisioning isolated test workspaces for manual cross-tenant
|
|
821
|
+
// e2e verification, or onboarding a paying user when the dashboard does
|
|
822
|
+
// not yet exist (cloud-launch-v1 close-out window).
|
|
823
|
+
//
|
|
824
|
+
// Auth: BEARER_SECRET_MASTER via masterOnlyMiddleware (mounted on /admin).
|
|
825
|
+
//
|
|
826
|
+
// Body schema:
|
|
827
|
+
// {
|
|
828
|
+
// scopeProfile: string, // REQUIRED — must exist in oauth_scope_profiles
|
|
829
|
+
// userId: string, // REQUIRED — caller-supplied user identifier
|
|
830
|
+
// clientId?: string, // optional — defaults to "admin-mint:<random>"
|
|
831
|
+
// scopes?: string[], // optional — defaults to ["mcp:full"]
|
|
832
|
+
// fromAllowList?: string[], // optional — defaults to profile.fromAllowList
|
|
833
|
+
// namespaceReadPrefixes?: string[], // optional — defaults to profile.namespaceReadPrefixes
|
|
834
|
+
// namespaceWritePrefixes?: string[], // optional — defaults to profile.namespaceWritePrefixes
|
|
835
|
+
// expiresInSec?: number, // optional — defaults to 86400 (24h), max 30d
|
|
836
|
+
// }
|
|
837
|
+
//
|
|
838
|
+
// Response (201):
|
|
839
|
+
// {
|
|
840
|
+
// access_token: <raw token, 64 hex chars — returned ONCE, never again>,
|
|
841
|
+
// token_type: "Bearer",
|
|
842
|
+
// expires_at: <unix ms>,
|
|
843
|
+
// expires_in: <seconds>,
|
|
844
|
+
// clientId: <effective>,
|
|
845
|
+
// userId: <effective>,
|
|
846
|
+
// scopes: <effective array>,
|
|
847
|
+
// scopeProfile: <effective>,
|
|
848
|
+
// fromAllowList: <effective array>,
|
|
849
|
+
// namespaceReadPrefixes: <effective array>,
|
|
850
|
+
// namespaceWritePrefixes: <effective array>
|
|
851
|
+
// }
|
|
852
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
853
|
+
admin.post("/oauth/access-tokens", async (c) => {
|
|
854
|
+
const masterToken = process.env.BEARER_SECRET_MASTER;
|
|
855
|
+
if (!masterToken)
|
|
856
|
+
return c.json({ error: "server_misconfigured" }, 500);
|
|
857
|
+
let body = {};
|
|
858
|
+
try {
|
|
859
|
+
body = await c.req.json();
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
return c.json({ error: "invalid_request", detail: "body must be valid JSON" }, 400);
|
|
863
|
+
}
|
|
864
|
+
const scopeProfileArg = typeof body.scopeProfile === "string" ? body.scopeProfile : null;
|
|
865
|
+
const userId = typeof body.userId === "string" ? body.userId : null;
|
|
866
|
+
if (!scopeProfileArg || !userId) {
|
|
867
|
+
return c.json({
|
|
868
|
+
error: "invalid_request",
|
|
869
|
+
detail: "scopeProfile and userId are required",
|
|
870
|
+
}, 400);
|
|
871
|
+
}
|
|
872
|
+
const loadedProfile = await loadScopeProfile(scopeProfileArg);
|
|
873
|
+
if (!loadedProfile) {
|
|
874
|
+
return c.json({ error: "invalid_scope_profile", scopeProfile: scopeProfileArg }, 400);
|
|
875
|
+
}
|
|
876
|
+
const profile = loadedProfile;
|
|
877
|
+
const clientId = typeof body.clientId === "string"
|
|
878
|
+
? body.clientId
|
|
879
|
+
: `admin-mint:${crypto.randomUUID()}`;
|
|
880
|
+
const scopes = Array.isArray(body.scopes)
|
|
881
|
+
? body.scopes.filter((x) => typeof x === "string")
|
|
882
|
+
: ["mcp:full"];
|
|
883
|
+
const arrayOrProfile = (key) => {
|
|
884
|
+
const v = body[key];
|
|
885
|
+
if (Array.isArray(v) && v.every((x) => typeof x === "string")) {
|
|
886
|
+
return v;
|
|
887
|
+
}
|
|
888
|
+
return profile[key];
|
|
889
|
+
};
|
|
890
|
+
const fromAllowList = arrayOrProfile("fromAllowList");
|
|
891
|
+
const namespaceReadPrefixes = arrayOrProfile("namespaceReadPrefixes");
|
|
892
|
+
const namespaceWritePrefixes = arrayOrProfile("namespaceWritePrefixes");
|
|
893
|
+
const expiresInSecRaw = typeof body.expiresInSec === "number" ? body.expiresInSec : 86400;
|
|
894
|
+
const MAX_EXPIRES_IN_SEC = 30 * 86400;
|
|
895
|
+
if (expiresInSecRaw <= 0 || expiresInSecRaw > MAX_EXPIRES_IN_SEC) {
|
|
896
|
+
return c.json({
|
|
897
|
+
error: "invalid_request",
|
|
898
|
+
detail: `expiresInSec must be in (0, ${MAX_EXPIRES_IN_SEC}]`,
|
|
899
|
+
}, 400);
|
|
900
|
+
}
|
|
901
|
+
const expiresAt = Date.now() + expiresInSecRaw * 1000;
|
|
902
|
+
const accessToken = randomOpaqueToken();
|
|
903
|
+
const tokenHash = await sha256Hex(accessToken);
|
|
904
|
+
try {
|
|
905
|
+
await internalClient().mutation(
|
|
906
|
+
// biome-ignore lint/suspicious/noExplicitAny: Convex string API
|
|
907
|
+
"oauth:createAccessToken", {
|
|
908
|
+
callerToken: masterToken,
|
|
909
|
+
tokenHash,
|
|
910
|
+
clientId,
|
|
911
|
+
userId,
|
|
912
|
+
scopes,
|
|
913
|
+
scopeProfile: scopeProfileArg,
|
|
914
|
+
fromAllowList,
|
|
915
|
+
namespaceReadPrefixes,
|
|
916
|
+
namespaceWritePrefixes,
|
|
917
|
+
expiresAt,
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
catch (err) {
|
|
921
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
922
|
+
console.error("[admin] createAccessToken failed:", message);
|
|
923
|
+
return c.json({ error: "server_error", detail: message }, 500);
|
|
924
|
+
}
|
|
925
|
+
return c.json({
|
|
926
|
+
access_token: accessToken,
|
|
927
|
+
token_type: "Bearer",
|
|
928
|
+
expires_at: expiresAt,
|
|
929
|
+
expires_in: expiresInSecRaw,
|
|
930
|
+
clientId,
|
|
931
|
+
userId,
|
|
932
|
+
scopes,
|
|
933
|
+
scopeProfile: scopeProfileArg,
|
|
934
|
+
fromAllowList,
|
|
935
|
+
namespaceReadPrefixes,
|
|
936
|
+
namespaceWritePrefixes,
|
|
937
|
+
}, 201);
|
|
938
|
+
});
|
|
939
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
940
|
+
// POST /admin/oauth/clients/:clientId/patch-scope — Day 92 LIVE
|
|
941
|
+
//
|
|
942
|
+
// Wraps Convex mutation `oauth:patchClientScopeAndRefreshTokens`. Re-targets
|
|
943
|
+
// the client to a new scope_profile and propagates the new
|
|
944
|
+
// `fromAllowList` + namespace prefixes into every live access token row
|
|
945
|
+
// for that clientId WITHOUT revoking refresh tokens — the bearer the
|
|
946
|
+
// operator already pasted into their MCP host keeps working, immediately
|
|
947
|
+
// gaining the new profile's allow list. Eliminates the customer
|
|
948
|
+
// re-paste step that profile rotation would otherwise force.
|
|
949
|
+
//
|
|
950
|
+
// Auth: BEARER_SECRET_MASTER via masterOnlyMiddleware.
|
|
951
|
+
//
|
|
952
|
+
// Body schema: { newScopeProfile: string, reason: string (≥20 chars) }
|
|
953
|
+
//
|
|
954
|
+
// Response (200): {
|
|
955
|
+
// clientPatched, previousScopeProfile, newScopeProfile,
|
|
956
|
+
// accessTokensRefreshed, auditLogId
|
|
957
|
+
// }
|
|
958
|
+
//
|
|
959
|
+
// Error mapping (Convex throw → HTTP status):
|
|
960
|
+
// "client not found" → 404
|
|
961
|
+
// "client is revoked" → 410 (Gone — re-mint required)
|
|
962
|
+
// "scope_profile not found" → 400
|
|
963
|
+
// "reason must be at least 20" → 400
|
|
964
|
+
// anything else → 500
|
|
965
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
966
|
+
admin.post("/oauth/clients/:clientId/patch-scope", async (c) => {
|
|
967
|
+
const masterToken = process.env.BEARER_SECRET_MASTER;
|
|
968
|
+
if (!masterToken)
|
|
969
|
+
return c.json({ error: "server_misconfigured" }, 500);
|
|
970
|
+
const clientId = c.req.param("clientId");
|
|
971
|
+
if (!clientId) {
|
|
972
|
+
return c.json({ error: "invalid_request", detail: "missing :clientId" }, 400);
|
|
973
|
+
}
|
|
974
|
+
let body = {};
|
|
975
|
+
try {
|
|
976
|
+
body = await c.req.json();
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
return c.json({ error: "invalid_request", detail: "body must be valid JSON" }, 400);
|
|
980
|
+
}
|
|
981
|
+
const newScopeProfile = typeof body.newScopeProfile === "string" ? body.newScopeProfile : null;
|
|
982
|
+
const reason = typeof body.reason === "string" ? body.reason : null;
|
|
983
|
+
if (!newScopeProfile || !reason) {
|
|
984
|
+
return c.json({
|
|
985
|
+
error: "invalid_request",
|
|
986
|
+
detail: "newScopeProfile and reason are required",
|
|
987
|
+
}, 400);
|
|
988
|
+
}
|
|
989
|
+
try {
|
|
990
|
+
const result = await internalClient().mutation(
|
|
991
|
+
// biome-ignore lint/suspicious/noExplicitAny: Convex string API
|
|
992
|
+
"oauth:patchClientScopeAndRefreshTokens", {
|
|
993
|
+
callerToken: masterToken,
|
|
994
|
+
clientId,
|
|
995
|
+
newScopeProfile,
|
|
996
|
+
reason,
|
|
997
|
+
});
|
|
998
|
+
return c.json(result, 200);
|
|
999
|
+
}
|
|
1000
|
+
catch (err) {
|
|
1001
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1002
|
+
if (/client not found/i.test(message)) {
|
|
1003
|
+
return c.json({ error: "not_found", detail: message }, 404);
|
|
1004
|
+
}
|
|
1005
|
+
if (/client is revoked/i.test(message)) {
|
|
1006
|
+
return c.json({ error: "gone", detail: message }, 410);
|
|
1007
|
+
}
|
|
1008
|
+
if (/scope_profile not found/i.test(message)) {
|
|
1009
|
+
return c.json({ error: "invalid_scope_profile", detail: message }, 400);
|
|
1010
|
+
}
|
|
1011
|
+
if (/reason must be at least/i.test(message)) {
|
|
1012
|
+
return c.json({ error: "invalid_request", detail: message }, 400);
|
|
1013
|
+
}
|
|
1014
|
+
console.error("[admin] patchClientScopeAndRefreshTokens failed:", message);
|
|
1015
|
+
return c.json({ error: "server_error", detail: message }, 500);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
1018
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1019
|
+
// POST /admin/oauth/clients/:clientId/revoke-access-tokens-only — Day 92 LIVE
|
|
1020
|
+
//
|
|
1021
|
+
// Wraps Convex mutation `oauth:revokeAccessTokensOnly`. Force-rotates every
|
|
1022
|
+
// live access token for the client by setting `revokedAt`, while leaving
|
|
1023
|
+
// refresh tokens untouched. The next API call from the connector hits 401
|
|
1024
|
+
// → connector silently runs the OAuth refresh-flow → fresh access token
|
|
1025
|
+
// minted from the current client scope_profile + catalog. Combined with
|
|
1026
|
+
// `patchClientScopeAndRefreshTokens` (which retargeted
|
|
1027
|
+
// refresh_tokens.scopeProfile in commit 40413bd) the next mint observes
|
|
1028
|
+
// the new profile end-to-end with zero customer re-paste.
|
|
1029
|
+
//
|
|
1030
|
+
// Auth: BEARER_SECRET_MASTER via masterOnlyMiddleware.
|
|
1031
|
+
//
|
|
1032
|
+
// Body schema: { reason: string (≥20 chars) }
|
|
1033
|
+
// Response (200): { clientId, accessTokensRevoked, refreshTokensPreserved }
|
|
1034
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
1035
|
+
admin.post("/oauth/clients/:clientId/revoke-access-tokens-only", async (c) => {
|
|
1036
|
+
const masterToken = process.env.BEARER_SECRET_MASTER;
|
|
1037
|
+
if (!masterToken)
|
|
1038
|
+
return c.json({ error: "server_misconfigured" }, 500);
|
|
1039
|
+
const clientId = c.req.param("clientId");
|
|
1040
|
+
if (!clientId) {
|
|
1041
|
+
return c.json({ error: "invalid_request", detail: "missing :clientId" }, 400);
|
|
1042
|
+
}
|
|
1043
|
+
let body = {};
|
|
1044
|
+
try {
|
|
1045
|
+
body = await c.req.json();
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
return c.json({ error: "invalid_request", detail: "body must be valid JSON" }, 400);
|
|
1049
|
+
}
|
|
1050
|
+
const reason = typeof body.reason === "string" ? body.reason : null;
|
|
1051
|
+
if (!reason) {
|
|
1052
|
+
return c.json({ error: "invalid_request", detail: "reason is required" }, 400);
|
|
1053
|
+
}
|
|
1054
|
+
try {
|
|
1055
|
+
const result = await internalClient().mutation(
|
|
1056
|
+
// biome-ignore lint/suspicious/noExplicitAny: Convex string API
|
|
1057
|
+
"oauth:revokeAccessTokensOnly", { callerToken: masterToken, clientId, reason });
|
|
1058
|
+
return c.json(result, 200);
|
|
1059
|
+
}
|
|
1060
|
+
catch (err) {
|
|
1061
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1062
|
+
if (/client not found/i.test(message)) {
|
|
1063
|
+
return c.json({ error: "not_found", detail: message }, 404);
|
|
1064
|
+
}
|
|
1065
|
+
if (/reason must be at least/i.test(message)) {
|
|
1066
|
+
return c.json({ error: "invalid_request", detail: message }, 400);
|
|
1067
|
+
}
|
|
1068
|
+
console.error("[admin] revokeAccessTokensOnly failed:", message);
|
|
1069
|
+
return c.json({ error: "server_error", detail: message }, 500);
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
660
1072
|
app.route("/admin", admin);
|
|
661
1073
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
662
1074
|
// MCP endpoint — authenticated, stateless per-request server
|
package/dist/server.js
CHANGED
|
@@ -102,7 +102,7 @@ const convexUrl = loadConvexUrl();
|
|
|
102
102
|
const convex = new ConvexHttpClient(convexUrl);
|
|
103
103
|
const server = new McpServer({
|
|
104
104
|
name: "vantage-peers",
|
|
105
|
-
version: "2.
|
|
105
|
+
version: "2.5.0",
|
|
106
106
|
});
|
|
107
107
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
108
|
// Helper: structured error response for MCP tool handlers
|
|
@@ -493,7 +493,7 @@ server.tool("list_memories", "List active memories for a namespace, ordered newe
|
|
|
493
493
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
494
494
|
server.tool("send_message", "Send a message to one, many, or all orchestrators. " +
|
|
495
495
|
"channel: 'broadcast' = all, 'tau' = role DM, 'pi-vps' = instance DM, 'tau,phi' = multi. " +
|
|
496
|
-
"Creates message + one receipt per recipient.
|
|
496
|
+
"Creates message + one receipt per recipient. Supersedes legacy send_message (pre-VantagePeers).", {
|
|
497
497
|
from: creatorSchema.describe("Sender role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, or any custom role)"),
|
|
498
498
|
fromInstanceId: z
|
|
499
499
|
.string()
|
|
@@ -540,7 +540,7 @@ server.tool("send_message", "Send a message to one, many, or all orchestrators.
|
|
|
540
540
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
541
541
|
server.tool("check_messages", "Check for unread messages. Returns messages with receiptIds for marking as read. " +
|
|
542
542
|
"If recipientInstanceId is provided, returns instance-targeted + role-level messages. " +
|
|
543
|
-
"
|
|
543
|
+
"Supersedes legacy check_messages (pre-VantagePeers).", {
|
|
544
544
|
recipient: creatorSchema.describe("Orchestrator role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, or any custom role)"),
|
|
545
545
|
recipientInstanceId: z
|
|
546
546
|
.string()
|
|
@@ -679,7 +679,7 @@ server.tool("set_summary", "Set a brief summary of what you are currently workin
|
|
|
679
679
|
// Tool: list_peers
|
|
680
680
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
681
681
|
server.tool("list_peers", "List all orchestrator profiles with their current status and summary. " +
|
|
682
|
-
"
|
|
682
|
+
"Supersedes legacy list_peers (pre-VantagePeers).", {}, async () => {
|
|
683
683
|
try {
|
|
684
684
|
const profiles = await convex.query("profiles:listProfiles", {});
|
|
685
685
|
const peers = profiles.map((p) => ({
|