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/index.js CHANGED
@@ -253,38 +253,31 @@ async function parseErrorBody(res) {
253
253
  }
254
254
  }
255
255
  var REQUEST_TIMEOUT_MS = 6e4;
256
- function withTimeout(options) {
257
- if (options && options.signal !== void 0) {
258
- return { options, cleanup: () => {
259
- } };
260
- }
261
- const controller = new AbortController();
262
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
263
- timer.unref();
264
- return {
265
- options: { ...options ?? {}, signal: controller.signal },
266
- cleanup: () => clearTimeout(timer)
267
- };
256
+ function isTimeoutAbort(err) {
257
+ return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
258
+ // "TimeoutError"; plain aborts (and older undici paths) surface
259
+ // as "AbortError" or carry "aborted" in the message.
260
+ (err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
268
261
  }
269
262
  async function mapAbort(p) {
270
263
  try {
271
264
  return await p;
272
265
  } catch (err) {
273
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
266
+ if (isTimeoutAbort(err)) {
274
267
  throw new CapsuleTimeoutError();
275
268
  }
276
269
  throw err;
277
270
  }
278
271
  }
279
272
  async function fetchWithTimeout(url, options) {
280
- const { options: opts, cleanup } = withTimeout(options);
281
273
  const startedAt = Date.now();
282
274
  try {
283
- const res = await fetch(url, opts);
284
- return { res, cleanup };
275
+ return await fetch(url, {
276
+ ...options ?? {},
277
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
278
+ });
285
279
  } catch (err) {
286
- cleanup();
287
- const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
280
+ const isAbort = isTimeoutAbort(err);
288
281
  emitCapsuleFailure(
289
282
  options?.method ?? "GET",
290
283
  url,
@@ -308,24 +301,22 @@ async function doFetch(url, options) {
308
301
  const startedAt = Date.now();
309
302
  const method = options?.method ?? "GET";
310
303
  const first = await fetchWithTimeout(url, options);
311
- if (first.res.status === 429) {
312
- const delay = parseRateLimitDelay(first.res);
313
- first.cleanup();
314
- await drainBody(first.res);
304
+ if (first.status === 429) {
305
+ const delay = parseRateLimitDelay(first);
306
+ await drainBody(first);
315
307
  await new Promise((resolve) => setTimeout(resolve, delay));
316
308
  const retried = await fetchWithTimeout(url, options);
317
- if (retried.res.status === 429) {
318
- retried.cleanup();
319
- await drainBody(retried.res);
309
+ if (retried.status === 429) {
310
+ await drainBody(retried);
320
311
  emitCapsuleRateLimited(method, url, Date.now() - startedAt);
321
312
  throw new CapsuleApiError(
322
313
  429,
323
314
  "Rate limit exceeded after one retry. Please slow down your requests."
324
315
  );
325
316
  }
326
- return { ...retried, startedAt, method, url, retriedAfter429: true };
317
+ return { res: retried, startedAt, method, url, retriedAfter429: true };
327
318
  }
328
- return { ...first, startedAt, method, url, retriedAfter429: false };
319
+ return { res: first, startedAt, method, url, retriedAfter429: false };
329
320
  }
330
321
  async function consumeBody(start, body) {
331
322
  try {
@@ -435,15 +426,19 @@ async function capsuleGet(path, params) {
435
426
  const token = getToken();
436
427
  const url = buildUrl(path, params);
437
428
  const start = await doFetch(url, { headers: baseHeaders(token) });
438
- try {
439
- return await consumeBody(start, async () => {
440
- const data = await handleResponse(start.res);
441
- const nextPage = parseNextPage(start.res.headers.get("Link"));
442
- return { data, nextPage };
443
- });
444
- } finally {
445
- start.cleanup();
446
- }
429
+ return consumeBody(start, async () => {
430
+ const data = await handleResponse(start.res);
431
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
432
+ return { data, nextPage };
433
+ });
434
+ }
435
+ async function capsuleGetList(path, params) {
436
+ const { data, nextPage } = await capsuleGet(path, params);
437
+ return { ...data, nextPage };
438
+ }
439
+ async function capsuleGetCachedList(path, params) {
440
+ const { data, nextPage } = await capsuleGetCached(path, params);
441
+ return { ...data, nextPage };
447
442
  }
448
443
  async function capsuleGetCached(path, params) {
449
444
  if (cacheDisabled()) return capsuleGet(path, params);
@@ -482,11 +477,7 @@ async function capsulePost(path, body) {
482
477
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
483
478
  body: JSON.stringify(body)
484
479
  });
485
- try {
486
- return await consumeBody(start, () => handleResponse(start.res));
487
- } finally {
488
- start.cleanup();
489
- }
480
+ return consumeBody(start, () => handleResponse(start.res));
490
481
  }
491
482
  async function capsulePostNoContent(path) {
492
483
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -496,15 +487,11 @@ async function capsulePostNoContent(path) {
496
487
  method: "POST",
497
488
  headers: baseHeaders(token)
498
489
  });
499
- try {
500
- await consumeBody(start, async () => {
501
- if (start.res.status === 204) return;
502
- await throwForStatus(start.res);
503
- await mapAbort(start.res.text());
504
- });
505
- } finally {
506
- start.cleanup();
507
- }
490
+ await consumeBody(start, async () => {
491
+ if (start.res.status === 204) return;
492
+ await throwForStatus(start.res);
493
+ await mapAbort(start.res.text());
494
+ });
508
495
  }
509
496
  async function capsuleSearch(path, body, params) {
510
497
  const token = getToken();
@@ -514,15 +501,11 @@ async function capsuleSearch(path, body, params) {
514
501
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
515
502
  body: JSON.stringify(body)
516
503
  });
517
- try {
518
- return await consumeBody(start, async () => {
519
- const data = await handleResponse(start.res);
520
- const nextPage = parseNextPage(start.res.headers.get("Link"));
521
- return { data, nextPage };
522
- });
523
- } finally {
524
- start.cleanup();
525
- }
504
+ return consumeBody(start, async () => {
505
+ const data = await handleResponse(start.res);
506
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
507
+ return { data, nextPage };
508
+ });
526
509
  }
527
510
  async function capsulePut(path, body) {
528
511
  if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
@@ -533,68 +516,60 @@ async function capsulePut(path, body) {
533
516
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
534
517
  body: JSON.stringify(body)
535
518
  });
536
- try {
537
- return await consumeBody(start, () => handleResponse(start.res));
538
- } finally {
539
- start.cleanup();
540
- }
519
+ return consumeBody(start, () => handleResponse(start.res));
541
520
  }
542
521
  async function capsuleGetBinary(path, maxBytes) {
543
522
  const token = getToken();
544
523
  const url = buildUrl(path);
545
524
  const start = await doFetch(url, { headers: baseHeaders(token) });
546
- try {
547
- return await consumeBody(start, async () => {
548
- const res = start.res;
549
- await throwForStatus(res);
550
- const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
551
- const declared = res.headers.get("Content-Length");
552
- const declaredBytes = declared ? Number(declared) : NaN;
553
- if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
554
- if (res.body) await res.body.cancel().catch(() => {
555
- });
525
+ return consumeBody(start, async () => {
526
+ const res = start.res;
527
+ await throwForStatus(res);
528
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
529
+ const declared = res.headers.get("Content-Length");
530
+ const declaredBytes = declared ? Number(declared) : NaN;
531
+ if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
532
+ if (res.body) await res.body.cancel().catch(() => {
533
+ });
534
+ return {
535
+ contentType,
536
+ buffer: Buffer.alloc(0),
537
+ truncated: true,
538
+ sizeBytes: declaredBytes
539
+ };
540
+ }
541
+ if (maxBytes !== void 0 && res.body) {
542
+ const reader = res.body.getReader();
543
+ const chunks = [];
544
+ let total = 0;
545
+ let truncated = false;
546
+ while (true) {
547
+ const { done, value } = await mapAbort(reader.read());
548
+ if (done) break;
549
+ total += value.byteLength;
550
+ if (total > maxBytes) {
551
+ truncated = true;
552
+ await reader.cancel().catch(() => {
553
+ });
554
+ break;
555
+ }
556
+ chunks.push(value);
557
+ }
558
+ if (truncated) {
556
559
  return {
557
560
  contentType,
558
561
  buffer: Buffer.alloc(0),
559
562
  truncated: true,
560
- sizeBytes: declaredBytes
563
+ sizeBytes: total
561
564
  };
562
565
  }
563
- if (maxBytes !== void 0 && res.body) {
564
- const reader = res.body.getReader();
565
- const chunks = [];
566
- let total = 0;
567
- let truncated = false;
568
- while (true) {
569
- const { done, value } = await mapAbort(reader.read());
570
- if (done) break;
571
- total += value.byteLength;
572
- if (total > maxBytes) {
573
- truncated = true;
574
- await reader.cancel().catch(() => {
575
- });
576
- break;
577
- }
578
- chunks.push(value);
579
- }
580
- if (truncated) {
581
- return {
582
- contentType,
583
- buffer: Buffer.alloc(0),
584
- truncated: true,
585
- sizeBytes: total
586
- };
587
- }
588
- const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
589
- return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
590
- }
591
- const arrayBuffer = await mapAbort(res.arrayBuffer());
592
- const buffer = Buffer.from(arrayBuffer);
593
- return { contentType, buffer, sizeBytes: buffer.length };
594
- });
595
- } finally {
596
- start.cleanup();
597
- }
566
+ const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
567
+ return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
568
+ }
569
+ const arrayBuffer = await mapAbort(res.arrayBuffer());
570
+ const buffer = Buffer.from(arrayBuffer);
571
+ return { contentType, buffer, sizeBytes: buffer.length };
572
+ });
598
573
  }
599
574
  async function capsulePostBinary(path, body, contentType, filename) {
600
575
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -610,11 +585,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
610
585
  },
611
586
  body
612
587
  });
613
- try {
614
- return await consumeBody(start, () => handleResponse(start.res));
615
- } finally {
616
- start.cleanup();
617
- }
588
+ return consumeBody(start, () => handleResponse(start.res));
618
589
  }
619
590
  async function capsuleDelete(path) {
620
591
  if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
@@ -624,15 +595,11 @@ async function capsuleDelete(path) {
624
595
  method: "DELETE",
625
596
  headers: baseHeaders(token)
626
597
  });
627
- try {
628
- await consumeBody(start, async () => {
629
- if (start.res.status === 204) return;
630
- await throwForStatus(start.res);
631
- await mapAbort(start.res.text());
632
- });
633
- } finally {
634
- start.cleanup();
635
- }
598
+ await consumeBody(start, async () => {
599
+ if (start.res.status === 204) return;
600
+ await throwForStatus(start.res);
601
+ await mapAbort(start.res.text());
602
+ });
636
603
  }
637
604
 
638
605
  // src/server.ts
@@ -668,6 +635,44 @@ var ICONS = [
668
635
  }
669
636
  ];
670
637
 
638
+ // src/server/tier.ts
639
+ var CORE_TOOLS = /* @__PURE__ */ new Set([
640
+ // Parties
641
+ "search_parties",
642
+ "filter_parties",
643
+ "get_party",
644
+ "create_party",
645
+ "update_party",
646
+ "list_party_entries",
647
+ // Opportunities
648
+ "search_opportunities",
649
+ "filter_opportunities",
650
+ "get_opportunity",
651
+ "create_opportunity",
652
+ "update_opportunity",
653
+ // Projects
654
+ "filter_projects",
655
+ "list_projects",
656
+ "get_project",
657
+ "create_project",
658
+ "update_project",
659
+ // Tasks
660
+ "list_tasks",
661
+ "get_task",
662
+ "create_task",
663
+ "update_task",
664
+ "complete_task",
665
+ // Timeline + tags + identity
666
+ "add_note",
667
+ "list_tags",
668
+ "add_tag",
669
+ "get_current_user"
670
+ ]);
671
+ function shouldRegister(name) {
672
+ if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
673
+ return CORE_TOOLS.has(name);
674
+ }
675
+
671
676
  // src/tasks/store.ts
672
677
  import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
673
678
  import {
@@ -876,27 +881,25 @@ function wrapAsText(result) {
876
881
  };
877
882
  }
878
883
  function registerTool(server2, name, description, schema, handler) {
884
+ if (!shouldRegister(name)) return;
879
885
  const registerWithSchema = server2.registerTool.bind(server2);
880
886
  const annotations = inferAnnotations(name);
881
- registerWithSchema(
882
- name,
883
- { description, inputSchema: schema, ...annotations ? { annotations } : {} },
884
- async (input) => {
885
- const startedAt = Date.now();
886
- const argFields = argFieldNames(input);
887
- const clientId = getRequestContext()?.clientId;
888
- try {
889
- const result = await handler(input);
890
- emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
891
- return wrapAsText(result);
892
- } catch (err) {
893
- emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
894
- throw err;
895
- }
887
+ registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
888
+ const startedAt = Date.now();
889
+ const argFields = argFieldNames(input);
890
+ const clientId = getRequestContext()?.clientId;
891
+ try {
892
+ const result = await handler(input);
893
+ emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
894
+ return wrapAsText(result);
895
+ } catch (err) {
896
+ emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
897
+ throw err;
896
898
  }
897
- );
899
+ });
898
900
  }
899
901
  function registerToolTask(server2, name, description, schema, handler) {
902
+ if (!shouldRegister(name)) return;
900
903
  const registerWithSchema = server2.experimental.tasks.registerToolTask.bind(
901
904
  server2.experimental.tasks
902
905
  );
@@ -907,7 +910,7 @@ function registerToolTask(server2, name, description, schema, handler) {
907
910
  description,
908
911
  inputSchema: schema,
909
912
  execution: { taskSupport: "optional" },
910
- ...annotations ? { annotations } : {}
913
+ annotations
911
914
  },
912
915
  {
913
916
  createTask: async (input, extra) => {
@@ -967,7 +970,7 @@ function registerToolTask(server2, name, description, schema, handler) {
967
970
  }
968
971
 
969
972
  // src/tools/parties.ts
970
- import { z as z6 } from "zod";
973
+ import { z as z7 } from "zod";
971
974
 
972
975
  // src/tools/body-helpers.ts
973
976
  function setRef(body, key, id) {
@@ -977,9 +980,22 @@ function setNullableRef(body, key, id) {
977
980
  if (id === null) body[key] = null;
978
981
  else if (id !== void 0) body[key] = { id };
979
982
  }
983
+ function assertSingleParentRef(toolName, refs, opts = {}) {
984
+ const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
985
+ (v) => typeof v === "number"
986
+ ).length;
987
+ if (opts.required && set !== 1) {
988
+ throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
989
+ }
990
+ if (set > 1) {
991
+ throw new Error(
992
+ `${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
993
+ );
994
+ }
995
+ }
980
996
 
981
997
  // src/tools/define-batch.ts
982
- import { z } from "zod";
998
+ import { z as z2 } from "zod";
983
999
 
984
1000
  // src/capsule/batch.ts
985
1001
  function chunk(arr, size) {
@@ -998,37 +1014,43 @@ function getBatchConcurrency() {
998
1014
  MAX_CONCURRENCY
999
1015
  );
1000
1016
  }
1001
- async function batchExecute(tool, items, action, options = {}) {
1002
- const concurrency = getBatchConcurrency();
1017
+ async function mapWithConcurrency(items, limit, fn) {
1003
1018
  const results = new Array(items.length);
1004
- const startedAt = Date.now();
1005
- const signal = options.signal;
1006
1019
  let cursor = 0;
1007
1020
  async function worker() {
1008
1021
  while (true) {
1009
1022
  const i = cursor;
1010
1023
  cursor += 1;
1011
1024
  if (i >= items.length) return;
1012
- if (signal?.aborted) {
1013
- results[i] = {
1014
- ok: false,
1015
- error: { message: "cancelled by tasks/cancel" }
1016
- };
1017
- continue;
1018
- }
1019
- try {
1020
- const result = await action(items[i], i);
1021
- results[i] = { ok: true, result };
1022
- } catch (err) {
1023
- results[i] = { ok: false, error: extractError(err) };
1024
- }
1025
+ results[i] = await fn(items[i], i);
1025
1026
  }
1026
1027
  }
1027
1028
  const workers = [];
1028
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1029
+ for (let w = 0; w < Math.min(limit, items.length); w++) {
1029
1030
  workers.push(worker());
1030
1031
  }
1031
1032
  await Promise.all(workers);
1033
+ return results;
1034
+ }
1035
+ async function batchExecute(tool, items, action, options = {}) {
1036
+ const concurrency = getBatchConcurrency();
1037
+ const startedAt = Date.now();
1038
+ const signal = options.signal;
1039
+ const results = await mapWithConcurrency(
1040
+ items,
1041
+ concurrency,
1042
+ async (item, i) => {
1043
+ if (signal?.aborted) {
1044
+ return { ok: false, error: { message: "cancelled by tasks/cancel" } };
1045
+ }
1046
+ try {
1047
+ const result = await action(item, i);
1048
+ return { ok: true, result };
1049
+ } catch (err) {
1050
+ return { ok: false, error: extractError(err) };
1051
+ }
1052
+ }
1053
+ );
1032
1054
  const succeeded = results.filter((r) => r.ok).length;
1033
1055
  const failed = results.length - succeeded;
1034
1056
  const summary = { total: results.length, succeeded, failed };
@@ -1073,10 +1095,52 @@ function topFailureReasons(results, n) {
1073
1095
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
1074
1096
  }
1075
1097
 
1098
+ // src/tools/strip-descriptions.ts
1099
+ import { z } from "zod";
1100
+ function cloneWithDef(node, patch) {
1101
+ const def = node.def;
1102
+ return node.clone({ ...def, ...patch });
1103
+ }
1104
+ function stripDescriptions(schema) {
1105
+ let node = schema;
1106
+ if (node instanceof z.ZodObject) {
1107
+ const shape = node.def.shape;
1108
+ const next = {};
1109
+ let changed = false;
1110
+ for (const [key, child] of Object.entries(shape)) {
1111
+ next[key] = stripDescriptions(child);
1112
+ if (next[key] !== child) changed = true;
1113
+ }
1114
+ if (changed) node = cloneWithDef(node, { shape: next });
1115
+ } else if (node instanceof z.ZodArray) {
1116
+ const element = stripDescriptions(node.def.element);
1117
+ if (element !== node.def.element) node = cloneWithDef(node, { element });
1118
+ } else if (node instanceof z.ZodOptional || node instanceof z.ZodNullable || node instanceof z.ZodDefault || node instanceof z.ZodReadonly) {
1119
+ const innerType = stripDescriptions(node.def.innerType);
1120
+ if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
1121
+ } else if (node instanceof z.ZodUnion) {
1122
+ const options = node.def.options.map(stripDescriptions);
1123
+ if (options.some((o, i) => o !== node.def.options[i])) {
1124
+ node = cloneWithDef(node, { options });
1125
+ }
1126
+ } else if (node instanceof z.ZodPipe) {
1127
+ const inSchema = stripDescriptions(node.def.in);
1128
+ const outSchema = stripDescriptions(node.def.out);
1129
+ if (inSchema !== node.def.in || outSchema !== node.def.out) {
1130
+ node = cloneWithDef(node, { in: inSchema, out: outSchema });
1131
+ }
1132
+ }
1133
+ if (node.description !== void 0) {
1134
+ node = node.meta({ description: void 0 });
1135
+ }
1136
+ return node;
1137
+ }
1138
+
1076
1139
  // src/tools/define-batch.ts
1077
1140
  function defineBatch(args) {
1078
- const schema = z.object({
1079
- items: z.array(args.itemSchema).min(1).max(50).describe(args.itemDescription)
1141
+ const itemSchema = stripDescriptions(args.itemSchema);
1142
+ const schema = z2.object({
1143
+ items: z2.array(itemSchema).min(1).max(50).describe(args.itemDescription)
1080
1144
  });
1081
1145
  async function handler(input, opts = {}) {
1082
1146
  return batchExecute(args.toolName, input.items, args.itemHandler, opts);
@@ -1089,22 +1153,30 @@ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'"
1089
1153
  var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
1090
1154
 
1091
1155
  // src/tools/define-delete.ts
1092
- import { z as z4 } from "zod";
1156
+ import { z as z5 } from "zod";
1093
1157
 
1094
1158
  // src/tools/confirm-flag.ts
1095
- import { z as z2 } from "zod";
1159
+ import { z as z3 } from "zod";
1096
1160
  var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1097
1161
  function confirmFlag() {
1098
- return z2.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1162
+ return z3.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1099
1163
  }
1100
1164
 
1101
1165
  // src/tools/shared-schemas.ts
1102
- import { z as z3 } from "zod";
1103
- var positiveId = z3.preprocess((input) => {
1166
+ import { z as z4 } from "zod";
1167
+ var positiveId = z4.preprocess((input) => {
1104
1168
  if (typeof input !== "string") return input;
1105
1169
  const trimmed = input.trim();
1106
1170
  return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
1107
- }, z3.number().int().positive());
1171
+ }, z4.number().int().positive());
1172
+ var paginationFields = {
1173
+ page: z4.number().int().positive().optional().default(1),
1174
+ perPage: z4.number().int().min(1).max(100).optional().default(25)
1175
+ };
1176
+ var paginationFieldsNoDefaults = {
1177
+ page: z4.number().int().positive().optional(),
1178
+ perPage: z4.number().int().min(1).max(100).optional()
1179
+ };
1108
1180
 
1109
1181
  // src/capsule/idempotent.ts
1110
1182
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
@@ -1131,7 +1203,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
1131
1203
  // src/tools/define-delete.ts
1132
1204
  function defineDelete(args) {
1133
1205
  const { toolName, pathPrefix, confirmHint, idDescription } = args;
1134
- const schema = z4.object({
1206
+ const schema = z5.object({
1135
1207
  id: idDescription ? positiveId.describe(idDescription) : positiveId,
1136
1208
  confirm: confirmFlag().describe(confirmHint)
1137
1209
  });
@@ -1179,12 +1251,12 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
1179
1251
  }
1180
1252
 
1181
1253
  // src/tools/custom-field-helpers.ts
1182
- import { z as z5 } from "zod";
1183
- var CustomFieldWriteSchema = z5.object({
1254
+ import { z as z6 } from "zod";
1255
+ var CustomFieldWriteSchema = z6.object({
1184
1256
  definitionId: positiveId.describe(
1185
1257
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
1186
1258
  ),
1187
- value: z5.union([z5.string(), z5.number(), z5.boolean(), z5.null()]).describe(
1259
+ value: z6.union([z6.string(), z6.number(), z6.boolean(), z6.null()]).describe(
1188
1260
  "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."
1189
1261
  )
1190
1262
  });
@@ -1200,24 +1272,24 @@ function mapFieldsForBody(fields) {
1200
1272
  }
1201
1273
 
1202
1274
  // src/tools/parties.ts
1203
- var EmailAddressSchema = z6.object({
1204
- address: z6.string().email(),
1205
- type: z6.string().optional()
1275
+ var EmailAddressSchema = z7.object({
1276
+ address: z7.string().email(),
1277
+ type: z7.string().optional()
1206
1278
  });
1207
- var PhoneNumberSchema = z6.object({
1279
+ var PhoneNumberSchema = z7.object({
1208
1280
  // Capsule rejects empty strings with `phoneNumber.number: number is
1209
1281
  // required`. Enforce at the schema layer to catch typos pre-call,
1210
1282
  // matching how EmailAddressSchema's address field behaves.
1211
- number: z6.string().min(1),
1212
- type: z6.string().optional()
1283
+ number: z7.string().min(1),
1284
+ type: z7.string().optional()
1213
1285
  });
1214
1286
  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.";
1215
- var AddressSchema = z6.object({
1216
- street: z6.string().optional(),
1217
- city: z6.string().optional(),
1218
- state: z6.string().optional(),
1219
- country: z6.string().optional().describe(CountryDescription),
1220
- zip: z6.string().optional()
1287
+ var AddressSchema = z7.object({
1288
+ street: z7.string().optional(),
1289
+ city: z7.string().optional(),
1290
+ state: z7.string().optional(),
1291
+ country: z7.string().optional().describe(CountryDescription),
1292
+ zip: z7.string().optional()
1221
1293
  });
1222
1294
  function validateWebsiteAddress(data, ctx) {
1223
1295
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1240,7 +1312,7 @@ function validateWebsiteAddress(data, ctx) {
1240
1312
  });
1241
1313
  }
1242
1314
  }
1243
- var WebsiteServiceEnum = z6.enum([
1315
+ var WebsiteServiceEnum = z7.enum([
1244
1316
  "URL",
1245
1317
  "SKYPE",
1246
1318
  "TWITTER",
@@ -1259,33 +1331,31 @@ var WebsiteServiceEnum = z6.enum([
1259
1331
  "BLUESKY",
1260
1332
  "SNAPCHAT"
1261
1333
  ]);
1262
- var WebsiteSchema = z6.object({
1263
- address: z6.string().min(1).describe(
1334
+ var WebsiteSchema = z7.object({
1335
+ address: z7.string().min(1).describe(
1264
1336
  "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."
1265
1337
  ),
1266
1338
  service: WebsiteServiceEnum.optional().describe(
1267
1339
  "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."
1268
1340
  )
1269
1341
  }).superRefine(validateWebsiteAddress);
1270
- var searchPartiesSchema = z6.object({
1271
- q: z6.string().optional().describe("Free-text search query"),
1272
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1273
- page: z6.number().int().positive().optional().default(1),
1274
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1342
+ var searchPartiesSchema = z7.object({
1343
+ q: z7.string().optional().describe("Free-text search query"),
1344
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1345
+ ...paginationFields
1275
1346
  });
1276
1347
  async function searchParties(input) {
1277
1348
  const path = input.q ? "/parties/search" : "/parties";
1278
- const { data, nextPage } = await capsuleGet(path, {
1349
+ return capsuleGetList(path, {
1279
1350
  q: input.q,
1280
1351
  embed: input.embed,
1281
1352
  page: input.page,
1282
1353
  perPage: input.perPage
1283
1354
  });
1284
- return { ...data, nextPage };
1285
1355
  }
1286
- var getPartySchema = z6.object({
1356
+ var getPartySchema = z7.object({
1287
1357
  id: positiveId.describe("Party ID"),
1288
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1358
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1289
1359
  });
1290
1360
  async function getParty(input) {
1291
1361
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1293,51 +1363,47 @@ async function getParty(input) {
1293
1363
  });
1294
1364
  return data;
1295
1365
  }
1296
- var getPartiesSchema = z6.object({
1297
- ids: z6.array(positiveId).min(1).max(50).describe(
1366
+ var getPartiesSchema = z7.object({
1367
+ ids: z7.array(positiveId).min(1).max(50).describe(
1298
1368
  "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."
1299
1369
  ),
1300
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1370
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1301
1371
  });
1302
1372
  async function getParties(input) {
1303
1373
  return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
1304
1374
  }
1305
- var listPartyOpportunitiesSchema = z6.object({
1375
+ var listPartyOpportunitiesSchema = z7.object({
1306
1376
  partyId: positiveId,
1307
- page: z6.number().int().positive().optional().default(1),
1308
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1377
+ ...paginationFields
1309
1378
  });
1310
1379
  async function listPartyOpportunities(input) {
1311
- const { data, nextPage } = await capsuleGet(
1312
- `/parties/${input.partyId}/opportunities`,
1313
- { page: input.page, perPage: input.perPage }
1314
- );
1315
- return { ...data, nextPage };
1380
+ return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
1381
+ page: input.page,
1382
+ perPage: input.perPage
1383
+ });
1316
1384
  }
1317
- var listPartyProjectsSchema = z6.object({
1385
+ var listPartyProjectsSchema = z7.object({
1318
1386
  partyId: positiveId,
1319
- page: z6.number().int().positive().optional().default(1),
1320
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1387
+ ...paginationFields
1321
1388
  });
1322
1389
  async function listPartyProjects(input) {
1323
- const { data, nextPage } = await capsuleGet(
1324
- `/parties/${input.partyId}/kases`,
1325
- { page: input.page, perPage: input.perPage }
1326
- );
1327
- return { ...data, nextPage };
1390
+ return capsuleGetList(`/parties/${input.partyId}/kases`, {
1391
+ page: input.page,
1392
+ perPage: input.perPage
1393
+ });
1328
1394
  }
1329
1395
  var PartyWriteBaseSchema = {
1330
- about: z6.string().optional(),
1331
- emailAddresses: z6.array(EmailAddressSchema).optional().describe(
1396
+ about: z7.string().optional(),
1397
+ emailAddresses: z7.array(EmailAddressSchema).optional().describe(
1332
1398
  "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)."
1333
1399
  ),
1334
- phoneNumbers: z6.array(PhoneNumberSchema).optional().describe(
1400
+ phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
1335
1401
  "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."
1336
1402
  ),
1337
- addresses: z6.array(AddressSchema).optional().describe(
1403
+ addresses: z7.array(AddressSchema).optional().describe(
1338
1404
  "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)."
1339
1405
  ),
1340
- websites: z6.array(WebsiteSchema).optional().describe(
1406
+ websites: z7.array(WebsiteSchema).optional().describe(
1341
1407
  "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."
1342
1408
  ),
1343
1409
  ownerId: positiveId.nullable().optional().describe(
@@ -1347,16 +1413,16 @@ var PartyWriteBaseSchema = {
1347
1413
  "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)."
1348
1414
  )
1349
1415
  };
1350
- var createPartySchema = z6.object({
1351
- type: z6.enum(["person", "organisation"]),
1416
+ var createPartySchema = z7.object({
1417
+ type: z7.enum(["person", "organisation"]),
1352
1418
  // person
1353
- firstName: z6.string().optional(),
1354
- lastName: z6.string().optional(),
1355
- title: z6.string().optional(),
1356
- jobTitle: z6.string().optional(),
1419
+ firstName: z7.string().optional(),
1420
+ lastName: z7.string().optional(),
1421
+ title: z7.string().optional(),
1422
+ jobTitle: z7.string().optional(),
1357
1423
  organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
1358
1424
  // organisation
1359
- name: z6.string().optional(),
1425
+ name: z7.string().optional(),
1360
1426
  ...PartyWriteBaseSchema,
1361
1427
  ownerId: positiveId.optional().describe(
1362
1428
  "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`."
@@ -1364,7 +1430,7 @@ var createPartySchema = z6.object({
1364
1430
  teamId: positiveId.optional().describe(
1365
1431
  "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."
1366
1432
  ),
1367
- fields: z6.array(CustomFieldWriteSchema).optional().describe(
1433
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(
1368
1434
  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."
1369
1435
  )
1370
1436
  });
@@ -1378,17 +1444,17 @@ async function createParty(input) {
1378
1444
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1379
1445
  return capsulePost("/parties", { party: body });
1380
1446
  }
1381
- var updatePartySchema = z6.object({
1447
+ var updatePartySchema = z7.object({
1382
1448
  id: positiveId,
1383
- firstName: z6.string().optional(),
1384
- lastName: z6.string().optional(),
1385
- title: z6.string().optional(),
1386
- jobTitle: z6.string().optional(),
1387
- name: z6.string().optional(),
1449
+ firstName: z7.string().optional(),
1450
+ lastName: z7.string().optional(),
1451
+ title: z7.string().optional(),
1452
+ jobTitle: z7.string().optional(),
1453
+ name: z7.string().optional(),
1388
1454
  organisationId: positiveId.nullable().optional().describe(
1389
1455
  "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."
1390
1456
  ),
1391
- fields: z6.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1457
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1392
1458
  ...PartyWriteBaseSchema
1393
1459
  });
1394
1460
  async function updateParty(input) {
@@ -1419,10 +1485,42 @@ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1419
1485
  pathPrefix: "/parties",
1420
1486
  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."
1421
1487
  });
1422
- var addPartyEmailAddressSchema = z6.object({
1488
+ function definePartySubResourceRemove(opts) {
1489
+ const shape = {
1490
+ partyId: positiveId,
1491
+ [opts.idField]: positiveId.describe(
1492
+ `Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
1493
+ )
1494
+ };
1495
+ const schema = z7.object(shape);
1496
+ async function handler(input) {
1497
+ const partyId = input["partyId"];
1498
+ const rowId = input[opts.idField];
1499
+ return idempotentWithResult(
1500
+ () => capsulePut(`/parties/${partyId}`, {
1501
+ party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
1502
+ }),
1503
+ (result) => ({
1504
+ removed: true,
1505
+ alreadyRemoved: false,
1506
+ partyId,
1507
+ [opts.idField]: rowId,
1508
+ ...result
1509
+ }),
1510
+ () => ({
1511
+ removed: true,
1512
+ alreadyRemoved: true,
1513
+ partyId,
1514
+ [opts.idField]: rowId
1515
+ })
1516
+ );
1517
+ }
1518
+ return { schema, handler };
1519
+ }
1520
+ var addPartyEmailAddressSchema = z7.object({
1423
1521
  partyId: positiveId,
1424
- address: z6.string().email(),
1425
- type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1522
+ address: z7.string().email(),
1523
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1426
1524
  });
1427
1525
  async function addPartyEmailAddress(input) {
1428
1526
  const { partyId, address, type } = input;
@@ -1432,32 +1530,17 @@ async function addPartyEmailAddress(input) {
1432
1530
  party: { emailAddresses: [item] }
1433
1531
  });
1434
1532
  }
1435
- var removePartyEmailAddressByIdSchema = z6.object({
1436
- partyId: positiveId,
1437
- emailAddressId: positiveId.describe(
1438
- "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1439
- )
1533
+ var removePartyEmailAddress = definePartySubResourceRemove({
1534
+ arrayKey: "emailAddresses",
1535
+ idField: "emailAddressId",
1536
+ rowNoun: "email-address"
1440
1537
  });
1441
- async function removePartyEmailAddressById(input) {
1442
- const { partyId, emailAddressId } = input;
1443
- return idempotentWithResult(
1444
- () => capsulePut(`/parties/${partyId}`, {
1445
- party: { emailAddresses: [{ id: emailAddressId, _delete: true }] }
1446
- }),
1447
- (result) => ({
1448
- removed: true,
1449
- alreadyRemoved: false,
1450
- partyId,
1451
- emailAddressId,
1452
- ...result
1453
- }),
1454
- () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1455
- );
1456
- }
1457
- var addPartyPhoneNumberSchema = z6.object({
1538
+ var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
1539
+ var removePartyEmailAddressById = removePartyEmailAddress.handler;
1540
+ var addPartyPhoneNumberSchema = z7.object({
1458
1541
  partyId: positiveId,
1459
- number: z6.string().min(1),
1460
- type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1542
+ number: z7.string().min(1),
1543
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1461
1544
  });
1462
1545
  async function addPartyPhoneNumber(input) {
1463
1546
  const { partyId, number, type } = input;
@@ -1467,36 +1550,21 @@ async function addPartyPhoneNumber(input) {
1467
1550
  party: { phoneNumbers: [item] }
1468
1551
  });
1469
1552
  }
1470
- var removePartyPhoneNumberByIdSchema = z6.object({
1471
- partyId: positiveId,
1472
- phoneNumberId: positiveId.describe(
1473
- "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1474
- )
1553
+ var removePartyPhoneNumber = definePartySubResourceRemove({
1554
+ arrayKey: "phoneNumbers",
1555
+ idField: "phoneNumberId",
1556
+ rowNoun: "phone-number"
1475
1557
  });
1476
- async function removePartyPhoneNumberById(input) {
1477
- const { partyId, phoneNumberId } = input;
1478
- return idempotentWithResult(
1479
- () => capsulePut(`/parties/${partyId}`, {
1480
- party: { phoneNumbers: [{ id: phoneNumberId, _delete: true }] }
1481
- }),
1482
- (result) => ({
1483
- removed: true,
1484
- alreadyRemoved: false,
1485
- partyId,
1486
- phoneNumberId,
1487
- ...result
1488
- }),
1489
- () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1490
- );
1491
- }
1492
- var addPartyAddressSchema = z6.object({
1558
+ var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
1559
+ var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
1560
+ var addPartyAddressSchema = z7.object({
1493
1561
  partyId: positiveId,
1494
- street: z6.string().optional(),
1495
- city: z6.string().optional(),
1496
- state: z6.string().optional(),
1497
- country: z6.string().optional().describe(CountryDescription),
1498
- zip: z6.string().optional(),
1499
- type: z6.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1562
+ street: z7.string().optional(),
1563
+ city: z7.string().optional(),
1564
+ state: z7.string().optional(),
1565
+ country: z7.string().optional().describe(CountryDescription),
1566
+ zip: z7.string().optional(),
1567
+ type: z7.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1500
1568
  });
1501
1569
  async function addPartyAddress(input) {
1502
1570
  const { partyId, ...rest } = input;
@@ -1508,31 +1576,16 @@ async function addPartyAddress(input) {
1508
1576
  party: { addresses: [item] }
1509
1577
  });
1510
1578
  }
1511
- var removePartyAddressByIdSchema = z6.object({
1512
- partyId: positiveId,
1513
- addressId: positiveId.describe(
1514
- "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1515
- )
1579
+ var removePartyAddress = definePartySubResourceRemove({
1580
+ arrayKey: "addresses",
1581
+ idField: "addressId",
1582
+ rowNoun: "address"
1516
1583
  });
1517
- async function removePartyAddressById(input) {
1518
- const { partyId, addressId } = input;
1519
- return idempotentWithResult(
1520
- () => capsulePut(`/parties/${partyId}`, {
1521
- party: { addresses: [{ id: addressId, _delete: true }] }
1522
- }),
1523
- (result) => ({
1524
- removed: true,
1525
- alreadyRemoved: false,
1526
- partyId,
1527
- addressId,
1528
- ...result
1529
- }),
1530
- () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1531
- );
1532
- }
1533
- var addPartyWebsiteSchema = z6.object({
1584
+ var removePartyAddressByIdSchema = removePartyAddress.schema;
1585
+ var removePartyAddressById = removePartyAddress.handler;
1586
+ var addPartyWebsiteSchema = z7.object({
1534
1587
  partyId: positiveId,
1535
- address: z6.string().min(1).describe(
1588
+ address: z7.string().min(1).describe(
1536
1589
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1537
1590
  ),
1538
1591
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1545,58 +1598,41 @@ async function addPartyWebsite(input) {
1545
1598
  party: { websites: [item] }
1546
1599
  });
1547
1600
  }
1548
- var removePartyWebsiteByIdSchema = z6.object({
1549
- partyId: positiveId,
1550
- websiteId: positiveId.describe(
1551
- "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1552
- )
1601
+ var removePartyWebsite = definePartySubResourceRemove({
1602
+ arrayKey: "websites",
1603
+ idField: "websiteId",
1604
+ rowNoun: "website"
1553
1605
  });
1554
- async function removePartyWebsiteById(input) {
1555
- const { partyId, websiteId } = input;
1556
- return idempotentWithResult(
1557
- () => capsulePut(`/parties/${partyId}`, {
1558
- party: { websites: [{ id: websiteId, _delete: true }] }
1559
- }),
1560
- (result) => ({
1561
- removed: true,
1562
- alreadyRemoved: false,
1563
- partyId,
1564
- websiteId,
1565
- ...result
1566
- }),
1567
- () => ({ removed: true, alreadyRemoved: true, partyId, websiteId })
1568
- );
1569
- }
1606
+ var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
1607
+ var removePartyWebsiteById = removePartyWebsite.handler;
1570
1608
 
1571
1609
  // src/tools/opportunities.ts
1572
- import { z as z7 } from "zod";
1573
- var OpportunityValueSchema = z7.object({
1574
- amount: z7.number().nonnegative(),
1575
- currency: z7.string({
1610
+ import { z as z8 } from "zod";
1611
+ var OpportunityValueSchema = z8.object({
1612
+ amount: z8.number().nonnegative(),
1613
+ currency: z8.string({
1576
1614
  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
1577
1615
  }).length(3).describe(
1578
1616
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1579
1617
  )
1580
1618
  });
1581
- var searchOpportunitiesSchema = z7.object({
1582
- q: z7.string().optional().describe("Free-text search query"),
1583
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1584
- page: z7.number().int().positive().optional().default(1),
1585
- perPage: z7.number().int().min(1).max(100).optional().default(25)
1619
+ var searchOpportunitiesSchema = z8.object({
1620
+ q: z8.string().optional().describe("Free-text search query"),
1621
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1622
+ ...paginationFields
1586
1623
  });
1587
1624
  async function searchOpportunities(input) {
1588
1625
  const path = input.q ? "/opportunities/search" : "/opportunities";
1589
- const { data, nextPage } = await capsuleGet(path, {
1626
+ return capsuleGetList(path, {
1590
1627
  q: input.q,
1591
1628
  embed: input.embed,
1592
1629
  page: input.page,
1593
1630
  perPage: input.perPage
1594
1631
  });
1595
- return { ...data, nextPage };
1596
1632
  }
1597
- var getOpportunitySchema = z7.object({
1633
+ var getOpportunitySchema = z8.object({
1598
1634
  id: positiveId,
1599
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1635
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1600
1636
  });
1601
1637
  async function getOpportunity(input) {
1602
1638
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1604,32 +1640,32 @@ async function getOpportunity(input) {
1604
1640
  });
1605
1641
  return data;
1606
1642
  }
1607
- var getOpportunitiesSchema = z7.object({
1608
- ids: z7.array(positiveId).min(1).max(50).describe(
1643
+ var getOpportunitiesSchema = z8.object({
1644
+ ids: z8.array(positiveId).min(1).max(50).describe(
1609
1645
  "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."
1610
1646
  ),
1611
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1647
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1612
1648
  });
1613
1649
  async function getOpportunities(input) {
1614
1650
  return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
1615
1651
  }
1616
- var createOpportunitySchema = z7.object({
1617
- name: z7.string().min(1),
1652
+ var createOpportunitySchema = z8.object({
1653
+ name: z8.string().min(1),
1618
1654
  partyId: positiveId.describe("ID of the party this opportunity belongs to"),
1619
1655
  milestoneId: positiveId.describe(
1620
1656
  "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."
1621
1657
  ),
1622
- description: z7.string().optional(),
1658
+ description: z8.string().optional(),
1623
1659
  value: OpportunityValueSchema.optional(),
1624
- expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1625
- probability: z7.number().int().min(0).max(100).optional(),
1660
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1661
+ probability: z8.number().int().min(0).max(100).optional(),
1626
1662
  ownerId: positiveId.optional().describe(
1627
1663
  "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."
1628
1664
  ),
1629
1665
  teamId: positiveId.optional().describe(
1630
1666
  "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)."
1631
1667
  ),
1632
- fields: z7.array(CustomFieldWriteSchema).optional().describe(
1668
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(
1633
1669
  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."
1634
1670
  )
1635
1671
  });
@@ -1646,19 +1682,19 @@ async function createOpportunity(input) {
1646
1682
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1647
1683
  return capsulePost("/opportunities", { opportunity: body });
1648
1684
  }
1649
- var updateOpportunitySchema = z7.object({
1685
+ var updateOpportunitySchema = z8.object({
1650
1686
  id: positiveId,
1651
- name: z7.string().min(1).optional(),
1687
+ name: z8.string().min(1).optional(),
1652
1688
  partyId: positiveId.optional().describe(
1653
1689
  "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`."
1654
1690
  ),
1655
1691
  milestoneId: positiveId.optional().describe(
1656
1692
  "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."
1657
1693
  ),
1658
- description: z7.string().optional(),
1694
+ description: z8.string().optional(),
1659
1695
  value: OpportunityValueSchema.optional(),
1660
- expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1661
- probability: z7.number().int().min(0).max(100).optional().describe(
1696
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1697
+ probability: z8.number().int().min(0).max(100).optional().describe(
1662
1698
  "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)."
1663
1699
  ),
1664
1700
  lostReasonId: positiveId.optional().describe(
@@ -1670,7 +1706,7 @@ var updateOpportunitySchema = z7.object({
1670
1706
  teamId: positiveId.nullable().optional().describe(
1671
1707
  "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."
1672
1708
  ),
1673
- fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1709
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1674
1710
  });
1675
1711
  async function updateOpportunity(input) {
1676
1712
  const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
@@ -1706,25 +1742,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
1706
1742
  });
1707
1743
 
1708
1744
  // src/tools/projects.ts
1709
- import { z as z8 } from "zod";
1710
- var listProjectsSchema = z8.object({
1711
- status: z8.enum(["OPEN", "CLOSED"]).optional(),
1712
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1713
- page: z8.number().int().positive().optional().default(1),
1714
- perPage: z8.number().int().min(1).max(100).optional().default(25)
1745
+ import { z as z9 } from "zod";
1746
+ var listProjectsSchema = z9.object({
1747
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
1748
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1749
+ ...paginationFields
1715
1750
  });
1716
1751
  async function listProjects(input) {
1717
- const { data, nextPage } = await capsuleGet("/kases", {
1752
+ return capsuleGetList("/kases", {
1718
1753
  status: input.status,
1719
1754
  embed: input.embed,
1720
1755
  page: input.page,
1721
1756
  perPage: input.perPage
1722
1757
  });
1723
- return { ...data, nextPage };
1724
1758
  }
1725
- var getProjectSchema = z8.object({
1759
+ var getProjectSchema = z9.object({
1726
1760
  id: positiveId,
1727
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1761
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1728
1762
  });
1729
1763
  async function getProject(input) {
1730
1764
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -1732,20 +1766,20 @@ async function getProject(input) {
1732
1766
  });
1733
1767
  return data;
1734
1768
  }
1735
- var getProjectsSchema = z8.object({
1736
- ids: z8.array(positiveId).min(1).max(50).describe(
1769
+ var getProjectsSchema = z9.object({
1770
+ ids: z9.array(positiveId).min(1).max(50).describe(
1737
1771
  "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."
1738
1772
  ),
1739
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1773
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1740
1774
  });
1741
1775
  async function getProjects(input) {
1742
1776
  return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
1743
1777
  }
1744
- var createProjectSchema = z8.object({
1745
- name: z8.string().min(1),
1778
+ var createProjectSchema = z9.object({
1779
+ name: z9.string().min(1),
1746
1780
  partyId: positiveId.describe("ID of the party linked to this project"),
1747
- description: z8.string().optional(),
1748
- status: z8.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1781
+ description: z9.string().optional(),
1782
+ status: z9.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1749
1783
  ownerId: positiveId.optional().describe(
1750
1784
  "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."
1751
1785
  ),
@@ -1755,8 +1789,8 @@ var createProjectSchema = z8.object({
1755
1789
  stageId: positiveId.optional().describe(
1756
1790
  "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."
1757
1791
  ),
1758
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1759
- fields: z8.array(CustomFieldWriteSchema).optional().describe(
1792
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1793
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
1760
1794
  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."
1761
1795
  )
1762
1796
  });
@@ -1774,11 +1808,11 @@ async function createProject(input) {
1774
1808
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1775
1809
  return capsulePost("/kases", { kase: body });
1776
1810
  }
1777
- var updateProjectSchema = z8.object({
1811
+ var updateProjectSchema = z9.object({
1778
1812
  id: positiveId,
1779
- name: z8.string().min(1).optional(),
1780
- description: z8.string().optional(),
1781
- status: z8.enum(["OPEN", "CLOSED"]).optional(),
1813
+ name: z9.string().min(1).optional(),
1814
+ description: z9.string().optional(),
1815
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
1782
1816
  partyId: positiveId.optional().describe(
1783
1817
  "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`."
1784
1818
  ),
@@ -1791,8 +1825,8 @@ var updateProjectSchema = z8.object({
1791
1825
  stageId: positiveId.nullable().optional().describe(
1792
1826
  "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."
1793
1827
  ),
1794
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1795
- fields: z8.array(CustomFieldWriteSchema).optional().describe(
1828
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1829
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
1796
1830
  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."
1797
1831
  )
1798
1832
  });
@@ -1831,23 +1865,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
1831
1865
  });
1832
1866
 
1833
1867
  // src/tools/tasks.ts
1834
- import { z as z9 } from "zod";
1835
- var listTasksSchema = z9.object({
1868
+ import { z as z10 } from "zod";
1869
+ var listTasksSchema = z10.object({
1836
1870
  // Note: Capsule has a third internal status `PENDING` (a task that's
1837
1871
  // part of an active track but not yet "open"), but it can only be
1838
1872
  // reached via track machinery — it is NOT directly settable by
1839
1873
  // /tasks PUT, and a list filter for it returns the same as OPEN
1840
1874
  // anyway. We expose only the two values that are actually filterable
1841
1875
  // by the v2 API.
1842
- status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1876
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
1843
1877
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
1844
1878
  ),
1845
1879
  ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
1846
- page: z9.number().int().positive().optional().default(1),
1847
- perPage: z9.number().int().min(1).max(100).optional().default(25)
1880
+ ...paginationFields
1848
1881
  });
1849
1882
  async function listTasks(input) {
1850
- const { data, nextPage } = await capsuleGet("/tasks", {
1883
+ return capsuleGetList("/tasks", {
1851
1884
  // Default 'OPEN' applied here (not via zod .default()) so that
1852
1885
  // z.infer keeps `status` optional for callers that omit it.
1853
1886
  status: input.status ?? "OPEN",
@@ -1856,28 +1889,27 @@ async function listTasks(input) {
1856
1889
  page: input.page,
1857
1890
  perPage: input.perPage
1858
1891
  });
1859
- return { ...data, nextPage };
1860
1892
  }
1861
- var getTaskSchema = z9.object({
1893
+ var getTaskSchema = z10.object({
1862
1894
  id: positiveId.describe("Task ID")
1863
1895
  });
1864
1896
  async function getTask(input) {
1865
1897
  const { data } = await capsuleGet(`/tasks/${input.id}`);
1866
1898
  return data;
1867
1899
  }
1868
- var getTasksSchema = z9.object({
1869
- ids: z9.array(positiveId).min(1).max(50).describe(
1900
+ var getTasksSchema = z10.object({
1901
+ ids: z10.array(positiveId).min(1).max(50).describe(
1870
1902
  "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."
1871
1903
  )
1872
1904
  });
1873
1905
  async function getTasks(input) {
1874
1906
  return chunkedMultiGet("/tasks", "tasks", input.ids);
1875
1907
  }
1876
- var createTaskSchema = z9.object({
1877
- description: z9.string().min(1),
1878
- dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1879
- dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1880
- detail: z9.string().optional(),
1908
+ var createTaskSchema = z10.object({
1909
+ description: z10.string().min(1),
1910
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1911
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1912
+ detail: z10.string().optional(),
1881
1913
  ownerId: positiveId.optional().describe(
1882
1914
  "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."
1883
1915
  ),
@@ -1886,10 +1918,7 @@ var createTaskSchema = z9.object({
1886
1918
  projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
1887
1919
  });
1888
1920
  async function createTask(input) {
1889
- const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
1890
- if (linked.length > 1) {
1891
- throw new Error("Provide at most one of partyId, opportunityId, or projectId");
1892
- }
1921
+ assertSingleParentRef("create_task", input);
1893
1922
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
1894
1923
  const body = { ...rest };
1895
1924
  setRef(body, "owner", ownerId);
@@ -1898,16 +1927,16 @@ async function createTask(input) {
1898
1927
  setRef(body, "kase", projectId);
1899
1928
  return capsulePost("/tasks", { task: body });
1900
1929
  }
1901
- var updateTaskSchema = z9.object({
1930
+ var updateTaskSchema = z10.object({
1902
1931
  id: positiveId,
1903
- description: z9.string().min(1).optional(),
1904
- dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1905
- dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1906
- detail: z9.string().optional(),
1932
+ description: z10.string().min(1).optional(),
1933
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1934
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1935
+ detail: z10.string().optional(),
1907
1936
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
1908
1937
  // internal state) with 422 "cannot set task status to PENDING".
1909
1938
  // Only OPEN and COMPLETED are settable here.
1910
- status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1939
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
1911
1940
  "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)."
1912
1941
  ),
1913
1942
  ownerId: positiveId.optional().describe(
@@ -1925,12 +1954,7 @@ var updateTaskSchema = z9.object({
1925
1954
  });
1926
1955
  async function updateTask(input) {
1927
1956
  const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
1928
- const setCount = [partyId, opportunityId, projectId].filter((v) => typeof v === "number").length;
1929
- if (setCount > 1) {
1930
- throw new Error(
1931
- "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')"
1932
- );
1933
- }
1957
+ assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
1934
1958
  const body = {};
1935
1959
  for (const [k, v] of Object.entries(rest)) {
1936
1960
  if (v !== void 0) body[k] = v;
@@ -1941,7 +1965,7 @@ async function updateTask(input) {
1941
1965
  setNullableRef(body, "kase", projectId);
1942
1966
  return capsulePut(`/tasks/${id}`, { task: body });
1943
1967
  }
1944
- var completeTaskSchema = z9.object({
1968
+ var completeTaskSchema = z10.object({
1945
1969
  id: positiveId
1946
1970
  });
1947
1971
  async function completeTask(input) {
@@ -1949,8 +1973,8 @@ async function completeTask(input) {
1949
1973
  task: { status: "COMPLETED" }
1950
1974
  });
1951
1975
  }
1952
- var batchCompleteTaskSchema = z9.object({
1953
- ids: z9.array(positiveId).min(1).max(50).describe(
1976
+ var batchCompleteTaskSchema = z10.object({
1977
+ ids: z10.array(positiveId).min(1).max(50).describe(
1954
1978
  "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."
1955
1979
  )
1956
1980
  });
@@ -1964,77 +1988,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
1964
1988
  });
1965
1989
 
1966
1990
  // src/tools/entries.ts
1967
- import { z as z10 } from "zod";
1991
+ import { z as z11 } from "zod";
1968
1992
  var listEntriesPagination = {
1969
- page: z10.number().int().positive().optional().default(1),
1970
- perPage: z10.number().int().min(1).max(100).optional().default(25),
1971
- embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1993
+ ...paginationFields,
1994
+ embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1972
1995
  };
1973
- var listPartyEntriesSchema = z10.object({
1996
+ var listPartyEntriesSchema = z11.object({
1974
1997
  partyId: positiveId,
1975
1998
  ...listEntriesPagination,
1976
- includeLinkedPersons: z10.boolean().optional().describe(
1999
+ includeLinkedPersons: z11.boolean().optional().describe(
1977
2000
  "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."
1978
2001
  )
1979
2002
  });
2003
+ var PER_PARTY_FETCH_CAP = 100;
1980
2004
  async function fanOutPartyEntries(partyIds, embed, perPage) {
1981
- const concurrency = getBatchConcurrency();
1982
- const results = new Array(partyIds.length);
1983
- let cursor = 0;
1984
- async function worker() {
1985
- while (true) {
1986
- const i = cursor;
1987
- cursor += 1;
1988
- if (i >= partyIds.length) return;
1989
- const id = partyIds[i];
1990
- const { data, nextPage } = await capsuleGet(
1991
- `/parties/${id}/entries`,
1992
- {
1993
- embed,
1994
- page: 1,
1995
- perPage
1996
- }
1997
- );
1998
- results[i] = { entries: data.entries, nextPage };
1999
- }
2000
- }
2001
- const workers = [];
2002
- for (let w = 0; w < Math.min(concurrency, partyIds.length); w++) {
2003
- workers.push(worker());
2004
- }
2005
- await Promise.all(workers);
2006
- return results;
2005
+ return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
2006
+ const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
2007
+ embed,
2008
+ page: 1,
2009
+ perPage
2010
+ });
2011
+ return { entries: data.entries, nextPage };
2012
+ });
2007
2013
  }
2008
2014
  function mergedTimelineCandidatePerParty(page, perPage) {
2009
- return Math.min(page * perPage, 100);
2015
+ return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
2010
2016
  }
2011
2017
  function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
2012
2018
  const requestedWindowEnd = page * perPage;
2013
2019
  if (mergedLength > requestedWindowEnd) return page + 1;
2014
- const nextWindowWithinCap = requestedWindowEnd < 100;
2020
+ const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
2015
2021
  if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
2016
2022
  return void 0;
2017
2023
  }
2018
2024
  async function listPartyEntries(input) {
2019
2025
  const { partyId, embed, page, perPage, includeLinkedPersons } = input;
2020
2026
  if (!includeLinkedPersons) {
2021
- const { data, nextPage: nextPage2 } = await capsuleGet(
2022
- `/parties/${partyId}/entries`,
2023
- { embed, page, perPage }
2024
- );
2025
- return { ...data, nextPage: nextPage2 };
2027
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2028
+ embed,
2029
+ page,
2030
+ perPage
2031
+ });
2026
2032
  }
2027
2033
  const { data: peopleData } = await capsuleGet(
2028
2034
  `/parties/${partyId}/people`,
2029
- { page: 1, perPage: 100 }
2035
+ { page: 1, perPage: PER_PARTY_FETCH_CAP }
2030
2036
  );
2031
2037
  const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
2032
2038
  if (peopleIds.length === 0) {
2033
- const { data, nextPage: nextPage2 } = await capsuleGet(
2034
- `/parties/${partyId}/entries`,
2035
- { embed, page, perPage }
2036
- );
2037
- return { ...data, nextPage: nextPage2 };
2039
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2040
+ embed,
2041
+ page,
2042
+ perPage
2043
+ });
2038
2044
  }
2039
2045
  const targetIds = [partyId, ...peopleIds];
2040
2046
  const perPartyPages = await fanOutPartyEntries(
@@ -2069,31 +2075,31 @@ async function listPartyEntries(input) {
2069
2075
  );
2070
2076
  return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
2071
2077
  }
2072
- var listOpportunityEntriesSchema = z10.object({
2078
+ var listOpportunityEntriesSchema = z11.object({
2073
2079
  opportunityId: positiveId,
2074
2080
  ...listEntriesPagination
2075
2081
  });
2076
2082
  async function listOpportunityEntries(input) {
2077
- const { data, nextPage } = await capsuleGet(
2078
- `/opportunities/${input.opportunityId}/entries`,
2079
- { embed: input.embed, page: input.page, perPage: input.perPage }
2080
- );
2081
- return { ...data, nextPage };
2083
+ return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
2084
+ embed: input.embed,
2085
+ page: input.page,
2086
+ perPage: input.perPage
2087
+ });
2082
2088
  }
2083
- var listProjectEntriesSchema = z10.object({
2089
+ var listProjectEntriesSchema = z11.object({
2084
2090
  projectId: positiveId,
2085
2091
  ...listEntriesPagination
2086
2092
  });
2087
2093
  async function listProjectEntries(input) {
2088
- const { data, nextPage } = await capsuleGet(
2089
- `/kases/${input.projectId}/entries`,
2090
- { embed: input.embed, page: input.page, perPage: input.perPage }
2091
- );
2092
- return { ...data, nextPage };
2094
+ return capsuleGetList(`/kases/${input.projectId}/entries`, {
2095
+ embed: input.embed,
2096
+ page: input.page,
2097
+ perPage: input.perPage
2098
+ });
2093
2099
  }
2094
- var getEntrySchema = z10.object({
2100
+ var getEntrySchema = z11.object({
2095
2101
  id: positiveId,
2096
- embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2102
+ embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2097
2103
  });
2098
2104
  async function getEntry(input) {
2099
2105
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2101,34 +2107,30 @@ async function getEntry(input) {
2101
2107
  });
2102
2108
  return data;
2103
2109
  }
2104
- var listEntriesSchema = z10.object({
2110
+ var listEntriesSchema = z11.object({
2105
2111
  ...listEntriesPagination
2106
2112
  });
2107
2113
  async function listEntries(input) {
2108
- const { data, nextPage } = await capsuleGet("/entries", {
2114
+ return capsuleGetList("/entries", {
2109
2115
  embed: input.embed,
2110
2116
  page: input.page,
2111
2117
  perPage: input.perPage
2112
2118
  });
2113
- return { ...data, nextPage };
2114
2119
  }
2115
- var addNoteSchema = z10.object({
2116
- content: z10.string().min(1).describe(
2120
+ var addNoteSchema = z11.object({
2121
+ content: z11.string().min(1).describe(
2117
2122
  "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."
2118
2123
  ),
2119
2124
  partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2120
2125
  opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2121
2126
  projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
2122
- entryAt: z10.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
2127
+ 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(
2123
2128
  "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)."
2124
2129
  )
2125
2130
  });
2126
2131
  async function addNote(input) {
2127
2132
  const { content, partyId, opportunityId, projectId, entryAt } = input;
2128
- const linked = [partyId, opportunityId, projectId].filter(Boolean);
2129
- if (linked.length !== 1) {
2130
- throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
2131
- }
2133
+ assertSingleParentRef("add_note", input, { required: true });
2132
2134
  const body = { type: "note", content };
2133
2135
  setRef(body, "party", partyId);
2134
2136
  setRef(body, "opportunity", opportunityId);
@@ -2136,12 +2138,12 @@ async function addNote(input) {
2136
2138
  if (entryAt !== void 0) body["entryAt"] = entryAt;
2137
2139
  return capsulePost("/entries", { entry: body });
2138
2140
  }
2139
- var updateEntrySchema = z10.object({
2141
+ var updateEntrySchema = z11.object({
2140
2142
  id: positiveId.describe("Entry ID to update"),
2141
- content: z10.string().min(1).optional().describe(
2143
+ content: z11.string().min(1).optional().describe(
2142
2144
  "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."
2143
2145
  ),
2144
- subject: z10.string().optional().describe(
2146
+ subject: z11.string().optional().describe(
2145
2147
  "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`."
2146
2148
  )
2147
2149
  });
@@ -2163,62 +2165,50 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
2163
2165
  });
2164
2166
 
2165
2167
  // src/tools/pipelines.ts
2166
- import { z as z11 } from "zod";
2167
- var paginationFields = {
2168
- page: z11.number().int().positive().optional(),
2169
- perPage: z11.number().int().min(1).max(100).optional()
2170
- };
2171
- var listPipelinesSchema = z11.object({ ...paginationFields });
2168
+ import { z as z12 } from "zod";
2169
+ var listPipelinesSchema = z12.object({ ...paginationFieldsNoDefaults });
2172
2170
  async function listPipelines(input) {
2173
- const { data, nextPage } = await capsuleGetCached("/pipelines", {
2171
+ return capsuleGetCachedList("/pipelines", {
2174
2172
  page: input.page ?? 1,
2175
2173
  perPage: input.perPage ?? 100
2176
2174
  });
2177
- return { ...data, nextPage };
2178
2175
  }
2179
- var listMilestonesSchema = z11.object({
2176
+ var listMilestonesSchema = z12.object({
2180
2177
  pipelineId: positiveId,
2181
- ...paginationFields
2178
+ ...paginationFieldsNoDefaults
2182
2179
  });
2183
2180
  async function listMilestones(input) {
2184
- const { data, nextPage } = await capsuleGetCached(
2181
+ return capsuleGetCachedList(
2185
2182
  `/pipelines/${input.pipelineId}/milestones`,
2186
2183
  { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2187
2184
  );
2188
- return { ...data, nextPage };
2189
2185
  }
2190
2186
 
2191
2187
  // src/tools/boards.ts
2192
- import { z as z12 } from "zod";
2193
- var paginationFields2 = {
2194
- page: z12.number().int().positive().optional(),
2195
- perPage: z12.number().int().min(1).max(100).optional()
2196
- };
2197
- var listBoardsSchema = z12.object({ ...paginationFields2 });
2188
+ import { z as z13 } from "zod";
2189
+ var listBoardsSchema = z13.object({ ...paginationFieldsNoDefaults });
2198
2190
  async function listBoards(input) {
2199
- const { data, nextPage } = await capsuleGetCached("/boards", {
2191
+ return capsuleGetCachedList("/boards", {
2200
2192
  page: input.page ?? 1,
2201
2193
  perPage: input.perPage ?? 100
2202
2194
  });
2203
- return { ...data, nextPage };
2204
2195
  }
2205
- var listStagesSchema = z12.object({
2196
+ var listStagesSchema = z13.object({
2206
2197
  boardId: positiveId.optional().describe(
2207
2198
  "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."
2208
2199
  ),
2209
- ...paginationFields2
2200
+ ...paginationFieldsNoDefaults
2210
2201
  });
2211
2202
  async function listStages(input) {
2212
2203
  const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
2213
- const { data, nextPage } = await capsuleGetCached(path, {
2204
+ return capsuleGetCachedList(path, {
2214
2205
  page: input.page ?? 1,
2215
2206
  perPage: input.perPage ?? 100
2216
2207
  });
2217
- return { ...data, nextPage };
2218
2208
  }
2219
2209
 
2220
2210
  // src/tools/tags.ts
2221
- import { z as z13 } from "zod";
2211
+ import { z as z14 } from "zod";
2222
2212
  var TAG_LIST_PATH = {
2223
2213
  parties: "/parties/tags",
2224
2214
  opportunities: "/opportunities/tags",
@@ -2229,24 +2219,22 @@ var ENTITY_TO_WRAPPER = {
2229
2219
  opportunities: "opportunity",
2230
2220
  kases: "kase"
2231
2221
  };
2232
- var TagEntity = z13.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2233
- var listTagsSchema = z13.object({
2234
- entity: z13.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2235
- page: z13.number().int().positive().optional(),
2236
- perPage: z13.number().int().min(1).max(100).optional()
2222
+ var TagEntity = z14.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2223
+ var listTagsSchema = z14.object({
2224
+ entity: z14.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2225
+ ...paginationFieldsNoDefaults
2237
2226
  });
2238
2227
  async function listTags(input) {
2239
2228
  const path = TAG_LIST_PATH[input.entity];
2240
- const { data, nextPage } = await capsuleGetCached(path, {
2229
+ return capsuleGetCachedList(path, {
2241
2230
  page: input.page ?? 1,
2242
2231
  perPage: input.perPage ?? 100
2243
2232
  });
2244
- return { ...data, nextPage };
2245
2233
  }
2246
- var addTagSchema = z13.object({
2234
+ var addTagSchema = z14.object({
2247
2235
  entity: TagEntity,
2248
2236
  entityId: positiveId.describe("The party/opportunity/kase id."),
2249
- tagName: z13.string().min(1).describe(
2237
+ tagName: z14.string().min(1).describe(
2250
2238
  "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."
2251
2239
  )
2252
2240
  });
@@ -2259,7 +2247,7 @@ async function addTag(input) {
2259
2247
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2260
2248
  return result;
2261
2249
  }
2262
- var removeTagByIdSchema = z13.object({
2250
+ var removeTagByIdSchema = z14.object({
2263
2251
  entity: TagEntity,
2264
2252
  entityId: positiveId.describe("The party/opportunity/kase id."),
2265
2253
  tagId: positiveId.describe(
@@ -2290,7 +2278,7 @@ async function removeTagById(input) {
2290
2278
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2291
2279
  return result;
2292
2280
  }
2293
- var deleteTagDefinitionSchema = z13.object({
2281
+ var deleteTagDefinitionSchema = z14.object({
2294
2282
  entity: TagEntity,
2295
2283
  tagId: positiveId.describe(
2296
2284
  "The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
@@ -2326,44 +2314,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
2326
2314
  });
2327
2315
 
2328
2316
  // src/tools/users.ts
2329
- import { z as z14 } from "zod";
2330
- var listUsersSchema = z14.object({
2331
- page: z14.number().int().positive().optional(),
2332
- perPage: z14.number().int().min(1).max(100).optional()
2317
+ import { z as z15 } from "zod";
2318
+ var listUsersSchema = z15.object({
2319
+ ...paginationFieldsNoDefaults
2333
2320
  });
2334
2321
  async function listUsers(input) {
2335
- const { data, nextPage } = await capsuleGetCached("/users", {
2322
+ return capsuleGetCachedList("/users", {
2336
2323
  page: input.page ?? 1,
2337
2324
  perPage: input.perPage ?? 100
2338
2325
  });
2339
- return { ...data, nextPage };
2340
2326
  }
2341
- var getCurrentUserSchema = z14.object({});
2327
+ var getCurrentUserSchema = z15.object({});
2342
2328
  async function getCurrentUser(_input) {
2343
2329
  const { data } = await capsuleGet("/users/current");
2344
2330
  return data;
2345
2331
  }
2346
2332
 
2347
2333
  // src/tools/filters.ts
2348
- import { z as z15 } from "zod";
2349
- var FilterConditionSchema = z15.object({
2350
- field: z15.string().describe(
2334
+ import { z as z16 } from "zod";
2335
+ var FilterConditionSchema = z16.object({
2336
+ field: z16.string().describe(
2351
2337
  "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"
2352
2338
  ),
2353
- operator: z15.string().describe(
2339
+ operator: z16.string().describe(
2354
2340
  "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."
2355
2341
  ),
2356
- value: z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]).describe(
2342
+ value: z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]).describe(
2357
2343
  "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."
2358
2344
  )
2359
2345
  });
2360
- var FilterInputSchema = z15.object({
2361
- conditions: z15.array(FilterConditionSchema).min(1).describe(
2346
+ var FilterInputSchema = z16.object({
2347
+ conditions: z16.array(FilterConditionSchema).min(1).describe(
2362
2348
  "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)."
2363
2349
  ),
2364
- embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2365
- page: z15.number().int().positive().optional().default(1),
2366
- perPage: z15.number().int().min(1).max(100).optional().default(25)
2350
+ embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2351
+ ...paginationFields
2367
2352
  });
2368
2353
  async function runFilter(entityPath, input) {
2369
2354
  const { data, nextPage } = await capsuleSearch(
@@ -2383,10 +2368,7 @@ async function filterParties(input) {
2383
2368
  }
2384
2369
  var filterOpportunitiesSchema = FilterInputSchema;
2385
2370
  async function filterOpportunities(input) {
2386
- return runFilter(
2387
- "opportunities",
2388
- input
2389
- );
2371
+ return runFilter("opportunities", input);
2390
2372
  }
2391
2373
  var filterProjectsSchema = FilterInputSchema;
2392
2374
  async function filterProjects(input) {
@@ -2394,139 +2376,126 @@ async function filterProjects(input) {
2394
2376
  }
2395
2377
 
2396
2378
  // src/tools/metadata.ts
2397
- import { z as z16 } from "zod";
2398
- var paginationFields3 = {
2399
- page: z16.number().int().positive().optional(),
2400
- perPage: z16.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2379
+ import { z as z17 } from "zod";
2380
+ var paginationFields2 = {
2381
+ ...paginationFieldsNoDefaults,
2382
+ perPage: paginationFieldsNoDefaults.perPage.describe(
2383
+ "Page size, max 100. Defaults to 100 for reference data."
2384
+ )
2401
2385
  };
2402
- var listTeamsSchema = z16.object({ ...paginationFields3 });
2386
+ var listTeamsSchema = z17.object({ ...paginationFields2 });
2403
2387
  async function listTeams(input) {
2404
- const { data, nextPage } = await capsuleGetCached("/teams", {
2388
+ return capsuleGetCachedList("/teams", {
2405
2389
  page: input.page ?? 1,
2406
2390
  perPage: input.perPage ?? 100
2407
2391
  });
2408
- return { ...data, nextPage };
2409
2392
  }
2410
- var listLostReasonsSchema = z16.object({ ...paginationFields3 });
2393
+ var listLostReasonsSchema = z17.object({ ...paginationFields2 });
2411
2394
  async function listLostReasons(input) {
2412
- const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2395
+ return capsuleGetCachedList("/lostreasons", {
2413
2396
  page: input.page ?? 1,
2414
2397
  perPage: input.perPage ?? 100
2415
2398
  });
2416
- return { ...data, nextPage };
2417
2399
  }
2418
- var listActivityTypesSchema = z16.object({ ...paginationFields3 });
2400
+ var listActivityTypesSchema = z17.object({ ...paginationFields2 });
2419
2401
  async function listActivityTypes(input) {
2420
- const { data, nextPage } = await capsuleGetCached(
2421
- "/activitytypes",
2422
- {
2423
- page: input.page ?? 1,
2424
- perPage: input.perPage ?? 100
2425
- }
2426
- );
2427
- return { ...data, nextPage };
2402
+ return capsuleGetCachedList("/activitytypes", {
2403
+ page: input.page ?? 1,
2404
+ perPage: input.perPage ?? 100
2405
+ });
2428
2406
  }
2429
- var getSiteSchema = z16.object({});
2407
+ var getSiteSchema = z17.object({});
2430
2408
  async function getSite(_input) {
2431
2409
  const { data } = await capsuleGetCached("/site");
2432
2410
  return data;
2433
2411
  }
2434
- var listTrackDefinitionsSchema = z16.object({ ...paginationFields3 });
2412
+ var listTrackDefinitionsSchema = z17.object({ ...paginationFields2 });
2435
2413
  async function listTrackDefinitions(input) {
2436
- const { data, nextPage } = await capsuleGetCached(
2437
- "/trackdefinitions",
2438
- { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2439
- );
2440
- return { ...data, nextPage };
2414
+ return capsuleGetCachedList("/trackdefinitions", {
2415
+ page: input.page ?? 1,
2416
+ perPage: input.perPage ?? 100
2417
+ });
2441
2418
  }
2442
- var listCategoriesSchema = z16.object({ ...paginationFields3 });
2419
+ var listCategoriesSchema = z17.object({ ...paginationFields2 });
2443
2420
  async function listCategories(input) {
2444
- const { data, nextPage } = await capsuleGetCached("/categories", {
2421
+ return capsuleGetCachedList("/categories", {
2445
2422
  page: input.page ?? 1,
2446
2423
  perPage: input.perPage ?? 100
2447
2424
  });
2448
- return { ...data, nextPage };
2449
2425
  }
2450
- var listGoalsSchema = z16.object({ ...paginationFields3 });
2426
+ var listGoalsSchema = z17.object({ ...paginationFields2 });
2451
2427
  async function listGoals(input) {
2452
- const { data, nextPage } = await capsuleGetCached("/goals", {
2428
+ return capsuleGetCachedList("/goals", {
2453
2429
  page: input.page ?? 1,
2454
2430
  perPage: input.perPage ?? 100
2455
2431
  });
2456
- return { ...data, nextPage };
2457
2432
  }
2458
2433
 
2459
2434
  // src/tools/audit.ts
2460
- import { z as z17 } from "zod";
2461
- var listEmployeesSchema = z17.object({
2435
+ import { z as z18 } from "zod";
2436
+ var listEmployeesSchema = z18.object({
2462
2437
  partyId: positiveId.describe(
2463
2438
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2464
2439
  ),
2465
- page: z17.number().int().positive().optional().default(1),
2466
- perPage: z17.number().int().min(1).max(100).optional().default(25),
2467
- embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2440
+ ...paginationFields,
2441
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2468
2442
  });
2469
2443
  async function listEmployees(input) {
2470
- const { data, nextPage } = await capsuleGet(
2471
- `/parties/${input.partyId}/people`,
2472
- { page: input.page, perPage: input.perPage, embed: input.embed }
2473
- );
2474
- return { ...data, nextPage };
2444
+ return capsuleGetList(`/parties/${input.partyId}/people`, {
2445
+ page: input.page,
2446
+ perPage: input.perPage,
2447
+ embed: input.embed
2448
+ });
2475
2449
  }
2476
- var DeletedSinceSchema = z17.string().describe(
2450
+ var DeletedSinceSchema = z18.string().describe(
2477
2451
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2478
2452
  );
2479
2453
  var DeletedPagination = {
2480
2454
  since: DeletedSinceSchema,
2481
- page: z17.number().int().positive().optional().default(1),
2482
- perPage: z17.number().int().min(1).max(100).optional().default(25)
2455
+ ...paginationFields
2483
2456
  };
2484
- var listDeletedPartiesSchema = z17.object(DeletedPagination);
2457
+ var listDeletedPartiesSchema = z18.object(DeletedPagination);
2485
2458
  async function listDeletedParties(input) {
2486
- const { data, nextPage } = await capsuleGet("/parties/deleted", {
2459
+ return capsuleGetList("/parties/deleted", {
2487
2460
  since: input.since,
2488
2461
  page: input.page,
2489
2462
  perPage: input.perPage
2490
2463
  });
2491
- return { ...data, nextPage };
2492
2464
  }
2493
- var listDeletedOpportunitiesSchema = z17.object(DeletedPagination);
2465
+ var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
2494
2466
  async function listDeletedOpportunities(input) {
2495
- const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2467
+ return capsuleGetList("/opportunities/deleted", {
2496
2468
  since: input.since,
2497
2469
  page: input.page,
2498
2470
  perPage: input.perPage
2499
2471
  });
2500
- return { ...data, nextPage };
2501
2472
  }
2502
- var listDeletedProjectsSchema = z17.object(DeletedPagination);
2473
+ var listDeletedProjectsSchema = z18.object(DeletedPagination);
2503
2474
  async function listDeletedProjects(input) {
2504
- const { data, nextPage } = await capsuleGet("/kases/deleted", {
2475
+ return capsuleGetList("/kases/deleted", {
2505
2476
  since: input.since,
2506
2477
  page: input.page,
2507
2478
  perPage: input.perPage
2508
2479
  });
2509
- return { ...data, nextPage };
2510
2480
  }
2511
2481
 
2512
2482
  // src/tools/relationships.ts
2513
- import { z as z18 } from "zod";
2514
- var RelationshipEntity = z18.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2515
- var listAdditionalPartiesSchema = z18.object({
2483
+ import { z as z19 } from "zod";
2484
+ var RelationshipEntity = z19.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2485
+ var listAdditionalPartiesSchema = z19.object({
2516
2486
  entity: RelationshipEntity,
2517
2487
  entityId: positiveId.describe("ID of the opportunity or project."),
2518
- embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2519
- page: z18.number().int().positive().optional().default(1),
2520
- perPage: z18.number().int().min(1).max(100).optional().default(25)
2488
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2489
+ ...paginationFields
2521
2490
  });
2522
2491
  async function listAdditionalParties(input) {
2523
- const { data, nextPage } = await capsuleGet(
2524
- `/${input.entity}/${input.entityId}/parties`,
2525
- { embed: input.embed, page: input.page, perPage: input.perPage }
2526
- );
2527
- return { ...data, nextPage };
2492
+ return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
2493
+ embed: input.embed,
2494
+ page: input.page,
2495
+ perPage: input.perPage
2496
+ });
2528
2497
  }
2529
- var addAdditionalPartySchema = z18.object({
2498
+ var addAdditionalPartySchema = z19.object({
2530
2499
  entity: RelationshipEntity,
2531
2500
  entityId: positiveId,
2532
2501
  partyId: positiveId.describe(
@@ -2559,7 +2528,7 @@ async function addAdditionalParty(input) {
2559
2528
  throw err;
2560
2529
  }
2561
2530
  }
2562
- var removeAdditionalPartySchema = z18.object({
2531
+ var removeAdditionalPartySchema = z19.object({
2563
2532
  entity: RelationshipEntity,
2564
2533
  entityId: positiveId,
2565
2534
  partyId: positiveId,
@@ -2589,24 +2558,23 @@ async function removeAdditionalParty(input) {
2589
2558
  })
2590
2559
  );
2591
2560
  }
2592
- var listAssociatedProjectsSchema = z18.object({
2561
+ var listAssociatedProjectsSchema = z19.object({
2593
2562
  opportunityId: positiveId,
2594
- embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2595
- page: z18.number().int().positive().optional().default(1),
2596
- perPage: z18.number().int().min(1).max(100).optional().default(25)
2563
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2564
+ ...paginationFields
2597
2565
  });
2598
2566
  async function listAssociatedProjects(input) {
2599
- const { data, nextPage } = await capsuleGet(
2600
- `/opportunities/${input.opportunityId}/kases`,
2601
- { embed: input.embed, page: input.page, perPage: input.perPage }
2602
- );
2603
- return { ...data, nextPage };
2567
+ return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
2568
+ embed: input.embed,
2569
+ page: input.page,
2570
+ perPage: input.perPage
2571
+ });
2604
2572
  }
2605
2573
 
2606
2574
  // src/tools/custom-fields.ts
2607
- import { z as z19 } from "zod";
2608
- var CustomFieldEntity = z19.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2609
- var listCustomFieldsSchema = z19.object({
2575
+ import { z as z20 } from "zod";
2576
+ var CustomFieldEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2577
+ var listCustomFieldsSchema = z20.object({
2610
2578
  entity: CustomFieldEntity
2611
2579
  });
2612
2580
  async function listCustomFields(input) {
@@ -2615,7 +2583,7 @@ async function listCustomFields(input) {
2615
2583
  );
2616
2584
  return data;
2617
2585
  }
2618
- var getCustomFieldSchema = z19.object({
2586
+ var getCustomFieldSchema = z20.object({
2619
2587
  entity: CustomFieldEntity,
2620
2588
  fieldId: positiveId.describe("Custom field definition id.")
2621
2589
  });
@@ -2627,9 +2595,9 @@ async function getCustomField(input) {
2627
2595
  }
2628
2596
 
2629
2597
  // src/tools/tracks.ts
2630
- import { z as z20 } from "zod";
2631
- var TrackEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2632
- var listEntityTracksSchema = z20.object({
2598
+ import { z as z21 } from "zod";
2599
+ var TrackEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2600
+ var listEntityTracksSchema = z21.object({
2633
2601
  entity: TrackEntity,
2634
2602
  entityId: positiveId
2635
2603
  });
@@ -2639,20 +2607,20 @@ async function listEntityTracks(input) {
2639
2607
  );
2640
2608
  return data;
2641
2609
  }
2642
- var showTrackSchema = z20.object({
2610
+ var showTrackSchema = z21.object({
2643
2611
  trackId: positiveId
2644
2612
  });
2645
2613
  async function showTrack(input) {
2646
2614
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2647
2615
  return data;
2648
2616
  }
2649
- var applyTrackSchema = z20.object({
2650
- entity: z20.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2617
+ var applyTrackSchema = z21.object({
2618
+ entity: z21.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2651
2619
  entityId: positiveId,
2652
2620
  trackDefinitionId: positiveId.describe(
2653
2621
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2654
2622
  ),
2655
- startDate: z20.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2623
+ startDate: z21.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2656
2624
  "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."
2657
2625
  )
2658
2626
  });
@@ -2665,9 +2633,9 @@ async function applyTrack(input) {
2665
2633
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2666
2634
  return capsulePost("/tracks", { track });
2667
2635
  }
2668
- var updateTrackSchema = z20.object({
2636
+ var updateTrackSchema = z21.object({
2669
2637
  trackId: positiveId,
2670
- fields: z20.record(z20.string(), z20.unknown()).describe(
2638
+ fields: z21.record(z21.string(), z21.unknown()).describe(
2671
2639
  "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."
2672
2640
  )
2673
2641
  });
@@ -2679,7 +2647,7 @@ async function updateTrack(input) {
2679
2647
  track: input.fields
2680
2648
  });
2681
2649
  }
2682
- var removeTrackSchema = z20.object({
2650
+ var removeTrackSchema = z21.object({
2683
2651
  trackId: positiveId,
2684
2652
  confirm: confirmFlag().describe(
2685
2653
  "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."
@@ -2697,13 +2665,13 @@ async function removeTrack(input) {
2697
2665
  }
2698
2666
 
2699
2667
  // src/tools/attachments.ts
2700
- import { z as z21 } from "zod";
2668
+ import { z as z22 } from "zod";
2701
2669
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2702
2670
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2703
2671
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
2704
- var getAttachmentSchema = z21.object({
2672
+ var getAttachmentSchema = z22.object({
2705
2673
  id: positiveId.describe("Attachment ID."),
2706
- maxSizeBytes: z21.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2674
+ maxSizeBytes: z22.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2707
2675
  `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.`
2708
2676
  )
2709
2677
  });
@@ -2718,17 +2686,17 @@ async function getAttachment(input) {
2718
2686
  }
2719
2687
  return { contentType, buffer, sizeBytes };
2720
2688
  }
2721
- var uploadAttachmentSchema = z21.object({
2722
- filename: z21.string().min(1).describe(
2689
+ var uploadAttachmentSchema = z22.object({
2690
+ filename: z22.string().min(1).describe(
2723
2691
  "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."
2724
2692
  ),
2725
- contentType: z21.string().min(1).describe(
2693
+ contentType: z22.string().min(1).describe(
2726
2694
  "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."
2727
2695
  ),
2728
- dataBase64: z21.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2696
+ dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2729
2697
  "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."
2730
2698
  ),
2731
- content: z21.string().optional().describe(
2699
+ content: z22.string().optional().describe(
2732
2700
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
2733
2701
  ),
2734
2702
  partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
@@ -2746,12 +2714,7 @@ function decodedBase64Size(s) {
2746
2714
  return s.length / 4 * 3 - padding;
2747
2715
  }
2748
2716
  async function uploadAttachment(input) {
2749
- const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
2750
- if (linked.length !== 1) {
2751
- throw new Error(
2752
- "upload_attachment: provide exactly one of partyId, opportunityId, or projectId"
2753
- );
2754
- }
2717
+ assertSingleParentRef("upload_attachment", input, { required: true });
2755
2718
  if (!isValidBase64(input.dataBase64)) {
2756
2719
  throw new Error(
2757
2720
  "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)."
@@ -2776,37 +2739,36 @@ async function uploadAttachment(input) {
2776
2739
  content: input.content ?? "[attachment]",
2777
2740
  attachments: [{ token }]
2778
2741
  };
2779
- if (input.partyId) entryBody["party"] = { id: input.partyId };
2780
- if (input.opportunityId) entryBody["opportunity"] = { id: input.opportunityId };
2781
- if (input.projectId) entryBody["kase"] = { id: input.projectId };
2742
+ setRef(entryBody, "party", input.partyId);
2743
+ setRef(entryBody, "opportunity", input.opportunityId);
2744
+ setRef(entryBody, "kase", input.projectId);
2782
2745
  return capsulePost("/entries", { entry: entryBody });
2783
2746
  }
2784
2747
 
2785
2748
  // src/tools/saved-filters.ts
2786
- import { z as z22 } from "zod";
2787
- var EntitySchema = z22.enum(["parties", "opportunities", "kases"]).describe(
2749
+ import { z as z23 } from "zod";
2750
+ var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
2788
2751
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
2789
2752
  );
2790
- var listSavedFiltersSchema = z22.object({
2753
+ var listSavedFiltersSchema = z23.object({
2791
2754
  entity: EntitySchema
2792
2755
  });
2793
2756
  async function listSavedFilters(input) {
2794
2757
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
2795
2758
  return data;
2796
2759
  }
2797
- var runSavedFilterSchema = z22.object({
2760
+ var runSavedFilterSchema = z23.object({
2798
2761
  entity: EntitySchema,
2799
2762
  id: positiveId.describe("The saved filter id (from list_saved_filters)."),
2800
- embed: z22.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2801
- page: z22.number().int().positive().optional().default(1),
2802
- perPage: z22.number().int().min(1).max(100).optional().default(25)
2763
+ embed: z23.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2764
+ ...paginationFields
2803
2765
  });
2804
2766
  async function runSavedFilter(input) {
2805
- const { data, nextPage } = await capsuleGet(
2806
- `/${input.entity}/filters/${input.id}/results`,
2807
- { page: input.page, perPage: input.perPage, embed: input.embed }
2808
- );
2809
- return { ...data, nextPage };
2767
+ return capsuleGetList(`/${input.entity}/filters/${input.id}/results`, {
2768
+ page: input.page,
2769
+ perPage: input.perPage,
2770
+ embed: input.embed
2771
+ });
2810
2772
  }
2811
2773
 
2812
2774
  // src/server.ts
@@ -2817,7 +2779,7 @@ function createCapsuleMcpServer(opts) {
2817
2779
  const server2 = new McpServer(
2818
2780
  {
2819
2781
  name: "capsulemcp",
2820
- version: "1.8.0",
2782
+ version: "1.8.1",
2821
2783
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
2822
2784
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
2823
2785
  icons: ICONS
@@ -3267,20 +3229,72 @@ function createCapsuleMcpServer(opts) {
3267
3229
  listEntriesSchema,
3268
3230
  listEntries
3269
3231
  );
3270
- server2.tool(
3271
- "get_attachment",
3272
- "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.",
3273
- getAttachmentSchema.shape,
3274
- // get_attachment is read-only — downloads a binary, never mutates.
3275
- // Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
3276
- // false}` that `registerTool` applies to every other `get_*` tool.
3277
- // Explicit destructiveHint: false is load-bearing MCP spec
3278
- // defaults destructiveHint to `true`, so omitting it would (in
3279
- // some client implementations) classify this read as destructive.
3280
- { readOnlyHint: true, destructiveHint: false },
3281
- async (input) => {
3282
- const result = await getAttachment(input);
3283
- if (result.truncated) {
3232
+ if (shouldRegister("get_attachment")) {
3233
+ server2.tool(
3234
+ "get_attachment",
3235
+ "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.",
3236
+ getAttachmentSchema.shape,
3237
+ // get_attachment is read-only downloads a binary, never mutates.
3238
+ // Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
3239
+ // false}` that `registerTool` applies to every other `get_*` tool.
3240
+ // Explicit destructiveHint: false is load-bearing MCP spec
3241
+ // defaults destructiveHint to `true`, so omitting it would (in
3242
+ // some client implementations) classify this read as destructive.
3243
+ { readOnlyHint: true, destructiveHint: false },
3244
+ async (input) => {
3245
+ const result = await getAttachment(input);
3246
+ if (result.truncated) {
3247
+ return {
3248
+ content: [
3249
+ {
3250
+ type: "text",
3251
+ text: JSON.stringify(
3252
+ {
3253
+ id: input.id,
3254
+ contentType: result.contentType,
3255
+ sizeBytes: result.sizeBytes,
3256
+ truncated: true,
3257
+ message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
3258
+ },
3259
+ null,
3260
+ 2
3261
+ )
3262
+ }
3263
+ ]
3264
+ };
3265
+ }
3266
+ const baseType = result.contentType.split(";")[0].trim().toLowerCase();
3267
+ if (baseType.startsWith("image/")) {
3268
+ return {
3269
+ content: [
3270
+ {
3271
+ type: "image",
3272
+ data: result.buffer.toString("base64"),
3273
+ mimeType: result.contentType
3274
+ }
3275
+ ]
3276
+ };
3277
+ }
3278
+ const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
3279
+ if (isText) {
3280
+ return {
3281
+ content: [
3282
+ {
3283
+ type: "text",
3284
+ text: JSON.stringify(
3285
+ {
3286
+ id: input.id,
3287
+ contentType: result.contentType,
3288
+ sizeBytes: result.sizeBytes
3289
+ },
3290
+ null,
3291
+ 2
3292
+ )
3293
+ },
3294
+ { type: "text", text: result.buffer.toString("utf8") }
3295
+ ]
3296
+ };
3297
+ }
3284
3298
  return {
3285
3299
  content: [
3286
3300
  {
@@ -3290,8 +3304,7 @@ function createCapsuleMcpServer(opts) {
3290
3304
  id: input.id,
3291
3305
  contentType: result.contentType,
3292
3306
  sizeBytes: result.sizeBytes,
3293
- truncated: true,
3294
- message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
3307
+ base64: result.buffer.toString("base64")
3295
3308
  },
3296
3309
  null,
3297
3310
  2
@@ -3300,57 +3313,8 @@ function createCapsuleMcpServer(opts) {
3300
3313
  ]
3301
3314
  };
3302
3315
  }
3303
- const baseType = result.contentType.split(";")[0].trim().toLowerCase();
3304
- if (baseType.startsWith("image/")) {
3305
- return {
3306
- content: [
3307
- {
3308
- type: "image",
3309
- data: result.buffer.toString("base64"),
3310
- mimeType: result.contentType
3311
- }
3312
- ]
3313
- };
3314
- }
3315
- const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
3316
- if (isText) {
3317
- return {
3318
- content: [
3319
- {
3320
- type: "text",
3321
- text: JSON.stringify(
3322
- {
3323
- id: input.id,
3324
- contentType: result.contentType,
3325
- sizeBytes: result.sizeBytes
3326
- },
3327
- null,
3328
- 2
3329
- )
3330
- },
3331
- { type: "text", text: result.buffer.toString("utf8") }
3332
- ]
3333
- };
3334
- }
3335
- return {
3336
- content: [
3337
- {
3338
- type: "text",
3339
- text: JSON.stringify(
3340
- {
3341
- id: input.id,
3342
- contentType: result.contentType,
3343
- sizeBytes: result.sizeBytes,
3344
- base64: result.buffer.toString("base64")
3345
- },
3346
- null,
3347
- 2
3348
- )
3349
- }
3350
- ]
3351
- };
3352
- }
3353
- );
3316
+ );
3317
+ }
3354
3318
  if (!readOnly) {
3355
3319
  registerTool(
3356
3320
  server2,
@@ -3560,6 +3524,22 @@ var transport = new StdioServerTransport();
3560
3524
  if (isReadOnly()) {
3561
3525
  console.error("[capsulemcp] read-only mode: write/delete tools are not registered");
3562
3526
  }
3527
+ function exitOnDisconnect() {
3528
+ let exiting = false;
3529
+ const die = () => {
3530
+ if (exiting) return;
3531
+ exiting = true;
3532
+ process.exit(0);
3533
+ };
3534
+ process.stdin.on("end", die);
3535
+ process.stdin.on("close", die);
3536
+ process.stdin.on("error", die);
3537
+ process.stdout.on("error", die);
3538
+ const orphanCheck = setInterval(() => {
3539
+ if (process.ppid === 1) die();
3540
+ }, 3e4);
3541
+ orphanCheck.unref?.();
3542
+ }
3563
3543
  try {
3564
3544
  await server.connect(transport);
3565
3545
  } catch (err) {
@@ -3567,3 +3547,4 @@ try {
3567
3547
  console.error(`[capsulemcp] Failed to start: ${message}`);
3568
3548
  process.exit(1);
3569
3549
  }
3550
+ exitOnDisconnect();