capsulemcp 1.8.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
@@ -271,38 +271,31 @@ async function parseErrorBody(res) {
271
271
  }
272
272
  }
273
273
  var REQUEST_TIMEOUT_MS = 6e4;
274
- function withTimeout(options) {
275
- if (options && options.signal !== void 0) {
276
- return { options, cleanup: () => {
277
- } };
278
- }
279
- const controller = new AbortController();
280
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
281
- timer.unref();
282
- return {
283
- options: { ...options ?? {}, signal: controller.signal },
284
- cleanup: () => clearTimeout(timer)
285
- };
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));
286
279
  }
287
280
  async function mapAbort(p) {
288
281
  try {
289
282
  return await p;
290
283
  } catch (err) {
291
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
284
+ if (isTimeoutAbort(err)) {
292
285
  throw new CapsuleTimeoutError();
293
286
  }
294
287
  throw err;
295
288
  }
296
289
  }
297
290
  async function fetchWithTimeout(url, options) {
298
- const { options: opts, cleanup } = withTimeout(options);
299
291
  const startedAt = Date.now();
300
292
  try {
301
- const res = await fetch(url, opts);
302
- return { res, cleanup };
293
+ return await fetch(url, {
294
+ ...options ?? {},
295
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
296
+ });
303
297
  } catch (err) {
304
- cleanup();
305
- const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
298
+ const isAbort = isTimeoutAbort(err);
306
299
  emitCapsuleFailure(
307
300
  options?.method ?? "GET",
308
301
  url,
@@ -326,24 +319,22 @@ async function doFetch(url, options) {
326
319
  const startedAt = Date.now();
327
320
  const method = options?.method ?? "GET";
328
321
  const first = await fetchWithTimeout(url, options);
329
- if (first.res.status === 429) {
330
- const delay = parseRateLimitDelay(first.res);
331
- first.cleanup();
332
- await drainBody(first.res);
322
+ if (first.status === 429) {
323
+ const delay = parseRateLimitDelay(first);
324
+ await drainBody(first);
333
325
  await new Promise((resolve) => setTimeout(resolve, delay));
334
326
  const retried = await fetchWithTimeout(url, options);
335
- if (retried.res.status === 429) {
336
- retried.cleanup();
337
- await drainBody(retried.res);
327
+ if (retried.status === 429) {
328
+ await drainBody(retried);
338
329
  emitCapsuleRateLimited(method, url, Date.now() - startedAt);
339
330
  throw new CapsuleApiError(
340
331
  429,
341
332
  "Rate limit exceeded after one retry. Please slow down your requests."
342
333
  );
343
334
  }
344
- return { ...retried, startedAt, method, url, retriedAfter429: true };
335
+ return { res: retried, startedAt, method, url, retriedAfter429: true };
345
336
  }
346
- return { ...first, startedAt, method, url, retriedAfter429: false };
337
+ return { res: first, startedAt, method, url, retriedAfter429: false };
347
338
  }
348
339
  async function consumeBody(start, body) {
349
340
  try {
@@ -453,15 +444,19 @@ async function capsuleGet(path, params) {
453
444
  const token = getToken();
454
445
  const url = buildUrl(path, params);
455
446
  const start = await doFetch(url, { headers: baseHeaders(token) });
456
- try {
457
- return await consumeBody(start, async () => {
458
- const data = await handleResponse(start.res);
459
- const nextPage = parseNextPage(start.res.headers.get("Link"));
460
- return { data, nextPage };
461
- });
462
- } finally {
463
- start.cleanup();
464
- }
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 };
465
460
  }
466
461
  async function capsuleGetCached(path, params) {
467
462
  if (cacheDisabled()) return capsuleGet(path, params);
@@ -500,11 +495,7 @@ async function capsulePost(path, body) {
500
495
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
501
496
  body: JSON.stringify(body)
502
497
  });
503
- try {
504
- return await consumeBody(start, () => handleResponse(start.res));
505
- } finally {
506
- start.cleanup();
507
- }
498
+ return consumeBody(start, () => handleResponse(start.res));
508
499
  }
509
500
  async function capsulePostNoContent(path) {
510
501
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -514,15 +505,11 @@ async function capsulePostNoContent(path) {
514
505
  method: "POST",
515
506
  headers: baseHeaders(token)
516
507
  });
517
- try {
518
- await consumeBody(start, async () => {
519
- if (start.res.status === 204) return;
520
- await throwForStatus(start.res);
521
- await mapAbort(start.res.text());
522
- });
523
- } finally {
524
- start.cleanup();
525
- }
508
+ await consumeBody(start, async () => {
509
+ if (start.res.status === 204) return;
510
+ await throwForStatus(start.res);
511
+ await mapAbort(start.res.text());
512
+ });
526
513
  }
527
514
  async function capsuleSearch(path, body, params) {
528
515
  const token = getToken();
@@ -532,15 +519,11 @@ async function capsuleSearch(path, body, params) {
532
519
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
533
520
  body: JSON.stringify(body)
534
521
  });
535
- try {
536
- return await consumeBody(start, async () => {
537
- const data = await handleResponse(start.res);
538
- const nextPage = parseNextPage(start.res.headers.get("Link"));
539
- return { data, nextPage };
540
- });
541
- } finally {
542
- start.cleanup();
543
- }
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
+ });
544
527
  }
545
528
  async function capsulePut(path, body) {
546
529
  if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
@@ -551,68 +534,60 @@ async function capsulePut(path, body) {
551
534
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
552
535
  body: JSON.stringify(body)
553
536
  });
554
- try {
555
- return await consumeBody(start, () => handleResponse(start.res));
556
- } finally {
557
- start.cleanup();
558
- }
537
+ return consumeBody(start, () => handleResponse(start.res));
559
538
  }
560
539
  async function capsuleGetBinary(path, maxBytes) {
561
540
  const token = getToken();
562
541
  const url = buildUrl(path);
563
542
  const start = await doFetch(url, { headers: baseHeaders(token) });
564
- try {
565
- return await consumeBody(start, async () => {
566
- const res = start.res;
567
- await throwForStatus(res);
568
- const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
569
- const declared = res.headers.get("Content-Length");
570
- const declaredBytes = declared ? Number(declared) : NaN;
571
- if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
572
- if (res.body) await res.body.cancel().catch(() => {
573
- });
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) {
574
577
  return {
575
578
  contentType,
576
579
  buffer: Buffer.alloc(0),
577
580
  truncated: true,
578
- sizeBytes: declaredBytes
581
+ sizeBytes: total
579
582
  };
580
583
  }
581
- if (maxBytes !== void 0 && res.body) {
582
- const reader = res.body.getReader();
583
- const chunks = [];
584
- let total = 0;
585
- let truncated = false;
586
- while (true) {
587
- const { done, value } = await mapAbort(reader.read());
588
- if (done) break;
589
- total += value.byteLength;
590
- if (total > maxBytes) {
591
- truncated = true;
592
- await reader.cancel().catch(() => {
593
- });
594
- break;
595
- }
596
- chunks.push(value);
597
- }
598
- if (truncated) {
599
- return {
600
- contentType,
601
- buffer: Buffer.alloc(0),
602
- truncated: true,
603
- sizeBytes: total
604
- };
605
- }
606
- const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
607
- return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
608
- }
609
- const arrayBuffer = await mapAbort(res.arrayBuffer());
610
- const buffer = Buffer.from(arrayBuffer);
611
- return { contentType, buffer, sizeBytes: buffer.length };
612
- });
613
- } finally {
614
- start.cleanup();
615
- }
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
+ });
616
591
  }
617
592
  async function capsulePostBinary(path, body, contentType, filename) {
618
593
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -628,11 +603,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
628
603
  },
629
604
  body
630
605
  });
631
- try {
632
- return await consumeBody(start, () => handleResponse(start.res));
633
- } finally {
634
- start.cleanup();
635
- }
606
+ return consumeBody(start, () => handleResponse(start.res));
636
607
  }
637
608
  async function capsuleDelete(path) {
638
609
  if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
@@ -642,15 +613,11 @@ async function capsuleDelete(path) {
642
613
  method: "DELETE",
643
614
  headers: baseHeaders(token)
644
615
  });
645
- try {
646
- await consumeBody(start, async () => {
647
- if (start.res.status === 204) return;
648
- await throwForStatus(start.res);
649
- await mapAbort(start.res.text());
650
- });
651
- } finally {
652
- start.cleanup();
653
- }
616
+ await consumeBody(start, async () => {
617
+ if (start.res.status === 204) return;
618
+ await throwForStatus(start.res);
619
+ await mapAbort(start.res.text());
620
+ });
654
621
  }
655
622
 
656
623
  // src/auth/provider.ts
@@ -1171,6 +1138,44 @@ var ICONS = [
1171
1138
  }
1172
1139
  ];
1173
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
+
1174
1179
  // src/tasks/store.ts
1175
1180
  import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
1176
1181
  import {
@@ -1379,27 +1384,25 @@ function wrapAsText(result) {
1379
1384
  };
1380
1385
  }
1381
1386
  function registerTool(server, name, description, schema, handler) {
1387
+ if (!shouldRegister(name)) return;
1382
1388
  const registerWithSchema = server.registerTool.bind(server);
1383
1389
  const annotations = inferAnnotations(name);
1384
- registerWithSchema(
1385
- name,
1386
- { description, inputSchema: schema, ...annotations ? { annotations } : {} },
1387
- async (input) => {
1388
- const startedAt = Date.now();
1389
- const argFields = argFieldNames(input);
1390
- const clientId = getRequestContext()?.clientId;
1391
- try {
1392
- const result = await handler(input);
1393
- emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
1394
- return wrapAsText(result);
1395
- } catch (err) {
1396
- emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
1397
- throw err;
1398
- }
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;
1399
1401
  }
1400
- );
1402
+ });
1401
1403
  }
1402
1404
  function registerToolTask(server, name, description, schema, handler) {
1405
+ if (!shouldRegister(name)) return;
1403
1406
  const registerWithSchema = server.experimental.tasks.registerToolTask.bind(
1404
1407
  server.experimental.tasks
1405
1408
  );
@@ -1410,7 +1413,7 @@ function registerToolTask(server, name, description, schema, handler) {
1410
1413
  description,
1411
1414
  inputSchema: schema,
1412
1415
  execution: { taskSupport: "optional" },
1413
- ...annotations ? { annotations } : {}
1416
+ annotations
1414
1417
  },
1415
1418
  {
1416
1419
  createTask: async (input, extra) => {
@@ -1470,7 +1473,7 @@ function registerToolTask(server, name, description, schema, handler) {
1470
1473
  }
1471
1474
 
1472
1475
  // src/tools/parties.ts
1473
- import { z as z7 } from "zod";
1476
+ import { z as z8 } from "zod";
1474
1477
 
1475
1478
  // src/tools/body-helpers.ts
1476
1479
  function setRef(body, key, id) {
@@ -1480,9 +1483,22 @@ function setNullableRef(body, key, id) {
1480
1483
  if (id === null) body[key] = null;
1481
1484
  else if (id !== void 0) body[key] = { id };
1482
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
+ }
1483
1499
 
1484
1500
  // src/tools/define-batch.ts
1485
- import { z as z2 } from "zod";
1501
+ import { z as z3 } from "zod";
1486
1502
 
1487
1503
  // src/capsule/batch.ts
1488
1504
  function chunk(arr, size) {
@@ -1501,37 +1517,43 @@ function getBatchConcurrency() {
1501
1517
  MAX_CONCURRENCY
1502
1518
  );
1503
1519
  }
1504
- async function batchExecute(tool, items, action, options = {}) {
1505
- const concurrency = getBatchConcurrency();
1520
+ async function mapWithConcurrency(items, limit, fn) {
1506
1521
  const results = new Array(items.length);
1507
- const startedAt = Date.now();
1508
- const signal = options.signal;
1509
1522
  let cursor = 0;
1510
1523
  async function worker() {
1511
1524
  while (true) {
1512
1525
  const i = cursor;
1513
1526
  cursor += 1;
1514
1527
  if (i >= items.length) return;
1515
- if (signal?.aborted) {
1516
- results[i] = {
1517
- ok: false,
1518
- error: { message: "cancelled by tasks/cancel" }
1519
- };
1520
- continue;
1521
- }
1522
- try {
1523
- const result = await action(items[i], i);
1524
- results[i] = { ok: true, result };
1525
- } catch (err) {
1526
- results[i] = { ok: false, error: extractError(err) };
1527
- }
1528
+ results[i] = await fn(items[i], i);
1528
1529
  }
1529
1530
  }
1530
1531
  const workers = [];
1531
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1532
+ for (let w = 0; w < Math.min(limit, items.length); w++) {
1532
1533
  workers.push(worker());
1533
1534
  }
1534
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
+ );
1535
1557
  const succeeded = results.filter((r) => r.ok).length;
1536
1558
  const failed = results.length - succeeded;
1537
1559
  const summary = { total: results.length, succeeded, failed };
@@ -1576,10 +1598,52 @@ function topFailureReasons(results, n) {
1576
1598
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
1577
1599
  }
1578
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
+
1579
1642
  // src/tools/define-batch.ts
1580
1643
  function defineBatch(args) {
1581
- const schema = z2.object({
1582
- 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)
1583
1647
  });
1584
1648
  async function handler(input, opts = {}) {
1585
1649
  return batchExecute(args.toolName, input.items, args.itemHandler, opts);
@@ -1592,22 +1656,30 @@ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'"
1592
1656
  var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
1593
1657
 
1594
1658
  // src/tools/define-delete.ts
1595
- import { z as z5 } from "zod";
1659
+ import { z as z6 } from "zod";
1596
1660
 
1597
1661
  // src/tools/confirm-flag.ts
1598
- import { z as z3 } from "zod";
1662
+ import { z as z4 } from "zod";
1599
1663
  var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1600
1664
  function confirmFlag() {
1601
- return z3.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1665
+ return z4.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1602
1666
  }
1603
1667
 
1604
1668
  // src/tools/shared-schemas.ts
1605
- import { z as z4 } from "zod";
1606
- var positiveId = z4.preprocess((input) => {
1669
+ import { z as z5 } from "zod";
1670
+ var positiveId = z5.preprocess((input) => {
1607
1671
  if (typeof input !== "string") return input;
1608
1672
  const trimmed = input.trim();
1609
1673
  return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
1610
- }, 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
+ };
1611
1683
 
1612
1684
  // src/capsule/idempotent.ts
1613
1685
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
@@ -1634,7 +1706,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
1634
1706
  // src/tools/define-delete.ts
1635
1707
  function defineDelete(args) {
1636
1708
  const { toolName, pathPrefix, confirmHint, idDescription } = args;
1637
- const schema = z5.object({
1709
+ const schema = z6.object({
1638
1710
  id: idDescription ? positiveId.describe(idDescription) : positiveId,
1639
1711
  confirm: confirmFlag().describe(confirmHint)
1640
1712
  });
@@ -1682,12 +1754,12 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
1682
1754
  }
1683
1755
 
1684
1756
  // src/tools/custom-field-helpers.ts
1685
- import { z as z6 } from "zod";
1686
- var CustomFieldWriteSchema = z6.object({
1757
+ import { z as z7 } from "zod";
1758
+ var CustomFieldWriteSchema = z7.object({
1687
1759
  definitionId: positiveId.describe(
1688
1760
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
1689
1761
  ),
1690
- 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(
1691
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."
1692
1764
  )
1693
1765
  });
@@ -1703,24 +1775,24 @@ function mapFieldsForBody(fields) {
1703
1775
  }
1704
1776
 
1705
1777
  // src/tools/parties.ts
1706
- var EmailAddressSchema = z7.object({
1707
- address: z7.string().email(),
1708
- type: z7.string().optional()
1778
+ var EmailAddressSchema = z8.object({
1779
+ address: z8.string().email(),
1780
+ type: z8.string().optional()
1709
1781
  });
1710
- var PhoneNumberSchema = z7.object({
1782
+ var PhoneNumberSchema = z8.object({
1711
1783
  // Capsule rejects empty strings with `phoneNumber.number: number is
1712
1784
  // required`. Enforce at the schema layer to catch typos pre-call,
1713
1785
  // matching how EmailAddressSchema's address field behaves.
1714
- number: z7.string().min(1),
1715
- type: z7.string().optional()
1786
+ number: z8.string().min(1),
1787
+ type: z8.string().optional()
1716
1788
  });
1717
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.";
1718
- var AddressSchema = z7.object({
1719
- street: z7.string().optional(),
1720
- city: z7.string().optional(),
1721
- state: z7.string().optional(),
1722
- country: z7.string().optional().describe(CountryDescription),
1723
- 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()
1724
1796
  });
1725
1797
  function validateWebsiteAddress(data, ctx) {
1726
1798
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1743,7 +1815,7 @@ function validateWebsiteAddress(data, ctx) {
1743
1815
  });
1744
1816
  }
1745
1817
  }
1746
- var WebsiteServiceEnum = z7.enum([
1818
+ var WebsiteServiceEnum = z8.enum([
1747
1819
  "URL",
1748
1820
  "SKYPE",
1749
1821
  "TWITTER",
@@ -1762,33 +1834,31 @@ var WebsiteServiceEnum = z7.enum([
1762
1834
  "BLUESKY",
1763
1835
  "SNAPCHAT"
1764
1836
  ]);
1765
- var WebsiteSchema = z7.object({
1766
- address: z7.string().min(1).describe(
1837
+ var WebsiteSchema = z8.object({
1838
+ address: z8.string().min(1).describe(
1767
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."
1768
1840
  ),
1769
1841
  service: WebsiteServiceEnum.optional().describe(
1770
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."
1771
1843
  )
1772
1844
  }).superRefine(validateWebsiteAddress);
1773
- var searchPartiesSchema = z7.object({
1774
- q: z7.string().optional().describe("Free-text search query"),
1775
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1776
- page: z7.number().int().positive().optional().default(1),
1777
- 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
1778
1849
  });
1779
1850
  async function searchParties(input) {
1780
1851
  const path = input.q ? "/parties/search" : "/parties";
1781
- const { data, nextPage } = await capsuleGet(path, {
1852
+ return capsuleGetList(path, {
1782
1853
  q: input.q,
1783
1854
  embed: input.embed,
1784
1855
  page: input.page,
1785
1856
  perPage: input.perPage
1786
1857
  });
1787
- return { ...data, nextPage };
1788
1858
  }
1789
- var getPartySchema = z7.object({
1859
+ var getPartySchema = z8.object({
1790
1860
  id: positiveId.describe("Party ID"),
1791
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1861
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1792
1862
  });
1793
1863
  async function getParty(input) {
1794
1864
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1796,51 +1866,47 @@ async function getParty(input) {
1796
1866
  });
1797
1867
  return data;
1798
1868
  }
1799
- var getPartiesSchema = z7.object({
1800
- ids: z7.array(positiveId).min(1).max(50).describe(
1869
+ var getPartiesSchema = z8.object({
1870
+ ids: z8.array(positiveId).min(1).max(50).describe(
1801
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."
1802
1872
  ),
1803
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1873
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1804
1874
  });
1805
1875
  async function getParties(input) {
1806
1876
  return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
1807
1877
  }
1808
- var listPartyOpportunitiesSchema = z7.object({
1878
+ var listPartyOpportunitiesSchema = z8.object({
1809
1879
  partyId: positiveId,
1810
- page: z7.number().int().positive().optional().default(1),
1811
- perPage: z7.number().int().min(1).max(100).optional().default(25)
1880
+ ...paginationFields
1812
1881
  });
1813
1882
  async function listPartyOpportunities(input) {
1814
- const { data, nextPage } = await capsuleGet(
1815
- `/parties/${input.partyId}/opportunities`,
1816
- { page: input.page, perPage: input.perPage }
1817
- );
1818
- return { ...data, nextPage };
1883
+ return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
1884
+ page: input.page,
1885
+ perPage: input.perPage
1886
+ });
1819
1887
  }
1820
- var listPartyProjectsSchema = z7.object({
1888
+ var listPartyProjectsSchema = z8.object({
1821
1889
  partyId: positiveId,
1822
- page: z7.number().int().positive().optional().default(1),
1823
- perPage: z7.number().int().min(1).max(100).optional().default(25)
1890
+ ...paginationFields
1824
1891
  });
1825
1892
  async function listPartyProjects(input) {
1826
- const { data, nextPage } = await capsuleGet(
1827
- `/parties/${input.partyId}/kases`,
1828
- { page: input.page, perPage: input.perPage }
1829
- );
1830
- return { ...data, nextPage };
1893
+ return capsuleGetList(`/parties/${input.partyId}/kases`, {
1894
+ page: input.page,
1895
+ perPage: input.perPage
1896
+ });
1831
1897
  }
1832
1898
  var PartyWriteBaseSchema = {
1833
- about: z7.string().optional(),
1834
- emailAddresses: z7.array(EmailAddressSchema).optional().describe(
1899
+ about: z8.string().optional(),
1900
+ emailAddresses: z8.array(EmailAddressSchema).optional().describe(
1835
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)."
1836
1902
  ),
1837
- phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
1903
+ phoneNumbers: z8.array(PhoneNumberSchema).optional().describe(
1838
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."
1839
1905
  ),
1840
- addresses: z7.array(AddressSchema).optional().describe(
1906
+ addresses: z8.array(AddressSchema).optional().describe(
1841
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)."
1842
1908
  ),
1843
- websites: z7.array(WebsiteSchema).optional().describe(
1909
+ websites: z8.array(WebsiteSchema).optional().describe(
1844
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."
1845
1911
  ),
1846
1912
  ownerId: positiveId.nullable().optional().describe(
@@ -1850,16 +1916,16 @@ var PartyWriteBaseSchema = {
1850
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)."
1851
1917
  )
1852
1918
  };
1853
- var createPartySchema = z7.object({
1854
- type: z7.enum(["person", "organisation"]),
1919
+ var createPartySchema = z8.object({
1920
+ type: z8.enum(["person", "organisation"]),
1855
1921
  // person
1856
- firstName: z7.string().optional(),
1857
- lastName: z7.string().optional(),
1858
- title: z7.string().optional(),
1859
- jobTitle: z7.string().optional(),
1922
+ firstName: z8.string().optional(),
1923
+ lastName: z8.string().optional(),
1924
+ title: z8.string().optional(),
1925
+ jobTitle: z8.string().optional(),
1860
1926
  organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
1861
1927
  // organisation
1862
- name: z7.string().optional(),
1928
+ name: z8.string().optional(),
1863
1929
  ...PartyWriteBaseSchema,
1864
1930
  ownerId: positiveId.optional().describe(
1865
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`."
@@ -1867,7 +1933,7 @@ var createPartySchema = z7.object({
1867
1933
  teamId: positiveId.optional().describe(
1868
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."
1869
1935
  ),
1870
- fields: z7.array(CustomFieldWriteSchema).optional().describe(
1936
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(
1871
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."
1872
1938
  )
1873
1939
  });
@@ -1881,17 +1947,17 @@ async function createParty(input) {
1881
1947
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1882
1948
  return capsulePost("/parties", { party: body });
1883
1949
  }
1884
- var updatePartySchema = z7.object({
1950
+ var updatePartySchema = z8.object({
1885
1951
  id: positiveId,
1886
- firstName: z7.string().optional(),
1887
- lastName: z7.string().optional(),
1888
- title: z7.string().optional(),
1889
- jobTitle: z7.string().optional(),
1890
- 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(),
1891
1957
  organisationId: positiveId.nullable().optional().describe(
1892
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."
1893
1959
  ),
1894
- fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1960
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1895
1961
  ...PartyWriteBaseSchema
1896
1962
  });
1897
1963
  async function updateParty(input) {
@@ -1922,10 +1988,42 @@ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1922
1988
  pathPrefix: "/parties",
1923
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."
1924
1990
  });
1925
- 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({
1926
2024
  partyId: positiveId,
1927
- address: z7.string().email(),
1928
- 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'.")
1929
2027
  });
1930
2028
  async function addPartyEmailAddress(input) {
1931
2029
  const { partyId, address, type } = input;
@@ -1935,32 +2033,17 @@ async function addPartyEmailAddress(input) {
1935
2033
  party: { emailAddresses: [item] }
1936
2034
  });
1937
2035
  }
1938
- var removePartyEmailAddressByIdSchema = z7.object({
1939
- partyId: positiveId,
1940
- emailAddressId: positiveId.describe(
1941
- "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1942
- )
2036
+ var removePartyEmailAddress = definePartySubResourceRemove({
2037
+ arrayKey: "emailAddresses",
2038
+ idField: "emailAddressId",
2039
+ rowNoun: "email-address"
1943
2040
  });
1944
- async function removePartyEmailAddressById(input) {
1945
- const { partyId, emailAddressId } = input;
1946
- return idempotentWithResult(
1947
- () => capsulePut(`/parties/${partyId}`, {
1948
- party: { emailAddresses: [{ id: emailAddressId, _delete: true }] }
1949
- }),
1950
- (result) => ({
1951
- removed: true,
1952
- alreadyRemoved: false,
1953
- partyId,
1954
- emailAddressId,
1955
- ...result
1956
- }),
1957
- () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1958
- );
1959
- }
1960
- var addPartyPhoneNumberSchema = z7.object({
2041
+ var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
2042
+ var removePartyEmailAddressById = removePartyEmailAddress.handler;
2043
+ var addPartyPhoneNumberSchema = z8.object({
1961
2044
  partyId: positiveId,
1962
- number: z7.string().min(1),
1963
- 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'.")
1964
2047
  });
1965
2048
  async function addPartyPhoneNumber(input) {
1966
2049
  const { partyId, number, type } = input;
@@ -1970,36 +2053,21 @@ async function addPartyPhoneNumber(input) {
1970
2053
  party: { phoneNumbers: [item] }
1971
2054
  });
1972
2055
  }
1973
- var removePartyPhoneNumberByIdSchema = z7.object({
1974
- partyId: positiveId,
1975
- phoneNumberId: positiveId.describe(
1976
- "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1977
- )
2056
+ var removePartyPhoneNumber = definePartySubResourceRemove({
2057
+ arrayKey: "phoneNumbers",
2058
+ idField: "phoneNumberId",
2059
+ rowNoun: "phone-number"
1978
2060
  });
1979
- async function removePartyPhoneNumberById(input) {
1980
- const { partyId, phoneNumberId } = input;
1981
- return idempotentWithResult(
1982
- () => capsulePut(`/parties/${partyId}`, {
1983
- party: { phoneNumbers: [{ id: phoneNumberId, _delete: true }] }
1984
- }),
1985
- (result) => ({
1986
- removed: true,
1987
- alreadyRemoved: false,
1988
- partyId,
1989
- phoneNumberId,
1990
- ...result
1991
- }),
1992
- () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1993
- );
1994
- }
1995
- var addPartyAddressSchema = z7.object({
2061
+ var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
2062
+ var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
2063
+ var addPartyAddressSchema = z8.object({
1996
2064
  partyId: positiveId,
1997
- street: z7.string().optional(),
1998
- city: z7.string().optional(),
1999
- state: z7.string().optional(),
2000
- country: z7.string().optional().describe(CountryDescription),
2001
- zip: z7.string().optional(),
2002
- 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'.")
2003
2071
  });
2004
2072
  async function addPartyAddress(input) {
2005
2073
  const { partyId, ...rest } = input;
@@ -2011,31 +2079,16 @@ async function addPartyAddress(input) {
2011
2079
  party: { addresses: [item] }
2012
2080
  });
2013
2081
  }
2014
- var removePartyAddressByIdSchema = z7.object({
2015
- partyId: positiveId,
2016
- addressId: positiveId.describe(
2017
- "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
2018
- )
2082
+ var removePartyAddress = definePartySubResourceRemove({
2083
+ arrayKey: "addresses",
2084
+ idField: "addressId",
2085
+ rowNoun: "address"
2019
2086
  });
2020
- async function removePartyAddressById(input) {
2021
- const { partyId, addressId } = input;
2022
- return idempotentWithResult(
2023
- () => capsulePut(`/parties/${partyId}`, {
2024
- party: { addresses: [{ id: addressId, _delete: true }] }
2025
- }),
2026
- (result) => ({
2027
- removed: true,
2028
- alreadyRemoved: false,
2029
- partyId,
2030
- addressId,
2031
- ...result
2032
- }),
2033
- () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
2034
- );
2035
- }
2036
- var addPartyWebsiteSchema = z7.object({
2087
+ var removePartyAddressByIdSchema = removePartyAddress.schema;
2088
+ var removePartyAddressById = removePartyAddress.handler;
2089
+ var addPartyWebsiteSchema = z8.object({
2037
2090
  partyId: positiveId,
2038
- address: z7.string().min(1).describe(
2091
+ address: z8.string().min(1).describe(
2039
2092
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
2040
2093
  ),
2041
2094
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -2048,58 +2101,41 @@ async function addPartyWebsite(input) {
2048
2101
  party: { websites: [item] }
2049
2102
  });
2050
2103
  }
2051
- var removePartyWebsiteByIdSchema = z7.object({
2052
- partyId: positiveId,
2053
- websiteId: positiveId.describe(
2054
- "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
2055
- )
2104
+ var removePartyWebsite = definePartySubResourceRemove({
2105
+ arrayKey: "websites",
2106
+ idField: "websiteId",
2107
+ rowNoun: "website"
2056
2108
  });
2057
- async function removePartyWebsiteById(input) {
2058
- const { partyId, websiteId } = input;
2059
- return idempotentWithResult(
2060
- () => capsulePut(`/parties/${partyId}`, {
2061
- party: { websites: [{ id: websiteId, _delete: true }] }
2062
- }),
2063
- (result) => ({
2064
- removed: true,
2065
- alreadyRemoved: false,
2066
- partyId,
2067
- websiteId,
2068
- ...result
2069
- }),
2070
- () => ({ removed: true, alreadyRemoved: true, partyId, websiteId })
2071
- );
2072
- }
2109
+ var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
2110
+ var removePartyWebsiteById = removePartyWebsite.handler;
2073
2111
 
2074
2112
  // src/tools/opportunities.ts
2075
- import { z as z8 } from "zod";
2076
- var OpportunityValueSchema = z8.object({
2077
- amount: z8.number().nonnegative(),
2078
- currency: z8.string({
2113
+ import { z as z9 } from "zod";
2114
+ var OpportunityValueSchema = z9.object({
2115
+ amount: z9.number().nonnegative(),
2116
+ currency: z9.string({
2079
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
2080
2118
  }).length(3).describe(
2081
2119
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
2082
2120
  )
2083
2121
  });
2084
- var searchOpportunitiesSchema = z8.object({
2085
- q: z8.string().optional().describe("Free-text search query"),
2086
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2087
- page: z8.number().int().positive().optional().default(1),
2088
- 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
2089
2126
  });
2090
2127
  async function searchOpportunities(input) {
2091
2128
  const path = input.q ? "/opportunities/search" : "/opportunities";
2092
- const { data, nextPage } = await capsuleGet(path, {
2129
+ return capsuleGetList(path, {
2093
2130
  q: input.q,
2094
2131
  embed: input.embed,
2095
2132
  page: input.page,
2096
2133
  perPage: input.perPage
2097
2134
  });
2098
- return { ...data, nextPage };
2099
2135
  }
2100
- var getOpportunitySchema = z8.object({
2136
+ var getOpportunitySchema = z9.object({
2101
2137
  id: positiveId,
2102
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2138
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2103
2139
  });
2104
2140
  async function getOpportunity(input) {
2105
2141
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -2107,32 +2143,32 @@ async function getOpportunity(input) {
2107
2143
  });
2108
2144
  return data;
2109
2145
  }
2110
- var getOpportunitiesSchema = z8.object({
2111
- ids: z8.array(positiveId).min(1).max(50).describe(
2146
+ var getOpportunitiesSchema = z9.object({
2147
+ ids: z9.array(positiveId).min(1).max(50).describe(
2112
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."
2113
2149
  ),
2114
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2150
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2115
2151
  });
2116
2152
  async function getOpportunities(input) {
2117
2153
  return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
2118
2154
  }
2119
- var createOpportunitySchema = z8.object({
2120
- name: z8.string().min(1),
2155
+ var createOpportunitySchema = z9.object({
2156
+ name: z9.string().min(1),
2121
2157
  partyId: positiveId.describe("ID of the party this opportunity belongs to"),
2122
2158
  milestoneId: positiveId.describe(
2123
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."
2124
2160
  ),
2125
- description: z8.string().optional(),
2161
+ description: z9.string().optional(),
2126
2162
  value: OpportunityValueSchema.optional(),
2127
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2128
- 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(),
2129
2165
  ownerId: positiveId.optional().describe(
2130
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."
2131
2167
  ),
2132
2168
  teamId: positiveId.optional().describe(
2133
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)."
2134
2170
  ),
2135
- fields: z8.array(CustomFieldWriteSchema).optional().describe(
2171
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
2136
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."
2137
2173
  )
2138
2174
  });
@@ -2149,19 +2185,19 @@ async function createOpportunity(input) {
2149
2185
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2150
2186
  return capsulePost("/opportunities", { opportunity: body });
2151
2187
  }
2152
- var updateOpportunitySchema = z8.object({
2188
+ var updateOpportunitySchema = z9.object({
2153
2189
  id: positiveId,
2154
- name: z8.string().min(1).optional(),
2190
+ name: z9.string().min(1).optional(),
2155
2191
  partyId: positiveId.optional().describe(
2156
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`."
2157
2193
  ),
2158
2194
  milestoneId: positiveId.optional().describe(
2159
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."
2160
2196
  ),
2161
- description: z8.string().optional(),
2197
+ description: z9.string().optional(),
2162
2198
  value: OpportunityValueSchema.optional(),
2163
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
2164
- 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(
2165
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)."
2166
2202
  ),
2167
2203
  lostReasonId: positiveId.optional().describe(
@@ -2173,7 +2209,7 @@ var updateOpportunitySchema = z8.object({
2173
2209
  teamId: positiveId.nullable().optional().describe(
2174
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."
2175
2211
  ),
2176
- fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2212
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
2177
2213
  });
2178
2214
  async function updateOpportunity(input) {
2179
2215
  const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
@@ -2209,25 +2245,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
2209
2245
  });
2210
2246
 
2211
2247
  // src/tools/projects.ts
2212
- import { z as z9 } from "zod";
2213
- var listProjectsSchema = z9.object({
2214
- status: z9.enum(["OPEN", "CLOSED"]).optional(),
2215
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2216
- page: z9.number().int().positive().optional().default(1),
2217
- 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
2218
2253
  });
2219
2254
  async function listProjects(input) {
2220
- const { data, nextPage } = await capsuleGet("/kases", {
2255
+ return capsuleGetList("/kases", {
2221
2256
  status: input.status,
2222
2257
  embed: input.embed,
2223
2258
  page: input.page,
2224
2259
  perPage: input.perPage
2225
2260
  });
2226
- return { ...data, nextPage };
2227
2261
  }
2228
- var getProjectSchema = z9.object({
2262
+ var getProjectSchema = z10.object({
2229
2263
  id: positiveId,
2230
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2264
+ embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2231
2265
  });
2232
2266
  async function getProject(input) {
2233
2267
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -2235,20 +2269,20 @@ async function getProject(input) {
2235
2269
  });
2236
2270
  return data;
2237
2271
  }
2238
- var getProjectsSchema = z9.object({
2239
- ids: z9.array(positiveId).min(1).max(50).describe(
2272
+ var getProjectsSchema = z10.object({
2273
+ ids: z10.array(positiveId).min(1).max(50).describe(
2240
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."
2241
2275
  ),
2242
- embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2276
+ embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2243
2277
  });
2244
2278
  async function getProjects(input) {
2245
2279
  return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
2246
2280
  }
2247
- var createProjectSchema = z9.object({
2248
- name: z9.string().min(1),
2281
+ var createProjectSchema = z10.object({
2282
+ name: z10.string().min(1),
2249
2283
  partyId: positiveId.describe("ID of the party linked to this project"),
2250
- description: z9.string().optional(),
2251
- 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."),
2252
2286
  ownerId: positiveId.optional().describe(
2253
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."
2254
2288
  ),
@@ -2258,8 +2292,8 @@ var createProjectSchema = z9.object({
2258
2292
  stageId: positiveId.optional().describe(
2259
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."
2260
2294
  ),
2261
- expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2262
- 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(
2263
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."
2264
2298
  )
2265
2299
  });
@@ -2277,11 +2311,11 @@ async function createProject(input) {
2277
2311
  if (mappedFields !== void 0) body["fields"] = mappedFields;
2278
2312
  return capsulePost("/kases", { kase: body });
2279
2313
  }
2280
- var updateProjectSchema = z9.object({
2314
+ var updateProjectSchema = z10.object({
2281
2315
  id: positiveId,
2282
- name: z9.string().min(1).optional(),
2283
- description: z9.string().optional(),
2284
- 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(),
2285
2319
  partyId: positiveId.optional().describe(
2286
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`."
2287
2321
  ),
@@ -2294,8 +2328,8 @@ var updateProjectSchema = z9.object({
2294
2328
  stageId: positiveId.nullable().optional().describe(
2295
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."
2296
2330
  ),
2297
- expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2298
- 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(
2299
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."
2300
2334
  )
2301
2335
  });
@@ -2334,23 +2368,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
2334
2368
  });
2335
2369
 
2336
2370
  // src/tools/tasks.ts
2337
- import { z as z10 } from "zod";
2338
- var listTasksSchema = z10.object({
2371
+ import { z as z11 } from "zod";
2372
+ var listTasksSchema = z11.object({
2339
2373
  // Note: Capsule has a third internal status `PENDING` (a task that's
2340
2374
  // part of an active track but not yet "open"), but it can only be
2341
2375
  // reached via track machinery — it is NOT directly settable by
2342
2376
  // /tasks PUT, and a list filter for it returns the same as OPEN
2343
2377
  // anyway. We expose only the two values that are actually filterable
2344
2378
  // by the v2 API.
2345
- status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2379
+ status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
2346
2380
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
2347
2381
  ),
2348
2382
  ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
2349
- page: z10.number().int().positive().optional().default(1),
2350
- perPage: z10.number().int().min(1).max(100).optional().default(25)
2383
+ ...paginationFields
2351
2384
  });
2352
2385
  async function listTasks(input) {
2353
- const { data, nextPage } = await capsuleGet("/tasks", {
2386
+ return capsuleGetList("/tasks", {
2354
2387
  // Default 'OPEN' applied here (not via zod .default()) so that
2355
2388
  // z.infer keeps `status` optional for callers that omit it.
2356
2389
  status: input.status ?? "OPEN",
@@ -2359,28 +2392,27 @@ async function listTasks(input) {
2359
2392
  page: input.page,
2360
2393
  perPage: input.perPage
2361
2394
  });
2362
- return { ...data, nextPage };
2363
2395
  }
2364
- var getTaskSchema = z10.object({
2396
+ var getTaskSchema = z11.object({
2365
2397
  id: positiveId.describe("Task ID")
2366
2398
  });
2367
2399
  async function getTask(input) {
2368
2400
  const { data } = await capsuleGet(`/tasks/${input.id}`);
2369
2401
  return data;
2370
2402
  }
2371
- var getTasksSchema = z10.object({
2372
- ids: z10.array(positiveId).min(1).max(50).describe(
2403
+ var getTasksSchema = z11.object({
2404
+ ids: z11.array(positiveId).min(1).max(50).describe(
2373
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."
2374
2406
  )
2375
2407
  });
2376
2408
  async function getTasks(input) {
2377
2409
  return chunkedMultiGet("/tasks", "tasks", input.ids);
2378
2410
  }
2379
- var createTaskSchema = z10.object({
2380
- description: z10.string().min(1),
2381
- dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
2382
- dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2383
- 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(),
2384
2416
  ownerId: positiveId.optional().describe(
2385
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."
2386
2418
  ),
@@ -2389,10 +2421,7 @@ var createTaskSchema = z10.object({
2389
2421
  projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
2390
2422
  });
2391
2423
  async function createTask(input) {
2392
- const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
2393
- if (linked.length > 1) {
2394
- throw new Error("Provide at most one of partyId, opportunityId, or projectId");
2395
- }
2424
+ assertSingleParentRef("create_task", input);
2396
2425
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
2397
2426
  const body = { ...rest };
2398
2427
  setRef(body, "owner", ownerId);
@@ -2401,16 +2430,16 @@ async function createTask(input) {
2401
2430
  setRef(body, "kase", projectId);
2402
2431
  return capsulePost("/tasks", { task: body });
2403
2432
  }
2404
- var updateTaskSchema = z10.object({
2433
+ var updateTaskSchema = z11.object({
2405
2434
  id: positiveId,
2406
- description: z10.string().min(1).optional(),
2407
- dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2408
- dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2409
- 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(),
2410
2439
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
2411
2440
  // internal state) with 422 "cannot set task status to PENDING".
2412
2441
  // Only OPEN and COMPLETED are settable here.
2413
- status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
2442
+ status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
2414
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)."
2415
2444
  ),
2416
2445
  ownerId: positiveId.optional().describe(
@@ -2428,12 +2457,7 @@ var updateTaskSchema = z10.object({
2428
2457
  });
2429
2458
  async function updateTask(input) {
2430
2459
  const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
2431
- const setCount = [partyId, opportunityId, projectId].filter((v) => typeof v === "number").length;
2432
- if (setCount > 1) {
2433
- throw new Error(
2434
- "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')"
2435
- );
2436
- }
2460
+ assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
2437
2461
  const body = {};
2438
2462
  for (const [k, v] of Object.entries(rest)) {
2439
2463
  if (v !== void 0) body[k] = v;
@@ -2444,7 +2468,7 @@ async function updateTask(input) {
2444
2468
  setNullableRef(body, "kase", projectId);
2445
2469
  return capsulePut(`/tasks/${id}`, { task: body });
2446
2470
  }
2447
- var completeTaskSchema = z10.object({
2471
+ var completeTaskSchema = z11.object({
2448
2472
  id: positiveId
2449
2473
  });
2450
2474
  async function completeTask(input) {
@@ -2452,8 +2476,8 @@ async function completeTask(input) {
2452
2476
  task: { status: "COMPLETED" }
2453
2477
  });
2454
2478
  }
2455
- var batchCompleteTaskSchema = z10.object({
2456
- ids: z10.array(positiveId).min(1).max(50).describe(
2479
+ var batchCompleteTaskSchema = z11.object({
2480
+ ids: z11.array(positiveId).min(1).max(50).describe(
2457
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."
2458
2482
  )
2459
2483
  });
@@ -2467,77 +2491,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
2467
2491
  });
2468
2492
 
2469
2493
  // src/tools/entries.ts
2470
- import { z as z11 } from "zod";
2494
+ import { z as z12 } from "zod";
2471
2495
  var listEntriesPagination = {
2472
- page: z11.number().int().positive().optional().default(1),
2473
- perPage: z11.number().int().min(1).max(100).optional().default(25),
2474
- embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2496
+ ...paginationFields,
2497
+ embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2475
2498
  };
2476
- var listPartyEntriesSchema = z11.object({
2499
+ var listPartyEntriesSchema = z12.object({
2477
2500
  partyId: positiveId,
2478
2501
  ...listEntriesPagination,
2479
- includeLinkedPersons: z11.boolean().optional().describe(
2502
+ includeLinkedPersons: z12.boolean().optional().describe(
2480
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."
2481
2504
  )
2482
2505
  });
2506
+ var PER_PARTY_FETCH_CAP = 100;
2483
2507
  async function fanOutPartyEntries(partyIds, embed, perPage) {
2484
- const concurrency = getBatchConcurrency();
2485
- const results = new Array(partyIds.length);
2486
- let cursor = 0;
2487
- async function worker() {
2488
- while (true) {
2489
- const i = cursor;
2490
- cursor += 1;
2491
- if (i >= partyIds.length) return;
2492
- const id = partyIds[i];
2493
- const { data, nextPage } = await capsuleGet(
2494
- `/parties/${id}/entries`,
2495
- {
2496
- embed,
2497
- page: 1,
2498
- perPage
2499
- }
2500
- );
2501
- results[i] = { entries: data.entries, nextPage };
2502
- }
2503
- }
2504
- const workers = [];
2505
- for (let w = 0; w < Math.min(concurrency, partyIds.length); w++) {
2506
- workers.push(worker());
2507
- }
2508
- await Promise.all(workers);
2509
- 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
+ });
2510
2516
  }
2511
2517
  function mergedTimelineCandidatePerParty(page, perPage) {
2512
- return Math.min(page * perPage, 100);
2518
+ return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
2513
2519
  }
2514
2520
  function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
2515
2521
  const requestedWindowEnd = page * perPage;
2516
2522
  if (mergedLength > requestedWindowEnd) return page + 1;
2517
- const nextWindowWithinCap = requestedWindowEnd < 100;
2523
+ const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
2518
2524
  if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
2519
2525
  return void 0;
2520
2526
  }
2521
2527
  async function listPartyEntries(input) {
2522
2528
  const { partyId, embed, page, perPage, includeLinkedPersons } = input;
2523
2529
  if (!includeLinkedPersons) {
2524
- const { data, nextPage: nextPage2 } = await capsuleGet(
2525
- `/parties/${partyId}/entries`,
2526
- { embed, page, perPage }
2527
- );
2528
- return { ...data, nextPage: nextPage2 };
2530
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2531
+ embed,
2532
+ page,
2533
+ perPage
2534
+ });
2529
2535
  }
2530
2536
  const { data: peopleData } = await capsuleGet(
2531
2537
  `/parties/${partyId}/people`,
2532
- { page: 1, perPage: 100 }
2538
+ { page: 1, perPage: PER_PARTY_FETCH_CAP }
2533
2539
  );
2534
2540
  const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
2535
2541
  if (peopleIds.length === 0) {
2536
- const { data, nextPage: nextPage2 } = await capsuleGet(
2537
- `/parties/${partyId}/entries`,
2538
- { embed, page, perPage }
2539
- );
2540
- return { ...data, nextPage: nextPage2 };
2542
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2543
+ embed,
2544
+ page,
2545
+ perPage
2546
+ });
2541
2547
  }
2542
2548
  const targetIds = [partyId, ...peopleIds];
2543
2549
  const perPartyPages = await fanOutPartyEntries(
@@ -2572,31 +2578,31 @@ async function listPartyEntries(input) {
2572
2578
  );
2573
2579
  return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
2574
2580
  }
2575
- var listOpportunityEntriesSchema = z11.object({
2581
+ var listOpportunityEntriesSchema = z12.object({
2576
2582
  opportunityId: positiveId,
2577
2583
  ...listEntriesPagination
2578
2584
  });
2579
2585
  async function listOpportunityEntries(input) {
2580
- const { data, nextPage } = await capsuleGet(
2581
- `/opportunities/${input.opportunityId}/entries`,
2582
- { embed: input.embed, page: input.page, perPage: input.perPage }
2583
- );
2584
- return { ...data, nextPage };
2586
+ return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
2587
+ embed: input.embed,
2588
+ page: input.page,
2589
+ perPage: input.perPage
2590
+ });
2585
2591
  }
2586
- var listProjectEntriesSchema = z11.object({
2592
+ var listProjectEntriesSchema = z12.object({
2587
2593
  projectId: positiveId,
2588
2594
  ...listEntriesPagination
2589
2595
  });
2590
2596
  async function listProjectEntries(input) {
2591
- const { data, nextPage } = await capsuleGet(
2592
- `/kases/${input.projectId}/entries`,
2593
- { embed: input.embed, page: input.page, perPage: input.perPage }
2594
- );
2595
- return { ...data, nextPage };
2597
+ return capsuleGetList(`/kases/${input.projectId}/entries`, {
2598
+ embed: input.embed,
2599
+ page: input.page,
2600
+ perPage: input.perPage
2601
+ });
2596
2602
  }
2597
- var getEntrySchema = z11.object({
2603
+ var getEntrySchema = z12.object({
2598
2604
  id: positiveId,
2599
- embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2605
+ embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2600
2606
  });
2601
2607
  async function getEntry(input) {
2602
2608
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2604,34 +2610,30 @@ async function getEntry(input) {
2604
2610
  });
2605
2611
  return data;
2606
2612
  }
2607
- var listEntriesSchema = z11.object({
2613
+ var listEntriesSchema = z12.object({
2608
2614
  ...listEntriesPagination
2609
2615
  });
2610
2616
  async function listEntries(input) {
2611
- const { data, nextPage } = await capsuleGet("/entries", {
2617
+ return capsuleGetList("/entries", {
2612
2618
  embed: input.embed,
2613
2619
  page: input.page,
2614
2620
  perPage: input.perPage
2615
2621
  });
2616
- return { ...data, nextPage };
2617
2622
  }
2618
- var addNoteSchema = z11.object({
2619
- content: z11.string().min(1).describe(
2623
+ var addNoteSchema = z12.object({
2624
+ content: z12.string().min(1).describe(
2620
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."
2621
2626
  ),
2622
2627
  partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2623
2628
  opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2624
2629
  projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
2625
- 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(
2626
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)."
2627
2632
  )
2628
2633
  });
2629
2634
  async function addNote(input) {
2630
2635
  const { content, partyId, opportunityId, projectId, entryAt } = input;
2631
- const linked = [partyId, opportunityId, projectId].filter(Boolean);
2632
- if (linked.length !== 1) {
2633
- throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
2634
- }
2636
+ assertSingleParentRef("add_note", input, { required: true });
2635
2637
  const body = { type: "note", content };
2636
2638
  setRef(body, "party", partyId);
2637
2639
  setRef(body, "opportunity", opportunityId);
@@ -2639,12 +2641,12 @@ async function addNote(input) {
2639
2641
  if (entryAt !== void 0) body["entryAt"] = entryAt;
2640
2642
  return capsulePost("/entries", { entry: body });
2641
2643
  }
2642
- var updateEntrySchema = z11.object({
2644
+ var updateEntrySchema = z12.object({
2643
2645
  id: positiveId.describe("Entry ID to update"),
2644
- content: z11.string().min(1).optional().describe(
2646
+ content: z12.string().min(1).optional().describe(
2645
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."
2646
2648
  ),
2647
- subject: z11.string().optional().describe(
2649
+ subject: z12.string().optional().describe(
2648
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`."
2649
2651
  )
2650
2652
  });
@@ -2666,62 +2668,50 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
2666
2668
  });
2667
2669
 
2668
2670
  // src/tools/pipelines.ts
2669
- import { z as z12 } from "zod";
2670
- var paginationFields = {
2671
- page: z12.number().int().positive().optional(),
2672
- perPage: z12.number().int().min(1).max(100).optional()
2673
- };
2674
- var listPipelinesSchema = z12.object({ ...paginationFields });
2671
+ import { z as z13 } from "zod";
2672
+ var listPipelinesSchema = z13.object({ ...paginationFieldsNoDefaults });
2675
2673
  async function listPipelines(input) {
2676
- const { data, nextPage } = await capsuleGetCached("/pipelines", {
2674
+ return capsuleGetCachedList("/pipelines", {
2677
2675
  page: input.page ?? 1,
2678
2676
  perPage: input.perPage ?? 100
2679
2677
  });
2680
- return { ...data, nextPage };
2681
2678
  }
2682
- var listMilestonesSchema = z12.object({
2679
+ var listMilestonesSchema = z13.object({
2683
2680
  pipelineId: positiveId,
2684
- ...paginationFields
2681
+ ...paginationFieldsNoDefaults
2685
2682
  });
2686
2683
  async function listMilestones(input) {
2687
- const { data, nextPage } = await capsuleGetCached(
2684
+ return capsuleGetCachedList(
2688
2685
  `/pipelines/${input.pipelineId}/milestones`,
2689
2686
  { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2690
2687
  );
2691
- return { ...data, nextPage };
2692
2688
  }
2693
2689
 
2694
2690
  // src/tools/boards.ts
2695
- import { z as z13 } from "zod";
2696
- var paginationFields2 = {
2697
- page: z13.number().int().positive().optional(),
2698
- perPage: z13.number().int().min(1).max(100).optional()
2699
- };
2700
- var listBoardsSchema = z13.object({ ...paginationFields2 });
2691
+ import { z as z14 } from "zod";
2692
+ var listBoardsSchema = z14.object({ ...paginationFieldsNoDefaults });
2701
2693
  async function listBoards(input) {
2702
- const { data, nextPage } = await capsuleGetCached("/boards", {
2694
+ return capsuleGetCachedList("/boards", {
2703
2695
  page: input.page ?? 1,
2704
2696
  perPage: input.perPage ?? 100
2705
2697
  });
2706
- return { ...data, nextPage };
2707
2698
  }
2708
- var listStagesSchema = z13.object({
2699
+ var listStagesSchema = z14.object({
2709
2700
  boardId: positiveId.optional().describe(
2710
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."
2711
2702
  ),
2712
- ...paginationFields2
2703
+ ...paginationFieldsNoDefaults
2713
2704
  });
2714
2705
  async function listStages(input) {
2715
2706
  const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
2716
- const { data, nextPage } = await capsuleGetCached(path, {
2707
+ return capsuleGetCachedList(path, {
2717
2708
  page: input.page ?? 1,
2718
2709
  perPage: input.perPage ?? 100
2719
2710
  });
2720
- return { ...data, nextPage };
2721
2711
  }
2722
2712
 
2723
2713
  // src/tools/tags.ts
2724
- import { z as z14 } from "zod";
2714
+ import { z as z15 } from "zod";
2725
2715
  var TAG_LIST_PATH = {
2726
2716
  parties: "/parties/tags",
2727
2717
  opportunities: "/opportunities/tags",
@@ -2732,24 +2722,22 @@ var ENTITY_TO_WRAPPER = {
2732
2722
  opportunities: "opportunity",
2733
2723
  kases: "kase"
2734
2724
  };
2735
- var TagEntity = z14.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2736
- var listTagsSchema = z14.object({
2737
- entity: z14.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2738
- page: z14.number().int().positive().optional(),
2739
- 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
2740
2729
  });
2741
2730
  async function listTags(input) {
2742
2731
  const path = TAG_LIST_PATH[input.entity];
2743
- const { data, nextPage } = await capsuleGetCached(path, {
2732
+ return capsuleGetCachedList(path, {
2744
2733
  page: input.page ?? 1,
2745
2734
  perPage: input.perPage ?? 100
2746
2735
  });
2747
- return { ...data, nextPage };
2748
2736
  }
2749
- var addTagSchema = z14.object({
2737
+ var addTagSchema = z15.object({
2750
2738
  entity: TagEntity,
2751
2739
  entityId: positiveId.describe("The party/opportunity/kase id."),
2752
- tagName: z14.string().min(1).describe(
2740
+ tagName: z15.string().min(1).describe(
2753
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."
2754
2742
  )
2755
2743
  });
@@ -2762,7 +2750,7 @@ async function addTag(input) {
2762
2750
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2763
2751
  return result;
2764
2752
  }
2765
- var removeTagByIdSchema = z14.object({
2753
+ var removeTagByIdSchema = z15.object({
2766
2754
  entity: TagEntity,
2767
2755
  entityId: positiveId.describe("The party/opportunity/kase id."),
2768
2756
  tagId: positiveId.describe(
@@ -2793,7 +2781,7 @@ async function removeTagById(input) {
2793
2781
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2794
2782
  return result;
2795
2783
  }
2796
- var deleteTagDefinitionSchema = z14.object({
2784
+ var deleteTagDefinitionSchema = z15.object({
2797
2785
  entity: TagEntity,
2798
2786
  tagId: positiveId.describe(
2799
2787
  "The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
@@ -2829,44 +2817,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
2829
2817
  });
2830
2818
 
2831
2819
  // src/tools/users.ts
2832
- import { z as z15 } from "zod";
2833
- var listUsersSchema = z15.object({
2834
- page: z15.number().int().positive().optional(),
2835
- perPage: z15.number().int().min(1).max(100).optional()
2820
+ import { z as z16 } from "zod";
2821
+ var listUsersSchema = z16.object({
2822
+ ...paginationFieldsNoDefaults
2836
2823
  });
2837
2824
  async function listUsers(input) {
2838
- const { data, nextPage } = await capsuleGetCached("/users", {
2825
+ return capsuleGetCachedList("/users", {
2839
2826
  page: input.page ?? 1,
2840
2827
  perPage: input.perPage ?? 100
2841
2828
  });
2842
- return { ...data, nextPage };
2843
2829
  }
2844
- var getCurrentUserSchema = z15.object({});
2830
+ var getCurrentUserSchema = z16.object({});
2845
2831
  async function getCurrentUser(_input) {
2846
2832
  const { data } = await capsuleGet("/users/current");
2847
2833
  return data;
2848
2834
  }
2849
2835
 
2850
2836
  // src/tools/filters.ts
2851
- import { z as z16 } from "zod";
2852
- var FilterConditionSchema = z16.object({
2853
- field: z16.string().describe(
2837
+ import { z as z17 } from "zod";
2838
+ var FilterConditionSchema = z17.object({
2839
+ field: z17.string().describe(
2854
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"
2855
2841
  ),
2856
- operator: z16.string().describe(
2842
+ operator: z17.string().describe(
2857
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."
2858
2844
  ),
2859
- 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(
2860
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."
2861
2847
  )
2862
2848
  });
2863
- var FilterInputSchema = z16.object({
2864
- conditions: z16.array(FilterConditionSchema).min(1).describe(
2849
+ var FilterInputSchema = z17.object({
2850
+ conditions: z17.array(FilterConditionSchema).min(1).describe(
2865
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)."
2866
2852
  ),
2867
- embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2868
- page: z16.number().int().positive().optional().default(1),
2869
- perPage: z16.number().int().min(1).max(100).optional().default(25)
2853
+ embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2854
+ ...paginationFields
2870
2855
  });
2871
2856
  async function runFilter(entityPath, input) {
2872
2857
  const { data, nextPage } = await capsuleSearch(
@@ -2886,10 +2871,7 @@ async function filterParties(input) {
2886
2871
  }
2887
2872
  var filterOpportunitiesSchema = FilterInputSchema;
2888
2873
  async function filterOpportunities(input) {
2889
- return runFilter(
2890
- "opportunities",
2891
- input
2892
- );
2874
+ return runFilter("opportunities", input);
2893
2875
  }
2894
2876
  var filterProjectsSchema = FilterInputSchema;
2895
2877
  async function filterProjects(input) {
@@ -2897,139 +2879,126 @@ async function filterProjects(input) {
2897
2879
  }
2898
2880
 
2899
2881
  // src/tools/metadata.ts
2900
- import { z as z17 } from "zod";
2901
- var paginationFields3 = {
2902
- page: z17.number().int().positive().optional(),
2903
- 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
+ )
2904
2888
  };
2905
- var listTeamsSchema = z17.object({ ...paginationFields3 });
2889
+ var listTeamsSchema = z18.object({ ...paginationFields2 });
2906
2890
  async function listTeams(input) {
2907
- const { data, nextPage } = await capsuleGetCached("/teams", {
2891
+ return capsuleGetCachedList("/teams", {
2908
2892
  page: input.page ?? 1,
2909
2893
  perPage: input.perPage ?? 100
2910
2894
  });
2911
- return { ...data, nextPage };
2912
2895
  }
2913
- var listLostReasonsSchema = z17.object({ ...paginationFields3 });
2896
+ var listLostReasonsSchema = z18.object({ ...paginationFields2 });
2914
2897
  async function listLostReasons(input) {
2915
- const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2898
+ return capsuleGetCachedList("/lostreasons", {
2916
2899
  page: input.page ?? 1,
2917
2900
  perPage: input.perPage ?? 100
2918
2901
  });
2919
- return { ...data, nextPage };
2920
2902
  }
2921
- var listActivityTypesSchema = z17.object({ ...paginationFields3 });
2903
+ var listActivityTypesSchema = z18.object({ ...paginationFields2 });
2922
2904
  async function listActivityTypes(input) {
2923
- const { data, nextPage } = await capsuleGetCached(
2924
- "/activitytypes",
2925
- {
2926
- page: input.page ?? 1,
2927
- perPage: input.perPage ?? 100
2928
- }
2929
- );
2930
- return { ...data, nextPage };
2905
+ return capsuleGetCachedList("/activitytypes", {
2906
+ page: input.page ?? 1,
2907
+ perPage: input.perPage ?? 100
2908
+ });
2931
2909
  }
2932
- var getSiteSchema = z17.object({});
2910
+ var getSiteSchema = z18.object({});
2933
2911
  async function getSite(_input) {
2934
2912
  const { data } = await capsuleGetCached("/site");
2935
2913
  return data;
2936
2914
  }
2937
- var listTrackDefinitionsSchema = z17.object({ ...paginationFields3 });
2915
+ var listTrackDefinitionsSchema = z18.object({ ...paginationFields2 });
2938
2916
  async function listTrackDefinitions(input) {
2939
- const { data, nextPage } = await capsuleGetCached(
2940
- "/trackdefinitions",
2941
- { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2942
- );
2943
- return { ...data, nextPage };
2917
+ return capsuleGetCachedList("/trackdefinitions", {
2918
+ page: input.page ?? 1,
2919
+ perPage: input.perPage ?? 100
2920
+ });
2944
2921
  }
2945
- var listCategoriesSchema = z17.object({ ...paginationFields3 });
2922
+ var listCategoriesSchema = z18.object({ ...paginationFields2 });
2946
2923
  async function listCategories(input) {
2947
- const { data, nextPage } = await capsuleGetCached("/categories", {
2924
+ return capsuleGetCachedList("/categories", {
2948
2925
  page: input.page ?? 1,
2949
2926
  perPage: input.perPage ?? 100
2950
2927
  });
2951
- return { ...data, nextPage };
2952
2928
  }
2953
- var listGoalsSchema = z17.object({ ...paginationFields3 });
2929
+ var listGoalsSchema = z18.object({ ...paginationFields2 });
2954
2930
  async function listGoals(input) {
2955
- const { data, nextPage } = await capsuleGetCached("/goals", {
2931
+ return capsuleGetCachedList("/goals", {
2956
2932
  page: input.page ?? 1,
2957
2933
  perPage: input.perPage ?? 100
2958
2934
  });
2959
- return { ...data, nextPage };
2960
2935
  }
2961
2936
 
2962
2937
  // src/tools/audit.ts
2963
- import { z as z18 } from "zod";
2964
- var listEmployeesSchema = z18.object({
2938
+ import { z as z19 } from "zod";
2939
+ var listEmployeesSchema = z19.object({
2965
2940
  partyId: positiveId.describe(
2966
2941
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2967
2942
  ),
2968
- page: z18.number().int().positive().optional().default(1),
2969
- perPage: z18.number().int().min(1).max(100).optional().default(25),
2970
- embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2943
+ ...paginationFields,
2944
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2971
2945
  });
2972
2946
  async function listEmployees(input) {
2973
- const { data, nextPage } = await capsuleGet(
2974
- `/parties/${input.partyId}/people`,
2975
- { page: input.page, perPage: input.perPage, embed: input.embed }
2976
- );
2977
- return { ...data, nextPage };
2947
+ return capsuleGetList(`/parties/${input.partyId}/people`, {
2948
+ page: input.page,
2949
+ perPage: input.perPage,
2950
+ embed: input.embed
2951
+ });
2978
2952
  }
2979
- var DeletedSinceSchema = z18.string().describe(
2953
+ var DeletedSinceSchema = z19.string().describe(
2980
2954
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2981
2955
  );
2982
2956
  var DeletedPagination = {
2983
2957
  since: DeletedSinceSchema,
2984
- page: z18.number().int().positive().optional().default(1),
2985
- perPage: z18.number().int().min(1).max(100).optional().default(25)
2958
+ ...paginationFields
2986
2959
  };
2987
- var listDeletedPartiesSchema = z18.object(DeletedPagination);
2960
+ var listDeletedPartiesSchema = z19.object(DeletedPagination);
2988
2961
  async function listDeletedParties(input) {
2989
- const { data, nextPage } = await capsuleGet("/parties/deleted", {
2962
+ return capsuleGetList("/parties/deleted", {
2990
2963
  since: input.since,
2991
2964
  page: input.page,
2992
2965
  perPage: input.perPage
2993
2966
  });
2994
- return { ...data, nextPage };
2995
2967
  }
2996
- var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
2968
+ var listDeletedOpportunitiesSchema = z19.object(DeletedPagination);
2997
2969
  async function listDeletedOpportunities(input) {
2998
- const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2970
+ return capsuleGetList("/opportunities/deleted", {
2999
2971
  since: input.since,
3000
2972
  page: input.page,
3001
2973
  perPage: input.perPage
3002
2974
  });
3003
- return { ...data, nextPage };
3004
2975
  }
3005
- var listDeletedProjectsSchema = z18.object(DeletedPagination);
2976
+ var listDeletedProjectsSchema = z19.object(DeletedPagination);
3006
2977
  async function listDeletedProjects(input) {
3007
- const { data, nextPage } = await capsuleGet("/kases/deleted", {
2978
+ return capsuleGetList("/kases/deleted", {
3008
2979
  since: input.since,
3009
2980
  page: input.page,
3010
2981
  perPage: input.perPage
3011
2982
  });
3012
- return { ...data, nextPage };
3013
2983
  }
3014
2984
 
3015
2985
  // src/tools/relationships.ts
3016
- import { z as z19 } from "zod";
3017
- var RelationshipEntity = z19.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
3018
- 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({
3019
2989
  entity: RelationshipEntity,
3020
2990
  entityId: positiveId.describe("ID of the opportunity or project."),
3021
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3022
- page: z19.number().int().positive().optional().default(1),
3023
- perPage: z19.number().int().min(1).max(100).optional().default(25)
2991
+ embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2992
+ ...paginationFields
3024
2993
  });
3025
2994
  async function listAdditionalParties(input) {
3026
- const { data, nextPage } = await capsuleGet(
3027
- `/${input.entity}/${input.entityId}/parties`,
3028
- { embed: input.embed, page: input.page, perPage: input.perPage }
3029
- );
3030
- return { ...data, nextPage };
2995
+ return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
2996
+ embed: input.embed,
2997
+ page: input.page,
2998
+ perPage: input.perPage
2999
+ });
3031
3000
  }
3032
- var addAdditionalPartySchema = z19.object({
3001
+ var addAdditionalPartySchema = z20.object({
3033
3002
  entity: RelationshipEntity,
3034
3003
  entityId: positiveId,
3035
3004
  partyId: positiveId.describe(
@@ -3062,7 +3031,7 @@ async function addAdditionalParty(input) {
3062
3031
  throw err;
3063
3032
  }
3064
3033
  }
3065
- var removeAdditionalPartySchema = z19.object({
3034
+ var removeAdditionalPartySchema = z20.object({
3066
3035
  entity: RelationshipEntity,
3067
3036
  entityId: positiveId,
3068
3037
  partyId: positiveId,
@@ -3092,24 +3061,23 @@ async function removeAdditionalParty(input) {
3092
3061
  })
3093
3062
  );
3094
3063
  }
3095
- var listAssociatedProjectsSchema = z19.object({
3064
+ var listAssociatedProjectsSchema = z20.object({
3096
3065
  opportunityId: positiveId,
3097
- embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3098
- page: z19.number().int().positive().optional().default(1),
3099
- perPage: z19.number().int().min(1).max(100).optional().default(25)
3066
+ embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3067
+ ...paginationFields
3100
3068
  });
3101
3069
  async function listAssociatedProjects(input) {
3102
- const { data, nextPage } = await capsuleGet(
3103
- `/opportunities/${input.opportunityId}/kases`,
3104
- { embed: input.embed, page: input.page, perPage: input.perPage }
3105
- );
3106
- return { ...data, nextPage };
3070
+ return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
3071
+ embed: input.embed,
3072
+ page: input.page,
3073
+ perPage: input.perPage
3074
+ });
3107
3075
  }
3108
3076
 
3109
3077
  // src/tools/custom-fields.ts
3110
- import { z as z20 } from "zod";
3111
- var CustomFieldEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
3112
- 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({
3113
3081
  entity: CustomFieldEntity
3114
3082
  });
3115
3083
  async function listCustomFields(input) {
@@ -3118,7 +3086,7 @@ async function listCustomFields(input) {
3118
3086
  );
3119
3087
  return data;
3120
3088
  }
3121
- var getCustomFieldSchema = z20.object({
3089
+ var getCustomFieldSchema = z21.object({
3122
3090
  entity: CustomFieldEntity,
3123
3091
  fieldId: positiveId.describe("Custom field definition id.")
3124
3092
  });
@@ -3130,9 +3098,9 @@ async function getCustomField(input) {
3130
3098
  }
3131
3099
 
3132
3100
  // src/tools/tracks.ts
3133
- import { z as z21 } from "zod";
3134
- var TrackEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
3135
- 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({
3136
3104
  entity: TrackEntity,
3137
3105
  entityId: positiveId
3138
3106
  });
@@ -3142,20 +3110,20 @@ async function listEntityTracks(input) {
3142
3110
  );
3143
3111
  return data;
3144
3112
  }
3145
- var showTrackSchema = z21.object({
3113
+ var showTrackSchema = z22.object({
3146
3114
  trackId: positiveId
3147
3115
  });
3148
3116
  async function showTrack(input) {
3149
3117
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
3150
3118
  return data;
3151
3119
  }
3152
- var applyTrackSchema = z21.object({
3153
- 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."),
3154
3122
  entityId: positiveId,
3155
3123
  trackDefinitionId: positiveId.describe(
3156
3124
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
3157
3125
  ),
3158
- 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(
3159
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."
3160
3128
  )
3161
3129
  });
@@ -3168,9 +3136,9 @@ async function applyTrack(input) {
3168
3136
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
3169
3137
  return capsulePost("/tracks", { track });
3170
3138
  }
3171
- var updateTrackSchema = z21.object({
3139
+ var updateTrackSchema = z22.object({
3172
3140
  trackId: positiveId,
3173
- fields: z21.record(z21.string(), z21.unknown()).describe(
3141
+ fields: z22.record(z22.string(), z22.unknown()).describe(
3174
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."
3175
3143
  )
3176
3144
  });
@@ -3182,7 +3150,7 @@ async function updateTrack(input) {
3182
3150
  track: input.fields
3183
3151
  });
3184
3152
  }
3185
- var removeTrackSchema = z21.object({
3153
+ var removeTrackSchema = z22.object({
3186
3154
  trackId: positiveId,
3187
3155
  confirm: confirmFlag().describe(
3188
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."
@@ -3200,13 +3168,13 @@ async function removeTrack(input) {
3200
3168
  }
3201
3169
 
3202
3170
  // src/tools/attachments.ts
3203
- import { z as z22 } from "zod";
3171
+ import { z as z23 } from "zod";
3204
3172
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
3205
3173
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
3206
3174
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
3207
- var getAttachmentSchema = z22.object({
3175
+ var getAttachmentSchema = z23.object({
3208
3176
  id: positiveId.describe("Attachment ID."),
3209
- 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(
3210
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.`
3211
3179
  )
3212
3180
  });
@@ -3221,17 +3189,17 @@ async function getAttachment(input) {
3221
3189
  }
3222
3190
  return { contentType, buffer, sizeBytes };
3223
3191
  }
3224
- var uploadAttachmentSchema = z22.object({
3225
- filename: z22.string().min(1).describe(
3192
+ var uploadAttachmentSchema = z23.object({
3193
+ filename: z23.string().min(1).describe(
3226
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."
3227
3195
  ),
3228
- contentType: z22.string().min(1).describe(
3196
+ contentType: z23.string().min(1).describe(
3229
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."
3230
3198
  ),
3231
- dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
3199
+ dataBase64: z23.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
3232
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."
3233
3201
  ),
3234
- content: z22.string().optional().describe(
3202
+ content: z23.string().optional().describe(
3235
3203
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
3236
3204
  ),
3237
3205
  partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
@@ -3249,12 +3217,7 @@ function decodedBase64Size(s) {
3249
3217
  return s.length / 4 * 3 - padding;
3250
3218
  }
3251
3219
  async function uploadAttachment(input) {
3252
- const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
3253
- if (linked.length !== 1) {
3254
- throw new Error(
3255
- "upload_attachment: provide exactly one of partyId, opportunityId, or projectId"
3256
- );
3257
- }
3220
+ assertSingleParentRef("upload_attachment", input, { required: true });
3258
3221
  if (!isValidBase64(input.dataBase64)) {
3259
3222
  throw new Error(
3260
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)."
@@ -3279,37 +3242,36 @@ async function uploadAttachment(input) {
3279
3242
  content: input.content ?? "[attachment]",
3280
3243
  attachments: [{ token }]
3281
3244
  };
3282
- if (input.partyId) entryBody["party"] = { id: input.partyId };
3283
- if (input.opportunityId) entryBody["opportunity"] = { id: input.opportunityId };
3284
- 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);
3285
3248
  return capsulePost("/entries", { entry: entryBody });
3286
3249
  }
3287
3250
 
3288
3251
  // src/tools/saved-filters.ts
3289
- import { z as z23 } from "zod";
3290
- var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
3252
+ import { z as z24 } from "zod";
3253
+ var EntitySchema = z24.enum(["parties", "opportunities", "kases"]).describe(
3291
3254
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
3292
3255
  );
3293
- var listSavedFiltersSchema = z23.object({
3256
+ var listSavedFiltersSchema = z24.object({
3294
3257
  entity: EntitySchema
3295
3258
  });
3296
3259
  async function listSavedFilters(input) {
3297
3260
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
3298
3261
  return data;
3299
3262
  }
3300
- var runSavedFilterSchema = z23.object({
3263
+ var runSavedFilterSchema = z24.object({
3301
3264
  entity: EntitySchema,
3302
3265
  id: positiveId.describe("The saved filter id (from list_saved_filters)."),
3303
- embed: z23.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3304
- page: z23.number().int().positive().optional().default(1),
3305
- perPage: z23.number().int().min(1).max(100).optional().default(25)
3266
+ embed: z24.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
3267
+ ...paginationFields
3306
3268
  });
3307
3269
  async function runSavedFilter(input) {
3308
- const { data, nextPage } = await capsuleGet(
3309
- `/${input.entity}/filters/${input.id}/results`,
3310
- { page: input.page, perPage: input.perPage, embed: input.embed }
3311
- );
3312
- 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
+ });
3313
3275
  }
3314
3276
 
3315
3277
  // src/server.ts
@@ -3320,7 +3282,7 @@ function createCapsuleMcpServer(opts) {
3320
3282
  const server = new McpServer(
3321
3283
  {
3322
3284
  name: "capsulemcp",
3323
- version: "1.8.0",
3285
+ version: "1.8.1",
3324
3286
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
3325
3287
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
3326
3288
  icons: ICONS
@@ -3770,20 +3732,72 @@ function createCapsuleMcpServer(opts) {
3770
3732
  listEntriesSchema,
3771
3733
  listEntries
3772
3734
  );
3773
- server.tool(
3774
- "get_attachment",
3775
- "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.",
3776
- getAttachmentSchema.shape,
3777
- // get_attachment is read-only — downloads a binary, never mutates.
3778
- // Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
3779
- // false}` that `registerTool` applies to every other `get_*` tool.
3780
- // Explicit destructiveHint: false is load-bearing MCP spec
3781
- // defaults destructiveHint to `true`, so omitting it would (in
3782
- // some client implementations) classify this read as destructive.
3783
- { readOnlyHint: true, destructiveHint: false },
3784
- async (input) => {
3785
- const result = await getAttachment(input);
3786
- 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
+ }
3787
3801
  return {
3788
3802
  content: [
3789
3803
  {
@@ -3793,8 +3807,7 @@ function createCapsuleMcpServer(opts) {
3793
3807
  id: input.id,
3794
3808
  contentType: result.contentType,
3795
3809
  sizeBytes: result.sizeBytes,
3796
- truncated: true,
3797
- 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")
3798
3811
  },
3799
3812
  null,
3800
3813
  2
@@ -3803,57 +3816,8 @@ function createCapsuleMcpServer(opts) {
3803
3816
  ]
3804
3817
  };
3805
3818
  }
3806
- const baseType = result.contentType.split(";")[0].trim().toLowerCase();
3807
- if (baseType.startsWith("image/")) {
3808
- return {
3809
- content: [
3810
- {
3811
- type: "image",
3812
- data: result.buffer.toString("base64"),
3813
- mimeType: result.contentType
3814
- }
3815
- ]
3816
- };
3817
- }
3818
- const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
3819
- if (isText) {
3820
- return {
3821
- content: [
3822
- {
3823
- type: "text",
3824
- text: JSON.stringify(
3825
- {
3826
- id: input.id,
3827
- contentType: result.contentType,
3828
- sizeBytes: result.sizeBytes
3829
- },
3830
- null,
3831
- 2
3832
- )
3833
- },
3834
- { type: "text", text: result.buffer.toString("utf8") }
3835
- ]
3836
- };
3837
- }
3838
- return {
3839
- content: [
3840
- {
3841
- type: "text",
3842
- text: JSON.stringify(
3843
- {
3844
- id: input.id,
3845
- contentType: result.contentType,
3846
- sizeBytes: result.sizeBytes,
3847
- base64: result.buffer.toString("base64")
3848
- },
3849
- null,
3850
- 2
3851
- )
3852
- }
3853
- ]
3854
- };
3855
- }
3856
- );
3819
+ );
3820
+ }
3857
3821
  if (!readOnly) {
3858
3822
  registerTool(
3859
3823
  server,
@@ -4229,77 +4193,51 @@ a{color:#1e3a8a}
4229
4193
  }
4230
4194
  next();
4231
4195
  };
4232
- app2.post(
4233
- "/mcp",
4234
- guardOrigin,
4235
- requireBearerAuth({
4236
- verifier: oauthProvider2,
4237
- resourceMetadataUrl: mcpResourceMetadataUrl
4238
- }),
4239
- mcpRateLimit,
4240
- guardProtocolVersion,
4241
- express.json({ limit: jsonLimit2 }),
4242
- async (req, res) => {
4243
- try {
4244
- const clientId = req.auth?.clientId;
4245
- const server = createCapsuleMcpServer({ clientId });
4246
- const transport = new StreamableHTTPServerTransport({});
4247
- res.on("close", () => {
4248
- void transport.close();
4249
- void server.close();
4250
- });
4251
- await withRequestContext({ clientId }, async () => {
4252
- await server.connect(transport);
4253
- await transport.handleRequest(req, res, req.body);
4254
- });
4255
- } catch (err) {
4256
- const name = err instanceof Error ? err.name : typeof err;
4257
- const status = err && typeof err === "object" && "status" in err ? Number(err.status) : void 0;
4258
- const summary = status !== void 0 ? `${name} ${status}` : name;
4259
- if (process.env["MCP_HTTP_DEBUG"] === "1") {
4260
- const message = err instanceof Error ? err.message : String(err);
4261
- console.error(`[capsulemcp] /mcp error: ${summary} \u2014 ${message}`);
4262
- } else {
4263
- console.error(`[capsulemcp] /mcp error: ${summary}`);
4264
- }
4265
- if (!res.headersSent) {
4266
- res.status(500).json({ error: "internal_error" });
4267
- }
4268
- }
4269
- }
4270
- );
4271
- app2.get(
4272
- "/mcp",
4196
+ const mcpGuards = [
4273
4197
  guardOrigin,
4274
4198
  requireBearerAuth({
4275
4199
  verifier: oauthProvider2,
4276
4200
  resourceMetadataUrl: mcpResourceMetadataUrl
4277
4201
  }),
4278
4202
  mcpRateLimit,
4279
- guardProtocolVersion,
4280
- (_req, res) => {
4281
- res.set("Allow", "POST").status(405).json({
4282
- error: "method_not_allowed",
4283
- 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();
4284
4219
  });
4285
- }
4286
- );
4287
- app2.delete(
4288
- "/mcp",
4289
- guardOrigin,
4290
- requireBearerAuth({
4291
- verifier: oauthProvider2,
4292
- resourceMetadataUrl: mcpResourceMetadataUrl
4293
- }),
4294
- mcpRateLimit,
4295
- guardProtocolVersion,
4296
- (_req, res) => {
4297
- res.set("Allow", "POST").status(405).json({
4298
- error: "method_not_allowed",
4299
- 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);
4300
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
+ }
4301
4237
  }
4302
- );
4238
+ });
4239
+ app2.get("/mcp", ...mcpGuards, methodNotAllowed);
4240
+ app2.delete("/mcp", ...mcpGuards, methodNotAllowed);
4303
4241
  return app2;
4304
4242
  }
4305
4243