vantage-peers-mcp 2.4.1 → 2.4.3

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/README.md CHANGED
@@ -400,6 +400,9 @@ All orchestrator names are open strings — any lowercase name is accepted. The
400
400
 
401
401
  ## Changelog
402
402
 
403
+ ### 2.4.3 — 2026-05-31 (Day 89)
404
+ - fix(overflow): defensive byte-cap on all 17 `list_*` tools — `capListResponseBytes` truncates any list response above 60 KB and wraps the result in a `_meta` envelope (`_truncated`, `_showing`, `_total`, `_advice`) so MCP clients (Claude.ai, ChatGPT, Claude Code) never reject a list result for exceeding their token budget. Day 89 Pi 75,003-char `list_tasks` overflow incident reproduced and capped in regression test. PR #565.
405
+
403
406
  ### 2.4.1 — 2026-05-30 (Day 88)
404
407
  - fix(dcr): `oauthDcr:validateAccessToken` exposed as PUBLIC `query` (was `internalQuery`, unreachable via `ConvexHttpClient.query()` → Path 3 DCR returned 401 even with valid token) — issue #556 / PR #557.
405
408
  - fix(dcr): `WWW-Authenticate` header now emits `Bearer resource_metadata="..."` per MCP spec §Protected Resource Metadata Discovery (was `resource="..."` — broke Claude.ai PRM discovery bootstrap on 401) — PR #557.
package/dist/src/auth.js CHANGED
@@ -119,8 +119,20 @@ export function checkNamespaceRead(ctx, namespace) {
119
119
  return null;
120
120
  if (isMasterScope(ctx))
121
121
  return null;
122
- if (!namespace)
123
- return null; // no namespace filter list-across, which master-only in practice
122
+ if (!namespace) {
123
+ // Day 88 P0 fix: a list-across (namespace undefined) call from a
124
+ // non-master scope cannot be served safely — the underlying query
125
+ // returns rows across every tenant. Reject with an explicit message
126
+ // telling the caller to pass a namespace they own. Previously this
127
+ // returned null and leaked the whole memories/profiles/etc. table
128
+ // to any DCR-issued client.
129
+ const allowed = ctx.namespaceReadPrefixes.length > 0
130
+ ? ctx.namespaceReadPrefixes.join(", ")
131
+ : "(none — your client has no read scope)";
132
+ return ("Forbidden: this tool requires an explicit namespace argument when " +
133
+ `called with a non-master scope (current: ${ctx.scopeProfile}). ` +
134
+ `Pass namespace= one of: ${allowed}.`);
135
+ }
124
136
  if (checkNamespacePrefix(ctx.namespaceReadPrefixes, namespace))
125
137
  return null;
126
138
  return `Forbidden: namespace='${namespace}' is not readable by scope_profile=${ctx.scopeProfile}.`;
@@ -21,6 +21,8 @@ export declare const MAX_CONTENT_BYTES = 900000;
21
21
  * @param toolName Caller tool name (used only in the error message).
22
22
  */
23
23
  export declare function assertContentSize(content: string, toolName: string): number;
24
+ export declare const MAX_LIST_RESPONSE_BYTES = 60000;
25
+ export declare function capListResponseBytes(items: unknown, rawText: string, toolName: string, maxBytes?: number): string;
24
26
  /**
25
27
  * Convex document IDs are 32 lowercase alphanumeric characters (a-z0-9).
26
28
  * Exported so tests can validate the schema independently of the MCP server.
package/dist/src/tools.js CHANGED
@@ -71,6 +71,56 @@ export function assertContentSize(content, toolName) {
71
71
  return contentBytes;
72
72
  }
73
73
  // ─────────────────────────────────────────────────────────────────────────────
74
+ // List response byte cap (overflow protection for MCP clients)
75
+ //
76
+ // MCP clients (Claude.ai, ChatGPT, Claude Code) reject tool results that
77
+ // exceed their token budget — typical ceiling ~25k tokens ≈ 75 KB JSON.
78
+ // When that happens the entire response is lost to a downstream truncation
79
+ // error and the user must fall back to reading the on-disk overflow file.
80
+ //
81
+ // `capListResponseBytes` guards every bulk list_* tool: if the serialized
82
+ // payload exceeds MAX_LIST_RESPONSE_BYTES (60 KB), it truncates the items
83
+ // array (halving until it fits) and wraps the result in a _meta envelope
84
+ // that tells the caller exactly how to refine the query.
85
+ //
86
+ // 60 KB leaves headroom for tool-call JSON framing and the UI stream marker
87
+ // before any MCP client hits its own ceiling. The cap is byte-counted on the
88
+ // raw JSON string, not on item count, because content-heavy rows
89
+ // (memories / diaries / briefing notes) blow past 30 items easily.
90
+ // ─────────────────────────────────────────────────────────────────────────────
91
+ export const MAX_LIST_RESPONSE_BYTES = 60_000;
92
+ export function capListResponseBytes(items, rawText, toolName, maxBytes = MAX_LIST_RESPONSE_BYTES) {
93
+ const byteLen = Buffer.byteLength(rawText, "utf8");
94
+ if (byteLen <= maxBytes)
95
+ return rawText;
96
+ if (!Array.isArray(items) || items.length === 0)
97
+ return rawText;
98
+ let n = items.length;
99
+ let truncated = items;
100
+ let truncatedText = rawText;
101
+ while (n > 1) {
102
+ n = Math.max(1, Math.floor(n / 2));
103
+ truncated = items.slice(0, n);
104
+ truncatedText = JSON.stringify(truncated, null, 2);
105
+ if (Buffer.byteLength(truncatedText, "utf8") <= maxBytes - 600)
106
+ break;
107
+ }
108
+ const envelope = {
109
+ _meta: {
110
+ _truncated: true,
111
+ _showing: truncated.length,
112
+ _total: items.length,
113
+ _bytesOriginal: byteLen,
114
+ _bytesCap: maxBytes,
115
+ _tool: toolName,
116
+ _advice: `Response exceeded ${maxBytes} bytes. Showing first ${truncated.length}/${items.length}. ` +
117
+ `Pass fields="lite", a smaller limit, stricter filters (status, assignedTo, project, namespace, updatedSince), or paginate.`,
118
+ },
119
+ items: truncated,
120
+ };
121
+ return JSON.stringify(envelope, null, 2);
122
+ }
123
+ // ─────────────────────────────────────────────────────────────────────────────
74
124
  // Shared Zod schemas
75
125
  // ─────────────────────────────────────────────────────────────────────────────
76
126
  /**
@@ -508,6 +558,9 @@ export function registerTools(server, convex, oauthCtx) {
508
558
  title: "Get memory",
509
559
  }, async ({ memoryId }) => {
510
560
  try {
561
+ const _scopeDenied = guardMasterOnly("get_memory");
562
+ if (_scopeDenied)
563
+ return _scopeDenied;
511
564
  const memory = await convex.query("memories:getMemory", {
512
565
  memoryId,
513
566
  });
@@ -725,6 +778,9 @@ export function registerTools(server, convex, oauthCtx) {
725
778
  title: "Get orchestrator profile",
726
779
  }, async ({ orchestratorId }) => {
727
780
  try {
781
+ const _scopeDenied = guardMasterOnly("get_profile");
782
+ if (_scopeDenied)
783
+ return _scopeDenied;
728
784
  const profile = await convex.query("profiles:getProfile", {
729
785
  orchestratorId,
730
786
  });
@@ -837,7 +893,7 @@ export function registerTools(server, convex, oauthCtx) {
837
893
  : Array.isArray(memories?.page)
838
894
  ? memories.page
839
895
  : [];
840
- const baseText = JSON.stringify(memories, null, 2);
896
+ const baseText = capListResponseBytes(memories, JSON.stringify(memories, null, 2), "list_memories");
841
897
  const text = appendMarkerIfEnabled(baseText, () => ({
842
898
  kind: "memory-quote",
843
899
  items: rawList.map((m) => ({
@@ -944,6 +1000,13 @@ export function registerTools(server, convex, oauthCtx) {
944
1000
  title: "Check messages",
945
1001
  }, async ({ recipient, recipientInstanceId, tenantId, since }) => {
946
1002
  try {
1003
+ // Non-master: force recipient to caller's own userId. Anything else
1004
+ // would let the client read another tenant's inbox.
1005
+ if (oauthCtx && !isMasterScope(oauthCtx)) {
1006
+ if (recipient !== oauthCtx.userId) {
1007
+ return mcpError(`Forbidden: check_messages can only read messages for your own identity ('${oauthCtx.userId}'), not '${recipient}'.`);
1008
+ }
1009
+ }
947
1010
  const messages = await convex.query("messages:checkNewMessages", {
948
1011
  recipient,
949
1012
  recipientInstanceId,
@@ -1105,6 +1168,9 @@ export function registerTools(server, convex, oauthCtx) {
1105
1168
  title: "List peers",
1106
1169
  }, async () => {
1107
1170
  try {
1171
+ const _scopeDenied = guardMasterOnly("list_peers");
1172
+ if (_scopeDenied)
1173
+ return _scopeDenied;
1108
1174
  const profiles = await convex.query("profiles:listProfiles", {});
1109
1175
  const peers = profiles.map((p) => ({
1110
1176
  id: p.orchestratorId,
@@ -1120,7 +1186,7 @@ export function registerTools(server, convex, oauthCtx) {
1120
1186
  content: [
1121
1187
  {
1122
1188
  type: "text",
1123
- text: JSON.stringify(peers, null, 2),
1189
+ text: capListResponseBytes(peers, JSON.stringify(peers, null, 2), "list_peers"),
1124
1190
  },
1125
1191
  ],
1126
1192
  };
@@ -1152,12 +1218,15 @@ export function registerTools(server, convex, oauthCtx) {
1152
1218
  title: "List messages",
1153
1219
  }, async ({ sessionDay, from, limit }) => {
1154
1220
  try {
1221
+ const _scopeDenied = guardMasterOnly("list_messages");
1222
+ if (_scopeDenied)
1223
+ return _scopeDenied;
1155
1224
  const messages = await convex.query("messages:listMessages", {
1156
1225
  sessionDay,
1157
1226
  from,
1158
1227
  limit: limit ?? 100,
1159
1228
  });
1160
- const baseText = JSON.stringify(messages, null, 2);
1229
+ const baseText = capListResponseBytes(messages, JSON.stringify(messages, null, 2), "list_messages");
1161
1230
  const text = appendMarkerIfEnabled(baseText, () => ({
1162
1231
  kind: "messages-feed",
1163
1232
  items: Array.isArray(messages)
@@ -1190,6 +1259,9 @@ export function registerTools(server, convex, oauthCtx) {
1190
1259
  title: "List broadcast status",
1191
1260
  }, async ({ messageId }) => {
1192
1261
  try {
1262
+ const _scopeDenied = guardMasterOnly("list_broadcast_status");
1263
+ if (_scopeDenied)
1264
+ return _scopeDenied;
1193
1265
  const status = await convex.query("messages:listBroadcastStatus", {
1194
1266
  messageId,
1195
1267
  });
@@ -1197,7 +1269,7 @@ export function registerTools(server, convex, oauthCtx) {
1197
1269
  content: [
1198
1270
  {
1199
1271
  type: "text",
1200
- text: JSON.stringify(status, null, 2),
1272
+ text: capListResponseBytes(status, JSON.stringify(status, null, 2), "list_broadcast_status"),
1201
1273
  },
1202
1274
  ],
1203
1275
  };
@@ -1314,6 +1386,16 @@ export function registerTools(server, convex, oauthCtx) {
1314
1386
  title: "List tasks",
1315
1387
  }, async ({ assignedTo, assignedToInstance, status, project, limit, fields, createdBy, updatedSince, }) => {
1316
1388
  try {
1389
+ // Non-master: must scope to own identity. If neither assignedTo
1390
+ // nor createdBy matches the caller's userId, reject — otherwise
1391
+ // the query would span the whole tenant table.
1392
+ if (oauthCtx && !isMasterScope(oauthCtx)) {
1393
+ const myId = oauthCtx.userId;
1394
+ const scopedToSelf = assignedTo === myId || createdBy === myId;
1395
+ if (!scopedToSelf) {
1396
+ return mcpError(`Forbidden: list_tasks requires assignedTo='${myId}' or createdBy='${myId}' for non-master scope (current: ${oauthCtx.scopeProfile}).`);
1397
+ }
1398
+ }
1317
1399
  const tasks = await convex.query("tasks:list", {
1318
1400
  assignedTo,
1319
1401
  assignedToInstance,
@@ -1324,7 +1406,7 @@ export function registerTools(server, convex, oauthCtx) {
1324
1406
  createdBy,
1325
1407
  updatedSince,
1326
1408
  });
1327
- const baseText = JSON.stringify(tasks, null, 2);
1409
+ const baseText = capListResponseBytes(tasks, JSON.stringify(tasks, null, 2), "list_tasks");
1328
1410
  const text = appendMarkerIfEnabled(baseText, () => ({
1329
1411
  kind: "tasks-table",
1330
1412
  items: Array.isArray(tasks)
@@ -1695,6 +1777,9 @@ export function registerTools(server, convex, oauthCtx) {
1695
1777
  title: "List tasks by mission",
1696
1778
  }, async ({ missionId, status, limit, fields, createdBy, updatedSince }) => {
1697
1779
  try {
1780
+ const _scopeDenied = guardMasterOnly("list_tasks_by_mission");
1781
+ if (_scopeDenied)
1782
+ return _scopeDenied;
1698
1783
  const tasks = await convex.query("tasks:listByMission", {
1699
1784
  missionId: missionId,
1700
1785
  status,
@@ -1707,7 +1792,7 @@ export function registerTools(server, convex, oauthCtx) {
1707
1792
  content: [
1708
1793
  {
1709
1794
  type: "text",
1710
- text: JSON.stringify(tasks, null, 2),
1795
+ text: capListResponseBytes(tasks, JSON.stringify(tasks, null, 2), "list_tasks_by_mission"),
1711
1796
  },
1712
1797
  ],
1713
1798
  };
@@ -1802,6 +1887,13 @@ export function registerTools(server, convex, oauthCtx) {
1802
1887
  title: "List missions",
1803
1888
  }, async ({ project, pilot, status, limit, fields, updatedSince }) => {
1804
1889
  try {
1890
+ // Non-master: must pilot=<own-userId>. Otherwise the query spans
1891
+ // every tenant's missions.
1892
+ if (oauthCtx && !isMasterScope(oauthCtx)) {
1893
+ if (pilot !== oauthCtx.userId) {
1894
+ return mcpError(`Forbidden: list_missions requires pilot='${oauthCtx.userId}' for non-master scope (current: ${oauthCtx.scopeProfile}).`);
1895
+ }
1896
+ }
1805
1897
  const missions = await convex.query("missions:list", {
1806
1898
  project,
1807
1899
  pilot,
@@ -1810,7 +1902,7 @@ export function registerTools(server, convex, oauthCtx) {
1810
1902
  fields,
1811
1903
  updatedSince,
1812
1904
  });
1813
- const baseText = JSON.stringify(missions, null, 2);
1905
+ const baseText = capListResponseBytes(missions, JSON.stringify(missions, null, 2), "list_missions");
1814
1906
  const text = appendMarkerIfEnabled(baseText, () => ({
1815
1907
  kind: "mission-timeline",
1816
1908
  items: Array.isArray(missions)
@@ -1843,6 +1935,9 @@ export function registerTools(server, convex, oauthCtx) {
1843
1935
  title: "Get mission",
1844
1936
  }, async ({ missionId }) => {
1845
1937
  try {
1938
+ const _scopeDenied = guardMasterOnly("get_mission");
1939
+ if (_scopeDenied)
1940
+ return _scopeDenied;
1846
1941
  const mission = await convex.query("missions:get", {
1847
1942
  missionId: missionId,
1848
1943
  });
@@ -2002,6 +2097,9 @@ export function registerTools(server, convex, oauthCtx) {
2002
2097
  title: "Get diary entry",
2003
2098
  }, async ({ date, orchestrator }) => {
2004
2099
  try {
2100
+ const _scopeDenied = guardMasterOnly("get_diary");
2101
+ if (_scopeDenied)
2102
+ return _scopeDenied;
2005
2103
  const entry = await convex.query("diary:get", {
2006
2104
  date,
2007
2105
  orchestrator,
@@ -2050,6 +2148,12 @@ export function registerTools(server, convex, oauthCtx) {
2050
2148
  title: "List diary entries",
2051
2149
  }, async ({ orchestrator, limit }) => {
2052
2150
  try {
2151
+ // Non-master: must scope to own orchestrator id.
2152
+ if (oauthCtx && !isMasterScope(oauthCtx)) {
2153
+ if (orchestrator !== oauthCtx.userId) {
2154
+ return mcpError(`Forbidden: list_diaries requires orchestrator='${oauthCtx.userId}' for non-master scope (current: ${oauthCtx.scopeProfile}).`);
2155
+ }
2156
+ }
2053
2157
  const entries = await convex.query("diary:list", {
2054
2158
  orchestrator,
2055
2159
  limit: limit ?? 20,
@@ -2058,7 +2162,7 @@ export function registerTools(server, convex, oauthCtx) {
2058
2162
  content: [
2059
2163
  {
2060
2164
  type: "text",
2061
- text: JSON.stringify(entries, null, 2),
2165
+ text: capListResponseBytes(entries, JSON.stringify(entries, null, 2), "list_diaries"),
2062
2166
  },
2063
2167
  ],
2064
2168
  };
@@ -2203,13 +2307,16 @@ export function registerTools(server, convex, oauthCtx) {
2203
2307
  title: "List briefing notes",
2204
2308
  }, async ({ topic, limit, fields, updatedSince }) => {
2205
2309
  try {
2310
+ const _scopeDenied = guardMasterOnly("list_briefing_notes");
2311
+ if (_scopeDenied)
2312
+ return _scopeDenied;
2206
2313
  const notes = await convex.query("briefingNotes:list", {
2207
2314
  topic,
2208
2315
  limit,
2209
2316
  fields,
2210
2317
  updatedSince,
2211
2318
  });
2212
- const baseText = JSON.stringify(notes, null, 2);
2319
+ const baseText = capListResponseBytes(notes, JSON.stringify(notes, null, 2), "list_briefing_notes");
2213
2320
  const text = appendMarkerIfEnabled(baseText, () => {
2214
2321
  const items = Array.isArray(notes) ? notes : [];
2215
2322
  if (items.length === 0)
@@ -2316,6 +2423,9 @@ export function registerTools(server, convex, oauthCtx) {
2316
2423
  title: "List components",
2317
2424
  }, async ({ type, team, limit }) => {
2318
2425
  try {
2426
+ const _scopeDenied = guardMasterOnly("list_components");
2427
+ if (_scopeDenied)
2428
+ return _scopeDenied;
2319
2429
  const components = await convex.query("components:list", {
2320
2430
  type,
2321
2431
  team,
@@ -2325,7 +2435,7 @@ export function registerTools(server, convex, oauthCtx) {
2325
2435
  content: [
2326
2436
  {
2327
2437
  type: "text",
2328
- text: JSON.stringify(components, null, 2),
2438
+ text: capListResponseBytes(components, JSON.stringify(components, null, 2), "list_components"),
2329
2439
  },
2330
2440
  ],
2331
2441
  };
@@ -2345,6 +2455,9 @@ export function registerTools(server, convex, oauthCtx) {
2345
2455
  title: "Get component",
2346
2456
  }, async ({ name, type }) => {
2347
2457
  try {
2458
+ const _scopeDenied = guardMasterOnly("get_component");
2459
+ if (_scopeDenied)
2460
+ return _scopeDenied;
2348
2461
  const component = await convex.query("components:get", {
2349
2462
  name,
2350
2463
  type,
@@ -2442,6 +2555,9 @@ export function registerTools(server, convex, oauthCtx) {
2442
2555
  title: "Search components",
2443
2556
  }, async ({ query, type, limit }) => {
2444
2557
  try {
2558
+ const _scopeDenied = guardMasterOnly("search_components");
2559
+ if (_scopeDenied)
2560
+ return _scopeDenied;
2445
2561
  const results = await convex.query("components:search", {
2446
2562
  query,
2447
2563
  type,
@@ -2532,13 +2648,16 @@ export function registerTools(server, convex, oauthCtx) {
2532
2648
  title: "List recurring tasks",
2533
2649
  }, async ({ assignedTo, active, limit }) => {
2534
2650
  try {
2651
+ const _scopeDenied = guardMasterOnly("list_recurring_tasks");
2652
+ if (_scopeDenied)
2653
+ return _scopeDenied;
2535
2654
  const tasks = await convex.query("recurringTasks:list", {
2536
2655
  assignedTo,
2537
2656
  active,
2538
2657
  limit: limit ?? 50,
2539
2658
  });
2540
2659
  return {
2541
- content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }],
2660
+ content: [{ type: "text", text: capListResponseBytes(tasks, JSON.stringify(tasks, null, 2), "list_recurring_tasks") }],
2542
2661
  };
2543
2662
  }
2544
2663
  catch (error) {
@@ -2875,6 +2994,9 @@ export function registerTools(server, convex, oauthCtx) {
2875
2994
  title: "List mandates",
2876
2995
  }, async ({ requestedBy, fulfilledBy, status, limit }) => {
2877
2996
  try {
2997
+ const _scopeDenied = guardMasterOnly("list_mandates");
2998
+ if (_scopeDenied)
2999
+ return _scopeDenied;
2878
3000
  const mandates = await convex.query("mandates:list", {
2879
3001
  requestedBy,
2880
3002
  fulfilledBy,
@@ -2885,7 +3007,7 @@ export function registerTools(server, convex, oauthCtx) {
2885
3007
  content: [
2886
3008
  {
2887
3009
  type: "text",
2888
- text: JSON.stringify(mandates, null, 2),
3010
+ text: capListResponseBytes(mandates, JSON.stringify(mandates, null, 2), "list_mandates"),
2889
3011
  },
2890
3012
  ],
2891
3013
  };
@@ -3041,6 +3163,9 @@ export function registerTools(server, convex, oauthCtx) {
3041
3163
  title: "Get BU",
3042
3164
  }, async ({ buId }) => {
3043
3165
  try {
3166
+ const _scopeDenied = guardMasterOnly("get_bu");
3167
+ if (_scopeDenied)
3168
+ return _scopeDenied;
3044
3169
  const bu = await convex.query("businessUnits:get", {
3045
3170
  buId: buId,
3046
3171
  });
@@ -3080,6 +3205,9 @@ export function registerTools(server, convex, oauthCtx) {
3080
3205
  title: "List BUs",
3081
3206
  }, async ({ orchestratorId, status, limit }) => {
3082
3207
  try {
3208
+ const _scopeDenied = guardMasterOnly("list_bus");
3209
+ if (_scopeDenied)
3210
+ return _scopeDenied;
3083
3211
  const bus = await convex.query("businessUnits:list", {
3084
3212
  orchestratorId,
3085
3213
  status,
@@ -3089,7 +3217,7 @@ export function registerTools(server, convex, oauthCtx) {
3089
3217
  content: [
3090
3218
  {
3091
3219
  type: "text",
3092
- text: JSON.stringify(bus, null, 2),
3220
+ text: capListResponseBytes(bus, JSON.stringify(bus, null, 2), "list_bus"),
3093
3221
  },
3094
3222
  ],
3095
3223
  };
@@ -3176,12 +3304,15 @@ export function registerTools(server, convex, oauthCtx) {
3176
3304
  title: "List repo mappings",
3177
3305
  }, async () => {
3178
3306
  try {
3307
+ const _scopeDenied = guardMasterOnly("list_repo_mappings");
3308
+ if (_scopeDenied)
3309
+ return _scopeDenied;
3179
3310
  const mappings = await convex.query("githubRepoMapping:list", {});
3180
3311
  return {
3181
3312
  content: [
3182
3313
  {
3183
3314
  type: "text",
3184
- text: JSON.stringify(mappings, null, 2),
3315
+ text: capListResponseBytes(mappings, JSON.stringify(mappings, null, 2), "list_repo_mappings"),
3185
3316
  },
3186
3317
  ],
3187
3318
  };
@@ -3247,6 +3378,9 @@ export function registerTools(server, convex, oauthCtx) {
3247
3378
  title: "List issues",
3248
3379
  }, async ({ project, status, assignedTo, limit }) => {
3249
3380
  try {
3381
+ const _scopeDenied = guardMasterOnly("list_issues");
3382
+ if (_scopeDenied)
3383
+ return _scopeDenied;
3250
3384
  let results;
3251
3385
  if (assignedTo) {
3252
3386
  results = await convex.query("issues:listByOrchestrator", {
@@ -3278,7 +3412,7 @@ export function registerTools(server, convex, oauthCtx) {
3278
3412
  content: [
3279
3413
  {
3280
3414
  type: "text",
3281
- text: JSON.stringify({ count: results.length, issues: results }, null, 2),
3415
+ text: capListResponseBytes(results, JSON.stringify({ count: results.length, issues: results }, null, 2), "list_issues"),
3282
3416
  },
3283
3417
  ],
3284
3418
  };
@@ -3300,6 +3434,9 @@ export function registerTools(server, convex, oauthCtx) {
3300
3434
  title: "Get issue",
3301
3435
  }, async ({ repo, issueNumber }) => {
3302
3436
  try {
3437
+ const _scopeDenied = guardMasterOnly("get_issue");
3438
+ if (_scopeDenied)
3439
+ return _scopeDenied;
3303
3440
  const issue = await convex.query("issues:getByRepoNumber", {
3304
3441
  repo,
3305
3442
  issueNumber,
@@ -3434,6 +3571,9 @@ export function registerTools(server, convex, oauthCtx) {
3434
3571
  title: "Issue statistics",
3435
3572
  }, async ({ project }) => {
3436
3573
  try {
3574
+ const _scopeDenied = guardMasterOnly("issue_stats");
3575
+ if (_scopeDenied)
3576
+ return _scopeDenied;
3437
3577
  const stats = await convex.query("issues:getStats", {
3438
3578
  project,
3439
3579
  });
@@ -3590,6 +3730,9 @@ export function registerTools(server, convex, oauthCtx) {
3590
3730
  title: "Search fix patterns",
3591
3731
  }, async ({ query, limit }) => {
3592
3732
  try {
3733
+ const _scopeDenied = guardMasterOnly("search_fix_patterns");
3734
+ if (_scopeDenied)
3735
+ return _scopeDenied;
3593
3736
  const results = await convex.action("search:searchFixPatterns", {
3594
3737
  query,
3595
3738
  limit,
@@ -3621,13 +3764,16 @@ export function registerTools(server, convex, oauthCtx) {
3621
3764
  title: "List fix patterns",
3622
3765
  }, async ({ project, limit }) => {
3623
3766
  try {
3767
+ const _scopeDenied = guardMasterOnly("list_fix_patterns");
3768
+ if (_scopeDenied)
3769
+ return _scopeDenied;
3624
3770
  if (project) {
3625
3771
  const results = await convex.query("fixPatterns:listByProject", {
3626
3772
  sourceProject: project,
3627
3773
  limit,
3628
3774
  });
3629
3775
  return {
3630
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
3776
+ content: [{ type: "text", text: capListResponseBytes(results, JSON.stringify(results, null, 2), "list_fix_patterns") }],
3631
3777
  };
3632
3778
  }
3633
3779
  const allResults = await convex.query("fixPatterns:listAll", {
@@ -3635,7 +3781,7 @@ export function registerTools(server, convex, oauthCtx) {
3635
3781
  });
3636
3782
  return {
3637
3783
  content: [
3638
- { type: "text", text: JSON.stringify(allResults, null, 2) },
3784
+ { type: "text", text: capListResponseBytes(allResults, JSON.stringify(allResults, null, 2), "list_fix_patterns") },
3639
3785
  ],
3640
3786
  };
3641
3787
  }
@@ -3682,6 +3828,9 @@ export function registerTools(server, convex, oauthCtx) {
3682
3828
  title: "Get mission template",
3683
3829
  }, async ({ name }) => {
3684
3830
  try {
3831
+ const _scopeDenied = guardMasterOnly("get_mission_template");
3832
+ if (_scopeDenied)
3833
+ return _scopeDenied;
3685
3834
  const template = await convex.query("missionTemplates:getByName", { name });
3686
3835
  return {
3687
3836
  content: [
@@ -3908,6 +4057,9 @@ export function registerTools(server, convex, oauthCtx) {
3908
4057
  title: "List errors",
3909
4058
  }, async ({ deployment, limit }) => {
3910
4059
  try {
4060
+ const _scopeDenied = guardMasterOnly("list_errors");
4061
+ if (_scopeDenied)
4062
+ return _scopeDenied;
3911
4063
  const errors = await convex.query("errorMonitor:listErrors", {
3912
4064
  deployment,
3913
4065
  limit: limit ?? 50,
@@ -3916,7 +4068,7 @@ export function registerTools(server, convex, oauthCtx) {
3916
4068
  content: [
3917
4069
  {
3918
4070
  type: "text",
3919
- text: JSON.stringify(errors, null, 2),
4071
+ text: capListResponseBytes(errors, JSON.stringify(errors, null, 2), "list_errors"),
3920
4072
  },
3921
4073
  ],
3922
4074
  };
@@ -3935,6 +4087,9 @@ export function registerTools(server, convex, oauthCtx) {
3935
4087
  title: "Get error",
3936
4088
  }, async ({ errorId }) => {
3937
4089
  try {
4090
+ const _scopeDenied = guardMasterOnly("get_error");
4091
+ if (_scopeDenied)
4092
+ return _scopeDenied;
3938
4093
  const error = await convex.query("errorMonitor:getError", {
3939
4094
  errorId: errorId,
3940
4095
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vantage-peers-mcp",
3
- "version": "2.4.1",
3
+ "version": "2.4.3",
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",