vantage-peers-mcp 2.2.0 → 2.3.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 ADDED
@@ -0,0 +1,54 @@
1
+ # Changelog
2
+
3
+ ## 2.3.0 — 2026-05-26
4
+
5
+ ### Added
6
+ - `list_tasks`, `list_missions`, `list_tasks_by_mission`, `list_briefing_notes` now accept `fields=lite` for compact payloads.
7
+ - Status filters on `list_tasks`, `list_tasks_by_mission`, and `list_missions` now accept arrays and aliases:
8
+ - `status=["todo","in_progress"]` — multi-value array
9
+ - `status="open"` — expands to non-terminal statuses (tasks: todo+in_progress+review+blocked; missions: brainstorm+plan+execute+validate)
10
+ - `status="active"` — in_progress only on tasks; plan+execute on missions
11
+ - `status="all"` — no filter applied
12
+
13
+ ### Backward compat
14
+ - Single-string status still accepted unchanged.
15
+ - Omitting `fields` defaults to `"full"` — existing callers unaffected.
16
+
17
+ ---
18
+
19
+ ## 2.2.0 — 2026-05-07
20
+
21
+ - 4 new fix-pattern tools: `create_fix_pattern`, `add_fix_attempt`, `validate_fix`, `link_issue_to_pattern`
22
+ - Detailed per-tool docs with arg tables and example calls in README
23
+ - New "Fix patterns cycle" section documenting the KB learning loop
24
+ - 41 new Zod input-validation unit tests for fix-pattern tools
25
+
26
+ ## 2.1.1 — 2026-05-04
27
+
28
+ - Defense-in-depth `memoryIdSchema` validation for `create_briefing_note` and `update_briefing_note`
29
+
30
+ ## 2.1.0 — 2026-04-25
31
+
32
+ - `update_briefing_note` MCP tool with RBAC
33
+
34
+ ## 2.0.2 — 2026-04-14
35
+
36
+ - Added badges (npm version, downloads, license, tool count) to the published README
37
+ - Added Orchestrator Roles reference table including alpha, lambda, victor
38
+ - Added note that any custom lowercase role name is accepted
39
+ - Added `bugs` URL and additional keywords to `package.json`
40
+
41
+ ## 2.0.1 — 2026-04-14
42
+
43
+ - Docstring fix in server.ts (minor)
44
+
45
+ ## 2.0.0
46
+
47
+ - Type-safe `api.ts` export for cross-deployment calls (`vantage-peers-mcp/api`)
48
+ - Deploy key authentication guide
49
+ - Mission Templates category (1 tool: `update_mission_template`)
50
+ - Programmatic API section in README
51
+
52
+ ## 1.x
53
+
54
+ - Initial public release with 82 MCP tools
package/README.md CHANGED
@@ -50,6 +50,22 @@ Add to `~/.claude.json` or project `.claude/settings.json`:
50
50
  }
51
51
  ```
52
52
 
53
+ ## OAuth 2.1 DCR endpoints
54
+
55
+ VantagePeers ships a built-in OAuth 2.1 authorization server so Claude.ai web can connect via "Add custom integration" without any extra configuration.
56
+
57
+ | Method | Path | Description |
58
+ |--------|------|-------------|
59
+ | `GET` | `/.well-known/oauth-authorization-server` | Authorization Server Metadata (RFC 8414) — advertises supported grant types, endpoints, and capabilities |
60
+ | `GET` | `/.well-known/oauth-protected-resource` | Protected Resource Metadata (RFC 9728) — links back to the authorization server |
61
+ | `POST` | `/register` | Dynamic Client Registration (RFC 7591) — Claude.ai registers itself automatically on first connect |
62
+ | `GET` | `/authorize` | Authorization endpoint — redirects the user to grant access |
63
+ | `POST` | `/token` | Token endpoint — issues access tokens per OAuth 2.1 |
64
+
65
+ **RFCs implemented:** RFC 8414 (AS Metadata), RFC 9728 (Protected Resource Metadata), RFC 7591 (Dynamic Client Registration), OAuth 2.1 draft.
66
+
67
+ **Backward compatibility:** the `BEARER_SECRET_MASTER` env var still works unchanged. Claude Code and Claude Desktop users do not need to change anything — static bearer auth remains the default for those clients. OAuth 2.1 DCR is used exclusively when a client initiates the discovery flow (e.g. Claude.ai web).
68
+
53
69
  ## Environment variables
54
70
 
55
71
  | Variable | Required | Description |
@@ -212,6 +228,70 @@ Example:
212
228
  ### Session (1)
213
229
  `set_summary`
214
230
 
231
+ ## Compact payloads and status aliases (v2.3.0)
232
+
233
+ ### `fields=lite` — reduced token payloads
234
+
235
+ `list_tasks`, `list_tasks_by_mission`, `list_missions`, and `list_briefing_notes` accept an optional `fields` parameter:
236
+
237
+ | Value | Behaviour |
238
+ |-------|-----------|
239
+ | `"full"` | Default. Returns the complete document (backward-compatible). |
240
+ | `"lite"` | Returns a compact projection — significantly fewer tokens. |
241
+
242
+ Lite projections per entity:
243
+
244
+ | Tool | Lite fields |
245
+ |------|------------|
246
+ | `list_tasks` / `list_tasks_by_mission` | `_id`, `_creationTime`, `title`, `status`, `priority`, `assignedTo`, `missionId` |
247
+ | `list_missions` | `_id`, `_creationTime`, `name`, `status`, `pilot`, `priority`, `project` |
248
+ | `list_briefing_notes` | `_id`, `_creationTime`, `topic`, `title`, `participants`, `createdBy` |
249
+
250
+ Example (tasks lite):
251
+ ```json
252
+ {
253
+ "tool": "list_tasks",
254
+ "arguments": { "assignedTo": "sigma", "fields": "lite", "limit": 20 }
255
+ }
256
+ ```
257
+ Returns:
258
+ ```json
259
+ [
260
+ { "_id": "k17e2r...", "title": "Prepare MCP v2.3.0", "status": "in_progress", "priority": "high", "assignedTo": "sigma", "missionId": "k572a..." }
261
+ ]
262
+ ```
263
+
264
+ ### `status` arrays and aliases
265
+
266
+ `list_tasks`, `list_tasks_by_mission`, and `list_missions` now accept `status` as a single string, an array, or one of the aliases below.
267
+
268
+ #### Task status aliases
269
+
270
+ | Alias | Expands to |
271
+ |-------|-----------|
272
+ | `"open"` | `["todo", "in_progress", "review", "blocked"]` — everything except `done` |
273
+ | `"active"` | `["todo", "in_progress"]` |
274
+ | `"all"` | No filter — returns all statuses |
275
+
276
+ #### Mission status aliases
277
+
278
+ | Alias | Expands to |
279
+ |-------|-----------|
280
+ | `"open"` | `["brainstorm", "plan", "execute", "validate"]` — everything except `complete` |
281
+ | `"active"` | `["plan", "execute"]` |
282
+ | `"all"` | No filter — returns all statuses |
283
+
284
+ Examples:
285
+
286
+ ```json
287
+ { "tool": "list_tasks", "arguments": { "status": "open" } }
288
+ { "tool": "list_tasks", "arguments": { "status": ["todo", "in_progress"] } }
289
+ { "tool": "list_missions", "arguments": { "status": "active", "fields": "lite" } }
290
+ { "tool": "list_tasks_by_mission", "arguments": { "missionId": "k572a...", "status": "all", "fields": "lite" } }
291
+ ```
292
+
293
+ Single-string status values still work unchanged — fully backward-compatible.
294
+
215
295
  ## Fix patterns cycle
216
296
 
217
297
  A fix pattern is a validated learning extracted from a resolved bug — symptom, root cause, and the fix that worked — stored in the VantagePeers knowledge base. Patterns accumulate across projects and agents so that the same bug is never debugged twice from scratch.
@@ -320,6 +400,11 @@ All orchestrator names are open strings — any lowercase name is accepted. The
320
400
 
321
401
  ## Changelog
322
402
 
403
+ ### 2.3.0 — 2026-05-26
404
+ - `list_tasks`, `list_missions`, `list_tasks_by_mission`, `list_briefing_notes` now accept `fields=lite` for compact payloads (less tokens).
405
+ - Status filters now accept arrays and aliases: `status=["todo","in_progress"]`, `status="open"` (expands to non-terminal), `status="active"` (in_progress only on tasks; plan+execute on missions), `status="all"` (no filter).
406
+ - Single-string status still accepted unchanged (backward-compatible).
407
+
323
408
  ### 2.2.0 — 2026-05-07
324
409
  - 4 new fix-pattern tools: `create_fix_pattern`, `add_fix_attempt`, `validate_fix`, `link_issue_to_pattern`
325
410
  - Detailed per-tool docs with arg tables and example calls in README
@@ -24,6 +24,7 @@
24
24
  * PORT — HTTP port (default 3000)
25
25
  * NODE_ENV — set to "production" on Railway
26
26
  */
27
+ import { readFileSync } from "node:fs";
27
28
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
28
29
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
29
30
  import { ConvexHttpClient } from "convex/browser";
@@ -31,10 +32,19 @@ import { Hono } from "hono";
31
32
  import { cors } from "hono/cors";
32
33
  import { bearerAuthMiddleware, internalClient, masterOnlyMiddleware, sha256Base64Url, sha256Hex, } from "./src/auth.js";
33
34
  import { registerTools } from "./src/tools.js";
35
+ let pkg;
36
+ try {
37
+ // Source mode: server-http.ts → ./package.json = mcp-server/package.json
38
+ pkg = JSON.parse(readFileSync(new URL("./package.json", import.meta.url), "utf-8"));
39
+ }
40
+ catch {
41
+ // Dist mode: dist/server-http.js → ../package.json = mcp-server/package.json
42
+ pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
43
+ }
34
44
  // ─────────────────────────────────────────────────────────────────────────────
35
45
  // Constants
36
46
  // ─────────────────────────────────────────────────────────────────────────────
37
- const PUBLIC_BASE_URL = process.env.PUBLIC_BASE_URL ??
47
+ const PUBLIC_BASE_URL_FALLBACK = process.env.PUBLIC_BASE_URL ??
38
48
  "https://vantage-peers-production.up.railway.app";
39
49
  const ACCESS_TOKEN_TTL_SECONDS = 3600; // 1 hour
40
50
  const REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 3600; // 30 days
@@ -46,9 +56,34 @@ const DEFAULT_PUBLIC_DCR_PROFILE = "client-generic";
46
56
  // ─────────────────────────────────────────────────────────────────────────────
47
57
  // Helpers
48
58
  // ─────────────────────────────────────────────────────────────────────────────
59
+ /**
60
+ * Compute the issuer/base URL dynamically from the incoming request's Host
61
+ * header + protocol. Falls back to PUBLIC_BASE_URL env var when Host is absent
62
+ * (e.g., in curl smoke tests without a Host header).
63
+ *
64
+ * RFC 8414 §2: the issuer MUST be the URL the client uses to reach the server,
65
+ * so deriving it from the request is more correct than a hard-coded constant,
66
+ * especially when deployed behind a Railway/Cloudflare proxy that rewrites Host.
67
+ */
68
+ function resolveIssuer(req) {
69
+ const host = req.headers.get("host");
70
+ if (host) {
71
+ // Use x-forwarded-proto when behind a reverse proxy; fall back to https.
72
+ const proto = req.headers.get("x-forwarded-proto") ??
73
+ (host.startsWith("localhost") || host.startsWith("127.")
74
+ ? "http"
75
+ : "https");
76
+ return `${proto}://${host}`;
77
+ }
78
+ return PUBLIC_BASE_URL_FALLBACK;
79
+ }
49
80
  function randomOpaqueToken() {
50
- // UUID ~256 bits of entropy. Strip dashes for compactness.
51
- return `${crypto.randomUUID()}${crypto.randomUUID()}`.replace(/-/g, "");
81
+ // 256-bit entropy via getRandomValues (32 bytes 64 hex chars).
82
+ const bytes = new Uint8Array(32);
83
+ crypto.getRandomValues(bytes);
84
+ return Array.from(bytes)
85
+ .map((b) => b.toString(16).padStart(2, "0"))
86
+ .join("");
52
87
  }
53
88
  async function loadScopeProfile(profileId) {
54
89
  return (await internalClient().query(
@@ -76,34 +111,65 @@ app.use("*", cors({
76
111
  // OAuth 2.0 discovery (unauthenticated)
77
112
  // ─────────────────────────────────────────────────────────────────────────────
78
113
  // RFC 9728 — OAuth 2.0 Protected Resource Metadata
79
- app.get("/.well-known/oauth-protected-resource", (c) => c.json({
80
- resource: `${PUBLIC_BASE_URL}/mcp`,
81
- authorization_servers: [PUBLIC_BASE_URL],
82
- bearer_methods_supported: ["header"],
83
- scopes_supported: ["vantage:read", "vantage:write"],
84
- }));
114
+ app.get("/.well-known/oauth-protected-resource", (c) => {
115
+ const issuer = resolveIssuer(c.req.raw);
116
+ return c.json({
117
+ resource: issuer,
118
+ authorization_servers: [issuer],
119
+ scopes_supported: ["mcp:full"],
120
+ });
121
+ });
85
122
  // RFC 8414 — OAuth 2.0 Authorization Server Metadata
86
- app.get("/.well-known/oauth-authorization-server", (c) => c.json({
87
- issuer: PUBLIC_BASE_URL,
88
- authorization_endpoint: `${PUBLIC_BASE_URL}/authorize`,
89
- token_endpoint: `${PUBLIC_BASE_URL}/token`,
90
- registration_endpoint: `${PUBLIC_BASE_URL}/register`,
91
- response_types_supported: ["code"],
92
- grant_types_supported: ["authorization_code", "refresh_token"],
93
- code_challenge_methods_supported: ["S256"],
94
- token_endpoint_auth_methods_supported: [
95
- "client_secret_post",
96
- "client_secret_basic",
97
- "none",
98
- ],
99
- scopes_supported: ["vantage:read", "vantage:write"],
100
- }));
123
+ app.get("/.well-known/oauth-authorization-server", (c) => {
124
+ const issuer = resolveIssuer(c.req.raw);
125
+ return c.json({
126
+ issuer,
127
+ authorization_endpoint: `${issuer}/authorize`,
128
+ token_endpoint: `${issuer}/token`,
129
+ registration_endpoint: `${issuer}/register`,
130
+ response_types_supported: ["code"],
131
+ grant_types_supported: ["authorization_code", "refresh_token"],
132
+ code_challenge_methods_supported: ["S256"],
133
+ token_endpoint_auth_methods_supported: [
134
+ "client_secret_basic",
135
+ "client_secret_post",
136
+ ],
137
+ scopes_supported: ["mcp:full"],
138
+ });
139
+ });
140
+ const registerRateBuckets = new Map();
141
+ const REGISTER_RATE_LIMIT = 5;
142
+ const REGISTER_RATE_WINDOW_MS = 60_000;
143
+ function checkRegisterRateLimit(ip) {
144
+ const now = Date.now();
145
+ const bucket = registerRateBuckets.get(ip);
146
+ if (!bucket || now - bucket.windowStart >= REGISTER_RATE_WINDOW_MS) {
147
+ registerRateBuckets.set(ip, { count: 1, windowStart: now });
148
+ return true;
149
+ }
150
+ if (bucket.count < REGISTER_RATE_LIMIT) {
151
+ bucket.count++;
152
+ return true;
153
+ }
154
+ return false;
155
+ }
101
156
  // ─────────────────────────────────────────────────────────────────────────────
102
157
  // RFC 7591 — Dynamic Client Registration
103
158
  // Anonymous registrations get DEFAULT_PUBLIC_DCR_PROFILE ("client-generic").
104
159
  // Pi must elevate the client via admin endpoint before real scopes are granted.
105
160
  // ─────────────────────────────────────────────────────────────────────────────
106
161
  app.post("/register", async (c) => {
162
+ // S2: rate limit by IP — 5 req/min
163
+ const clientIp = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ??
164
+ c.req.header("x-real-ip") ??
165
+ "unknown";
166
+ if (!checkRegisterRateLimit(clientIp)) {
167
+ c.header("Retry-After", "60");
168
+ return c.json({
169
+ error: "too_many_requests",
170
+ error_description: "Rate limit exceeded. Max 5 registrations per minute per IP.",
171
+ }, 429);
172
+ }
107
173
  let body = {};
108
174
  try {
109
175
  body = await c.req.json();
@@ -150,8 +216,8 @@ app.post("/register", async (c) => {
150
216
  token_endpoint_auth_method: "client_secret_post",
151
217
  grant_types: ["authorization_code", "refresh_token"],
152
218
  response_types: ["code"],
153
- scope: "vantage:read vantage:write",
154
- scope_profile: scopeProfile,
219
+ // SC: standardized on mcp:full — consistent with well-known metadata
220
+ scope: "mcp:full",
155
221
  }, 201);
156
222
  });
157
223
  // ─────────────────────────────────────────────────────────────────────────────
@@ -164,7 +230,8 @@ app.get("/authorize", async (c) => {
164
230
  const codeChallenge = q.code_challenge;
165
231
  const codeChallengeMethod = q.code_challenge_method ?? "S256";
166
232
  const state = q.state;
167
- const scope = q.scope ?? "vantage:read vantage:write";
233
+ // SC: standardize scope always mcp:full regardless of requested value
234
+ const scope = "mcp:full";
168
235
  const responseType = q.response_type;
169
236
  if (!clientId || !redirectUri || !codeChallenge) {
170
237
  return c.json({
@@ -357,7 +424,8 @@ app.post("/token", async (c) => {
357
424
  tokenHash: accessTokenHash,
358
425
  clientId: record.clientId,
359
426
  userId: record.userId,
360
- scopes: ["vantage:read", "vantage:write"],
427
+ // SC: standardized on mcp:full
428
+ scopes: ["mcp:full"],
361
429
  scopeProfile: profile.profileId,
362
430
  fromAllowList: profile.fromAllowList,
363
431
  namespaceReadPrefixes: profile.namespaceReadPrefixes,
@@ -370,7 +438,8 @@ app.post("/token", async (c) => {
370
438
  token_type: "Bearer",
371
439
  expires_in: ACCESS_TOKEN_TTL_SECONDS,
372
440
  refresh_token: refreshTokenRaw, // reused
373
- scope: "vantage:read vantage:write",
441
+ // SC: standardized on mcp:full
442
+ scope: "mcp:full",
374
443
  });
375
444
  }
376
445
  return c.json({ error: "unsupported_grant_type" }, 400);
@@ -381,9 +450,10 @@ app.post("/token", async (c) => {
381
450
  app.get("/health", (c) => c.json({
382
451
  status: "ok",
383
452
  service: "vantage-peers-mcp-http",
384
- version: "2.1.0",
453
+ version: pkg.version,
385
454
  transport: "streamable-http",
386
- oauth: "scoped-tokens",
455
+ oauth: "supported",
456
+ scopes: ["mcp:full"],
387
457
  }));
388
458
  // ─────────────────────────────────────────────────────────────────────────────
389
459
  // Admin endpoints — master token only
@@ -490,7 +560,7 @@ app.all("/mcp", bearerAuthMiddleware(), async (c) => {
490
560
  // Fresh McpServer per request — stateless mode, no session leakage
491
561
  const server = new McpServer({
492
562
  name: "vantage-peers",
493
- version: "2.1.0",
563
+ version: pkg.version,
494
564
  });
495
565
  registerTools(server, convex, oauthCtx);
496
566
  const transport = new WebStandardStreamableHTTPServerTransport();
package/dist/server.js CHANGED
@@ -433,7 +433,10 @@ server.tool("update_profile", "Create or update an orchestrator profile. Provide
433
433
  // ─────────────────────────────────────────────────────────────────────────────
434
434
  server.tool("list_memories", "List active memories for a namespace, ordered newest first. " +
435
435
  "Only returns isLatest=true memories (superseded memories are excluded by default). " +
436
- "Use type to filter to a specific memory category.", {
436
+ "Use type to filter to a specific memory category. " +
437
+ "Returns { value: Memory[], continueCursor: string|null, isDone: boolean }. " +
438
+ "Pass paginationOpts.cursor from a previous response to fetch the next page. " +
439
+ "Without paginationOpts, returns ≤50 rows with continueCursor=null and isDone=true.", {
437
440
  namespace: z
438
441
  .string()
439
442
  .describe("Namespace to list memories from — e.g. 'global', 'orchestrator/pi'"),
@@ -447,19 +450,36 @@ server.tool("list_memories", "List active memories for a namespace, ordered newe
447
450
  .max(200)
448
451
  .optional()
449
452
  .default(20)
450
- .describe("Maximum number of memories to return (default 20)"),
451
- }, async ({ namespace, type, limit }) => {
453
+ .describe("Maximum number of memories to return when paginationOpts is not provided (default 20, max 200)"),
454
+ paginationOpts: z
455
+ .object({
456
+ numItems: z
457
+ .number()
458
+ .int()
459
+ .min(1)
460
+ .max(200)
461
+ .describe("Number of items per page (max 200)"),
462
+ cursor: z
463
+ .union([z.string(), z.null()])
464
+ .describe("Cursor from a previous response continueCursor field, or null for the first page"),
465
+ })
466
+ .optional()
467
+ .describe("Optional cursor-based pagination. Pass { numItems, cursor: null } for the first page, " +
468
+ "then { numItems, cursor: <continueCursor from response> } for subsequent pages. " +
469
+ "When provided, isDone=false means more pages exist."),
470
+ }, async ({ namespace, type, limit, paginationOpts }) => {
452
471
  try {
453
- const memories = await convex.query("memories:listMemories", {
472
+ const result = await convex.query("memories:listMemories", {
454
473
  namespace,
455
474
  type,
456
475
  limit: limit ?? 20,
476
+ paginationOpts,
457
477
  });
458
478
  return {
459
479
  content: [
460
480
  {
461
481
  type: "text",
462
- text: JSON.stringify(memories, null, 2),
482
+ text: JSON.stringify(result, null, 2),
463
483
  },
464
484
  ],
465
485
  };
@@ -831,14 +851,25 @@ server.tool("create_task", "Create a task in VantagePeers. Tasks are assigned to
831
851
  // Tool: list_tasks
832
852
  // ─────────────────────────────────────────────────────────────────────────────
833
853
  server.tool("list_tasks", "List tasks from VantagePeers with optional filters. " +
834
- "Filter by assignee, instance, status, and/or project. Returns newest first.", {
854
+ "Filter by assignee, instance, status, and/or project. Returns newest first. " +
855
+ "status accepts a single value or an array, plus aliases: " +
856
+ "'open' (todo+in_progress+review+blocked), 'active' (todo+in_progress), 'all' (no filter). " +
857
+ "fields='lite' returns compact payloads ({_id,title,status,priority,assignedTo,missionId}) — fewer tokens.", {
835
858
  assignedTo: assigneeSchema.optional().describe("Filter by assignee"),
836
859
  assignedToInstance: z
837
860
  .string()
838
861
  .optional()
839
862
  .describe("Filter by instance — e.g. 'pi-vps'. Returns only tasks assigned to that instance."),
840
- status: taskStatusSchema.optional().describe("Filter by status"),
863
+ status: z
864
+ .union([taskStatusSchema, z.array(z.string()), z.string()])
865
+ .optional()
866
+ .describe("Filter by status. Single value, array, or alias: " +
867
+ "'open' (non-terminal), 'active' (in_progress only), 'all' (no filter)."),
841
868
  project: z.string().optional().describe("Filter by project name"),
869
+ fields: z
870
+ .enum(["lite", "full"])
871
+ .optional()
872
+ .describe("'lite' = compact payload (less tokens), 'full' = default with all fields"),
842
873
  limit: z
843
874
  .number()
844
875
  .int()
@@ -847,13 +878,14 @@ server.tool("list_tasks", "List tasks from VantagePeers with optional filters. "
847
878
  .optional()
848
879
  .default(50)
849
880
  .describe("Maximum number of tasks to return (default 50)"),
850
- }, async ({ assignedTo, assignedToInstance, status, project, limit }) => {
881
+ }, async ({ assignedTo, assignedToInstance, status, project, fields, limit }) => {
851
882
  try {
852
883
  const tasks = await convex.query("tasks:list", {
853
884
  assignedTo,
854
885
  assignedToInstance,
855
886
  status,
856
887
  project,
888
+ fields,
857
889
  limit: limit ?? 50,
858
890
  });
859
891
  return {
@@ -1102,9 +1134,20 @@ server.tool("add_task_dependency", "Add a dependency to a task. The task cannot
1102
1134
  // ─────────────────────────────────────────────────────────────────────────────
1103
1135
  // Tool: list_tasks_by_mission
1104
1136
  // ─────────────────────────────────────────────────────────────────────────────
1105
- server.tool("list_tasks_by_mission", "List all tasks linked to a specific mission. Optionally filter by status.", {
1137
+ server.tool("list_tasks_by_mission", "List all tasks linked to a specific mission. Optionally filter by status. " +
1138
+ "status accepts a single value or an array, plus aliases: " +
1139
+ "'open' (todo+in_progress+review+blocked), 'active' (todo+in_progress), 'all' (no filter). " +
1140
+ "fields='lite' returns compact payloads ({_id,title,status,priority,assignedTo,missionId}) — fewer tokens.", {
1106
1141
  missionId: z.string().describe("Convex document ID of the mission"),
1107
- status: taskStatusSchema.optional().describe("Filter by task status"),
1142
+ status: z
1143
+ .union([taskStatusSchema, z.array(z.string()), z.string()])
1144
+ .optional()
1145
+ .describe("Filter by task status. Single value, array, or alias: " +
1146
+ "'open' (non-terminal), 'active' (in_progress only), 'all' (no filter)."),
1147
+ fields: z
1148
+ .enum(["lite", "full"])
1149
+ .optional()
1150
+ .describe("'lite' = compact payload (less tokens), 'full' = default with all fields"),
1108
1151
  limit: z
1109
1152
  .number()
1110
1153
  .int()
@@ -1113,11 +1156,12 @@ server.tool("list_tasks_by_mission", "List all tasks linked to a specific missio
1113
1156
  .optional()
1114
1157
  .default(50)
1115
1158
  .describe("Maximum number of tasks to return (default 50)"),
1116
- }, async ({ missionId, status, limit }) => {
1159
+ }, async ({ missionId, status, fields, limit }) => {
1117
1160
  try {
1118
1161
  const tasks = await convex.query("tasks:listByMission", {
1119
1162
  missionId: missionId,
1120
1163
  status,
1164
+ fields,
1121
1165
  limit: limit ?? 50,
1122
1166
  });
1123
1167
  return {
@@ -1191,10 +1235,21 @@ server.tool("create_mission", "Create a mission in VantagePeers. Missions group
1191
1235
  // Tool: list_missions
1192
1236
  // ─────────────────────────────────────────────────────────────────────────────
1193
1237
  server.tool("list_missions", "List missions from VantagePeers with optional filters. " +
1194
- "Filter by project, pilot, and/or status. Returns newest first.", {
1238
+ "Filter by project, pilot, and/or status. Returns newest first. " +
1239
+ "status accepts a single value or an array, plus aliases: " +
1240
+ "'open' (brainstorm+plan+execute+validate), 'active' (plan+execute), 'all' (no filter). " +
1241
+ "fields='lite' returns compact payloads ({_id,name,status,pilot,priority,project}) — fewer tokens.", {
1195
1242
  project: z.string().optional().describe("Filter by project name"),
1196
1243
  pilot: creatorSchema.optional().describe("Filter by pilot orchestrator"),
1197
- status: missionStatusSchema.optional().describe("Filter by status"),
1244
+ status: z
1245
+ .union([missionStatusSchema, z.array(z.string()), z.string()])
1246
+ .optional()
1247
+ .describe("Filter by status. Single value, array, or alias: " +
1248
+ "'open' (non-terminal), 'active' (plan+execute), 'all' (no filter)."),
1249
+ fields: z
1250
+ .enum(["lite", "full"])
1251
+ .optional()
1252
+ .describe("'lite' = compact payload (less tokens), 'full' = default with all fields"),
1198
1253
  limit: z
1199
1254
  .number()
1200
1255
  .int()
@@ -1203,12 +1258,13 @@ server.tool("list_missions", "List missions from VantagePeers with optional filt
1203
1258
  .optional()
1204
1259
  .default(50)
1205
1260
  .describe("Maximum number of missions to return (default 50)"),
1206
- }, async ({ project, pilot, status, limit }) => {
1261
+ }, async ({ project, pilot, status, fields, limit }) => {
1207
1262
  try {
1208
1263
  const missions = await convex.query("missions:list", {
1209
1264
  project,
1210
1265
  pilot,
1211
1266
  status,
1267
+ fields,
1212
1268
  limit: limit ?? 50,
1213
1269
  });
1214
1270
  return {
@@ -1413,6 +1469,31 @@ server.tool("list_diaries", "List diary entries, optionally filtered by orchestr
1413
1469
  }
1414
1470
  });
1415
1471
  // ─────────────────────────────────────────────────────────────────────────────
1472
+ // Tool: delete_diary
1473
+ // ─────────────────────────────────────────────────────────────────────────────
1474
+ server.tool("delete_diary", "Permanently delete a diary entry by ID. Only the owner (or system) can delete.", {
1475
+ diaryId: z.string().describe("Convex document ID of the diary entry to delete"),
1476
+ callerOrchestrator: creatorSchema.optional().describe("Optional RBAC — must be the owner or system"),
1477
+ }, async ({ diaryId, callerOrchestrator }) => {
1478
+ try {
1479
+ const result = await convex.mutation("diary:deleteDiary", {
1480
+ diaryId: diaryId,
1481
+ callerOrchestrator,
1482
+ });
1483
+ return {
1484
+ content: [
1485
+ {
1486
+ type: "text",
1487
+ text: JSON.stringify(result, null, 2),
1488
+ },
1489
+ ],
1490
+ };
1491
+ }
1492
+ catch (error) {
1493
+ return mcpError(error.message ?? String(error));
1494
+ }
1495
+ });
1496
+ // ─────────────────────────────────────────────────────────────────────────────
1416
1497
  // Tool: create_briefing_note
1417
1498
  // ─────────────────────────────────────────────────────────────────────────────
1418
1499
  server.tool("create_briefing_note", "Create a briefing note — a structured record of a topic discussion, with participants, " +
@@ -1511,11 +1592,16 @@ server.tool("update_briefing_note", "Update an existing briefing note. Partial-u
1511
1592
  // ─────────────────────────────────────────────────────────────────────────────
1512
1593
  // Tool: list_briefing_notes
1513
1594
  // ─────────────────────────────────────────────────────────────────────────────
1514
- server.tool("list_briefing_notes", "List briefing notes, optionally filtered by topic. Returns newest first.", {
1595
+ server.tool("list_briefing_notes", "List briefing notes, optionally filtered by topic. Returns newest first. " +
1596
+ "fields='lite' returns compact payloads ({_id,topic,title,participants,createdBy}) — fewer tokens.", {
1515
1597
  topic: z
1516
1598
  .string()
1517
1599
  .optional()
1518
1600
  .describe("Filter to a specific topic — omit for all"),
1601
+ fields: z
1602
+ .enum(["lite", "full"])
1603
+ .optional()
1604
+ .describe("'lite' = compact payload (less tokens), 'full' = default with all fields"),
1519
1605
  limit: z
1520
1606
  .number()
1521
1607
  .int()
@@ -1524,10 +1610,11 @@ server.tool("list_briefing_notes", "List briefing notes, optionally filtered by
1524
1610
  .optional()
1525
1611
  .default(20)
1526
1612
  .describe("Maximum notes to return (default 20)"),
1527
- }, async ({ topic, limit }) => {
1613
+ }, async ({ topic, fields, limit }) => {
1528
1614
  try {
1529
1615
  const notes = await convex.query("briefingNotes:list", {
1530
1616
  topic,
1617
+ fields,
1531
1618
  limit: limit ?? 20,
1532
1619
  });
1533
1620
  return {
@@ -1544,6 +1631,31 @@ server.tool("list_briefing_notes", "List briefing notes, optionally filtered by
1544
1631
  }
1545
1632
  });
1546
1633
  // ─────────────────────────────────────────────────────────────────────────────
1634
+ // Tool: delete_briefing_note
1635
+ // ─────────────────────────────────────────────────────────────────────────────
1636
+ server.tool("delete_briefing_note", "Permanently delete a briefing note by ID. Only the creator (or system) can delete.", {
1637
+ noteId: z.string().describe("Convex document ID of the briefing note to delete"),
1638
+ callerOrchestrator: creatorSchema.optional().describe("Optional RBAC — must be creator or system"),
1639
+ }, async ({ noteId, callerOrchestrator }) => {
1640
+ try {
1641
+ const result = await convex.mutation("briefingNotes:deleteBriefingNote", {
1642
+ noteId: noteId,
1643
+ callerOrchestrator,
1644
+ });
1645
+ return {
1646
+ content: [
1647
+ {
1648
+ type: "text",
1649
+ text: JSON.stringify(result, null, 2),
1650
+ },
1651
+ ],
1652
+ };
1653
+ }
1654
+ catch (error) {
1655
+ return mcpError(error.message ?? String(error));
1656
+ }
1657
+ });
1658
+ // ─────────────────────────────────────────────────────────────────────────────
1547
1659
  // Tool: register_component
1548
1660
  // ─────────────────────────────────────────────────────────────────────────────
1549
1661
  server.tool("register_component", "Register or update a component (agent, skill, hook, or plugin) in the registry. " +
package/dist/src/auth.js CHANGED
@@ -212,7 +212,48 @@ export function bearerAuthMiddleware() {
212
212
  await next();
213
213
  return;
214
214
  }
215
- // ── (3) Legacy internal bearermcpTenants table ───────────────────────
215
+ // ── (3) DCR OAuth tokencheck oauthTokens via oauthDcr:validateAccessToken
216
+ // Uses raw token (not hashed) — the DCR table stores tokens in plaintext.
217
+ // This path handles Claude.ai clients registered via POST /register.
218
+ let dcrResult = null;
219
+ try {
220
+ dcrResult = (await internalClient().query(
221
+ // biome-ignore lint/suspicious/noExplicitAny: Convex string API
222
+ "oauthDcr:validateAccessToken", { accessToken: token }));
223
+ }
224
+ catch (err) {
225
+ const message = err instanceof Error ? err.message : String(err);
226
+ console.warn("[auth] DCR OAuth lookup skipped:", message);
227
+ }
228
+ if (dcrResult?.valid === true) {
229
+ const internalUrl = process.env.CONVEX_URL_INTERNAL;
230
+ if (!internalUrl) {
231
+ console.error("[auth] CONVEX_URL_INTERNAL not set — cannot route DCR OAuth token");
232
+ return c.json({ error: "Server misconfigured: internal deployment URL missing" }, 500);
233
+ }
234
+ // Map DCR single-scope string → OAuthContext fields.
235
+ // DCR tokens always carry "mcp:full" which maps to full access.
236
+ const scopes = dcrResult.scope.split(/\s+/).filter(Boolean);
237
+ const isFull = scopes.includes("mcp:full");
238
+ c.set("tenant", {
239
+ tenantName: `dcr:${dcrResult.clientId}`,
240
+ convexUrl: internalUrl,
241
+ });
242
+ c.set("oauthContext", {
243
+ clientId: dcrResult.clientId,
244
+ userId: dcrResult.clientId,
245
+ scopes,
246
+ scopeProfile: isFull ? "master" : "client-generic",
247
+ fromAllowList: isFull ? ["*"] : [],
248
+ namespaceReadPrefixes: isFull ? ["*"] : [],
249
+ namespaceWritePrefixes: isFull ? ["*"] : [],
250
+ expiresAt: dcrResult.expiresAt,
251
+ isMaster: false,
252
+ });
253
+ await next();
254
+ return;
255
+ }
256
+ // ── (4) Legacy internal bearer — mcpTenants table ───────────────────────
216
257
  let tenant;
217
258
  try {
218
259
  tenant = (await internalClient().query(
package/dist/src/tools.js CHANGED
@@ -59,7 +59,7 @@ const memoryTypeSchema = z
59
59
  .describe("Memory classification type");
60
60
  export const creatorSchema = z
61
61
  .string()
62
- .describe("Orchestrator role name (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, laurent, or any custom client role (lowercase string)). " +
62
+ .describe("Orchestrator role name (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, epsilon, omicron, upsilon, laurent, or any custom client role (lowercase string)). " +
63
63
  "New internal orchestrators use Greek letters (lowercase); external client orchestrators use free lowercase strings.");
64
64
  export const severitySchema = z
65
65
  .enum(["critical", "major", "minor"])
@@ -105,7 +105,7 @@ export const updateBriefingNoteSchema = z.object({
105
105
  });
106
106
  const assigneeSchema = z
107
107
  .string()
108
- .describe("Orchestrator to assign to (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, laurent, or any custom client role (lowercase string)). " +
108
+ .describe("Orchestrator to assign to (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, epsilon, omicron, upsilon, laurent, or any custom client role (lowercase string)). " +
109
109
  "New internal orchestrators use Greek letters (lowercase); external client orchestrators use free lowercase strings.");
110
110
  const prioritySchema = z
111
111
  .enum(["urgent", "high", "medium", "low"])
@@ -615,7 +615,7 @@ export function registerTools(server, convex, oauthCtx) {
615
615
  server.tool("send_message", "Send a message to one, many, or all orchestrators. " +
616
616
  "channel: 'broadcast' = all, 'tau' = role DM, 'pi-vps' = instance DM, 'tau,phi' = multi. " +
617
617
  "Creates message + one receipt per recipient. Replaces claude-peers send_message.", {
618
- from: creatorSchema.describe("Sender role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, or any custom role)"),
618
+ from: creatorSchema.describe("Sender role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, epsilon, omicron, upsilon, or any custom role)"),
619
619
  fromInstanceId: z
620
620
  .string()
621
621
  .optional()
@@ -673,7 +673,7 @@ export function registerTools(server, convex, oauthCtx) {
673
673
  server.tool("check_messages", "Check for unread messages. Returns messages with receiptIds for marking as read. " +
674
674
  "If recipientInstanceId is provided, returns instance-targeted + role-level messages. " +
675
675
  "Replaces claude-peers check_messages.", {
676
- recipient: creatorSchema.describe("Orchestrator role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, or any custom role)"),
676
+ recipient: creatorSchema.describe("Orchestrator role (e.g. pi, tau, phi, sigma, omega, zeta, eta, kappa, alpha, lambda, victor, epsilon, omicron, upsilon, or any custom role)"),
677
677
  recipientInstanceId: z
678
678
  .string()
679
679
  .optional()
@@ -2536,7 +2536,7 @@ export function registerTools(server, convex, oauthCtx) {
2536
2536
  server.tool("add_repo_mapping", "Add or update a GitHub repo → orchestrator mapping. Used by the webhook pipeline to route GitHub events to the right orchestrator.", {
2537
2537
  repo: z
2538
2538
  .string()
2539
- .describe("Full repo name — e.g. 'elpiarthera/vantage-peers'"),
2539
+ .describe("Full repo name — e.g. 'vantageos-agency/vantage-peers'"),
2540
2540
  orchestrator: z
2541
2541
  .string()
2542
2542
  .describe("Target orchestrator — e.g. 'sigma', 'omega', 'tau'"),
@@ -2590,7 +2590,7 @@ export function registerTools(server, convex, oauthCtx) {
2590
2590
  server.tool("remove_repo_mapping", "Remove a GitHub repo mapping by repo name. Stops routing webhook events for this repo.", {
2591
2591
  repo: z
2592
2592
  .string()
2593
- .describe("Full repo name to remove — e.g. 'elpiarthera/vantage-peers'"),
2593
+ .describe("Full repo name to remove — e.g. 'vantageos-agency/vantage-peers'"),
2594
2594
  }, async ({ repo }) => {
2595
2595
  try {
2596
2596
  const result = await convex.mutation("githubRepoMapping:remove", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vantage-peers-mcp",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "MCP server for VantagePeers — shared memory, messaging, and task coordination for AI agent teams",
5
5
  "type": "module",
6
6
  "main": "./dist/server.js",
@@ -16,7 +16,8 @@
16
16
  },
17
17
  "files": [
18
18
  "dist/",
19
- "README.md"
19
+ "README.md",
20
+ "CHANGELOG.md"
20
21
  ],
21
22
  "scripts": {
22
23
  "generate:api": "cd .. && npx convex-helpers ts-api-spec --prod --output-file mcp-server/api.ts",
@@ -56,7 +57,7 @@
56
57
  "author": "ElPi Corp",
57
58
  "license": "FSL-1.1-Apache-2.0",
58
59
  "dependencies": {
59
- "@modelcontextprotocol/sdk": "^1.27.1",
60
+ "@modelcontextprotocol/sdk": "^1.29.0",
60
61
  "convex": "^1.34.0",
61
62
  "dotenv": "^17.4.2",
62
63
  "zod": "^4.3.6"
@@ -68,6 +69,9 @@
68
69
  "typescript": "^5.9.3",
69
70
  "@types/node": "^24.12.2"
70
71
  },
72
+ "overrides": {
73
+ "path-to-regexp": "^8.4.0"
74
+ },
71
75
  "engines": {
72
76
  "node": ">=18"
73
77
  },