capsulemcp 1.7.0 → 1.8.1

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/dist/http.js CHANGED
@@ -26,6 +26,22 @@ var chainHandlers = {
26
26
  "capsule.request": (ctx) => {
27
27
  ctx.capsuleCalls += 1;
28
28
  },
29
+ // A timed-out or connection-failed call is still an attempt that
30
+ // never reaches the `capsule.request` emit (it throws at the fetch
31
+ // stage). Count it here so `tool.chain.capsuleCalls` stays honest and
32
+ // a chain whose duration ballooned is explained by a visible failure.
33
+ "capsule.timeout": (ctx) => {
34
+ ctx.capsuleCalls += 1;
35
+ },
36
+ "capsule.error": (ctx) => {
37
+ ctx.capsuleCalls += 1;
38
+ },
39
+ // A request that exhausted its 429 retry is a real (doubly-attempted)
40
+ // outbound call that throws before `capsule.request` fires — count it
41
+ // so a chain whose latency ballooned on rate-limit backoff is explained.
42
+ "capsule.ratelimit": (ctx) => {
43
+ ctx.capsuleCalls += 1;
44
+ },
29
45
  // Cache-hit events feed the aggregate so the chain stat is right
30
46
  // even on tools whose Capsule calls all hit the cache.
31
47
  "cache.hit": (ctx) => {
@@ -184,6 +200,15 @@ var CapsuleApiError = class extends Error {
184
200
  }
185
201
  status;
186
202
  };
203
+ var CapsuleTimeoutError = class extends CapsuleApiError {
204
+ constructor() {
205
+ super(
206
+ 504,
207
+ `Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
208
+ );
209
+ this.name = "CapsuleTimeoutError";
210
+ }
211
+ };
187
212
  function getToken() {
188
213
  const token = process.env["CAPSULE_API_TOKEN"];
189
214
  if (!token) {
@@ -246,72 +271,74 @@ async function parseErrorBody(res) {
246
271
  }
247
272
  }
248
273
  var REQUEST_TIMEOUT_MS = 6e4;
249
- function withTimeout(options) {
250
- if (options && options.signal !== void 0) {
251
- return { options, cleanup: () => {
252
- } };
253
- }
254
- const controller = new AbortController();
255
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
256
- timer.unref();
257
- return {
258
- options: { ...options ?? {}, signal: controller.signal },
259
- cleanup: () => clearTimeout(timer)
260
- };
274
+ function isTimeoutAbort(err) {
275
+ return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
276
+ // "TimeoutError"; plain aborts (and older undici paths) surface
277
+ // as "AbortError" or carry "aborted" in the message.
278
+ (err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
261
279
  }
262
280
  async function mapAbort(p) {
263
281
  try {
264
282
  return await p;
265
283
  } catch (err) {
266
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
267
- throw new CapsuleApiError(
268
- 504,
269
- `Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
270
- );
284
+ if (isTimeoutAbort(err)) {
285
+ throw new CapsuleTimeoutError();
271
286
  }
272
287
  throw err;
273
288
  }
274
289
  }
275
290
  async function fetchWithTimeout(url, options) {
276
- const { options: opts, cleanup } = withTimeout(options);
291
+ const startedAt = Date.now();
277
292
  try {
278
- const res = await fetch(url, opts);
279
- return { res, cleanup };
293
+ return await fetch(url, {
294
+ ...options ?? {},
295
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
296
+ });
280
297
  } catch (err) {
281
- cleanup();
282
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
283
- throw new CapsuleApiError(
284
- 504,
285
- `Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
286
- );
298
+ const isAbort = isTimeoutAbort(err);
299
+ emitCapsuleFailure(
300
+ options?.method ?? "GET",
301
+ url,
302
+ Date.now() - startedAt,
303
+ isAbort ? "timeout" : "network",
304
+ isAbort ? void 0 : err
305
+ );
306
+ if (isAbort) {
307
+ throw new CapsuleTimeoutError();
287
308
  }
288
309
  throw err;
289
310
  }
290
311
  }
312
+ async function drainBody(res) {
313
+ try {
314
+ await res.body?.cancel();
315
+ } catch {
316
+ }
317
+ }
291
318
  async function doFetch(url, options) {
292
319
  const startedAt = Date.now();
293
320
  const method = options?.method ?? "GET";
294
321
  const first = await fetchWithTimeout(url, options);
295
- if (first.res.status === 429) {
296
- const delay = parseRateLimitDelay(first.res);
297
- first.cleanup();
322
+ if (first.status === 429) {
323
+ const delay = parseRateLimitDelay(first);
324
+ await drainBody(first);
298
325
  await new Promise((resolve) => setTimeout(resolve, delay));
299
326
  const retried = await fetchWithTimeout(url, options);
300
- if (retried.res.status === 429) {
301
- retried.cleanup();
327
+ if (retried.status === 429) {
328
+ await drainBody(retried);
329
+ emitCapsuleRateLimited(method, url, Date.now() - startedAt);
302
330
  throw new CapsuleApiError(
303
331
  429,
304
332
  "Rate limit exceeded after one retry. Please slow down your requests."
305
333
  );
306
334
  }
307
- return { ...retried, startedAt, method, url, retriedAfter429: true };
335
+ return { res: retried, startedAt, method, url, retriedAfter429: true };
308
336
  }
309
- return { ...first, startedAt, method, url, retriedAfter429: false };
337
+ return { res: first, startedAt, method, url, retriedAfter429: false };
310
338
  }
311
339
  async function consumeBody(start, body) {
312
340
  try {
313
- return await body();
314
- } finally {
341
+ const result = await body();
315
342
  emitCapsuleRequest(
316
343
  start.method,
317
344
  start.url,
@@ -319,15 +346,31 @@ async function consumeBody(start, body) {
319
346
  Date.now() - start.startedAt,
320
347
  start.retriedAfter429
321
348
  );
349
+ return result;
350
+ } catch (err) {
351
+ if (err instanceof CapsuleTimeoutError) {
352
+ emitCapsuleFailure(start.method, start.url, Date.now() - start.startedAt, "timeout");
353
+ } else {
354
+ emitCapsuleRequest(
355
+ start.method,
356
+ start.url,
357
+ start.res,
358
+ Date.now() - start.startedAt,
359
+ start.retriedAfter429
360
+ );
361
+ }
362
+ throw err;
322
363
  }
323
364
  }
324
- function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
325
- let path = "";
365
+ function redactedPath(url) {
326
366
  try {
327
- path = redactPath(new URL(url).pathname);
367
+ return redactPath(new URL(url).pathname);
328
368
  } catch {
329
- path = "?";
369
+ return "?";
330
370
  }
371
+ }
372
+ function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
373
+ const path = redactedPath(url);
331
374
  const lenHeader = res.headers.get("content-length");
332
375
  const responseBytes = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
333
376
  logEvent("capsule.request", {
@@ -339,6 +382,37 @@ function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
339
382
  ...retriedAfter429 ? { retriedAfter429: true } : {}
340
383
  });
341
384
  }
385
+ function emitCapsuleFailure(method, url, elapsedMs, reason, err) {
386
+ const path = redactedPath(url);
387
+ if (reason === "timeout") {
388
+ logEvent(
389
+ "capsule.timeout",
390
+ { method, path, elapsedMs, timeoutMs: REQUEST_TIMEOUT_MS },
391
+ { force: true }
392
+ );
393
+ return;
394
+ }
395
+ const code = extractErrorCode(err);
396
+ logEvent(
397
+ "capsule.error",
398
+ { method, path, elapsedMs, ...code ? { code } : {} },
399
+ { force: true }
400
+ );
401
+ }
402
+ function emitCapsuleRateLimited(method, url, elapsedMs) {
403
+ logEvent(
404
+ "capsule.ratelimit",
405
+ { method, path: redactedPath(url), elapsedMs, status: 429 },
406
+ { force: true }
407
+ );
408
+ }
409
+ function extractErrorCode(err) {
410
+ const e = err;
411
+ const code = e?.cause?.code ?? e?.code;
412
+ if (typeof code === "string") return code;
413
+ if (typeof e?.name === "string" && e.name !== "Error") return e.name;
414
+ return void 0;
415
+ }
342
416
  async function throwForStatus(res) {
343
417
  if (res.status === 401) {
344
418
  const detail = await parseErrorBody(res);
@@ -370,15 +444,19 @@ async function capsuleGet(path, params) {
370
444
  const token = getToken();
371
445
  const url = buildUrl(path, params);
372
446
  const start = await doFetch(url, { headers: baseHeaders(token) });
373
- try {
374
- return await consumeBody(start, async () => {
375
- const data = await handleResponse(start.res);
376
- const nextPage = parseNextPage(start.res.headers.get("Link"));
377
- return { data, nextPage };
378
- });
379
- } finally {
380
- start.cleanup();
381
- }
447
+ return consumeBody(start, async () => {
448
+ const data = await handleResponse(start.res);
449
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
450
+ return { data, nextPage };
451
+ });
452
+ }
453
+ async function capsuleGetList(path, params) {
454
+ const { data, nextPage } = await capsuleGet(path, params);
455
+ return { ...data, nextPage };
456
+ }
457
+ async function capsuleGetCachedList(path, params) {
458
+ const { data, nextPage } = await capsuleGetCached(path, params);
459
+ return { ...data, nextPage };
382
460
  }
383
461
  async function capsuleGetCached(path, params) {
384
462
  if (cacheDisabled()) return capsuleGet(path, params);
@@ -417,11 +495,7 @@ async function capsulePost(path, body) {
417
495
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
418
496
  body: JSON.stringify(body)
419
497
  });
420
- try {
421
- return await consumeBody(start, () => handleResponse(start.res));
422
- } finally {
423
- start.cleanup();
424
- }
498
+ return consumeBody(start, () => handleResponse(start.res));
425
499
  }
426
500
  async function capsulePostNoContent(path) {
427
501
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -431,15 +505,11 @@ async function capsulePostNoContent(path) {
431
505
  method: "POST",
432
506
  headers: baseHeaders(token)
433
507
  });
434
- try {
435
- await consumeBody(start, async () => {
436
- if (start.res.status === 204) return;
437
- await throwForStatus(start.res);
438
- await mapAbort(start.res.text());
439
- });
440
- } finally {
441
- start.cleanup();
442
- }
508
+ await consumeBody(start, async () => {
509
+ if (start.res.status === 204) return;
510
+ await throwForStatus(start.res);
511
+ await mapAbort(start.res.text());
512
+ });
443
513
  }
444
514
  async function capsuleSearch(path, body, params) {
445
515
  const token = getToken();
@@ -449,15 +519,11 @@ async function capsuleSearch(path, body, params) {
449
519
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
450
520
  body: JSON.stringify(body)
451
521
  });
452
- try {
453
- return await consumeBody(start, async () => {
454
- const data = await handleResponse(start.res);
455
- const nextPage = parseNextPage(start.res.headers.get("Link"));
456
- return { data, nextPage };
457
- });
458
- } finally {
459
- start.cleanup();
460
- }
522
+ return consumeBody(start, async () => {
523
+ const data = await handleResponse(start.res);
524
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
525
+ return { data, nextPage };
526
+ });
461
527
  }
462
528
  async function capsulePut(path, body) {
463
529
  if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
@@ -468,68 +534,60 @@ async function capsulePut(path, body) {
468
534
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
469
535
  body: JSON.stringify(body)
470
536
  });
471
- try {
472
- return await consumeBody(start, () => handleResponse(start.res));
473
- } finally {
474
- start.cleanup();
475
- }
537
+ return consumeBody(start, () => handleResponse(start.res));
476
538
  }
477
539
  async function capsuleGetBinary(path, maxBytes) {
478
540
  const token = getToken();
479
541
  const url = buildUrl(path);
480
542
  const start = await doFetch(url, { headers: baseHeaders(token) });
481
- try {
482
- return await consumeBody(start, async () => {
483
- const res = start.res;
484
- await throwForStatus(res);
485
- const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
486
- const declared = res.headers.get("Content-Length");
487
- const declaredBytes = declared ? Number(declared) : NaN;
488
- if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
489
- if (res.body) await res.body.cancel().catch(() => {
490
- });
543
+ return consumeBody(start, async () => {
544
+ const res = start.res;
545
+ await throwForStatus(res);
546
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
547
+ const declared = res.headers.get("Content-Length");
548
+ const declaredBytes = declared ? Number(declared) : NaN;
549
+ if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
550
+ if (res.body) await res.body.cancel().catch(() => {
551
+ });
552
+ return {
553
+ contentType,
554
+ buffer: Buffer.alloc(0),
555
+ truncated: true,
556
+ sizeBytes: declaredBytes
557
+ };
558
+ }
559
+ if (maxBytes !== void 0 && res.body) {
560
+ const reader = res.body.getReader();
561
+ const chunks = [];
562
+ let total = 0;
563
+ let truncated = false;
564
+ while (true) {
565
+ const { done, value } = await mapAbort(reader.read());
566
+ if (done) break;
567
+ total += value.byteLength;
568
+ if (total > maxBytes) {
569
+ truncated = true;
570
+ await reader.cancel().catch(() => {
571
+ });
572
+ break;
573
+ }
574
+ chunks.push(value);
575
+ }
576
+ if (truncated) {
491
577
  return {
492
578
  contentType,
493
579
  buffer: Buffer.alloc(0),
494
580
  truncated: true,
495
- sizeBytes: declaredBytes
581
+ sizeBytes: total
496
582
  };
497
583
  }
498
- if (maxBytes !== void 0 && res.body) {
499
- const reader = res.body.getReader();
500
- const chunks = [];
501
- let total = 0;
502
- let truncated = false;
503
- while (true) {
504
- const { done, value } = await mapAbort(reader.read());
505
- if (done) break;
506
- total += value.byteLength;
507
- if (total > maxBytes) {
508
- truncated = true;
509
- await reader.cancel().catch(() => {
510
- });
511
- break;
512
- }
513
- chunks.push(value);
514
- }
515
- if (truncated) {
516
- return {
517
- contentType,
518
- buffer: Buffer.alloc(0),
519
- truncated: true,
520
- sizeBytes: total
521
- };
522
- }
523
- const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
524
- return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
525
- }
526
- const arrayBuffer = await mapAbort(res.arrayBuffer());
527
- const buffer = Buffer.from(arrayBuffer);
528
- return { contentType, buffer, sizeBytes: buffer.length };
529
- });
530
- } finally {
531
- start.cleanup();
532
- }
584
+ const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
585
+ return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
586
+ }
587
+ const arrayBuffer = await mapAbort(res.arrayBuffer());
588
+ const buffer = Buffer.from(arrayBuffer);
589
+ return { contentType, buffer, sizeBytes: buffer.length };
590
+ });
533
591
  }
534
592
  async function capsulePostBinary(path, body, contentType, filename) {
535
593
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -545,11 +603,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
545
603
  },
546
604
  body
547
605
  });
548
- try {
549
- return await consumeBody(start, () => handleResponse(start.res));
550
- } finally {
551
- start.cleanup();
552
- }
606
+ return consumeBody(start, () => handleResponse(start.res));
553
607
  }
554
608
  async function capsuleDelete(path) {
555
609
  if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
@@ -559,15 +613,11 @@ async function capsuleDelete(path) {
559
613
  method: "DELETE",
560
614
  headers: baseHeaders(token)
561
615
  });
562
- try {
563
- await consumeBody(start, async () => {
564
- if (start.res.status === 204) return;
565
- await throwForStatus(start.res);
566
- await mapAbort(start.res.text());
567
- });
568
- } finally {
569
- start.cleanup();
570
- }
616
+ await consumeBody(start, async () => {
617
+ if (start.res.status === 204) return;
618
+ await throwForStatus(start.res);
619
+ await mapAbort(start.res.text());
620
+ });
571
621
  }
572
622
 
573
623
  // src/auth/provider.ts
@@ -1088,6 +1138,44 @@ var ICONS = [
1088
1138
  }
1089
1139
  ];
1090
1140
 
1141
+ // src/server/tier.ts
1142
+ var CORE_TOOLS = /* @__PURE__ */ new Set([
1143
+ // Parties
1144
+ "search_parties",
1145
+ "filter_parties",
1146
+ "get_party",
1147
+ "create_party",
1148
+ "update_party",
1149
+ "list_party_entries",
1150
+ // Opportunities
1151
+ "search_opportunities",
1152
+ "filter_opportunities",
1153
+ "get_opportunity",
1154
+ "create_opportunity",
1155
+ "update_opportunity",
1156
+ // Projects
1157
+ "filter_projects",
1158
+ "list_projects",
1159
+ "get_project",
1160
+ "create_project",
1161
+ "update_project",
1162
+ // Tasks
1163
+ "list_tasks",
1164
+ "get_task",
1165
+ "create_task",
1166
+ "update_task",
1167
+ "complete_task",
1168
+ // Timeline + tags + identity
1169
+ "add_note",
1170
+ "list_tags",
1171
+ "add_tag",
1172
+ "get_current_user"
1173
+ ]);
1174
+ function shouldRegister(name) {
1175
+ if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
1176
+ return CORE_TOOLS.has(name);
1177
+ }
1178
+
1091
1179
  // src/tasks/store.ts
1092
1180
  import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
1093
1181
  import {
@@ -1142,6 +1230,22 @@ var abortControllers = /* @__PURE__ */ new Map();
1142
1230
  function registerAbortController(taskId, controller) {
1143
1231
  abortControllers.set(taskId, controller);
1144
1232
  }
1233
+ var evictionTimers = /* @__PURE__ */ new Map();
1234
+ var taskTtls = /* @__PURE__ */ new Map();
1235
+ function scheduleEviction(taskId, clientId, ttlMs) {
1236
+ const existing = evictionTimers.get(taskId);
1237
+ if (existing) clearTimeout(existing);
1238
+ taskTtls.set(taskId, ttlMs);
1239
+ const timer = setTimeout(() => {
1240
+ owners.delete(taskId);
1241
+ abortControllers.delete(taskId);
1242
+ evictionTimers.delete(taskId);
1243
+ taskTtls.delete(taskId);
1244
+ logEvent("task.evicted", { taskId, clientId, reason: "ttl" });
1245
+ }, ttlMs);
1246
+ timer.unref?.();
1247
+ evictionTimers.set(taskId, timer);
1248
+ }
1145
1249
  function countPerClient(clientId) {
1146
1250
  let n = 0;
1147
1251
  for (const owner of owners.values()) {
@@ -1195,12 +1299,7 @@ function createScopedTaskStore(clientId) {
1195
1299
  sessionId
1196
1300
  );
1197
1301
  owners.set(task.taskId, clientId);
1198
- const timer = setTimeout(() => {
1199
- owners.delete(task.taskId);
1200
- abortControllers.delete(task.taskId);
1201
- logEvent("task.evicted", { taskId: task.taskId, clientId, reason: "ttl" });
1202
- }, clampedTtl);
1203
- timer.unref?.();
1302
+ scheduleEviction(task.taskId, clientId, clampedTtl);
1204
1303
  logEvent("task.created", {
1205
1304
  taskId: task.taskId,
1206
1305
  clientId,
@@ -1219,6 +1318,7 @@ function createScopedTaskStore(clientId) {
1219
1318
  }
1220
1319
  logEvent("task.transition", { taskId, clientId, status });
1221
1320
  await global.storeTaskResult(taskId, status, result, sessionId);
1321
+ scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
1222
1322
  },
1223
1323
  async getTaskResult(taskId, sessionId) {
1224
1324
  if (owners.get(taskId) !== clientId) {
@@ -1238,6 +1338,7 @@ function createScopedTaskStore(clientId) {
1238
1338
  }
1239
1339
  if (status === "completed" || status === "failed" || status === "cancelled") {
1240
1340
  abortControllers.delete(taskId);
1341
+ scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
1241
1342
  }
1242
1343
  },
1243
1344
  async listTasks(cursor, sessionId) {
@@ -1283,27 +1384,25 @@ function wrapAsText(result) {
1283
1384
  };
1284
1385
  }
1285
1386
  function registerTool(server, name, description, schema, handler) {
1387
+ if (!shouldRegister(name)) return;
1286
1388
  const registerWithSchema = server.registerTool.bind(server);
1287
1389
  const annotations = inferAnnotations(name);
1288
- registerWithSchema(
1289
- name,
1290
- { description, inputSchema: schema, ...annotations ? { annotations } : {} },
1291
- async (input) => {
1292
- const startedAt = Date.now();
1293
- const argFields = argFieldNames(input);
1294
- const clientId = getRequestContext()?.clientId;
1295
- try {
1296
- const result = await handler(input);
1297
- emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
1298
- return wrapAsText(result);
1299
- } catch (err) {
1300
- emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
1301
- throw err;
1302
- }
1390
+ registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
1391
+ const startedAt = Date.now();
1392
+ const argFields = argFieldNames(input);
1393
+ const clientId = getRequestContext()?.clientId;
1394
+ try {
1395
+ const result = await handler(input);
1396
+ emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
1397
+ return wrapAsText(result);
1398
+ } catch (err) {
1399
+ emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
1400
+ throw err;
1303
1401
  }
1304
- );
1402
+ });
1305
1403
  }
1306
1404
  function registerToolTask(server, name, description, schema, handler) {
1405
+ if (!shouldRegister(name)) return;
1307
1406
  const registerWithSchema = server.experimental.tasks.registerToolTask.bind(
1308
1407
  server.experimental.tasks
1309
1408
  );
@@ -1314,7 +1413,7 @@ function registerToolTask(server, name, description, schema, handler) {
1314
1413
  description,
1315
1414
  inputSchema: schema,
1316
1415
  execution: { taskSupport: "optional" },
1317
- ...annotations ? { annotations } : {}
1416
+ annotations
1318
1417
  },
1319
1418
  {
1320
1419
  createTask: async (input, extra) => {
@@ -1374,7 +1473,7 @@ function registerToolTask(server, name, description, schema, handler) {
1374
1473
  }
1375
1474
 
1376
1475
  // src/tools/parties.ts
1377
- import { z as z7 } from "zod";
1476
+ import { z as z8 } from "zod";
1378
1477
 
1379
1478
  // src/tools/body-helpers.ts
1380
1479
  function setRef(body, key, id) {
@@ -1384,9 +1483,22 @@ function setNullableRef(body, key, id) {
1384
1483
  if (id === null) body[key] = null;
1385
1484
  else if (id !== void 0) body[key] = { id };
1386
1485
  }
1486
+ function assertSingleParentRef(toolName, refs, opts = {}) {
1487
+ const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
1488
+ (v) => typeof v === "number"
1489
+ ).length;
1490
+ if (opts.required && set !== 1) {
1491
+ throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
1492
+ }
1493
+ if (set > 1) {
1494
+ throw new Error(
1495
+ `${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
1496
+ );
1497
+ }
1498
+ }
1387
1499
 
1388
1500
  // src/tools/define-batch.ts
1389
- import { z as z2 } from "zod";
1501
+ import { z as z3 } from "zod";
1390
1502
 
1391
1503
  // src/capsule/batch.ts
1392
1504
  function chunk(arr, size) {
@@ -1405,37 +1517,43 @@ function getBatchConcurrency() {
1405
1517
  MAX_CONCURRENCY
1406
1518
  );
1407
1519
  }
1408
- async function batchExecute(tool, items, action, options = {}) {
1409
- const concurrency = getBatchConcurrency();
1520
+ async function mapWithConcurrency(items, limit, fn) {
1410
1521
  const results = new Array(items.length);
1411
- const startedAt = Date.now();
1412
- const signal = options.signal;
1413
1522
  let cursor = 0;
1414
1523
  async function worker() {
1415
1524
  while (true) {
1416
1525
  const i = cursor;
1417
1526
  cursor += 1;
1418
1527
  if (i >= items.length) return;
1419
- if (signal?.aborted) {
1420
- results[i] = {
1421
- ok: false,
1422
- error: { message: "cancelled by tasks/cancel" }
1423
- };
1424
- continue;
1425
- }
1426
- try {
1427
- const result = await action(items[i], i);
1428
- results[i] = { ok: true, result };
1429
- } catch (err) {
1430
- results[i] = { ok: false, error: extractError(err) };
1431
- }
1528
+ results[i] = await fn(items[i], i);
1432
1529
  }
1433
1530
  }
1434
1531
  const workers = [];
1435
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1532
+ for (let w = 0; w < Math.min(limit, items.length); w++) {
1436
1533
  workers.push(worker());
1437
1534
  }
1438
1535
  await Promise.all(workers);
1536
+ return results;
1537
+ }
1538
+ async function batchExecute(tool, items, action, options = {}) {
1539
+ const concurrency = getBatchConcurrency();
1540
+ const startedAt = Date.now();
1541
+ const signal = options.signal;
1542
+ const results = await mapWithConcurrency(
1543
+ items,
1544
+ concurrency,
1545
+ async (item, i) => {
1546
+ if (signal?.aborted) {
1547
+ return { ok: false, error: { message: "cancelled by tasks/cancel" } };
1548
+ }
1549
+ try {
1550
+ const result = await action(item, i);
1551
+ return { ok: true, result };
1552
+ } catch (err) {
1553
+ return { ok: false, error: extractError(err) };
1554
+ }
1555
+ }
1556
+ );
1439
1557
  const succeeded = results.filter((r) => r.ok).length;
1440
1558
  const failed = results.length - succeeded;
1441
1559
  const summary = { total: results.length, succeeded, failed };
@@ -1480,10 +1598,52 @@ function topFailureReasons(results, n) {
1480
1598
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
1481
1599
  }
1482
1600
 
1601
+ // src/tools/strip-descriptions.ts
1602
+ import { z as z2 } from "zod";
1603
+ function cloneWithDef(node, patch) {
1604
+ const def = node.def;
1605
+ return node.clone({ ...def, ...patch });
1606
+ }
1607
+ function stripDescriptions(schema) {
1608
+ let node = schema;
1609
+ if (node instanceof z2.ZodObject) {
1610
+ const shape = node.def.shape;
1611
+ const next = {};
1612
+ let changed = false;
1613
+ for (const [key, child] of Object.entries(shape)) {
1614
+ next[key] = stripDescriptions(child);
1615
+ if (next[key] !== child) changed = true;
1616
+ }
1617
+ if (changed) node = cloneWithDef(node, { shape: next });
1618
+ } else if (node instanceof z2.ZodArray) {
1619
+ const element = stripDescriptions(node.def.element);
1620
+ if (element !== node.def.element) node = cloneWithDef(node, { element });
1621
+ } else if (node instanceof z2.ZodOptional || node instanceof z2.ZodNullable || node instanceof z2.ZodDefault || node instanceof z2.ZodReadonly) {
1622
+ const innerType = stripDescriptions(node.def.innerType);
1623
+ if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
1624
+ } else if (node instanceof z2.ZodUnion) {
1625
+ const options = node.def.options.map(stripDescriptions);
1626
+ if (options.some((o, i) => o !== node.def.options[i])) {
1627
+ node = cloneWithDef(node, { options });
1628
+ }
1629
+ } else if (node instanceof z2.ZodPipe) {
1630
+ const inSchema = stripDescriptions(node.def.in);
1631
+ const outSchema = stripDescriptions(node.def.out);
1632
+ if (inSchema !== node.def.in || outSchema !== node.def.out) {
1633
+ node = cloneWithDef(node, { in: inSchema, out: outSchema });
1634
+ }
1635
+ }
1636
+ if (node.description !== void 0) {
1637
+ node = node.meta({ description: void 0 });
1638
+ }
1639
+ return node;
1640
+ }
1641
+
1483
1642
  // src/tools/define-batch.ts
1484
1643
  function defineBatch(args) {
1485
- const schema = z2.object({
1486
- items: z2.array(args.itemSchema).min(1).max(50).describe(args.itemDescription)
1644
+ const itemSchema = stripDescriptions(args.itemSchema);
1645
+ const schema = z3.object({
1646
+ items: z3.array(itemSchema).min(1).max(50).describe(args.itemDescription)
1487
1647
  });
1488
1648
  async function handler(input, opts = {}) {
1489
1649
  return batchExecute(args.toolName, input.items, args.itemHandler, opts);
@@ -1496,22 +1656,30 @@ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'"
1496
1656
  var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
1497
1657
 
1498
1658
  // src/tools/define-delete.ts
1499
- import { z as z5 } from "zod";
1659
+ import { z as z6 } from "zod";
1500
1660
 
1501
1661
  // src/tools/confirm-flag.ts
1502
- import { z as z3 } from "zod";
1662
+ import { z as z4 } from "zod";
1503
1663
  var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1504
1664
  function confirmFlag() {
1505
- return z3.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1665
+ return z4.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1506
1666
  }
1507
1667
 
1508
1668
  // src/tools/shared-schemas.ts
1509
- import { z as z4 } from "zod";
1510
- var positiveId = z4.preprocess((input) => {
1669
+ import { z as z5 } from "zod";
1670
+ var positiveId = z5.preprocess((input) => {
1511
1671
  if (typeof input !== "string") return input;
1512
1672
  const trimmed = input.trim();
1513
1673
  return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
1514
- }, z4.number().int().positive());
1674
+ }, z5.number().int().positive());
1675
+ var paginationFields = {
1676
+ page: z5.number().int().positive().optional().default(1),
1677
+ perPage: z5.number().int().min(1).max(100).optional().default(25)
1678
+ };
1679
+ var paginationFieldsNoDefaults = {
1680
+ page: z5.number().int().positive().optional(),
1681
+ perPage: z5.number().int().min(1).max(100).optional()
1682
+ };
1515
1683
 
1516
1684
  // src/capsule/idempotent.ts
1517
1685
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
@@ -1538,7 +1706,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
1538
1706
  // src/tools/define-delete.ts
1539
1707
  function defineDelete(args) {
1540
1708
  const { toolName, pathPrefix, confirmHint, idDescription } = args;
1541
- const schema = z5.object({
1709
+ const schema = z6.object({
1542
1710
  id: idDescription ? positiveId.describe(idDescription) : positiveId,
1543
1711
  confirm: confirmFlag().describe(confirmHint)
1544
1712
  });
@@ -1581,16 +1749,17 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
1581
1749
  (chunkIds) => capsuleGet(`${base}/${chunkIds.join(",")}`, params)
1582
1750
  )
1583
1751
  );
1584
- return { [responseKey]: responses.flatMap((r) => r.data[responseKey] ?? []) };
1752
+ const merged = responses.flatMap((r) => r.data[responseKey] ?? []);
1753
+ return { ...responses[0]?.data ?? {}, [responseKey]: merged };
1585
1754
  }
1586
1755
 
1587
1756
  // src/tools/custom-field-helpers.ts
1588
- import { z as z6 } from "zod";
1589
- var CustomFieldWriteSchema = z6.object({
1757
+ import { z as z7 } from "zod";
1758
+ var CustomFieldWriteSchema = z7.object({
1590
1759
  definitionId: positiveId.describe(
1591
1760
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
1592
1761
  ),
1593
- value: z6.union([z6.string(), z6.number(), z6.boolean(), z6.null()]).describe(
1762
+ value: z7.union([z7.string(), z7.number(), z7.boolean(), z7.null()]).describe(
1594
1763
  "The new value. String for TEXT / DATE / LIST / LARGE_TEXT / LINK fields, number for NUMBER fields, boolean for BOOLEAN fields. Clearing: pass null for TEXT / NUMBER / DATE / LIST (Capsule removes the row). BOOLEAN does NOT accept null (Capsule returns 422 'invalid type for field'); use `value: false` instead. Note BOOLEAN fields are observably **two-state**: a row exists with `value: true`, or no row exists. Setting `value: false` removes the row entirely \u2014 readers should treat absent BOOLEAN rows as equivalent to false. Tri-state BOOLEAN semantics (true / false / unknown) are not achievable through Capsule's API. Audit-log noise: sending value=null on a field that's already empty/cleared is accepted by Capsule but still bumps the parent entity's `updatedAt`. Read the current value via embed='fields' first if `updatedAt` is being used as a 'last meaningful change' signal. NUMBER quirks: Capsule stores numerics correctly but the read-back via embed=fields returns them as STRINGS (e.g. value=3 reads as '3'); callers comparing values must coerce. TEXT quirks: value='' has the same observable effect as value=null (row removed); empty-string and never-set are indistinguishable."
1595
1764
  )
1596
1765
  });
@@ -1606,24 +1775,24 @@ function mapFieldsForBody(fields) {
1606
1775
  }
1607
1776
 
1608
1777
  // src/tools/parties.ts
1609
- var EmailAddressSchema = z7.object({
1610
- address: z7.string().email(),
1611
- type: z7.string().optional()
1778
+ var EmailAddressSchema = z8.object({
1779
+ address: z8.string().email(),
1780
+ type: z8.string().optional()
1612
1781
  });
1613
- var PhoneNumberSchema = z7.object({
1782
+ var PhoneNumberSchema = z8.object({
1614
1783
  // Capsule rejects empty strings with `phoneNumber.number: number is
1615
1784
  // required`. Enforce at the schema layer to catch typos pre-call,
1616
1785
  // matching how EmailAddressSchema's address field behaves.
1617
- number: z7.string().min(1),
1618
- type: z7.string().optional()
1786
+ number: z8.string().min(1),
1787
+ type: z8.string().optional()
1619
1788
  });
1620
1789
  var CountryDescription = "Country name. Capsule validates this against a small canonical-English-name dictionary; inputs not in the dictionary are REJECTED with 422 'address.country: unknown country' (NOT silently passed through or normalised). Probed examples \u2014 accepted: `United States`, `United Kingdom`, `Czechia`, `Germany`. Aliased: `USA \u2192 United States`. Rejected: `United States of America`, `Czech Republic` (use `Czechia`), `UK`/`Britain` (use `United Kingdom`), `Deutschland` (use `Germany`). Empty string is accepted and stored as `null` \u2014 a de-facto 'clear' shape. To discover an accepted name, read an existing party that already has the country set.";
1621
- var AddressSchema = z7.object({
1622
- street: z7.string().optional(),
1623
- city: z7.string().optional(),
1624
- state: z7.string().optional(),
1625
- country: z7.string().optional().describe(CountryDescription),
1626
- zip: z7.string().optional()
1790
+ var AddressSchema = z8.object({
1791
+ street: z8.string().optional(),
1792
+ city: z8.string().optional(),
1793
+ state: z8.string().optional(),
1794
+ country: z8.string().optional().describe(CountryDescription),
1795
+ zip: z8.string().optional()
1627
1796
  });
1628
1797
  function validateWebsiteAddress(data, ctx) {
1629
1798
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1646,7 +1815,7 @@ function validateWebsiteAddress(data, ctx) {
1646
1815
  });
1647
1816
  }
1648
1817
  }
1649
- var WebsiteServiceEnum = z7.enum([
1818
+ var WebsiteServiceEnum = z8.enum([
1650
1819
  "URL",
1651
1820
  "SKYPE",
1652
1821
  "TWITTER",
@@ -1665,33 +1834,31 @@ var WebsiteServiceEnum = z7.enum([
1665
1834
  "BLUESKY",
1666
1835
  "SNAPCHAT"
1667
1836
  ]);
1668
- var WebsiteSchema = z7.object({
1669
- address: z7.string().min(1).describe(
1837
+ var WebsiteSchema = z8.object({
1838
+ address: z8.string().min(1).describe(
1670
1839
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services like 'TWITTER', 'INSTAGRAM'. Capsule names this field `address` regardless of service type."
1671
1840
  ),
1672
1841
  service: WebsiteServiceEnum.optional().describe(
1673
1842
  "Service type. One of: URL, SKYPE, TWITTER, LINKED_IN, FACEBOOK, XING, FEED, GOOGLE_PLUS, FLICKR, GITHUB, YOUTUBE, INSTAGRAM, PINTEREST, TIKTOK, THREADS, BLUESKY, SNAPCHAT. Defaults to 'URL' if omitted."
1674
1843
  )
1675
1844
  }).superRefine(validateWebsiteAddress);
1676
- var searchPartiesSchema = z7.object({
1677
- q: z7.string().optional().describe("Free-text search query"),
1678
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1679
- page: z7.number().int().positive().optional().default(1),
1680
- perPage: z7.number().int().min(1).max(100).optional().default(25)
1845
+ var searchPartiesSchema = z8.object({
1846
+ q: z8.string().optional().describe("Free-text search query"),
1847
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1848
+ ...paginationFields
1681
1849
  });
1682
1850
  async function searchParties(input) {
1683
1851
  const path = input.q ? "/parties/search" : "/parties";
1684
- const { data, nextPage } = await capsuleGet(path, {
1852
+ return capsuleGetList(path, {
1685
1853
  q: input.q,
1686
1854
  embed: input.embed,
1687
1855
  page: input.page,
1688
1856
  perPage: input.perPage
1689
1857
  });
1690
- return { ...data, nextPage };
1691
1858
  }
1692
- var getPartySchema = z7.object({
1859
+ var getPartySchema = z8.object({
1693
1860
  id: positiveId.describe("Party ID"),
1694
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1861
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1695
1862
  });
1696
1863
  async function getParty(input) {
1697
1864
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1699,51 +1866,47 @@ async function getParty(input) {
1699
1866
  });
1700
1867
  return data;
1701
1868
  }
1702
- var getPartiesSchema = z7.object({
1703
- ids: z7.array(positiveId).min(1).max(50).describe(
1869
+ var getPartiesSchema = z8.object({
1870
+ ids: z8.array(positiveId).min(1).max(50).describe(
1704
1871
  "Array of party IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel. Result shape is identical regardless of input size."
1705
1872
  ),
1706
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1873
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1707
1874
  });
1708
1875
  async function getParties(input) {
1709
1876
  return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
1710
1877
  }
1711
- var listPartyOpportunitiesSchema = z7.object({
1878
+ var listPartyOpportunitiesSchema = z8.object({
1712
1879
  partyId: positiveId,
1713
- page: z7.number().int().positive().optional().default(1),
1714
- perPage: z7.number().int().min(1).max(100).optional().default(25)
1880
+ ...paginationFields
1715
1881
  });
1716
1882
  async function listPartyOpportunities(input) {
1717
- const { data, nextPage } = await capsuleGet(
1718
- `/parties/${input.partyId}/opportunities`,
1719
- { page: input.page, perPage: input.perPage }
1720
- );
1721
- return { ...data, nextPage };
1883
+ return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
1884
+ page: input.page,
1885
+ perPage: input.perPage
1886
+ });
1722
1887
  }
1723
- var listPartyProjectsSchema = z7.object({
1888
+ var listPartyProjectsSchema = z8.object({
1724
1889
  partyId: positiveId,
1725
- page: z7.number().int().positive().optional().default(1),
1726
- perPage: z7.number().int().min(1).max(100).optional().default(25)
1890
+ ...paginationFields
1727
1891
  });
1728
1892
  async function listPartyProjects(input) {
1729
- const { data, nextPage } = await capsuleGet(
1730
- `/parties/${input.partyId}/kases`,
1731
- { page: input.page, perPage: input.perPage }
1732
- );
1733
- return { ...data, nextPage };
1893
+ return capsuleGetList(`/parties/${input.partyId}/kases`, {
1894
+ page: input.page,
1895
+ perPage: input.perPage
1896
+ });
1734
1897
  }
1735
1898
  var PartyWriteBaseSchema = {
1736
- about: z7.string().optional(),
1737
- emailAddresses: z7.array(EmailAddressSchema).optional().describe(
1899
+ about: z8.string().optional(),
1900
+ emailAddresses: z8.array(EmailAddressSchema).optional().describe(
1738
1901
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_email_address and remove_party_email_address_by_id. Passing `[]` here is a silent no-op (does not clear the list and does not advance updatedAt)."
1739
1902
  ),
1740
- phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
1903
+ phoneNumbers: z8.array(PhoneNumberSchema).optional().describe(
1741
1904
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_phone_number and remove_party_phone_number_by_id."
1742
1905
  ),
1743
- addresses: z7.array(AddressSchema).optional().describe(
1906
+ addresses: z8.array(AddressSchema).optional().describe(
1744
1907
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_address and remove_party_address_by_id. The `country` field is mapped through Capsule's country dictionary \u2014 see `add_party_address.country` for the dictionary edges (small canonical-English-name list; inputs not in the dictionary are REJECTED with 422, not silently dropped)."
1745
1908
  ),
1746
- websites: z7.array(WebsiteSchema).optional().describe(
1909
+ websites: z8.array(WebsiteSchema).optional().describe(
1747
1910
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
1748
1911
  ),
1749
1912
  ownerId: positiveId.nullable().optional().describe(
@@ -1753,16 +1916,16 @@ var PartyWriteBaseSchema = {
1753
1916
  "Assign to team ID (discover via list_teams). Pass a team ID to set, or `null` to unassign. Capsule enforces the owner\u2208team membership constraint \u2014 passing a team the current owner doesn't belong to returns 422 'owner is not a member of the team'. Combine `ownerId: null` + `teamId: <T>` in one call to transfer a party to team-ownership with no specific user (verified empirically in v1.6.4 wire-trace; the membership rule doesn't fire when owner is null)."
1754
1917
  )
1755
1918
  };
1756
- var createPartySchema = z7.object({
1757
- type: z7.enum(["person", "organisation"]),
1919
+ var createPartySchema = z8.object({
1920
+ type: z8.enum(["person", "organisation"]),
1758
1921
  // person
1759
- firstName: z7.string().optional(),
1760
- lastName: z7.string().optional(),
1761
- title: z7.string().optional(),
1762
- jobTitle: z7.string().optional(),
1922
+ firstName: z8.string().optional(),
1923
+ lastName: z8.string().optional(),
1924
+ title: z8.string().optional(),
1925
+ jobTitle: z8.string().optional(),
1763
1926
  organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
1764
1927
  // organisation
1765
- name: z7.string().optional(),
1928
+ name: z8.string().optional(),
1766
1929
  ...PartyWriteBaseSchema,
1767
1930
  ownerId: positiveId.optional().describe(
1768
1931
  "Assign to user ID. Defaults to the API-token owner when omitted. To create a team-owned party with no specific user, first create the party, then call update_party with `ownerId: null` and `teamId`."
@@ -1770,7 +1933,7 @@ var createPartySchema = z7.object({
1770
1933
  teamId: positiveId.optional().describe(
1771
1934
  "Assign to team ID (discover via list_teams). Omit to leave team unset on create. To clear an existing team or create a team-owned party with no specific owner, use update_party after creation."
1772
1935
  ),
1773
- fields: z7.array(CustomFieldWriteSchema).optional().describe(
1936
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(
1774
1937
  fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
1775
1938
  )
1776
1939
  });
@@ -1784,17 +1947,17 @@ async function createParty(input) {
1784
1947
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1785
1948
  return capsulePost("/parties", { party: body });
1786
1949
  }
1787
- var updatePartySchema = z7.object({
1950
+ var updatePartySchema = z8.object({
1788
1951
  id: positiveId,
1789
- firstName: z7.string().optional(),
1790
- lastName: z7.string().optional(),
1791
- title: z7.string().optional(),
1792
- jobTitle: z7.string().optional(),
1793
- name: z7.string().optional(),
1952
+ firstName: z8.string().optional(),
1953
+ lastName: z8.string().optional(),
1954
+ title: z8.string().optional(),
1955
+ jobTitle: z8.string().optional(),
1956
+ name: z8.string().optional(),
1794
1957
  organisationId: positiveId.nullable().optional().describe(
1795
1958
  "For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
1796
1959
  ),
1797
- fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1960
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1798
1961
  ...PartyWriteBaseSchema
1799
1962
  });
1800
1963
  async function updateParty(input) {
@@ -1825,10 +1988,42 @@ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1825
1988
  pathPrefix: "/parties",
1826
1989
  confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
1827
1990
  });
1828
- var addPartyEmailAddressSchema = z7.object({
1991
+ function definePartySubResourceRemove(opts) {
1992
+ const shape = {
1993
+ partyId: positiveId,
1994
+ [opts.idField]: positiveId.describe(
1995
+ `Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
1996
+ )
1997
+ };
1998
+ const schema = z8.object(shape);
1999
+ async function handler(input) {
2000
+ const partyId = input["partyId"];
2001
+ const rowId = input[opts.idField];
2002
+ return idempotentWithResult(
2003
+ () => capsulePut(`/parties/${partyId}`, {
2004
+ party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
2005
+ }),
2006
+ (result) => ({
2007
+ removed: true,
2008
+ alreadyRemoved: false,
2009
+ partyId,
2010
+ [opts.idField]: rowId,
2011
+ ...result
2012
+ }),
2013
+ () => ({
2014
+ removed: true,
2015
+ alreadyRemoved: true,
2016
+ partyId,
2017
+ [opts.idField]: rowId
2018
+ })
2019
+ );
2020
+ }
2021
+ return { schema, handler };
2022
+ }
2023
+ var addPartyEmailAddressSchema = z8.object({
1829
2024
  partyId: positiveId,
1830
- address: z7.string().email(),
1831
- type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
2025
+ address: z8.string().email(),
2026
+ type: z8.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1832
2027
  });
1833
2028
  async function addPartyEmailAddress(input) {
1834
2029
  const { partyId, address, type } = input;
@@ -1838,32 +2033,17 @@ async function addPartyEmailAddress(input) {
1838
2033
  party: { emailAddresses: [item] }
1839
2034
  });
1840
2035
  }
1841
- var removePartyEmailAddressByIdSchema = z7.object({
1842
- partyId: positiveId,
1843
- emailAddressId: positiveId.describe(
1844
- "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1845
- )
2036
+ var removePartyEmailAddress = definePartySubResourceRemove({
2037
+ arrayKey: "emailAddresses",
2038
+ idField: "emailAddressId",
2039
+ rowNoun: "email-address"
1846
2040
  });
1847
- async function removePartyEmailAddressById(input) {
1848
- const { partyId, emailAddressId } = input;
1849
- return idempotentWithResult(
1850
- () => capsulePut(`/parties/${partyId}`, {
1851
- party: { emailAddresses: [{ id: emailAddressId, _delete: true }] }
1852
- }),
1853
- (result) => ({
1854
- removed: true,
1855
- alreadyRemoved: false,
1856
- partyId,
1857
- emailAddressId,
1858
- ...result
1859
- }),
1860
- () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1861
- );
1862
- }
1863
- var addPartyPhoneNumberSchema = z7.object({
2041
+ var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
2042
+ var removePartyEmailAddressById = removePartyEmailAddress.handler;
2043
+ var addPartyPhoneNumberSchema = z8.object({
1864
2044
  partyId: positiveId,
1865
- number: z7.string().min(1),
1866
- type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
2045
+ number: z8.string().min(1),
2046
+ type: z8.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1867
2047
  });
1868
2048
  async function addPartyPhoneNumber(input) {
1869
2049
  const { partyId, number, type } = input;
@@ -1873,36 +2053,21 @@ async function addPartyPhoneNumber(input) {
1873
2053
  party: { phoneNumbers: [item] }
1874
2054
  });
1875
2055
  }
1876
- var removePartyPhoneNumberByIdSchema = z7.object({
1877
- partyId: positiveId,
1878
- phoneNumberId: positiveId.describe(
1879
- "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1880
- )
2056
+ var removePartyPhoneNumber = definePartySubResourceRemove({
2057
+ arrayKey: "phoneNumbers",
2058
+ idField: "phoneNumberId",
2059
+ rowNoun: "phone-number"
1881
2060
  });
1882
- async function removePartyPhoneNumberById(input) {
1883
- const { partyId, phoneNumberId } = input;
1884
- return idempotentWithResult(
1885
- () => capsulePut(`/parties/${partyId}`, {
1886
- party: { phoneNumbers: [{ id: phoneNumberId, _delete: true }] }
1887
- }),
1888
- (result) => ({
1889
- removed: true,
1890
- alreadyRemoved: false,
1891
- partyId,
1892
- phoneNumberId,
1893
- ...result
1894
- }),
1895
- () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1896
- );
1897
- }
1898
- var addPartyAddressSchema = z7.object({
2061
+ var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
2062
+ var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
2063
+ var addPartyAddressSchema = z8.object({
1899
2064
  partyId: positiveId,
1900
- street: z7.string().optional(),
1901
- city: z7.string().optional(),
1902
- state: z7.string().optional(),
1903
- country: z7.string().optional().describe(CountryDescription),
1904
- zip: z7.string().optional(),
1905
- type: z7.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
2065
+ street: z8.string().optional(),
2066
+ city: z8.string().optional(),
2067
+ state: z8.string().optional(),
2068
+ country: z8.string().optional().describe(CountryDescription),
2069
+ zip: z8.string().optional(),
2070
+ type: z8.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1906
2071
  });
1907
2072
  async function addPartyAddress(input) {
1908
2073
  const { partyId, ...rest } = input;
@@ -1914,31 +2079,16 @@ async function addPartyAddress(input) {
1914
2079
  party: { addresses: [item] }
1915
2080
  });
1916
2081
  }
1917
- var removePartyAddressByIdSchema = z7.object({
1918
- partyId: positiveId,
1919
- addressId: positiveId.describe(
1920
- "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1921
- )
2082
+ var removePartyAddress = definePartySubResourceRemove({
2083
+ arrayKey: "addresses",
2084
+ idField: "addressId",
2085
+ rowNoun: "address"
1922
2086
  });
1923
- async function removePartyAddressById(input) {
1924
- const { partyId, addressId } = input;
1925
- return idempotentWithResult(
1926
- () => capsulePut(`/parties/${partyId}`, {
1927
- party: { addresses: [{ id: addressId, _delete: true }] }
1928
- }),
1929
- (result) => ({
1930
- removed: true,
1931
- alreadyRemoved: false,
1932
- partyId,
1933
- addressId,
1934
- ...result
1935
- }),
1936
- () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1937
- );
1938
- }
1939
- var addPartyWebsiteSchema = z7.object({
2087
+ var removePartyAddressByIdSchema = removePartyAddress.schema;
2088
+ var removePartyAddressById = removePartyAddress.handler;
2089
+ var addPartyWebsiteSchema = z8.object({
1940
2090
  partyId: positiveId,
1941
- address: z7.string().min(1).describe(
2091
+ address: z8.string().min(1).describe(
1942
2092
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1943
2093
  ),
1944
2094
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1951,58 +2101,41 @@ async function addPartyWebsite(input) {
1951
2101
  party: { websites: [item] }
1952
2102
  });
1953
2103
  }
1954
- var removePartyWebsiteByIdSchema = z7.object({
1955
- partyId: positiveId,
1956
- websiteId: positiveId.describe(
1957
- "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1958
- )
2104
+ var removePartyWebsite = definePartySubResourceRemove({
2105
+ arrayKey: "websites",
2106
+ idField: "websiteId",
2107
+ rowNoun: "website"
1959
2108
  });
1960
- async function removePartyWebsiteById(input) {
1961
- const { partyId, websiteId } = input;
1962
- return idempotentWithResult(
1963
- () => capsulePut(`/parties/${partyId}`, {
1964
- party: { websites: [{ id: websiteId, _delete: true }] }
1965
- }),
1966
- (result) => ({
1967
- removed: true,
1968
- alreadyRemoved: false,
1969
- partyId,
1970
- websiteId,
1971
- ...result
1972
- }),
1973
- () => ({ removed: true, alreadyRemoved: true, partyId, websiteId })
1974
- );
1975
- }
2109
+ var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
2110
+ var removePartyWebsiteById = removePartyWebsite.handler;
1976
2111
 
1977
2112
  // src/tools/opportunities.ts
1978
- import { z as z8 } from "zod";
1979
- var OpportunityValueSchema = z8.object({
1980
- amount: z8.number().nonnegative(),
1981
- currency: z8.string({
2113
+ import { z as z9 } from "zod";
2114
+ var OpportunityValueSchema = z9.object({
2115
+ amount: z9.number().nonnegative(),
2116
+ currency: z9.string({
1982
2117
  error: (iss) => iss.code === "invalid_type" && iss.input === void 0 ? "currency is required when amount is set (3-letter ISO 4217 code, e.g. 'USD', 'EUR', 'GBP')" : void 0
1983
2118
  }).length(3).describe(
1984
2119
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1985
2120
  )
1986
2121
  });
1987
- var searchOpportunitiesSchema = z8.object({
1988
- q: z8.string().optional().describe("Free-text search query"),
1989
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1990
- page: z8.number().int().positive().optional().default(1),
1991
- perPage: z8.number().int().min(1).max(100).optional().default(25)
2122
+ var searchOpportunitiesSchema = z9.object({
2123
+ q: z9.string().optional().describe("Free-text search query"),
2124
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2125
+ ...paginationFields
1992
2126
  });
1993
2127
  async function searchOpportunities(input) {
1994
2128
  const path = input.q ? "/opportunities/search" : "/opportunities";
1995
- const { data, nextPage } = await capsuleGet(path, {
2129
+ return capsuleGetList(path, {
1996
2130
  q: input.q,
1997
2131
  embed: input.embed,
1998
2132
  page: input.page,
1999
2133
  perPage: input.perPage
2000
2134
  });
2001
- return { ...data, nextPage };
2002
2135
  }
2003
- var getOpportunitySchema = z8.object({
2136
+ var getOpportunitySchema = z9.object({
2004
2137
  id: positiveId,
2005
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2138
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2006
2139
  });
2007
2140
  async function getOpportunity(input) {
2008
2141
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -2010,32 +2143,32 @@ async function getOpportunity(input) {
2010
2143
  });
2011
2144
  return data;
2012
2145
  }
2013
- var getOpportunitiesSchema = z8.object({
2014
- ids: z8.array(positiveId).min(1).max(50).describe(
2146
+ var getOpportunitiesSchema = z9.object({
2147
+ ids: z9.array(positiveId).min(1).max(50).describe(
2015
2148
  "Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
2016
2149
  ),
2017
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2150
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2018
2151
  });
2019
2152
  async function getOpportunities(input) {
2020
2153
  return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
2021
2154
  }
2022
- var createOpportunitySchema = z8.object({
2023
- name: z8.string().min(1),
2155
+ var createOpportunitySchema = z9.object({
2156
+ name: z9.string().min(1),
2024
2157
  partyId: positiveId.describe("ID of the party this opportunity belongs to"),
2025
2158
  milestoneId: positiveId.describe(
2026
2159
  "ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones. NOTE: some Capsule tenants configure **pipeline / milestone-reached automation rules** that mutate `owner` and/or `team` immediately after creation \u2014 e.g. an 'Assign to a Team' action that fires on entry to a specific milestone and has been observed to clear `owner` as an automation side-effect. If you observe a newly-created opp landing with `owner: null` despite passing `ownerId`, the cause is almost certainly a milestone automation on the destination pipeline rather than the connector. Documented workaround: follow `create_opportunity` with an immediate `batch_update_opportunity({items: [{id, ownerId, teamId}]})` carrying both fields \u2014 PUT does not re-fire milestone-reached triggers, so the owner sticks."
2027
2160
  ),
2028
- description: z8.string().optional(),
2161
+ description: z9.string().optional(),
2029
2162
  value: OpportunityValueSchema.optional(),
2030
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2031
- probability: z8.number().int().min(0).max(100).optional(),
2163
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2164
+ probability: z9.number().int().min(0).max(100).optional(),
2032
2165
  ownerId: positiveId.optional().describe(
2033
2166
  "Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. To clear owner later, call update_opportunity with `ownerId: null`. Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
2034
2167
  ),
2035
2168
  teamId: positiveId.optional().describe(
2036
2169
  "Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
2037
2170
  ),
2038
- fields: z8.array(CustomFieldWriteSchema).optional().describe(
2171
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
2039
2172
  fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
2040
2173
  )
2041
2174
  });
@@ -2052,19 +2185,19 @@ async function createOpportunity(input) {
2052
2185
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2053
2186
  return capsulePost("/opportunities", { opportunity: body });
2054
2187
  }
2055
- var updateOpportunitySchema = z8.object({
2188
+ var updateOpportunitySchema = z9.object({
2056
2189
  id: positiveId,
2057
- name: z8.string().min(1).optional(),
2190
+ name: z9.string().min(1).optional(),
2058
2191
  partyId: positiveId.optional().describe(
2059
2192
  "Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_project.partyId`."
2060
2193
  ),
2061
2194
  milestoneId: positiveId.optional().describe(
2062
2195
  "Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
2063
2196
  ),
2064
- description: z8.string().optional(),
2197
+ description: z9.string().optional(),
2065
2198
  value: OpportunityValueSchema.optional(),
2066
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
2067
- probability: z8.number().int().min(0).max(100).optional().describe(
2199
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
2200
+ probability: z9.number().int().min(0).max(100).optional().describe(
2068
2201
  "Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
2069
2202
  ),
2070
2203
  lostReasonId: positiveId.optional().describe(
@@ -2076,7 +2209,7 @@ var updateOpportunitySchema = z8.object({
2076
2209
  teamId: positiveId.nullable().optional().describe(
2077
2210
  "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
2078
2211
  ),
2079
- fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2212
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2080
2213
  });
2081
2214
  async function updateOpportunity(input) {
2082
2215
  const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
@@ -2112,25 +2245,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
2112
2245
  });
2113
2246
 
2114
2247
  // src/tools/projects.ts
2115
- import { z as z9 } from "zod";
2116
- var listProjectsSchema = z9.object({
2117
- status: z9.enum(["OPEN", "CLOSED"]).optional(),
2118
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2119
- page: z9.number().int().positive().optional().default(1),
2120
- perPage: z9.number().int().min(1).max(100).optional().default(25)
2248
+ import { z as z10 } from "zod";
2249
+ var listProjectsSchema = z10.object({
2250
+ status: z10.enum(["OPEN", "CLOSED"]).optional(),
2251
+ embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2252
+ ...paginationFields
2121
2253
  });
2122
2254
  async function listProjects(input) {
2123
- const { data, nextPage } = await capsuleGet("/kases", {
2255
+ return capsuleGetList("/kases", {
2124
2256
  status: input.status,
2125
2257
  embed: input.embed,
2126
2258
  page: input.page,
2127
2259
  perPage: input.perPage
2128
2260
  });
2129
- return { ...data, nextPage };
2130
2261
  }
2131
- var getProjectSchema = z9.object({
2262
+ var getProjectSchema = z10.object({
2132
2263
  id: positiveId,
2133
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2264
+ embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2134
2265
  });
2135
2266
  async function getProject(input) {
2136
2267
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -2138,20 +2269,20 @@ async function getProject(input) {
2138
2269
  });
2139
2270
  return data;
2140
2271
  }
2141
- var getProjectsSchema = z9.object({
2142
- ids: z9.array(positiveId).min(1).max(50).describe(
2272
+ var getProjectsSchema = z10.object({
2273
+ ids: z10.array(positiveId).min(1).max(50).describe(
2143
2274
  "Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
2144
2275
  ),
2145
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2276
+ embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2146
2277
  });
2147
2278
  async function getProjects(input) {
2148
2279
  return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
2149
2280
  }
2150
- var createProjectSchema = z9.object({
2151
- name: z9.string().min(1),
2281
+ var createProjectSchema = z10.object({
2282
+ name: z10.string().min(1),
2152
2283
  partyId: positiveId.describe("ID of the party linked to this project"),
2153
- description: z9.string().optional(),
2154
- status: z9.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
2284
+ description: z10.string().optional(),
2285
+ status: z10.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
2155
2286
  ownerId: positiveId.optional().describe(
2156
2287
  "Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation \u2014 e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied."
2157
2288
  ),
@@ -2161,8 +2292,8 @@ var createProjectSchema = z9.object({
2161
2292
  stageId: positiveId.optional().describe(
2162
2293
  "Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
2163
2294
  ),
2164
- expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2165
- fields: z9.array(CustomFieldWriteSchema).optional().describe(
2295
+ expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2296
+ fields: z10.array(CustomFieldWriteSchema).optional().describe(
2166
2297
  fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
2167
2298
  )
2168
2299
  });
@@ -2180,11 +2311,11 @@ async function createProject(input) {
2180
2311
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2181
2312
  return capsulePost("/kases", { kase: body });
2182
2313
  }
2183
- var updateProjectSchema = z9.object({
2314
+ var updateProjectSchema = z10.object({
2184
2315
  id: positiveId,
2185
- name: z9.string().min(1).optional(),
2186
- description: z9.string().optional(),
2187
- status: z9.enum(["OPEN", "CLOSED"]).optional(),
2316
+ name: z10.string().min(1).optional(),
2317
+ description: z10.string().optional(),
2318
+ status: z10.enum(["OPEN", "CLOSED"]).optional(),
2188
2319
  partyId: positiveId.optional().describe(
2189
2320
  "Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_opportunity.partyId`."
2190
2321
  ),
@@ -2197,8 +2328,8 @@ var updateProjectSchema = z9.object({
2197
2328
  stageId: positiveId.nullable().optional().describe(
2198
2329
  "Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
2199
2330
  ),
2200
- expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2201
- fields: z9.array(CustomFieldWriteSchema).optional().describe(
2331
+ expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2332
+ fields: z10.array(CustomFieldWriteSchema).optional().describe(
2202
2333
  fieldsArrayDescriptor("get_project") + " Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
2203
2334
  )
2204
2335
  });
@@ -2237,23 +2368,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
2237
2368
  });
2238
2369
 
2239
2370
  // src/tools/tasks.ts
2240
- import { z as z10 } from "zod";
2241
- var listTasksSchema = z10.object({
2371
+ import { z as z11 } from "zod";
2372
+ var listTasksSchema = z11.object({
2242
2373
  // Note: Capsule has a third internal status `PENDING` (a task that's
2243
2374
  // part of an active track but not yet "open"), but it can only be
2244
2375
  // reached via track machinery — it is NOT directly settable by
2245
2376
  // /tasks PUT, and a list filter for it returns the same as OPEN
2246
2377
  // anyway. We expose only the two values that are actually filterable
2247
2378
  // by the v2 API.
2248
- status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2379
+ status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
2249
2380
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
2250
2381
  ),
2251
2382
  ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
2252
- page: z10.number().int().positive().optional().default(1),
2253
- perPage: z10.number().int().min(1).max(100).optional().default(25)
2383
+ ...paginationFields
2254
2384
  });
2255
2385
  async function listTasks(input) {
2256
- const { data, nextPage } = await capsuleGet("/tasks", {
2386
+ return capsuleGetList("/tasks", {
2257
2387
  // Default 'OPEN' applied here (not via zod .default()) so that
2258
2388
  // z.infer keeps `status` optional for callers that omit it.
2259
2389
  status: input.status ?? "OPEN",
@@ -2262,28 +2392,27 @@ async function listTasks(input) {
2262
2392
  page: input.page,
2263
2393
  perPage: input.perPage
2264
2394
  });
2265
- return { ...data, nextPage };
2266
2395
  }
2267
- var getTaskSchema = z10.object({
2396
+ var getTaskSchema = z11.object({
2268
2397
  id: positiveId.describe("Task ID")
2269
2398
  });
2270
2399
  async function getTask(input) {
2271
2400
  const { data } = await capsuleGet(`/tasks/${input.id}`);
2272
2401
  return data;
2273
2402
  }
2274
- var getTasksSchema = z10.object({
2275
- ids: z10.array(positiveId).min(1).max(50).describe(
2403
+ var getTasksSchema = z11.object({
2404
+ ids: z11.array(positiveId).min(1).max(50).describe(
2276
2405
  "Array of task IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
2277
2406
  )
2278
2407
  });
2279
2408
  async function getTasks(input) {
2280
2409
  return chunkedMultiGet("/tasks", "tasks", input.ids);
2281
2410
  }
2282
- var createTaskSchema = z10.object({
2283
- description: z10.string().min(1),
2284
- dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
2285
- dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2286
- detail: z10.string().optional(),
2411
+ var createTaskSchema = z11.object({
2412
+ description: z11.string().min(1),
2413
+ dueOn: z11.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
2414
+ dueTime: z11.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2415
+ detail: z11.string().optional(),
2287
2416
  ownerId: positiveId.optional().describe(
2288
2417
  "Assign to user ID. Defaults to the API-token owner when omitted. Once set, this connector cannot clear the owner back to null \u2014 use Capsule's web UI for that."
2289
2418
  ),
@@ -2292,10 +2421,7 @@ var createTaskSchema = z10.object({
2292
2421
  projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
2293
2422
  });
2294
2423
  async function createTask(input) {
2295
- const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
2296
- if (linked.length > 1) {
2297
- throw new Error("Provide at most one of partyId, opportunityId, or projectId");
2298
- }
2424
+ assertSingleParentRef("create_task", input);
2299
2425
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
2300
2426
  const body = { ...rest };
2301
2427
  setRef(body, "owner", ownerId);
@@ -2304,16 +2430,16 @@ async function createTask(input) {
2304
2430
  setRef(body, "kase", projectId);
2305
2431
  return capsulePost("/tasks", { task: body });
2306
2432
  }
2307
- var updateTaskSchema = z10.object({
2433
+ var updateTaskSchema = z11.object({
2308
2434
  id: positiveId,
2309
- description: z10.string().min(1).optional(),
2310
- dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2311
- dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2312
- detail: z10.string().optional(),
2435
+ description: z11.string().min(1).optional(),
2436
+ dueOn: z11.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2437
+ dueTime: z11.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2438
+ detail: z11.string().optional(),
2313
2439
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
2314
2440
  // internal state) with 422 "cannot set task status to PENDING".
2315
2441
  // Only OPEN and COMPLETED are settable here.
2316
- status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2442
+ status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
2317
2443
  "Set to OPEN or COMPLETED. (PENDING exists internally for track-driven tasks but cannot be set directly via this tool \u2014 Capsule rejects it.) Setting status: OPEN on an already-open task is a true no-op (does not advance updatedAt)."
2318
2444
  ),
2319
2445
  ownerId: positiveId.optional().describe(
@@ -2331,12 +2457,7 @@ var updateTaskSchema = z10.object({
2331
2457
  });
2332
2458
  async function updateTask(input) {
2333
2459
  const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
2334
- const setCount = [partyId, opportunityId, projectId].filter((v) => typeof v === "number").length;
2335
- if (setCount > 1) {
2336
- throw new Error(
2337
- "update_task: provide at most one of partyId, opportunityId, or projectId (Capsule rejects multi-parent tasks with 422 'task can be related to at most one entity')"
2338
- );
2339
- }
2460
+ assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
2340
2461
  const body = {};
2341
2462
  for (const [k, v] of Object.entries(rest)) {
2342
2463
  if (v !== void 0) body[k] = v;
@@ -2347,7 +2468,7 @@ async function updateTask(input) {
2347
2468
  setNullableRef(body, "kase", projectId);
2348
2469
  return capsulePut(`/tasks/${id}`, { task: body });
2349
2470
  }
2350
- var completeTaskSchema = z10.object({
2471
+ var completeTaskSchema = z11.object({
2351
2472
  id: positiveId
2352
2473
  });
2353
2474
  async function completeTask(input) {
@@ -2355,8 +2476,8 @@ async function completeTask(input) {
2355
2476
  task: { status: "COMPLETED" }
2356
2477
  });
2357
2478
  }
2358
- var batchCompleteTaskSchema = z10.object({
2359
- ids: z10.array(positiveId).min(1).max(50).describe(
2479
+ var batchCompleteTaskSchema = z11.object({
2480
+ ids: z11.array(positiveId).min(1).max(50).describe(
2360
2481
  "Array of 1\u201350 task ids to mark COMPLETED in parallel. Each id resolves to one PUT /tasks/{id}; failures (e.g. 404 for a deleted task) surface per-item in the result array, the rest still complete. Capped at 50."
2361
2482
  )
2362
2483
  });
@@ -2370,77 +2491,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
2370
2491
  });
2371
2492
 
2372
2493
  // src/tools/entries.ts
2373
- import { z as z11 } from "zod";
2494
+ import { z as z12 } from "zod";
2374
2495
  var listEntriesPagination = {
2375
- page: z11.number().int().positive().optional().default(1),
2376
- perPage: z11.number().int().min(1).max(100).optional().default(25),
2377
- embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2496
+ ...paginationFields,
2497
+ embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2378
2498
  };
2379
- var listPartyEntriesSchema = z11.object({
2499
+ var listPartyEntriesSchema = z12.object({
2380
2500
  partyId: positiveId,
2381
2501
  ...listEntriesPagination,
2382
- includeLinkedPersons: z11.boolean().optional().describe(
2502
+ includeLinkedPersons: z12.boolean().optional().describe(
2383
2503
  "When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422 'entry must be linked to either a party, opportunity or kase'). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
2384
2504
  )
2385
2505
  });
2506
+ var PER_PARTY_FETCH_CAP = 100;
2386
2507
  async function fanOutPartyEntries(partyIds, embed, perPage) {
2387
- const concurrency = getBatchConcurrency();
2388
- const results = new Array(partyIds.length);
2389
- let cursor = 0;
2390
- async function worker() {
2391
- while (true) {
2392
- const i = cursor;
2393
- cursor += 1;
2394
- if (i >= partyIds.length) return;
2395
- const id = partyIds[i];
2396
- const { data, nextPage } = await capsuleGet(
2397
- `/parties/${id}/entries`,
2398
- {
2399
- embed,
2400
- page: 1,
2401
- perPage
2402
- }
2403
- );
2404
- results[i] = { entries: data.entries, nextPage };
2405
- }
2406
- }
2407
- const workers = [];
2408
- for (let w = 0; w < Math.min(concurrency, partyIds.length); w++) {
2409
- workers.push(worker());
2410
- }
2411
- await Promise.all(workers);
2412
- return results;
2508
+ return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
2509
+ const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
2510
+ embed,
2511
+ page: 1,
2512
+ perPage
2513
+ });
2514
+ return { entries: data.entries, nextPage };
2515
+ });
2413
2516
  }
2414
2517
  function mergedTimelineCandidatePerParty(page, perPage) {
2415
- return Math.min(page * perPage, 100);
2518
+ return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
2416
2519
  }
2417
2520
  function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
2418
2521
  const requestedWindowEnd = page * perPage;
2419
2522
  if (mergedLength > requestedWindowEnd) return page + 1;
2420
- const nextWindowWithinCap = requestedWindowEnd < 100;
2523
+ const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
2421
2524
  if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
2422
2525
  return void 0;
2423
2526
  }
2424
2527
  async function listPartyEntries(input) {
2425
2528
  const { partyId, embed, page, perPage, includeLinkedPersons } = input;
2426
2529
  if (!includeLinkedPersons) {
2427
- const { data, nextPage: nextPage2 } = await capsuleGet(
2428
- `/parties/${partyId}/entries`,
2429
- { embed, page, perPage }
2430
- );
2431
- return { ...data, nextPage: nextPage2 };
2530
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2531
+ embed,
2532
+ page,
2533
+ perPage
2534
+ });
2432
2535
  }
2433
2536
  const { data: peopleData } = await capsuleGet(
2434
2537
  `/parties/${partyId}/people`,
2435
- { page: 1, perPage: 100 }
2538
+ { page: 1, perPage: PER_PARTY_FETCH_CAP }
2436
2539
  );
2437
2540
  const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
2438
2541
  if (peopleIds.length === 0) {
2439
- const { data, nextPage: nextPage2 } = await capsuleGet(
2440
- `/parties/${partyId}/entries`,
2441
- { embed, page, perPage }
2442
- );
2443
- return { ...data, nextPage: nextPage2 };
2542
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2543
+ embed,
2544
+ page,
2545
+ perPage
2546
+ });
2444
2547
  }
2445
2548
  const targetIds = [partyId, ...peopleIds];
2446
2549
  const perPartyPages = await fanOutPartyEntries(
@@ -2475,31 +2578,31 @@ async function listPartyEntries(input) {
2475
2578
  );
2476
2579
  return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
2477
2580
  }
2478
- var listOpportunityEntriesSchema = z11.object({
2581
+ var listOpportunityEntriesSchema = z12.object({
2479
2582
  opportunityId: positiveId,
2480
2583
  ...listEntriesPagination
2481
2584
  });
2482
2585
  async function listOpportunityEntries(input) {
2483
- const { data, nextPage } = await capsuleGet(
2484
- `/opportunities/${input.opportunityId}/entries`,
2485
- { embed: input.embed, page: input.page, perPage: input.perPage }
2486
- );
2487
- return { ...data, nextPage };
2586
+ return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
2587
+ embed: input.embed,
2588
+ page: input.page,
2589
+ perPage: input.perPage
2590
+ });
2488
2591
  }
2489
- var listProjectEntriesSchema = z11.object({
2592
+ var listProjectEntriesSchema = z12.object({
2490
2593
  projectId: positiveId,
2491
2594
  ...listEntriesPagination
2492
2595
  });
2493
2596
  async function listProjectEntries(input) {
2494
- const { data, nextPage } = await capsuleGet(
2495
- `/kases/${input.projectId}/entries`,
2496
- { embed: input.embed, page: input.page, perPage: input.perPage }
2497
- );
2498
- return { ...data, nextPage };
2597
+ return capsuleGetList(`/kases/${input.projectId}/entries`, {
2598
+ embed: input.embed,
2599
+ page: input.page,
2600
+ perPage: input.perPage
2601
+ });
2499
2602
  }
2500
- var getEntrySchema = z11.object({
2603
+ var getEntrySchema = z12.object({
2501
2604
  id: positiveId,
2502
- embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2605
+ embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2503
2606
  });
2504
2607
  async function getEntry(input) {
2505
2608
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2507,34 +2610,30 @@ async function getEntry(input) {
2507
2610
  });
2508
2611
  return data;
2509
2612
  }
2510
- var listEntriesSchema = z11.object({
2613
+ var listEntriesSchema = z12.object({
2511
2614
  ...listEntriesPagination
2512
2615
  });
2513
2616
  async function listEntries(input) {
2514
- const { data, nextPage } = await capsuleGet("/entries", {
2617
+ return capsuleGetList("/entries", {
2515
2618
  embed: input.embed,
2516
2619
  page: input.page,
2517
2620
  perPage: input.perPage
2518
2621
  });
2519
- return { ...data, nextPage };
2520
2622
  }
2521
- var addNoteSchema = z11.object({
2522
- content: z11.string().min(1).describe(
2623
+ var addNoteSchema = z12.object({
2624
+ content: z12.string().min(1).describe(
2523
2625
  "Note body text. Stored verbatim and treated as MARKDOWN \u2014 Capsule's web UI renders the markdown when displaying. Pass markdown source ('# Heading', '**bold**', '- bullet'), not HTML."
2524
2626
  ),
2525
2627
  partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2526
2628
  opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2527
2629
  projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
2528
- entryAt: z11.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
2630
+ entryAt: z12.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
2529
2631
  "ISO-8601 timestamp for when this note actually happened (e.g. '2024-03-15T14:30:00Z'). Defaults to now. Use this for backdating historical notes when migrating from another system. `entryAt` is preserved across subsequent update_entry calls; only `updatedAt` advances on edits. Note attribution flows to the API-token owner \u2014 there is no way to record a note as authored by a different user via this connector (a `creatorId` parameter would enable audit-attribution spoofing on shared-connector deployments, so it is intentionally not exposed)."
2530
2632
  )
2531
2633
  });
2532
2634
  async function addNote(input) {
2533
2635
  const { content, partyId, opportunityId, projectId, entryAt } = input;
2534
- const linked = [partyId, opportunityId, projectId].filter(Boolean);
2535
- if (linked.length !== 1) {
2536
- throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
2537
- }
2636
+ assertSingleParentRef("add_note", input, { required: true });
2538
2637
  const body = { type: "note", content };
2539
2638
  setRef(body, "party", partyId);
2540
2639
  setRef(body, "opportunity", opportunityId);
@@ -2542,12 +2641,12 @@ async function addNote(input) {
2542
2641
  if (entryAt !== void 0) body["entryAt"] = entryAt;
2543
2642
  return capsulePost("/entries", { entry: body });
2544
2643
  }
2545
- var updateEntrySchema = z11.object({
2644
+ var updateEntrySchema = z12.object({
2546
2645
  id: positiveId.describe("Entry ID to update"),
2547
- content: z11.string().min(1).optional().describe(
2646
+ content: z12.string().min(1).optional().describe(
2548
2647
  "New body text for the entry. For notes, this is the markdown content; for emails, the body. Provide only if you want to change it."
2549
2648
  ),
2550
- subject: z11.string().optional().describe(
2649
+ subject: z12.string().optional().describe(
2551
2650
  "New subject line. Mostly meaningful on email-type entries; on plain notes Capsule accepts the call (HTTP 200) but **does not store the subject and does not advance `updatedAt`** \u2014 a true no-op for inapplicable fields. `entryAt` (when the note was authored) is preserved across edits; `updatedAt` advances only when an applicable field actually changes. To sort/filter by 'when did this happen', use `entryAt`; for 'last touched', use `updatedAt`."
2552
2651
  )
2553
2652
  });
@@ -2569,62 +2668,50 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
2569
2668
  });
2570
2669
 
2571
2670
  // src/tools/pipelines.ts
2572
- import { z as z12 } from "zod";
2573
- var paginationFields = {
2574
- page: z12.number().int().positive().optional(),
2575
- perPage: z12.number().int().min(1).max(100).optional()
2576
- };
2577
- var listPipelinesSchema = z12.object({ ...paginationFields });
2671
+ import { z as z13 } from "zod";
2672
+ var listPipelinesSchema = z13.object({ ...paginationFieldsNoDefaults });
2578
2673
  async function listPipelines(input) {
2579
- const { data, nextPage } = await capsuleGetCached("/pipelines", {
2674
+ return capsuleGetCachedList("/pipelines", {
2580
2675
  page: input.page ?? 1,
2581
2676
  perPage: input.perPage ?? 100
2582
2677
  });
2583
- return { ...data, nextPage };
2584
2678
  }
2585
- var listMilestonesSchema = z12.object({
2679
+ var listMilestonesSchema = z13.object({
2586
2680
  pipelineId: positiveId,
2587
- ...paginationFields
2681
+ ...paginationFieldsNoDefaults
2588
2682
  });
2589
2683
  async function listMilestones(input) {
2590
- const { data, nextPage } = await capsuleGetCached(
2684
+ return capsuleGetCachedList(
2591
2685
  `/pipelines/${input.pipelineId}/milestones`,
2592
2686
  { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2593
2687
  );
2594
- return { ...data, nextPage };
2595
2688
  }
2596
2689
 
2597
2690
  // src/tools/boards.ts
2598
- import { z as z13 } from "zod";
2599
- var paginationFields2 = {
2600
- page: z13.number().int().positive().optional(),
2601
- perPage: z13.number().int().min(1).max(100).optional()
2602
- };
2603
- var listBoardsSchema = z13.object({ ...paginationFields2 });
2691
+ import { z as z14 } from "zod";
2692
+ var listBoardsSchema = z14.object({ ...paginationFieldsNoDefaults });
2604
2693
  async function listBoards(input) {
2605
- const { data, nextPage } = await capsuleGetCached("/boards", {
2694
+ return capsuleGetCachedList("/boards", {
2606
2695
  page: input.page ?? 1,
2607
2696
  perPage: input.perPage ?? 100
2608
2697
  });
2609
- return { ...data, nextPage };
2610
2698
  }
2611
- var listStagesSchema = z13.object({
2699
+ var listStagesSchema = z14.object({
2612
2700
  boardId: positiveId.optional().describe(
2613
2701
  "Optional. If provided, returns only the stages defined on that specific board (uses /boards/{id}/stages). Omit to get all stages across all boards in one call."
2614
2702
  ),
2615
- ...paginationFields2
2703
+ ...paginationFieldsNoDefaults
2616
2704
  });
2617
2705
  async function listStages(input) {
2618
2706
  const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
2619
- const { data, nextPage } = await capsuleGetCached(path, {
2707
+ return capsuleGetCachedList(path, {
2620
2708
  page: input.page ?? 1,
2621
2709
  perPage: input.perPage ?? 100
2622
2710
  });
2623
- return { ...data, nextPage };
2624
2711
  }
2625
2712
 
2626
2713
  // src/tools/tags.ts
2627
- import { z as z14 } from "zod";
2714
+ import { z as z15 } from "zod";
2628
2715
  var TAG_LIST_PATH = {
2629
2716
  parties: "/parties/tags",
2630
2717
  opportunities: "/opportunities/tags",
@@ -2635,24 +2722,22 @@ var ENTITY_TO_WRAPPER = {
2635
2722
  opportunities: "opportunity",
2636
2723
  kases: "kase"
2637
2724
  };
2638
- var TagEntity = z14.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2639
- var listTagsSchema = z14.object({
2640
- entity: z14.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2641
- page: z14.number().int().positive().optional(),
2642
- perPage: z14.number().int().min(1).max(100).optional()
2725
+ var TagEntity = z15.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2726
+ var listTagsSchema = z15.object({
2727
+ entity: z15.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2728
+ ...paginationFieldsNoDefaults
2643
2729
  });
2644
2730
  async function listTags(input) {
2645
2731
  const path = TAG_LIST_PATH[input.entity];
2646
- const { data, nextPage } = await capsuleGetCached(path, {
2732
+ return capsuleGetCachedList(path, {
2647
2733
  page: input.page ?? 1,
2648
2734
  perPage: input.perPage ?? 100
2649
2735
  });
2650
- return { ...data, nextPage };
2651
2736
  }
2652
- var addTagSchema = z14.object({
2737
+ var addTagSchema = z15.object({
2653
2738
  entity: TagEntity,
2654
2739
  entityId: positiveId.describe("The party/opportunity/kase id."),
2655
- tagName: z14.string().min(1).describe(
2740
+ tagName: z15.string().min(1).describe(
2656
2741
  "Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
2657
2742
  )
2658
2743
  });
@@ -2665,7 +2750,7 @@ async function addTag(input) {
2665
2750
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2666
2751
  return result;
2667
2752
  }
2668
- var removeTagByIdSchema = z14.object({
2753
+ var removeTagByIdSchema = z15.object({
2669
2754
  entity: TagEntity,
2670
2755
  entityId: positiveId.describe("The party/opportunity/kase id."),
2671
2756
  tagId: positiveId.describe(
@@ -2696,7 +2781,7 @@ async function removeTagById(input) {
2696
2781
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2697
2782
  return result;
2698
2783
  }
2699
- var deleteTagDefinitionSchema = z14.object({
2784
+ var deleteTagDefinitionSchema = z15.object({
2700
2785
  entity: TagEntity,
2701
2786
  tagId: positiveId.describe(
2702
2787
  "The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
@@ -2732,44 +2817,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
2732
2817
  });
2733
2818
 
2734
2819
  // src/tools/users.ts
2735
- import { z as z15 } from "zod";
2736
- var listUsersSchema = z15.object({
2737
- page: z15.number().int().positive().optional(),
2738
- perPage: z15.number().int().min(1).max(100).optional()
2820
+ import { z as z16 } from "zod";
2821
+ var listUsersSchema = z16.object({
2822
+ ...paginationFieldsNoDefaults
2739
2823
  });
2740
2824
  async function listUsers(input) {
2741
- const { data, nextPage } = await capsuleGetCached("/users", {
2825
+ return capsuleGetCachedList("/users", {
2742
2826
  page: input.page ?? 1,
2743
2827
  perPage: input.perPage ?? 100
2744
2828
  });
2745
- return { ...data, nextPage };
2746
2829
  }
2747
- var getCurrentUserSchema = z15.object({});
2830
+ var getCurrentUserSchema = z16.object({});
2748
2831
  async function getCurrentUser(_input) {
2749
2832
  const { data } = await capsuleGet("/users/current");
2750
2833
  return data;
2751
2834
  }
2752
2835
 
2753
2836
  // src/tools/filters.ts
2754
- import { z as z16 } from "zod";
2755
- var FilterConditionSchema = z16.object({
2756
- field: z16.string().describe(
2837
+ import { z as z17 } from "zod";
2838
+ var FilterConditionSchema = z17.object({
2839
+ field: z17.string().describe(
2757
2840
  "The Capsule filter-side field name (these differ from response field names \u2014 e.g. response.createdAt is filter-side 'addedOn', response.lastContactedAt is filter-side 'lastContactedOn'). Common: 'addedOn' (date created), 'updatedOn' (date last modified), 'lastContactedOn' (parties only), 'name', 'tag', 'owner', 'team', 'type' (parties: person|organisation), 'milestone' (opportunities), 'status' (opp/project: OPEN|CLOSED), 'closedOn' (opp/project), 'expectedCloseOn' (opp/project), 'hasTags', 'hasEmailAddress' (parties), 'isOpen', 'isStale' (opportunities), 'custom:{fieldId}'. Full per-entity list: https://developer.capsulecrm.com/v2/reference/filters"
2758
2841
  ),
2759
- operator: z16.string().describe(
2842
+ operator: z17.string().describe(
2760
2843
  "The filter operator. Common: 'is', 'is not' (use value=null to test for null), 'contains', 'does not contain', 'is greater than', 'is less than', 'is within last' (date fields, value=integer days), 'is more than' (date fields, value=integer days ago), 'starts with', 'ends with'. Operator validity depends on the field's type."
2761
2844
  ),
2762
- value: z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]).describe(
2845
+ value: z17.union([z17.string(), z17.number(), z17.boolean(), z17.null()]).describe(
2763
2846
  "The value to compare against. For 'is within last' on date fields, pass an integer number of days. For tag filters, pass the tag name (string) or tag id (number). For 'is not' null tests, pass null literally."
2764
2847
  )
2765
2848
  });
2766
- var FilterInputSchema = z16.object({
2767
- conditions: z16.array(FilterConditionSchema).min(1).describe(
2849
+ var FilterInputSchema = z17.object({
2850
+ conditions: z17.array(FilterConditionSchema).min(1).describe(
2768
2851
  "Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
2769
2852
  ),
2770
- embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2771
- page: z16.number().int().positive().optional().default(1),
2772
- perPage: z16.number().int().min(1).max(100).optional().default(25)
2853
+ embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2854
+ ...paginationFields
2773
2855
  });
2774
2856
  async function runFilter(entityPath, input) {
2775
2857
  const { data, nextPage } = await capsuleSearch(
@@ -2789,10 +2871,7 @@ async function filterParties(input) {
2789
2871
  }
2790
2872
  var filterOpportunitiesSchema = FilterInputSchema;
2791
2873
  async function filterOpportunities(input) {
2792
- return runFilter(
2793
- "opportunities",
2794
- input
2795
- );
2874
+ return runFilter("opportunities", input);
2796
2875
  }
2797
2876
  var filterProjectsSchema = FilterInputSchema;
2798
2877
  async function filterProjects(input) {
@@ -2800,139 +2879,126 @@ async function filterProjects(input) {
2800
2879
  }
2801
2880
 
2802
2881
  // src/tools/metadata.ts
2803
- import { z as z17 } from "zod";
2804
- var paginationFields3 = {
2805
- page: z17.number().int().positive().optional(),
2806
- perPage: z17.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2882
+ import { z as z18 } from "zod";
2883
+ var paginationFields2 = {
2884
+ ...paginationFieldsNoDefaults,
2885
+ perPage: paginationFieldsNoDefaults.perPage.describe(
2886
+ "Page size, max 100. Defaults to 100 for reference data."
2887
+ )
2807
2888
  };
2808
- var listTeamsSchema = z17.object({ ...paginationFields3 });
2889
+ var listTeamsSchema = z18.object({ ...paginationFields2 });
2809
2890
  async function listTeams(input) {
2810
- const { data, nextPage } = await capsuleGetCached("/teams", {
2891
+ return capsuleGetCachedList("/teams", {
2811
2892
  page: input.page ?? 1,
2812
2893
  perPage: input.perPage ?? 100
2813
2894
  });
2814
- return { ...data, nextPage };
2815
2895
  }
2816
- var listLostReasonsSchema = z17.object({ ...paginationFields3 });
2896
+ var listLostReasonsSchema = z18.object({ ...paginationFields2 });
2817
2897
  async function listLostReasons(input) {
2818
- const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2898
+ return capsuleGetCachedList("/lostreasons", {
2819
2899
  page: input.page ?? 1,
2820
2900
  perPage: input.perPage ?? 100
2821
2901
  });
2822
- return { ...data, nextPage };
2823
2902
  }
2824
- var listActivityTypesSchema = z17.object({ ...paginationFields3 });
2903
+ var listActivityTypesSchema = z18.object({ ...paginationFields2 });
2825
2904
  async function listActivityTypes(input) {
2826
- const { data, nextPage } = await capsuleGetCached(
2827
- "/activitytypes",
2828
- {
2829
- page: input.page ?? 1,
2830
- perPage: input.perPage ?? 100
2831
- }
2832
- );
2833
- return { ...data, nextPage };
2905
+ return capsuleGetCachedList("/activitytypes", {
2906
+ page: input.page ?? 1,
2907
+ perPage: input.perPage ?? 100
2908
+ });
2834
2909
  }
2835
- var getSiteSchema = z17.object({});
2910
+ var getSiteSchema = z18.object({});
2836
2911
  async function getSite(_input) {
2837
2912
  const { data } = await capsuleGetCached("/site");
2838
2913
  return data;
2839
2914
  }
2840
- var listTrackDefinitionsSchema = z17.object({ ...paginationFields3 });
2915
+ var listTrackDefinitionsSchema = z18.object({ ...paginationFields2 });
2841
2916
  async function listTrackDefinitions(input) {
2842
- const { data, nextPage } = await capsuleGetCached(
2843
- "/trackdefinitions",
2844
- { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2845
- );
2846
- return { ...data, nextPage };
2917
+ return capsuleGetCachedList("/trackdefinitions", {
2918
+ page: input.page ?? 1,
2919
+ perPage: input.perPage ?? 100
2920
+ });
2847
2921
  }
2848
- var listCategoriesSchema = z17.object({ ...paginationFields3 });
2922
+ var listCategoriesSchema = z18.object({ ...paginationFields2 });
2849
2923
  async function listCategories(input) {
2850
- const { data, nextPage } = await capsuleGetCached("/categories", {
2924
+ return capsuleGetCachedList("/categories", {
2851
2925
  page: input.page ?? 1,
2852
2926
  perPage: input.perPage ?? 100
2853
2927
  });
2854
- return { ...data, nextPage };
2855
2928
  }
2856
- var listGoalsSchema = z17.object({ ...paginationFields3 });
2929
+ var listGoalsSchema = z18.object({ ...paginationFields2 });
2857
2930
  async function listGoals(input) {
2858
- const { data, nextPage } = await capsuleGetCached("/goals", {
2931
+ return capsuleGetCachedList("/goals", {
2859
2932
  page: input.page ?? 1,
2860
2933
  perPage: input.perPage ?? 100
2861
2934
  });
2862
- return { ...data, nextPage };
2863
2935
  }
2864
2936
 
2865
2937
  // src/tools/audit.ts
2866
- import { z as z18 } from "zod";
2867
- var listEmployeesSchema = z18.object({
2938
+ import { z as z19 } from "zod";
2939
+ var listEmployeesSchema = z19.object({
2868
2940
  partyId: positiveId.describe(
2869
2941
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2870
2942
  ),
2871
- page: z18.number().int().positive().optional().default(1),
2872
- perPage: z18.number().int().min(1).max(100).optional().default(25),
2873
- embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2943
+ ...paginationFields,
2944
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2874
2945
  });
2875
2946
  async function listEmployees(input) {
2876
- const { data, nextPage } = await capsuleGet(
2877
- `/parties/${input.partyId}/people`,
2878
- { page: input.page, perPage: input.perPage, embed: input.embed }
2879
- );
2880
- return { ...data, nextPage };
2947
+ return capsuleGetList(`/parties/${input.partyId}/people`, {
2948
+ page: input.page,
2949
+ perPage: input.perPage,
2950
+ embed: input.embed
2951
+ });
2881
2952
  }
2882
- var DeletedSinceSchema = z18.string().describe(
2953
+ var DeletedSinceSchema = z19.string().describe(
2883
2954
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2884
2955
  );
2885
2956
  var DeletedPagination = {
2886
2957
  since: DeletedSinceSchema,
2887
- page: z18.number().int().positive().optional().default(1),
2888
- perPage: z18.number().int().min(1).max(100).optional().default(25)
2958
+ ...paginationFields
2889
2959
  };
2890
- var listDeletedPartiesSchema = z18.object(DeletedPagination);
2960
+ var listDeletedPartiesSchema = z19.object(DeletedPagination);
2891
2961
  async function listDeletedParties(input) {
2892
- const { data, nextPage } = await capsuleGet("/parties/deleted", {
2962
+ return capsuleGetList("/parties/deleted", {
2893
2963
  since: input.since,
2894
2964
  page: input.page,
2895
2965
  perPage: input.perPage
2896
2966
  });
2897
- return { ...data, nextPage };
2898
2967
  }
2899
- var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
2968
+ var listDeletedOpportunitiesSchema = z19.object(DeletedPagination);
2900
2969
  async function listDeletedOpportunities(input) {
2901
- const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2970
+ return capsuleGetList("/opportunities/deleted", {
2902
2971
  since: input.since,
2903
2972
  page: input.page,
2904
2973
  perPage: input.perPage
2905
2974
  });
2906
- return { ...data, nextPage };
2907
2975
  }
2908
- var listDeletedProjectsSchema = z18.object(DeletedPagination);
2976
+ var listDeletedProjectsSchema = z19.object(DeletedPagination);
2909
2977
  async function listDeletedProjects(input) {
2910
- const { data, nextPage } = await capsuleGet("/kases/deleted", {
2978
+ return capsuleGetList("/kases/deleted", {
2911
2979
  since: input.since,
2912
2980
  page: input.page,
2913
2981
  perPage: input.perPage
2914
2982
  });
2915
- return { ...data, nextPage };
2916
2983
  }
2917
2984
 
2918
2985
  // src/tools/relationships.ts
2919
- import { z as z19 } from "zod";
2920
- var RelationshipEntity = z19.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2921
- var listAdditionalPartiesSchema = z19.object({
2986
+ import { z as z20 } from "zod";
2987
+ var RelationshipEntity = z20.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2988
+ var listAdditionalPartiesSchema = z20.object({
2922
2989
  entity: RelationshipEntity,
2923
2990
  entityId: positiveId.describe("ID of the opportunity or project."),
2924
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2925
- page: z19.number().int().positive().optional().default(1),
2926
- perPage: z19.number().int().min(1).max(100).optional().default(25)
2991
+ embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2992
+ ...paginationFields
2927
2993
  });
2928
2994
  async function listAdditionalParties(input) {
2929
- const { data, nextPage } = await capsuleGet(
2930
- `/${input.entity}/${input.entityId}/parties`,
2931
- { embed: input.embed, page: input.page, perPage: input.perPage }
2932
- );
2933
- return { ...data, nextPage };
2995
+ return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
2996
+ embed: input.embed,
2997
+ page: input.page,
2998
+ perPage: input.perPage
2999
+ });
2934
3000
  }
2935
- var addAdditionalPartySchema = z19.object({
3001
+ var addAdditionalPartySchema = z20.object({
2936
3002
  entity: RelationshipEntity,
2937
3003
  entityId: positiveId,
2938
3004
  partyId: positiveId.describe(
@@ -2965,7 +3031,7 @@ async function addAdditionalParty(input) {
2965
3031
  throw err;
2966
3032
  }
2967
3033
  }
2968
- var removeAdditionalPartySchema = z19.object({
3034
+ var removeAdditionalPartySchema = z20.object({
2969
3035
  entity: RelationshipEntity,
2970
3036
  entityId: positiveId,
2971
3037
  partyId: positiveId,
@@ -2995,24 +3061,23 @@ async function removeAdditionalParty(input) {
2995
3061
  })
2996
3062
  );
2997
3063
  }
2998
- var listAssociatedProjectsSchema = z19.object({
3064
+ var listAssociatedProjectsSchema = z20.object({
2999
3065
  opportunityId: positiveId,
3000
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3001
- page: z19.number().int().positive().optional().default(1),
3002
- perPage: z19.number().int().min(1).max(100).optional().default(25)
3066
+ embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3067
+ ...paginationFields
3003
3068
  });
3004
3069
  async function listAssociatedProjects(input) {
3005
- const { data, nextPage } = await capsuleGet(
3006
- `/opportunities/${input.opportunityId}/kases`,
3007
- { embed: input.embed, page: input.page, perPage: input.perPage }
3008
- );
3009
- return { ...data, nextPage };
3070
+ return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
3071
+ embed: input.embed,
3072
+ page: input.page,
3073
+ perPage: input.perPage
3074
+ });
3010
3075
  }
3011
3076
 
3012
3077
  // src/tools/custom-fields.ts
3013
- import { z as z20 } from "zod";
3014
- var CustomFieldEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
3015
- var listCustomFieldsSchema = z20.object({
3078
+ import { z as z21 } from "zod";
3079
+ var CustomFieldEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
3080
+ var listCustomFieldsSchema = z21.object({
3016
3081
  entity: CustomFieldEntity
3017
3082
  });
3018
3083
  async function listCustomFields(input) {
@@ -3021,7 +3086,7 @@ async function listCustomFields(input) {
3021
3086
  );
3022
3087
  return data;
3023
3088
  }
3024
- var getCustomFieldSchema = z20.object({
3089
+ var getCustomFieldSchema = z21.object({
3025
3090
  entity: CustomFieldEntity,
3026
3091
  fieldId: positiveId.describe("Custom field definition id.")
3027
3092
  });
@@ -3033,9 +3098,9 @@ async function getCustomField(input) {
3033
3098
  }
3034
3099
 
3035
3100
  // src/tools/tracks.ts
3036
- import { z as z21 } from "zod";
3037
- var TrackEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
3038
- var listEntityTracksSchema = z21.object({
3101
+ import { z as z22 } from "zod";
3102
+ var TrackEntity = z22.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
3103
+ var listEntityTracksSchema = z22.object({
3039
3104
  entity: TrackEntity,
3040
3105
  entityId: positiveId
3041
3106
  });
@@ -3045,20 +3110,20 @@ async function listEntityTracks(input) {
3045
3110
  );
3046
3111
  return data;
3047
3112
  }
3048
- var showTrackSchema = z21.object({
3113
+ var showTrackSchema = z22.object({
3049
3114
  trackId: positiveId
3050
3115
  });
3051
3116
  async function showTrack(input) {
3052
3117
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
3053
3118
  return data;
3054
3119
  }
3055
- var applyTrackSchema = z21.object({
3056
- entity: z21.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
3120
+ var applyTrackSchema = z22.object({
3121
+ entity: z22.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
3057
3122
  entityId: positiveId,
3058
3123
  trackDefinitionId: positiveId.describe(
3059
3124
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
3060
3125
  ),
3061
- startDate: z21.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
3126
+ startDate: z22.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
3062
3127
  "Optional ISO-8601 date (YYYY-MM-DD) the track should start from \u2014 drives task due-date calculations (each task's `dueOn` is computed as startDate + the track-definition's `daysAfter` offset). Defaults to today if omitted. Useful for scheduling a renewal-queue track against a future contract end-date, or backfilling tracks for historical projects."
3063
3128
  )
3064
3129
  });
@@ -3071,9 +3136,9 @@ async function applyTrack(input) {
3071
3136
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
3072
3137
  return capsulePost("/tracks", { track });
3073
3138
  }
3074
- var updateTrackSchema = z21.object({
3139
+ var updateTrackSchema = z22.object({
3075
3140
  trackId: positiveId,
3076
- fields: z21.record(z21.string(), z21.unknown()).describe(
3141
+ fields: z22.record(z22.string(), z22.unknown()).describe(
3077
3142
  "Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
3078
3143
  )
3079
3144
  });
@@ -3085,7 +3150,7 @@ async function updateTrack(input) {
3085
3150
  track: input.fields
3086
3151
  });
3087
3152
  }
3088
- var removeTrackSchema = z21.object({
3153
+ var removeTrackSchema = z22.object({
3089
3154
  trackId: positiveId,
3090
3155
  confirm: confirmFlag().describe(
3091
3156
  "Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
@@ -3103,13 +3168,13 @@ async function removeTrack(input) {
3103
3168
  }
3104
3169
 
3105
3170
  // src/tools/attachments.ts
3106
- import { z as z22 } from "zod";
3171
+ import { z as z23 } from "zod";
3107
3172
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
3108
3173
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
3109
3174
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
3110
- var getAttachmentSchema = z22.object({
3175
+ var getAttachmentSchema = z23.object({
3111
3176
  id: positiveId.describe("Attachment ID."),
3112
- maxSizeBytes: z22.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
3177
+ maxSizeBytes: z23.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
3113
3178
  `Refuse to return content over this size (default ${DEFAULT_MAX_SIZE_BYTES} bytes \u2248 5MB; max ${HARD_MAX_SIZE_BYTES} bytes \u2248 25MB). Files exceeding the cap return metadata only with a 'truncated: true' flag.`
3114
3179
  )
3115
3180
  });
@@ -3124,17 +3189,17 @@ async function getAttachment(input) {
3124
3189
  }
3125
3190
  return { contentType, buffer, sizeBytes };
3126
3191
  }
3127
- var uploadAttachmentSchema = z22.object({
3128
- filename: z22.string().min(1).describe(
3192
+ var uploadAttachmentSchema = z23.object({
3193
+ filename: z23.string().min(1).describe(
3129
3194
  "Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes \u2014 a typo in either is accepted and the file is stored as labelled."
3130
3195
  ),
3131
- contentType: z22.string().min(1).describe(
3196
+ contentType: z23.string().min(1).describe(
3132
3197
  "MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes."
3133
3198
  ),
3134
- dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
3199
+ dataBase64: z23.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
3135
3200
  "File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary."
3136
3201
  ),
3137
- content: z22.string().optional().describe(
3202
+ content: z23.string().optional().describe(
3138
3203
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
3139
3204
  ),
3140
3205
  partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
@@ -3152,12 +3217,7 @@ function decodedBase64Size(s) {
3152
3217
  return s.length / 4 * 3 - padding;
3153
3218
  }
3154
3219
  async function uploadAttachment(input) {
3155
- const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
3156
- if (linked.length !== 1) {
3157
- throw new Error(
3158
- "upload_attachment: provide exactly one of partyId, opportunityId, or projectId"
3159
- );
3160
- }
3220
+ assertSingleParentRef("upload_attachment", input, { required: true });
3161
3221
  if (!isValidBase64(input.dataBase64)) {
3162
3222
  throw new Error(
3163
3223
  "upload_attachment: dataBase64 is not valid base64 \u2014 Node's tolerant decoder would silently produce corrupt bytes. Verify the encoding (RFC 4648, padded with '=' to a multiple of 4 chars)."
@@ -3182,37 +3242,36 @@ async function uploadAttachment(input) {
3182
3242
  content: input.content ?? "[attachment]",
3183
3243
  attachments: [{ token }]
3184
3244
  };
3185
- if (input.partyId) entryBody["party"] = { id: input.partyId };
3186
- if (input.opportunityId) entryBody["opportunity"] = { id: input.opportunityId };
3187
- if (input.projectId) entryBody["kase"] = { id: input.projectId };
3245
+ setRef(entryBody, "party", input.partyId);
3246
+ setRef(entryBody, "opportunity", input.opportunityId);
3247
+ setRef(entryBody, "kase", input.projectId);
3188
3248
  return capsulePost("/entries", { entry: entryBody });
3189
3249
  }
3190
3250
 
3191
3251
  // src/tools/saved-filters.ts
3192
- import { z as z23 } from "zod";
3193
- var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
3252
+ import { z as z24 } from "zod";
3253
+ var EntitySchema = z24.enum(["parties", "opportunities", "kases"]).describe(
3194
3254
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
3195
3255
  );
3196
- var listSavedFiltersSchema = z23.object({
3256
+ var listSavedFiltersSchema = z24.object({
3197
3257
  entity: EntitySchema
3198
3258
  });
3199
3259
  async function listSavedFilters(input) {
3200
3260
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
3201
3261
  return data;
3202
3262
  }
3203
- var runSavedFilterSchema = z23.object({
3263
+ var runSavedFilterSchema = z24.object({
3204
3264
  entity: EntitySchema,
3205
3265
  id: positiveId.describe("The saved filter id (from list_saved_filters)."),
3206
- embed: z23.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3207
- page: z23.number().int().positive().optional().default(1),
3208
- perPage: z23.number().int().min(1).max(100).optional().default(25)
3266
+ embed: z24.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3267
+ ...paginationFields
3209
3268
  });
3210
3269
  async function runSavedFilter(input) {
3211
- const { data, nextPage } = await capsuleGet(
3212
- `/${input.entity}/filters/${input.id}/results`,
3213
- { page: input.page, perPage: input.perPage, embed: input.embed }
3214
- );
3215
- return { ...data, nextPage };
3270
+ return capsuleGetList(`/${input.entity}/filters/${input.id}/results`, {
3271
+ page: input.page,
3272
+ perPage: input.perPage,
3273
+ embed: input.embed
3274
+ });
3216
3275
  }
3217
3276
 
3218
3277
  // src/server.ts
@@ -3223,7 +3282,7 @@ function createCapsuleMcpServer(opts) {
3223
3282
  const server = new McpServer(
3224
3283
  {
3225
3284
  name: "capsulemcp",
3226
- version: "1.7.0",
3285
+ version: "1.8.1",
3227
3286
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
3228
3287
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
3229
3288
  icons: ICONS
@@ -3673,20 +3732,72 @@ function createCapsuleMcpServer(opts) {
3673
3732
  listEntriesSchema,
3674
3733
  listEntries
3675
3734
  );
3676
- server.tool(
3677
- "get_attachment",
3678
- "Download an attachment by id. Returns image content for image/* types (Claude can describe it natively); decoded text for text/* and application/json (small files); JSON metadata + base64 payload for other binary types (PDF, Office docs, etc.). Files exceeding maxSizeBytes (default 5MB) return metadata only with a `truncated: true` flag.",
3679
- getAttachmentSchema.shape,
3680
- // get_attachment is read-only — downloads a binary, never mutates.
3681
- // Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
3682
- // false}` that `registerTool` applies to every other `get_*` tool.
3683
- // Explicit destructiveHint: false is load-bearing MCP spec
3684
- // defaults destructiveHint to `true`, so omitting it would (in
3685
- // some client implementations) classify this read as destructive.
3686
- { readOnlyHint: true, destructiveHint: false },
3687
- async (input) => {
3688
- const result = await getAttachment(input);
3689
- if (result.truncated) {
3735
+ if (shouldRegister("get_attachment")) {
3736
+ server.tool(
3737
+ "get_attachment",
3738
+ "Download an attachment by id. Returns image content for image/* types (Claude can describe it natively); decoded text for text/* and application/json (small files); JSON metadata + base64 payload for other binary types (PDF, Office docs, etc.). Files exceeding maxSizeBytes (default 5MB) return metadata only with a `truncated: true` flag.",
3739
+ getAttachmentSchema.shape,
3740
+ // get_attachment is read-only downloads a binary, never mutates.
3741
+ // Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
3742
+ // false}` that `registerTool` applies to every other `get_*` tool.
3743
+ // Explicit destructiveHint: false is load-bearing MCP spec
3744
+ // defaults destructiveHint to `true`, so omitting it would (in
3745
+ // some client implementations) classify this read as destructive.
3746
+ { readOnlyHint: true, destructiveHint: false },
3747
+ async (input) => {
3748
+ const result = await getAttachment(input);
3749
+ if (result.truncated) {
3750
+ return {
3751
+ content: [
3752
+ {
3753
+ type: "text",
3754
+ text: JSON.stringify(
3755
+ {
3756
+ id: input.id,
3757
+ contentType: result.contentType,
3758
+ sizeBytes: result.sizeBytes,
3759
+ truncated: true,
3760
+ message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
3761
+ },
3762
+ null,
3763
+ 2
3764
+ )
3765
+ }
3766
+ ]
3767
+ };
3768
+ }
3769
+ const baseType = result.contentType.split(";")[0].trim().toLowerCase();
3770
+ if (baseType.startsWith("image/")) {
3771
+ return {
3772
+ content: [
3773
+ {
3774
+ type: "image",
3775
+ data: result.buffer.toString("base64"),
3776
+ mimeType: result.contentType
3777
+ }
3778
+ ]
3779
+ };
3780
+ }
3781
+ const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
3782
+ if (isText) {
3783
+ return {
3784
+ content: [
3785
+ {
3786
+ type: "text",
3787
+ text: JSON.stringify(
3788
+ {
3789
+ id: input.id,
3790
+ contentType: result.contentType,
3791
+ sizeBytes: result.sizeBytes
3792
+ },
3793
+ null,
3794
+ 2
3795
+ )
3796
+ },
3797
+ { type: "text", text: result.buffer.toString("utf8") }
3798
+ ]
3799
+ };
3800
+ }
3690
3801
  return {
3691
3802
  content: [
3692
3803
  {
@@ -3696,8 +3807,7 @@ function createCapsuleMcpServer(opts) {
3696
3807
  id: input.id,
3697
3808
  contentType: result.contentType,
3698
3809
  sizeBytes: result.sizeBytes,
3699
- truncated: true,
3700
- message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
3810
+ base64: result.buffer.toString("base64")
3701
3811
  },
3702
3812
  null,
3703
3813
  2
@@ -3706,57 +3816,8 @@ function createCapsuleMcpServer(opts) {
3706
3816
  ]
3707
3817
  };
3708
3818
  }
3709
- const baseType = result.contentType.split(";")[0].trim().toLowerCase();
3710
- if (baseType.startsWith("image/")) {
3711
- return {
3712
- content: [
3713
- {
3714
- type: "image",
3715
- data: result.buffer.toString("base64"),
3716
- mimeType: result.contentType
3717
- }
3718
- ]
3719
- };
3720
- }
3721
- const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
3722
- if (isText) {
3723
- return {
3724
- content: [
3725
- {
3726
- type: "text",
3727
- text: JSON.stringify(
3728
- {
3729
- id: input.id,
3730
- contentType: result.contentType,
3731
- sizeBytes: result.sizeBytes
3732
- },
3733
- null,
3734
- 2
3735
- )
3736
- },
3737
- { type: "text", text: result.buffer.toString("utf8") }
3738
- ]
3739
- };
3740
- }
3741
- return {
3742
- content: [
3743
- {
3744
- type: "text",
3745
- text: JSON.stringify(
3746
- {
3747
- id: input.id,
3748
- contentType: result.contentType,
3749
- sizeBytes: result.sizeBytes,
3750
- base64: result.buffer.toString("base64")
3751
- },
3752
- null,
3753
- 2
3754
- )
3755
- }
3756
- ]
3757
- };
3758
- }
3759
- );
3819
+ );
3820
+ }
3760
3821
  if (!readOnly) {
3761
3822
  registerTool(
3762
3823
  server,
@@ -4132,77 +4193,51 @@ a{color:#1e3a8a}
4132
4193
  }
4133
4194
  next();
4134
4195
  };
4135
- app2.post(
4136
- "/mcp",
4196
+ const mcpGuards = [
4137
4197
  guardOrigin,
4138
4198
  requireBearerAuth({
4139
4199
  verifier: oauthProvider2,
4140
4200
  resourceMetadataUrl: mcpResourceMetadataUrl
4141
4201
  }),
4142
4202
  mcpRateLimit,
4143
- guardProtocolVersion,
4144
- express.json({ limit: jsonLimit2 }),
4145
- async (req, res) => {
4146
- try {
4147
- const clientId = req.auth?.clientId;
4148
- const server = createCapsuleMcpServer({ clientId });
4149
- const transport = new StreamableHTTPServerTransport({});
4150
- res.on("close", () => {
4151
- void transport.close();
4152
- void server.close();
4153
- });
4154
- await withRequestContext({ clientId }, async () => {
4155
- await server.connect(transport);
4156
- await transport.handleRequest(req, res, req.body);
4157
- });
4158
- } catch (err) {
4159
- const name = err instanceof Error ? err.name : typeof err;
4160
- const status = err && typeof err === "object" && "status" in err ? Number(err.status) : void 0;
4161
- const summary = status !== void 0 ? `${name} ${status}` : name;
4162
- if (process.env["MCP_HTTP_DEBUG"] === "1") {
4163
- const message = err instanceof Error ? err.message : String(err);
4164
- console.error(`[capsulemcp] /mcp error: ${summary} \u2014 ${message}`);
4165
- } else {
4166
- console.error(`[capsulemcp] /mcp error: ${summary}`);
4167
- }
4168
- if (!res.headersSent) {
4169
- res.status(500).json({ error: "internal_error" });
4170
- }
4171
- }
4172
- }
4173
- );
4174
- app2.get(
4175
- "/mcp",
4176
- guardOrigin,
4177
- requireBearerAuth({
4178
- verifier: oauthProvider2,
4179
- resourceMetadataUrl: mcpResourceMetadataUrl
4180
- }),
4181
- mcpRateLimit,
4182
- guardProtocolVersion,
4183
- (_req, res) => {
4184
- res.set("Allow", "POST").status(405).json({
4185
- error: "method_not_allowed",
4186
- message: "Use POST for MCP requests; this server runs in stateless mode."
4203
+ guardProtocolVersion
4204
+ ];
4205
+ const methodNotAllowed = (_req, res) => {
4206
+ res.set("Allow", "POST").status(405).json({
4207
+ error: "method_not_allowed",
4208
+ message: "Use POST for MCP requests; this server runs in stateless mode."
4209
+ });
4210
+ };
4211
+ app2.post("/mcp", ...mcpGuards, express.json({ limit: jsonLimit2 }), async (req, res) => {
4212
+ try {
4213
+ const clientId = req.auth?.clientId;
4214
+ const server = createCapsuleMcpServer({ clientId });
4215
+ const transport = new StreamableHTTPServerTransport({});
4216
+ res.on("close", () => {
4217
+ void transport.close();
4218
+ void server.close();
4187
4219
  });
4188
- }
4189
- );
4190
- app2.delete(
4191
- "/mcp",
4192
- guardOrigin,
4193
- requireBearerAuth({
4194
- verifier: oauthProvider2,
4195
- resourceMetadataUrl: mcpResourceMetadataUrl
4196
- }),
4197
- mcpRateLimit,
4198
- guardProtocolVersion,
4199
- (_req, res) => {
4200
- res.set("Allow", "POST").status(405).json({
4201
- error: "method_not_allowed",
4202
- message: "Use POST for MCP requests; this server runs in stateless mode."
4220
+ await withRequestContext({ clientId }, async () => {
4221
+ await server.connect(transport);
4222
+ await transport.handleRequest(req, res, req.body);
4203
4223
  });
4224
+ } catch (err) {
4225
+ const name = err instanceof Error ? err.name : typeof err;
4226
+ const status = err && typeof err === "object" && "status" in err ? Number(err.status) : void 0;
4227
+ const summary = status !== void 0 ? `${name} ${status}` : name;
4228
+ if (process.env["MCP_HTTP_DEBUG"] === "1") {
4229
+ const message = err instanceof Error ? err.message : String(err);
4230
+ console.error(`[capsulemcp] /mcp error: ${summary} \u2014 ${message}`);
4231
+ } else {
4232
+ console.error(`[capsulemcp] /mcp error: ${summary}`);
4233
+ }
4234
+ if (!res.headersSent) {
4235
+ res.status(500).json({ error: "internal_error" });
4236
+ }
4204
4237
  }
4205
- );
4238
+ });
4239
+ app2.get("/mcp", ...mcpGuards, methodNotAllowed);
4240
+ app2.delete("/mcp", ...mcpGuards, methodNotAllowed);
4206
4241
  return app2;
4207
4242
  }
4208
4243