capsulemcp 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -138,6 +138,26 @@ function invalidateByPrefix(pathPrefix, trigger) {
138
138
  }
139
139
  }
140
140
 
141
+ // src/capsule/normalize.ts
142
+ var KEY_RENAMES = {
143
+ kase: "project",
144
+ kases: "projects",
145
+ restrictedKases: "restrictedProjects"
146
+ };
147
+ function normalizeProjectKeys(value) {
148
+ if (Array.isArray(value)) {
149
+ return value.map(normalizeProjectKeys);
150
+ }
151
+ if (value !== null && typeof value === "object") {
152
+ const out = {};
153
+ for (const [key, v] of Object.entries(value)) {
154
+ out[KEY_RENAMES[key] ?? key] = normalizeProjectKeys(v);
155
+ }
156
+ return out;
157
+ }
158
+ return value;
159
+ }
160
+
141
161
  // src/capsule/client.ts
142
162
  var DEFAULT_BASE_URL = "https://api.capsulecrm.com/api/v2";
143
163
  function baseUrl() {
@@ -253,38 +273,31 @@ async function parseErrorBody(res) {
253
273
  }
254
274
  }
255
275
  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
- };
276
+ function isTimeoutAbort(err) {
277
+ return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
278
+ // "TimeoutError"; plain aborts (and older undici paths) surface
279
+ // as "AbortError" or carry "aborted" in the message.
280
+ (err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
268
281
  }
269
282
  async function mapAbort(p) {
270
283
  try {
271
284
  return await p;
272
285
  } catch (err) {
273
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
286
+ if (isTimeoutAbort(err)) {
274
287
  throw new CapsuleTimeoutError();
275
288
  }
276
289
  throw err;
277
290
  }
278
291
  }
279
292
  async function fetchWithTimeout(url, options) {
280
- const { options: opts, cleanup } = withTimeout(options);
281
293
  const startedAt = Date.now();
282
294
  try {
283
- const res = await fetch(url, opts);
284
- return { res, cleanup };
295
+ return await fetch(url, {
296
+ ...options ?? {},
297
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
298
+ });
285
299
  } catch (err) {
286
- cleanup();
287
- const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
300
+ const isAbort = isTimeoutAbort(err);
288
301
  emitCapsuleFailure(
289
302
  options?.method ?? "GET",
290
303
  url,
@@ -308,24 +321,22 @@ async function doFetch(url, options) {
308
321
  const startedAt = Date.now();
309
322
  const method = options?.method ?? "GET";
310
323
  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);
324
+ if (first.status === 429) {
325
+ const delay = parseRateLimitDelay(first);
326
+ await drainBody(first);
315
327
  await new Promise((resolve) => setTimeout(resolve, delay));
316
328
  const retried = await fetchWithTimeout(url, options);
317
- if (retried.res.status === 429) {
318
- retried.cleanup();
319
- await drainBody(retried.res);
329
+ if (retried.status === 429) {
330
+ await drainBody(retried);
320
331
  emitCapsuleRateLimited(method, url, Date.now() - startedAt);
321
332
  throw new CapsuleApiError(
322
333
  429,
323
334
  "Rate limit exceeded after one retry. Please slow down your requests."
324
335
  );
325
336
  }
326
- return { ...retried, startedAt, method, url, retriedAfter429: true };
337
+ return { res: retried, startedAt, method, url, retriedAfter429: true };
327
338
  }
328
- return { ...first, startedAt, method, url, retriedAfter429: false };
339
+ return { res: first, startedAt, method, url, retriedAfter429: false };
329
340
  }
330
341
  async function consumeBody(start, body) {
331
342
  try {
@@ -418,7 +429,8 @@ async function throwForStatus(res) {
418
429
  }
419
430
  async function handleResponse(res) {
420
431
  await throwForStatus(res);
421
- return mapAbort(res.json());
432
+ const body = await mapAbort(res.json());
433
+ return normalizeProjectKeys(body);
422
434
  }
423
435
  function buildUrl(path, params) {
424
436
  const url = new URL(`${baseUrl()}${path}`);
@@ -435,15 +447,19 @@ async function capsuleGet(path, params) {
435
447
  const token = getToken();
436
448
  const url = buildUrl(path, params);
437
449
  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
- }
450
+ return consumeBody(start, async () => {
451
+ const data = await handleResponse(start.res);
452
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
453
+ return { data, nextPage };
454
+ });
455
+ }
456
+ async function capsuleGetList(path, params) {
457
+ const { data, nextPage } = await capsuleGet(path, params);
458
+ return { ...data, nextPage };
459
+ }
460
+ async function capsuleGetCachedList(path, params) {
461
+ const { data, nextPage } = await capsuleGetCached(path, params);
462
+ return { ...data, nextPage };
447
463
  }
448
464
  async function capsuleGetCached(path, params) {
449
465
  if (cacheDisabled()) return capsuleGet(path, params);
@@ -482,11 +498,7 @@ async function capsulePost(path, body) {
482
498
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
483
499
  body: JSON.stringify(body)
484
500
  });
485
- try {
486
- return await consumeBody(start, () => handleResponse(start.res));
487
- } finally {
488
- start.cleanup();
489
- }
501
+ return consumeBody(start, () => handleResponse(start.res));
490
502
  }
491
503
  async function capsulePostNoContent(path) {
492
504
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -496,15 +508,11 @@ async function capsulePostNoContent(path) {
496
508
  method: "POST",
497
509
  headers: baseHeaders(token)
498
510
  });
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
- }
511
+ await consumeBody(start, async () => {
512
+ if (start.res.status === 204) return;
513
+ await throwForStatus(start.res);
514
+ await mapAbort(start.res.text());
515
+ });
508
516
  }
509
517
  async function capsuleSearch(path, body, params) {
510
518
  const token = getToken();
@@ -514,15 +522,11 @@ async function capsuleSearch(path, body, params) {
514
522
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
515
523
  body: JSON.stringify(body)
516
524
  });
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
- }
525
+ return consumeBody(start, async () => {
526
+ const data = await handleResponse(start.res);
527
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
528
+ return { data, nextPage };
529
+ });
526
530
  }
527
531
  async function capsulePut(path, body) {
528
532
  if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
@@ -533,68 +537,60 @@ async function capsulePut(path, body) {
533
537
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
534
538
  body: JSON.stringify(body)
535
539
  });
536
- try {
537
- return await consumeBody(start, () => handleResponse(start.res));
538
- } finally {
539
- start.cleanup();
540
- }
540
+ return consumeBody(start, () => handleResponse(start.res));
541
541
  }
542
542
  async function capsuleGetBinary(path, maxBytes) {
543
543
  const token = getToken();
544
544
  const url = buildUrl(path);
545
545
  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
- });
546
+ return consumeBody(start, async () => {
547
+ const res = start.res;
548
+ await throwForStatus(res);
549
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
550
+ const declared = res.headers.get("Content-Length");
551
+ const declaredBytes = declared ? Number(declared) : NaN;
552
+ if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
553
+ if (res.body) await res.body.cancel().catch(() => {
554
+ });
555
+ return {
556
+ contentType,
557
+ buffer: Buffer.alloc(0),
558
+ truncated: true,
559
+ sizeBytes: declaredBytes
560
+ };
561
+ }
562
+ if (maxBytes !== void 0 && res.body) {
563
+ const reader = res.body.getReader();
564
+ const chunks = [];
565
+ let total = 0;
566
+ let truncated = false;
567
+ while (true) {
568
+ const { done, value } = await mapAbort(reader.read());
569
+ if (done) break;
570
+ total += value.byteLength;
571
+ if (total > maxBytes) {
572
+ truncated = true;
573
+ await reader.cancel().catch(() => {
574
+ });
575
+ break;
576
+ }
577
+ chunks.push(value);
578
+ }
579
+ if (truncated) {
556
580
  return {
557
581
  contentType,
558
582
  buffer: Buffer.alloc(0),
559
583
  truncated: true,
560
- sizeBytes: declaredBytes
584
+ sizeBytes: total
561
585
  };
562
586
  }
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
- }
587
+ const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
588
+ return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
589
+ }
590
+ const arrayBuffer = await mapAbort(res.arrayBuffer());
591
+ const buffer = Buffer.from(arrayBuffer);
592
+ return { contentType, buffer, sizeBytes: buffer.length };
593
+ });
598
594
  }
599
595
  async function capsulePostBinary(path, body, contentType, filename) {
600
596
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -610,11 +606,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
610
606
  },
611
607
  body
612
608
  });
613
- try {
614
- return await consumeBody(start, () => handleResponse(start.res));
615
- } finally {
616
- start.cleanup();
617
- }
609
+ return consumeBody(start, () => handleResponse(start.res));
618
610
  }
619
611
  async function capsuleDelete(path) {
620
612
  if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
@@ -624,15 +616,11 @@ async function capsuleDelete(path) {
624
616
  method: "DELETE",
625
617
  headers: baseHeaders(token)
626
618
  });
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
- }
619
+ await consumeBody(start, async () => {
620
+ if (start.res.status === 204) return;
621
+ await throwForStatus(start.res);
622
+ await mapAbort(start.res.text());
623
+ });
636
624
  }
637
625
 
638
626
  // src/server.ts
@@ -668,6 +656,45 @@ var ICONS = [
668
656
  }
669
657
  ];
670
658
 
659
+ // src/server/tier.ts
660
+ var CORE_TOOLS = /* @__PURE__ */ new Set([
661
+ // Parties
662
+ "search_parties",
663
+ "filter_parties",
664
+ "get_party",
665
+ "create_party",
666
+ "update_party",
667
+ "list_party_entries",
668
+ // Opportunities
669
+ "search_opportunities",
670
+ "filter_opportunities",
671
+ "get_opportunity",
672
+ "create_opportunity",
673
+ "update_opportunity",
674
+ // Projects
675
+ "search_projects",
676
+ "filter_projects",
677
+ "list_projects",
678
+ "get_project",
679
+ "create_project",
680
+ "update_project",
681
+ // Tasks
682
+ "list_tasks",
683
+ "get_task",
684
+ "create_task",
685
+ "update_task",
686
+ "complete_task",
687
+ // Timeline + tags + identity
688
+ "add_note",
689
+ "list_tags",
690
+ "add_tag",
691
+ "get_current_user"
692
+ ]);
693
+ function shouldRegister(name) {
694
+ if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
695
+ return CORE_TOOLS.has(name);
696
+ }
697
+
671
698
  // src/tasks/store.ts
672
699
  import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
673
700
  import {
@@ -876,27 +903,25 @@ function wrapAsText(result) {
876
903
  };
877
904
  }
878
905
  function registerTool(server2, name, description, schema, handler) {
906
+ if (!shouldRegister(name)) return;
879
907
  const registerWithSchema = server2.registerTool.bind(server2);
880
908
  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
- }
909
+ registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
910
+ const startedAt = Date.now();
911
+ const argFields = argFieldNames(input);
912
+ const clientId = getRequestContext()?.clientId;
913
+ try {
914
+ const result = await handler(input);
915
+ emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
916
+ return wrapAsText(result);
917
+ } catch (err) {
918
+ emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
919
+ throw err;
896
920
  }
897
- );
921
+ });
898
922
  }
899
923
  function registerToolTask(server2, name, description, schema, handler) {
924
+ if (!shouldRegister(name)) return;
900
925
  const registerWithSchema = server2.experimental.tasks.registerToolTask.bind(
901
926
  server2.experimental.tasks
902
927
  );
@@ -907,7 +932,7 @@ function registerToolTask(server2, name, description, schema, handler) {
907
932
  description,
908
933
  inputSchema: schema,
909
934
  execution: { taskSupport: "optional" },
910
- ...annotations ? { annotations } : {}
935
+ annotations
911
936
  },
912
937
  {
913
938
  createTask: async (input, extra) => {
@@ -967,7 +992,7 @@ function registerToolTask(server2, name, description, schema, handler) {
967
992
  }
968
993
 
969
994
  // src/tools/parties.ts
970
- import { z as z6 } from "zod";
995
+ import { z as z7 } from "zod";
971
996
 
972
997
  // src/tools/body-helpers.ts
973
998
  function setRef(body, key, id) {
@@ -977,9 +1002,22 @@ function setNullableRef(body, key, id) {
977
1002
  if (id === null) body[key] = null;
978
1003
  else if (id !== void 0) body[key] = { id };
979
1004
  }
1005
+ function assertSingleParentRef(toolName, refs, opts = {}) {
1006
+ const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
1007
+ (v) => typeof v === "number"
1008
+ ).length;
1009
+ if (opts.required && set !== 1) {
1010
+ throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
1011
+ }
1012
+ if (set > 1) {
1013
+ throw new Error(
1014
+ `${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
1015
+ );
1016
+ }
1017
+ }
980
1018
 
981
1019
  // src/tools/define-batch.ts
982
- import { z } from "zod";
1020
+ import { z as z2 } from "zod";
983
1021
 
984
1022
  // src/capsule/batch.ts
985
1023
  function chunk(arr, size) {
@@ -998,37 +1036,43 @@ function getBatchConcurrency() {
998
1036
  MAX_CONCURRENCY
999
1037
  );
1000
1038
  }
1001
- async function batchExecute(tool, items, action, options = {}) {
1002
- const concurrency = getBatchConcurrency();
1039
+ async function mapWithConcurrency(items, limit, fn) {
1003
1040
  const results = new Array(items.length);
1004
- const startedAt = Date.now();
1005
- const signal = options.signal;
1006
1041
  let cursor = 0;
1007
1042
  async function worker() {
1008
1043
  while (true) {
1009
1044
  const i = cursor;
1010
1045
  cursor += 1;
1011
1046
  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
- }
1047
+ results[i] = await fn(items[i], i);
1025
1048
  }
1026
1049
  }
1027
1050
  const workers = [];
1028
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1051
+ for (let w = 0; w < Math.min(limit, items.length); w++) {
1029
1052
  workers.push(worker());
1030
1053
  }
1031
1054
  await Promise.all(workers);
1055
+ return results;
1056
+ }
1057
+ async function batchExecute(tool, items, action, options = {}) {
1058
+ const concurrency = getBatchConcurrency();
1059
+ const startedAt = Date.now();
1060
+ const signal = options.signal;
1061
+ const results = await mapWithConcurrency(
1062
+ items,
1063
+ concurrency,
1064
+ async (item, i) => {
1065
+ if (signal?.aborted) {
1066
+ return { ok: false, error: { message: "cancelled by tasks/cancel" } };
1067
+ }
1068
+ try {
1069
+ const result = await action(item, i);
1070
+ return { ok: true, result };
1071
+ } catch (err) {
1072
+ return { ok: false, error: extractError(err) };
1073
+ }
1074
+ }
1075
+ );
1032
1076
  const succeeded = results.filter((r) => r.ok).length;
1033
1077
  const failed = results.length - succeeded;
1034
1078
  const summary = { total: results.length, succeeded, failed };
@@ -1073,10 +1117,52 @@ function topFailureReasons(results, n) {
1073
1117
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
1074
1118
  }
1075
1119
 
1120
+ // src/tools/strip-descriptions.ts
1121
+ import { z } from "zod";
1122
+ function cloneWithDef(node, patch) {
1123
+ const def = node.def;
1124
+ return node.clone({ ...def, ...patch });
1125
+ }
1126
+ function stripDescriptions(schema) {
1127
+ let node = schema;
1128
+ if (node instanceof z.ZodObject) {
1129
+ const shape = node.def.shape;
1130
+ const next = {};
1131
+ let changed = false;
1132
+ for (const [key, child] of Object.entries(shape)) {
1133
+ next[key] = stripDescriptions(child);
1134
+ if (next[key] !== child) changed = true;
1135
+ }
1136
+ if (changed) node = cloneWithDef(node, { shape: next });
1137
+ } else if (node instanceof z.ZodArray) {
1138
+ const element = stripDescriptions(node.def.element);
1139
+ if (element !== node.def.element) node = cloneWithDef(node, { element });
1140
+ } else if (node instanceof z.ZodOptional || node instanceof z.ZodNullable || node instanceof z.ZodDefault || node instanceof z.ZodReadonly) {
1141
+ const innerType = stripDescriptions(node.def.innerType);
1142
+ if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
1143
+ } else if (node instanceof z.ZodUnion) {
1144
+ const options = node.def.options.map(stripDescriptions);
1145
+ if (options.some((o, i) => o !== node.def.options[i])) {
1146
+ node = cloneWithDef(node, { options });
1147
+ }
1148
+ } else if (node instanceof z.ZodPipe) {
1149
+ const inSchema = stripDescriptions(node.def.in);
1150
+ const outSchema = stripDescriptions(node.def.out);
1151
+ if (inSchema !== node.def.in || outSchema !== node.def.out) {
1152
+ node = cloneWithDef(node, { in: inSchema, out: outSchema });
1153
+ }
1154
+ }
1155
+ if (node.description !== void 0) {
1156
+ node = node.meta({ description: void 0 });
1157
+ }
1158
+ return node;
1159
+ }
1160
+
1076
1161
  // src/tools/define-batch.ts
1077
1162
  function defineBatch(args) {
1078
- const schema = z.object({
1079
- items: z.array(args.itemSchema).min(1).max(50).describe(args.itemDescription)
1163
+ const itemSchema = stripDescriptions(args.itemSchema);
1164
+ const schema = z2.object({
1165
+ items: z2.array(itemSchema).min(1).max(50).describe(args.itemDescription)
1080
1166
  });
1081
1167
  async function handler(input, opts = {}) {
1082
1168
  return batchExecute(args.toolName, input.items, args.itemHandler, opts);
@@ -1084,27 +1170,51 @@ function defineBatch(args) {
1084
1170
  return { schema, handler };
1085
1171
  }
1086
1172
 
1087
- // src/tools/descriptions.ts
1088
- var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
1089
- var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
1090
-
1091
1173
  // src/tools/define-delete.ts
1092
- import { z as z4 } from "zod";
1174
+ import { z as z5 } from "zod";
1093
1175
 
1094
1176
  // src/tools/confirm-flag.ts
1095
- import { z as z2 } from "zod";
1177
+ import { z as z3 } from "zod";
1096
1178
  var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1097
1179
  function confirmFlag() {
1098
- return z2.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1180
+ return z3.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1099
1181
  }
1100
1182
 
1101
1183
  // src/tools/shared-schemas.ts
1102
- import { z as z3 } from "zod";
1103
- var positiveId = z3.preprocess((input) => {
1184
+ import { z as z4 } from "zod";
1185
+ var positiveId = z4.preprocess((input) => {
1104
1186
  if (typeof input !== "string") return input;
1105
1187
  const trimmed = input.trim();
1106
1188
  return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
1107
- }, z3.number().int().positive());
1189
+ }, z4.number().int().positive());
1190
+ var paginationFields = {
1191
+ page: z4.number().int().positive().optional().default(1),
1192
+ perPage: z4.number().int().min(1).max(100).optional().default(25)
1193
+ };
1194
+ var paginationFieldsNoDefaults = {
1195
+ page: z4.number().int().positive().optional(),
1196
+ perPage: z4.number().int().min(1).max(100).optional()
1197
+ };
1198
+ var ENTITY_PATH = {
1199
+ parties: "parties",
1200
+ opportunities: "opportunities",
1201
+ projects: "kases"
1202
+ };
1203
+ function embedParam(allowed) {
1204
+ return z4.string().superRefine((value, ctx) => {
1205
+ const tokens = value.split(",").map((t) => t.trim());
1206
+ for (const token of tokens) {
1207
+ if (token === "" || !allowed.includes(token)) {
1208
+ ctx.addIssue({
1209
+ code: "custom",
1210
+ message: `Unknown embed token '${token}'. Valid tokens: ${allowed.join(", ")} (comma-separated). Capsule silently ignores unknown tokens, so this is rejected client-side to prevent silently-missing data.`
1211
+ });
1212
+ }
1213
+ }
1214
+ }).describe(`Comma-separated embeds. Valid tokens: ${allowed.join(", ")}.`).optional();
1215
+ }
1216
+ var RECORD_EMBEDS = ["tags", "fields", "missingImportantFields"];
1217
+ var ENTRY_EMBEDS = ["attachments", "participants"];
1108
1218
 
1109
1219
  // src/capsule/idempotent.ts
1110
1220
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
@@ -1131,7 +1241,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
1131
1241
  // src/tools/define-delete.ts
1132
1242
  function defineDelete(args) {
1133
1243
  const { toolName, pathPrefix, confirmHint, idDescription } = args;
1134
- const schema = z4.object({
1244
+ const schema = z5.object({
1135
1245
  id: idDescription ? positiveId.describe(idDescription) : positiveId,
1136
1246
  confirm: confirmFlag().describe(confirmHint)
1137
1247
  });
@@ -1179,12 +1289,12 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
1179
1289
  }
1180
1290
 
1181
1291
  // src/tools/custom-field-helpers.ts
1182
- import { z as z5 } from "zod";
1183
- var CustomFieldWriteSchema = z5.object({
1292
+ import { z as z6 } from "zod";
1293
+ var CustomFieldWriteSchema = z6.object({
1184
1294
  definitionId: positiveId.describe(
1185
1295
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
1186
1296
  ),
1187
- value: z5.union([z5.string(), z5.number(), z5.boolean(), z5.null()]).describe(
1297
+ value: z6.union([z6.string(), z6.number(), z6.boolean(), z6.null()]).describe(
1188
1298
  "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
1299
  )
1190
1300
  });
@@ -1200,24 +1310,24 @@ function mapFieldsForBody(fields) {
1200
1310
  }
1201
1311
 
1202
1312
  // src/tools/parties.ts
1203
- var EmailAddressSchema = z6.object({
1204
- address: z6.string().email(),
1205
- type: z6.string().optional()
1313
+ var EmailAddressSchema = z7.object({
1314
+ address: z7.string().email(),
1315
+ type: z7.string().optional()
1206
1316
  });
1207
- var PhoneNumberSchema = z6.object({
1317
+ var PhoneNumberSchema = z7.object({
1208
1318
  // Capsule rejects empty strings with `phoneNumber.number: number is
1209
1319
  // required`. Enforce at the schema layer to catch typos pre-call,
1210
1320
  // matching how EmailAddressSchema's address field behaves.
1211
- number: z6.string().min(1),
1212
- type: z6.string().optional()
1321
+ number: z7.string().min(1),
1322
+ type: z7.string().optional()
1213
1323
  });
1214
1324
  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()
1325
+ var AddressSchema = z7.object({
1326
+ street: z7.string().optional(),
1327
+ city: z7.string().optional(),
1328
+ state: z7.string().optional(),
1329
+ country: z7.string().optional().describe(CountryDescription),
1330
+ zip: z7.string().optional()
1221
1331
  });
1222
1332
  function validateWebsiteAddress(data, ctx) {
1223
1333
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1240,7 +1350,7 @@ function validateWebsiteAddress(data, ctx) {
1240
1350
  });
1241
1351
  }
1242
1352
  }
1243
- var WebsiteServiceEnum = z6.enum([
1353
+ var WebsiteServiceEnum = z7.enum([
1244
1354
  "URL",
1245
1355
  "SKYPE",
1246
1356
  "TWITTER",
@@ -1259,33 +1369,31 @@ var WebsiteServiceEnum = z6.enum([
1259
1369
  "BLUESKY",
1260
1370
  "SNAPCHAT"
1261
1371
  ]);
1262
- var WebsiteSchema = z6.object({
1263
- address: z6.string().min(1).describe(
1372
+ var WebsiteSchema = z7.object({
1373
+ address: z7.string().min(1).describe(
1264
1374
  "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
1375
  ),
1266
1376
  service: WebsiteServiceEnum.optional().describe(
1267
1377
  "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
1378
  )
1269
1379
  }).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)
1380
+ var searchPartiesSchema = z7.object({
1381
+ q: z7.string().optional().describe("Free-text search query"),
1382
+ embed: embedParam(RECORD_EMBEDS),
1383
+ ...paginationFields
1275
1384
  });
1276
1385
  async function searchParties(input) {
1277
1386
  const path = input.q ? "/parties/search" : "/parties";
1278
- const { data, nextPage } = await capsuleGet(path, {
1387
+ return capsuleGetList(path, {
1279
1388
  q: input.q,
1280
1389
  embed: input.embed,
1281
1390
  page: input.page,
1282
1391
  perPage: input.perPage
1283
1392
  });
1284
- return { ...data, nextPage };
1285
1393
  }
1286
- var getPartySchema = z6.object({
1394
+ var getPartySchema = z7.object({
1287
1395
  id: positiveId.describe("Party ID"),
1288
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1396
+ embed: embedParam(RECORD_EMBEDS)
1289
1397
  });
1290
1398
  async function getParty(input) {
1291
1399
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1293,51 +1401,47 @@ async function getParty(input) {
1293
1401
  });
1294
1402
  return data;
1295
1403
  }
1296
- var getPartiesSchema = z6.object({
1297
- ids: z6.array(positiveId).min(1).max(50).describe(
1404
+ var getPartiesSchema = z7.object({
1405
+ ids: z7.array(positiveId).min(1).max(50).describe(
1298
1406
  "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
1407
  ),
1300
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1408
+ embed: embedParam(RECORD_EMBEDS)
1301
1409
  });
1302
1410
  async function getParties(input) {
1303
1411
  return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
1304
1412
  }
1305
- var listPartyOpportunitiesSchema = z6.object({
1413
+ var listPartyOpportunitiesSchema = z7.object({
1306
1414
  partyId: positiveId,
1307
- page: z6.number().int().positive().optional().default(1),
1308
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1415
+ ...paginationFields
1309
1416
  });
1310
1417
  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 };
1418
+ return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
1419
+ page: input.page,
1420
+ perPage: input.perPage
1421
+ });
1316
1422
  }
1317
- var listPartyProjectsSchema = z6.object({
1423
+ var listPartyProjectsSchema = z7.object({
1318
1424
  partyId: positiveId,
1319
- page: z6.number().int().positive().optional().default(1),
1320
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1425
+ ...paginationFields
1321
1426
  });
1322
1427
  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 };
1428
+ return capsuleGetList(`/parties/${input.partyId}/kases`, {
1429
+ page: input.page,
1430
+ perPage: input.perPage
1431
+ });
1328
1432
  }
1329
1433
  var PartyWriteBaseSchema = {
1330
- about: z6.string().optional(),
1331
- emailAddresses: z6.array(EmailAddressSchema).optional().describe(
1434
+ about: z7.string().optional(),
1435
+ emailAddresses: z7.array(EmailAddressSchema).optional().describe(
1332
1436
  "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
1437
  ),
1334
- phoneNumbers: z6.array(PhoneNumberSchema).optional().describe(
1438
+ phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
1335
1439
  "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
1440
  ),
1337
- addresses: z6.array(AddressSchema).optional().describe(
1441
+ addresses: z7.array(AddressSchema).optional().describe(
1338
1442
  "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
1443
  ),
1340
- websites: z6.array(WebsiteSchema).optional().describe(
1444
+ websites: z7.array(WebsiteSchema).optional().describe(
1341
1445
  "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
1446
  ),
1343
1447
  ownerId: positiveId.nullable().optional().describe(
@@ -1347,16 +1451,16 @@ var PartyWriteBaseSchema = {
1347
1451
  "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
1452
  )
1349
1453
  };
1350
- var createPartySchema = z6.object({
1351
- type: z6.enum(["person", "organisation"]),
1454
+ var createPartySchema = z7.object({
1455
+ type: z7.enum(["person", "organisation"]),
1352
1456
  // person
1353
- firstName: z6.string().optional(),
1354
- lastName: z6.string().optional(),
1355
- title: z6.string().optional(),
1356
- jobTitle: z6.string().optional(),
1457
+ firstName: z7.string().optional(),
1458
+ lastName: z7.string().optional(),
1459
+ title: z7.string().optional(),
1460
+ jobTitle: z7.string().optional(),
1357
1461
  organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
1358
1462
  // organisation
1359
- name: z6.string().optional(),
1463
+ name: z7.string().optional(),
1360
1464
  ...PartyWriteBaseSchema,
1361
1465
  ownerId: positiveId.optional().describe(
1362
1466
  "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,9 +1468,24 @@ var createPartySchema = z6.object({
1364
1468
  teamId: positiveId.optional().describe(
1365
1469
  "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
1470
  ),
1367
- fields: z6.array(CustomFieldWriteSchema).optional().describe(
1471
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(
1368
1472
  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
1473
  )
1474
+ }).superRefine((data, ctx) => {
1475
+ if (data.type === "person" && !data.firstName && !data.lastName) {
1476
+ ctx.addIssue({
1477
+ code: "custom",
1478
+ path: ["firstName"],
1479
+ message: "create_party: a person requires firstName and/or lastName"
1480
+ });
1481
+ }
1482
+ if (data.type === "organisation" && !data.name) {
1483
+ ctx.addIssue({
1484
+ code: "custom",
1485
+ path: ["name"],
1486
+ message: "create_party: an organisation requires name"
1487
+ });
1488
+ }
1370
1489
  });
1371
1490
  async function createParty(input) {
1372
1491
  const { ownerId, teamId, organisationId, fields, ...rest } = input;
@@ -1378,17 +1497,17 @@ async function createParty(input) {
1378
1497
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1379
1498
  return capsulePost("/parties", { party: body });
1380
1499
  }
1381
- var updatePartySchema = z6.object({
1500
+ var updatePartySchema = z7.object({
1382
1501
  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(),
1502
+ firstName: z7.string().optional(),
1503
+ lastName: z7.string().optional(),
1504
+ title: z7.string().optional(),
1505
+ jobTitle: z7.string().optional(),
1506
+ name: z7.string().optional(),
1388
1507
  organisationId: positiveId.nullable().optional().describe(
1389
1508
  "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
1509
  ),
1391
- fields: z6.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1510
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1392
1511
  ...PartyWriteBaseSchema
1393
1512
  });
1394
1513
  async function updateParty(input) {
@@ -1417,12 +1536,44 @@ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch(
1417
1536
  var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1418
1537
  toolName: "delete_party",
1419
1538
  pathPrefix: "/parties",
1420
- 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."
1539
+ confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects. 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
1540
  });
1422
- var addPartyEmailAddressSchema = z6.object({
1541
+ function definePartySubResourceRemove(opts) {
1542
+ const shape = {
1543
+ partyId: positiveId,
1544
+ [opts.idField]: positiveId.describe(
1545
+ `Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
1546
+ )
1547
+ };
1548
+ const schema = z7.object(shape);
1549
+ async function handler(input) {
1550
+ const partyId = input["partyId"];
1551
+ const rowId = input[opts.idField];
1552
+ return idempotentWithResult(
1553
+ () => capsulePut(`/parties/${partyId}`, {
1554
+ party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
1555
+ }),
1556
+ (result) => ({
1557
+ removed: true,
1558
+ alreadyRemoved: false,
1559
+ partyId,
1560
+ [opts.idField]: rowId,
1561
+ ...result
1562
+ }),
1563
+ () => ({
1564
+ removed: true,
1565
+ alreadyRemoved: true,
1566
+ partyId,
1567
+ [opts.idField]: rowId
1568
+ })
1569
+ );
1570
+ }
1571
+ return { schema, handler };
1572
+ }
1573
+ var addPartyEmailAddressSchema = z7.object({
1423
1574
  partyId: positiveId,
1424
- address: z6.string().email(),
1425
- type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1575
+ address: z7.string().email(),
1576
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1426
1577
  });
1427
1578
  async function addPartyEmailAddress(input) {
1428
1579
  const { partyId, address, type } = input;
@@ -1432,32 +1583,17 @@ async function addPartyEmailAddress(input) {
1432
1583
  party: { emailAddresses: [item] }
1433
1584
  });
1434
1585
  }
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
- )
1586
+ var removePartyEmailAddress = definePartySubResourceRemove({
1587
+ arrayKey: "emailAddresses",
1588
+ idField: "emailAddressId",
1589
+ rowNoun: "email-address"
1440
1590
  });
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({
1591
+ var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
1592
+ var removePartyEmailAddressById = removePartyEmailAddress.handler;
1593
+ var addPartyPhoneNumberSchema = z7.object({
1458
1594
  partyId: positiveId,
1459
- number: z6.string().min(1),
1460
- type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1595
+ number: z7.string().min(1),
1596
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1461
1597
  });
1462
1598
  async function addPartyPhoneNumber(input) {
1463
1599
  const { partyId, number, type } = input;
@@ -1467,36 +1603,21 @@ async function addPartyPhoneNumber(input) {
1467
1603
  party: { phoneNumbers: [item] }
1468
1604
  });
1469
1605
  }
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
- )
1606
+ var removePartyPhoneNumber = definePartySubResourceRemove({
1607
+ arrayKey: "phoneNumbers",
1608
+ idField: "phoneNumberId",
1609
+ rowNoun: "phone-number"
1475
1610
  });
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({
1611
+ var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
1612
+ var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
1613
+ var addPartyAddressSchema = z7.object({
1493
1614
  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'.")
1615
+ street: z7.string().optional(),
1616
+ city: z7.string().optional(),
1617
+ state: z7.string().optional(),
1618
+ country: z7.string().optional().describe(CountryDescription),
1619
+ zip: z7.string().optional(),
1620
+ type: z7.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1500
1621
  });
1501
1622
  async function addPartyAddress(input) {
1502
1623
  const { partyId, ...rest } = input;
@@ -1508,31 +1629,16 @@ async function addPartyAddress(input) {
1508
1629
  party: { addresses: [item] }
1509
1630
  });
1510
1631
  }
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
- )
1632
+ var removePartyAddress = definePartySubResourceRemove({
1633
+ arrayKey: "addresses",
1634
+ idField: "addressId",
1635
+ rowNoun: "address"
1516
1636
  });
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({
1637
+ var removePartyAddressByIdSchema = removePartyAddress.schema;
1638
+ var removePartyAddressById = removePartyAddress.handler;
1639
+ var addPartyWebsiteSchema = z7.object({
1534
1640
  partyId: positiveId,
1535
- address: z6.string().min(1).describe(
1641
+ address: z7.string().min(1).describe(
1536
1642
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1537
1643
  ),
1538
1644
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1545,58 +1651,41 @@ async function addPartyWebsite(input) {
1545
1651
  party: { websites: [item] }
1546
1652
  });
1547
1653
  }
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
- )
1654
+ var removePartyWebsite = definePartySubResourceRemove({
1655
+ arrayKey: "websites",
1656
+ idField: "websiteId",
1657
+ rowNoun: "website"
1553
1658
  });
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
- }
1659
+ var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
1660
+ var removePartyWebsiteById = removePartyWebsite.handler;
1570
1661
 
1571
1662
  // 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({
1663
+ import { z as z8 } from "zod";
1664
+ var OpportunityValueSchema = z8.object({
1665
+ amount: z8.number().nonnegative(),
1666
+ currency: z8.string({
1576
1667
  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
1668
  }).length(3).describe(
1578
1669
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1579
1670
  )
1580
1671
  });
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)
1672
+ var searchOpportunitiesSchema = z8.object({
1673
+ q: z8.string().optional().describe("Free-text search query"),
1674
+ embed: embedParam(RECORD_EMBEDS),
1675
+ ...paginationFields
1586
1676
  });
1587
1677
  async function searchOpportunities(input) {
1588
1678
  const path = input.q ? "/opportunities/search" : "/opportunities";
1589
- const { data, nextPage } = await capsuleGet(path, {
1679
+ return capsuleGetList(path, {
1590
1680
  q: input.q,
1591
1681
  embed: input.embed,
1592
1682
  page: input.page,
1593
1683
  perPage: input.perPage
1594
1684
  });
1595
- return { ...data, nextPage };
1596
1685
  }
1597
- var getOpportunitySchema = z7.object({
1686
+ var getOpportunitySchema = z8.object({
1598
1687
  id: positiveId,
1599
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1688
+ embed: embedParam(RECORD_EMBEDS)
1600
1689
  });
1601
1690
  async function getOpportunity(input) {
1602
1691
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1604,32 +1693,32 @@ async function getOpportunity(input) {
1604
1693
  });
1605
1694
  return data;
1606
1695
  }
1607
- var getOpportunitiesSchema = z7.object({
1608
- ids: z7.array(positiveId).min(1).max(50).describe(
1696
+ var getOpportunitiesSchema = z8.object({
1697
+ ids: z8.array(positiveId).min(1).max(50).describe(
1609
1698
  "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
1699
  ),
1611
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1700
+ embed: embedParam(RECORD_EMBEDS)
1612
1701
  });
1613
1702
  async function getOpportunities(input) {
1614
1703
  return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
1615
1704
  }
1616
- var createOpportunitySchema = z7.object({
1617
- name: z7.string().min(1),
1705
+ var createOpportunitySchema = z8.object({
1706
+ name: z8.string().min(1),
1618
1707
  partyId: positiveId.describe("ID of the party this opportunity belongs to"),
1619
1708
  milestoneId: positiveId.describe(
1620
1709
  "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
1710
  ),
1622
- description: z7.string().optional(),
1711
+ description: z8.string().optional(),
1623
1712
  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(),
1713
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1714
+ probability: z8.number().int().min(0).max(100).optional(),
1626
1715
  ownerId: positiveId.optional().describe(
1627
1716
  "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
1717
  ),
1629
1718
  teamId: positiveId.optional().describe(
1630
1719
  "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
1720
  ),
1632
- fields: z7.array(CustomFieldWriteSchema).optional().describe(
1721
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(
1633
1722
  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
1723
  )
1635
1724
  });
@@ -1646,23 +1735,23 @@ async function createOpportunity(input) {
1646
1735
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1647
1736
  return capsulePost("/opportunities", { opportunity: body });
1648
1737
  }
1649
- var updateOpportunitySchema = z7.object({
1738
+ var updateOpportunitySchema = z8.object({
1650
1739
  id: positiveId,
1651
- name: z7.string().min(1).optional(),
1740
+ name: z8.string().min(1).optional(),
1652
1741
  partyId: positiveId.optional().describe(
1653
1742
  "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
1743
  ),
1655
1744
  milestoneId: positiveId.optional().describe(
1656
1745
  "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
1746
  ),
1658
- description: z7.string().optional(),
1747
+ description: z8.string().optional(),
1659
1748
  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(
1749
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1750
+ probability: z8.number().int().min(0).max(100).optional().describe(
1662
1751
  "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
1752
  ),
1664
1753
  lostReasonId: positiveId.optional().describe(
1665
- "Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lostreasons."
1754
+ "Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lost_reasons."
1666
1755
  ),
1667
1756
  ownerId: positiveId.nullable().optional().describe(
1668
1757
  "Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead. Combine `ownerId: null` + `teamId: <T>` in one call to transfer an opportunity to team-ownership with no specific user (verified empirically in v1.6.5; the owner-clears-team semantic doesn't fire when owner is being cleared to null)."
@@ -1670,7 +1759,7 @@ var updateOpportunitySchema = z7.object({
1670
1759
  teamId: positiveId.nullable().optional().describe(
1671
1760
  "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
1761
  ),
1673
- fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1762
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1674
1763
  });
1675
1764
  async function updateOpportunity(input) {
1676
1765
  const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
@@ -1706,25 +1795,37 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
1706
1795
  });
1707
1796
 
1708
1797
  // 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)
1798
+ import { z as z9 } from "zod";
1799
+ var searchProjectsSchema = z9.object({
1800
+ q: z9.string().optional().describe("Free-text search query"),
1801
+ embed: embedParam(RECORD_EMBEDS),
1802
+ ...paginationFields
1803
+ });
1804
+ async function searchProjects(input) {
1805
+ const path = input.q ? "/kases/search" : "/kases";
1806
+ return capsuleGetList(path, {
1807
+ q: input.q,
1808
+ embed: input.embed,
1809
+ page: input.page,
1810
+ perPage: input.perPage
1811
+ });
1812
+ }
1813
+ var listProjectsSchema = z9.object({
1814
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
1815
+ embed: embedParam(RECORD_EMBEDS),
1816
+ ...paginationFields
1715
1817
  });
1716
1818
  async function listProjects(input) {
1717
- const { data, nextPage } = await capsuleGet("/kases", {
1819
+ return capsuleGetList("/kases", {
1718
1820
  status: input.status,
1719
1821
  embed: input.embed,
1720
1822
  page: input.page,
1721
1823
  perPage: input.perPage
1722
1824
  });
1723
- return { ...data, nextPage };
1724
1825
  }
1725
- var getProjectSchema = z8.object({
1826
+ var getProjectSchema = z9.object({
1726
1827
  id: positiveId,
1727
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1828
+ embed: embedParam(RECORD_EMBEDS)
1728
1829
  });
1729
1830
  async function getProject(input) {
1730
1831
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -1732,20 +1833,20 @@ async function getProject(input) {
1732
1833
  });
1733
1834
  return data;
1734
1835
  }
1735
- var getProjectsSchema = z8.object({
1736
- ids: z8.array(positiveId).min(1).max(50).describe(
1836
+ var getProjectsSchema = z9.object({
1837
+ ids: z9.array(positiveId).min(1).max(50).describe(
1737
1838
  "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
1839
  ),
1739
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1840
+ embed: embedParam(RECORD_EMBEDS)
1740
1841
  });
1741
1842
  async function getProjects(input) {
1742
- return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
1843
+ return chunkedMultiGet("/kases", "projects", input.ids, { embed: input.embed });
1743
1844
  }
1744
- var createProjectSchema = z8.object({
1745
- name: z8.string().min(1),
1845
+ var createProjectSchema = z9.object({
1846
+ name: z9.string().min(1),
1746
1847
  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."),
1848
+ description: z9.string().optional(),
1849
+ status: z9.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1749
1850
  ownerId: positiveId.optional().describe(
1750
1851
  "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
1852
  ),
@@ -1755,8 +1856,8 @@ var createProjectSchema = z8.object({
1755
1856
  stageId: positiveId.optional().describe(
1756
1857
  "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
1858
  ),
1758
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1759
- fields: z8.array(CustomFieldWriteSchema).optional().describe(
1859
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1860
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
1760
1861
  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
1862
  )
1762
1863
  });
@@ -1774,11 +1875,11 @@ async function createProject(input) {
1774
1875
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1775
1876
  return capsulePost("/kases", { kase: body });
1776
1877
  }
1777
- var updateProjectSchema = z8.object({
1878
+ var updateProjectSchema = z9.object({
1778
1879
  id: positiveId,
1779
- name: z8.string().min(1).optional(),
1780
- description: z8.string().optional(),
1781
- status: z8.enum(["OPEN", "CLOSED"]).optional(),
1880
+ name: z9.string().min(1).optional(),
1881
+ description: z9.string().optional(),
1882
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
1782
1883
  partyId: positiveId.optional().describe(
1783
1884
  "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
1885
  ),
@@ -1791,8 +1892,8 @@ var updateProjectSchema = z8.object({
1791
1892
  stageId: positiveId.nullable().optional().describe(
1792
1893
  "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
1894
  ),
1794
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1795
- fields: z8.array(CustomFieldWriteSchema).optional().describe(
1895
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1896
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
1796
1897
  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
1898
  )
1798
1899
  });
@@ -1806,7 +1907,7 @@ async function updateProject(input) {
1806
1907
  let resolvedTeamId = teamId;
1807
1908
  let resolvedStageId = stageId;
1808
1909
  if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
1809
- const current = await readEntityRefs(`/kases/${id}`, "kase");
1910
+ const current = await readEntityRefs(`/kases/${id}`, "project");
1810
1911
  if (teamId === void 0) resolvedTeamId = current.teamId;
1811
1912
  if (stageId === void 0) resolvedStageId = current.stageId;
1812
1913
  }
@@ -1831,23 +1932,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
1831
1932
  });
1832
1933
 
1833
1934
  // src/tools/tasks.ts
1834
- import { z as z9 } from "zod";
1835
- var listTasksSchema = z9.object({
1935
+ import { z as z10 } from "zod";
1936
+ var listTasksSchema = z10.object({
1836
1937
  // Note: Capsule has a third internal status `PENDING` (a task that's
1837
1938
  // part of an active track but not yet "open"), but it can only be
1838
1939
  // reached via track machinery — it is NOT directly settable by
1839
1940
  // /tasks PUT, and a list filter for it returns the same as OPEN
1840
1941
  // anyway. We expose only the two values that are actually filterable
1841
1942
  // by the v2 API.
1842
- status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1943
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
1843
1944
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
1844
1945
  ),
1845
1946
  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)
1947
+ ...paginationFields
1848
1948
  });
1849
1949
  async function listTasks(input) {
1850
- const { data, nextPage } = await capsuleGet("/tasks", {
1950
+ return capsuleGetList("/tasks", {
1851
1951
  // Default 'OPEN' applied here (not via zod .default()) so that
1852
1952
  // z.infer keeps `status` optional for callers that omit it.
1853
1953
  status: input.status ?? "OPEN",
@@ -1856,28 +1956,27 @@ async function listTasks(input) {
1856
1956
  page: input.page,
1857
1957
  perPage: input.perPage
1858
1958
  });
1859
- return { ...data, nextPage };
1860
1959
  }
1861
- var getTaskSchema = z9.object({
1960
+ var getTaskSchema = z10.object({
1862
1961
  id: positiveId.describe("Task ID")
1863
1962
  });
1864
1963
  async function getTask(input) {
1865
1964
  const { data } = await capsuleGet(`/tasks/${input.id}`);
1866
1965
  return data;
1867
1966
  }
1868
- var getTasksSchema = z9.object({
1869
- ids: z9.array(positiveId).min(1).max(50).describe(
1967
+ var getTasksSchema = z10.object({
1968
+ ids: z10.array(positiveId).min(1).max(50).describe(
1870
1969
  "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
1970
  )
1872
1971
  });
1873
1972
  async function getTasks(input) {
1874
1973
  return chunkedMultiGet("/tasks", "tasks", input.ids);
1875
1974
  }
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(),
1975
+ var createTaskSchema = z10.object({
1976
+ description: z10.string().min(1),
1977
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1978
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1979
+ detail: z10.string().optional(),
1881
1980
  ownerId: positiveId.optional().describe(
1882
1981
  "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
1982
  ),
@@ -1886,10 +1985,7 @@ var createTaskSchema = z9.object({
1886
1985
  projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
1887
1986
  });
1888
1987
  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
- }
1988
+ assertSingleParentRef("create_task", input);
1893
1989
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
1894
1990
  const body = { ...rest };
1895
1991
  setRef(body, "owner", ownerId);
@@ -1898,16 +1994,16 @@ async function createTask(input) {
1898
1994
  setRef(body, "kase", projectId);
1899
1995
  return capsulePost("/tasks", { task: body });
1900
1996
  }
1901
- var updateTaskSchema = z9.object({
1997
+ var updateTaskSchema = z10.object({
1902
1998
  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(),
1999
+ description: z10.string().min(1).optional(),
2000
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
2001
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
2002
+ detail: z10.string().optional(),
1907
2003
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
1908
2004
  // internal state) with 422 "cannot set task status to PENDING".
1909
2005
  // Only OPEN and COMPLETED are settable here.
1910
- status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
2006
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
1911
2007
  "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
2008
  ),
1913
2009
  ownerId: positiveId.optional().describe(
@@ -1925,12 +2021,7 @@ var updateTaskSchema = z9.object({
1925
2021
  });
1926
2022
  async function updateTask(input) {
1927
2023
  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
- }
2024
+ assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
1934
2025
  const body = {};
1935
2026
  for (const [k, v] of Object.entries(rest)) {
1936
2027
  if (v !== void 0) body[k] = v;
@@ -1941,7 +2032,7 @@ async function updateTask(input) {
1941
2032
  setNullableRef(body, "kase", projectId);
1942
2033
  return capsulePut(`/tasks/${id}`, { task: body });
1943
2034
  }
1944
- var completeTaskSchema = z9.object({
2035
+ var completeTaskSchema = z10.object({
1945
2036
  id: positiveId
1946
2037
  });
1947
2038
  async function completeTask(input) {
@@ -1949,8 +2040,8 @@ async function completeTask(input) {
1949
2040
  task: { status: "COMPLETED" }
1950
2041
  });
1951
2042
  }
1952
- var batchCompleteTaskSchema = z9.object({
1953
- ids: z9.array(positiveId).min(1).max(50).describe(
2043
+ var batchCompleteTaskSchema = z10.object({
2044
+ ids: z10.array(positiveId).min(1).max(50).describe(
1954
2045
  "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
2046
  )
1956
2047
  });
@@ -1964,77 +2055,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
1964
2055
  });
1965
2056
 
1966
2057
  // src/tools/entries.ts
1967
- import { z as z10 } from "zod";
2058
+ import { z as z11 } from "zod";
1968
2059
  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)
2060
+ ...paginationFields,
2061
+ embed: embedParam(ENTRY_EMBEDS)
1972
2062
  };
1973
- var listPartyEntriesSchema = z10.object({
2063
+ var listPartyEntriesSchema = z11.object({
1974
2064
  partyId: positiveId,
1975
2065
  ...listEntriesPagination,
1976
- includeLinkedPersons: z10.boolean().optional().describe(
2066
+ includeLinkedPersons: z11.boolean().optional().describe(
1977
2067
  "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
2068
  )
1979
2069
  });
2070
+ var PER_PARTY_FETCH_CAP = 100;
1980
2071
  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;
2072
+ return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
2073
+ const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
2074
+ embed,
2075
+ page: 1,
2076
+ perPage
2077
+ });
2078
+ return { entries: data.entries, nextPage };
2079
+ });
2007
2080
  }
2008
2081
  function mergedTimelineCandidatePerParty(page, perPage) {
2009
- return Math.min(page * perPage, 100);
2082
+ return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
2010
2083
  }
2011
2084
  function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
2012
2085
  const requestedWindowEnd = page * perPage;
2013
2086
  if (mergedLength > requestedWindowEnd) return page + 1;
2014
- const nextWindowWithinCap = requestedWindowEnd < 100;
2087
+ const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
2015
2088
  if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
2016
2089
  return void 0;
2017
2090
  }
2018
2091
  async function listPartyEntries(input) {
2019
2092
  const { partyId, embed, page, perPage, includeLinkedPersons } = input;
2020
2093
  if (!includeLinkedPersons) {
2021
- const { data, nextPage: nextPage2 } = await capsuleGet(
2022
- `/parties/${partyId}/entries`,
2023
- { embed, page, perPage }
2024
- );
2025
- return { ...data, nextPage: nextPage2 };
2094
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2095
+ embed,
2096
+ page,
2097
+ perPage
2098
+ });
2026
2099
  }
2027
2100
  const { data: peopleData } = await capsuleGet(
2028
2101
  `/parties/${partyId}/people`,
2029
- { page: 1, perPage: 100 }
2102
+ { page: 1, perPage: PER_PARTY_FETCH_CAP }
2030
2103
  );
2031
2104
  const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
2032
2105
  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 };
2106
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2107
+ embed,
2108
+ page,
2109
+ perPage
2110
+ });
2038
2111
  }
2039
2112
  const targetIds = [partyId, ...peopleIds];
2040
2113
  const perPartyPages = await fanOutPartyEntries(
@@ -2069,31 +2142,31 @@ async function listPartyEntries(input) {
2069
2142
  );
2070
2143
  return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
2071
2144
  }
2072
- var listOpportunityEntriesSchema = z10.object({
2145
+ var listOpportunityEntriesSchema = z11.object({
2073
2146
  opportunityId: positiveId,
2074
2147
  ...listEntriesPagination
2075
2148
  });
2076
2149
  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 };
2150
+ return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
2151
+ embed: input.embed,
2152
+ page: input.page,
2153
+ perPage: input.perPage
2154
+ });
2082
2155
  }
2083
- var listProjectEntriesSchema = z10.object({
2156
+ var listProjectEntriesSchema = z11.object({
2084
2157
  projectId: positiveId,
2085
2158
  ...listEntriesPagination
2086
2159
  });
2087
2160
  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 };
2161
+ return capsuleGetList(`/kases/${input.projectId}/entries`, {
2162
+ embed: input.embed,
2163
+ page: input.page,
2164
+ perPage: input.perPage
2165
+ });
2093
2166
  }
2094
- var getEntrySchema = z10.object({
2167
+ var getEntrySchema = z11.object({
2095
2168
  id: positiveId,
2096
- embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2169
+ embed: embedParam(ENTRY_EMBEDS)
2097
2170
  });
2098
2171
  async function getEntry(input) {
2099
2172
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2101,34 +2174,30 @@ async function getEntry(input) {
2101
2174
  });
2102
2175
  return data;
2103
2176
  }
2104
- var listEntriesSchema = z10.object({
2177
+ var listEntriesSchema = z11.object({
2105
2178
  ...listEntriesPagination
2106
2179
  });
2107
2180
  async function listEntries(input) {
2108
- const { data, nextPage } = await capsuleGet("/entries", {
2181
+ return capsuleGetList("/entries", {
2109
2182
  embed: input.embed,
2110
2183
  page: input.page,
2111
2184
  perPage: input.perPage
2112
2185
  });
2113
- return { ...data, nextPage };
2114
2186
  }
2115
- var addNoteSchema = z10.object({
2116
- content: z10.string().min(1).describe(
2187
+ var addNoteSchema = z11.object({
2188
+ content: z11.string().min(1).describe(
2117
2189
  "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
2190
  ),
2119
2191
  partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2120
2192
  opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2121
2193
  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(
2194
+ 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
2195
  "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
2196
  )
2125
2197
  });
2126
2198
  async function addNote(input) {
2127
2199
  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
- }
2200
+ assertSingleParentRef("add_note", input, { required: true });
2132
2201
  const body = { type: "note", content };
2133
2202
  setRef(body, "party", partyId);
2134
2203
  setRef(body, "opportunity", opportunityId);
@@ -2136,12 +2205,12 @@ async function addNote(input) {
2136
2205
  if (entryAt !== void 0) body["entryAt"] = entryAt;
2137
2206
  return capsulePost("/entries", { entry: body });
2138
2207
  }
2139
- var updateEntrySchema = z10.object({
2208
+ var updateEntrySchema = z11.object({
2140
2209
  id: positiveId.describe("Entry ID to update"),
2141
- content: z10.string().min(1).optional().describe(
2210
+ content: z11.string().min(1).optional().describe(
2142
2211
  "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
2212
  ),
2144
- subject: z10.string().optional().describe(
2213
+ subject: z11.string().optional().describe(
2145
2214
  "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
2215
  )
2147
2216
  });
@@ -2163,103 +2232,89 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
2163
2232
  });
2164
2233
 
2165
2234
  // 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 });
2235
+ import { z as z12 } from "zod";
2236
+ var listPipelinesSchema = z12.object({ ...paginationFieldsNoDefaults });
2172
2237
  async function listPipelines(input) {
2173
- const { data, nextPage } = await capsuleGetCached("/pipelines", {
2238
+ return capsuleGetCachedList("/pipelines", {
2174
2239
  page: input.page ?? 1,
2175
2240
  perPage: input.perPage ?? 100
2176
2241
  });
2177
- return { ...data, nextPage };
2178
2242
  }
2179
- var listMilestonesSchema = z11.object({
2243
+ var listMilestonesSchema = z12.object({
2180
2244
  pipelineId: positiveId,
2181
- ...paginationFields
2245
+ ...paginationFieldsNoDefaults
2182
2246
  });
2183
2247
  async function listMilestones(input) {
2184
- const { data, nextPage } = await capsuleGetCached(
2248
+ return capsuleGetCachedList(
2185
2249
  `/pipelines/${input.pipelineId}/milestones`,
2186
2250
  { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2187
2251
  );
2188
- return { ...data, nextPage };
2189
2252
  }
2190
2253
 
2191
2254
  // 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 });
2255
+ import { z as z13 } from "zod";
2256
+ var listBoardsSchema = z13.object({ ...paginationFieldsNoDefaults });
2198
2257
  async function listBoards(input) {
2199
- const { data, nextPage } = await capsuleGetCached("/boards", {
2258
+ return capsuleGetCachedList("/boards", {
2200
2259
  page: input.page ?? 1,
2201
2260
  perPage: input.perPage ?? 100
2202
2261
  });
2203
- return { ...data, nextPage };
2204
2262
  }
2205
- var listStagesSchema = z12.object({
2263
+ var listStagesSchema = z13.object({
2206
2264
  boardId: positiveId.optional().describe(
2207
2265
  "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
2266
  ),
2209
- ...paginationFields2
2267
+ ...paginationFieldsNoDefaults
2210
2268
  });
2211
2269
  async function listStages(input) {
2212
2270
  const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
2213
- const { data, nextPage } = await capsuleGetCached(path, {
2271
+ return capsuleGetCachedList(path, {
2214
2272
  page: input.page ?? 1,
2215
2273
  perPage: input.perPage ?? 100
2216
2274
  });
2217
- return { ...data, nextPage };
2218
2275
  }
2219
2276
 
2220
2277
  // src/tools/tags.ts
2221
- import { z as z13 } from "zod";
2278
+ import { z as z14 } from "zod";
2222
2279
  var TAG_LIST_PATH = {
2223
2280
  parties: "/parties/tags",
2224
2281
  opportunities: "/opportunities/tags",
2225
- kases: "/kases/tags"
2282
+ projects: "/kases/tags"
2226
2283
  };
2227
2284
  var ENTITY_TO_WRAPPER = {
2228
2285
  parties: "party",
2229
2286
  opportunities: "opportunity",
2230
- kases: "kase"
2287
+ projects: "kase"
2231
2288
  };
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()
2289
+ var TagEntity = z14.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
2290
+ var listTagsSchema = z14.object({
2291
+ entity: z14.enum(["parties", "opportunities", "projects"]).describe("The resource type to list tags for"),
2292
+ ...paginationFieldsNoDefaults
2237
2293
  });
2238
2294
  async function listTags(input) {
2239
2295
  const path = TAG_LIST_PATH[input.entity];
2240
- const { data, nextPage } = await capsuleGetCached(path, {
2296
+ return capsuleGetCachedList(path, {
2241
2297
  page: input.page ?? 1,
2242
2298
  perPage: input.perPage ?? 100
2243
2299
  });
2244
- return { ...data, nextPage };
2245
2300
  }
2246
- var addTagSchema = z13.object({
2301
+ var addTagSchema = z14.object({
2247
2302
  entity: TagEntity,
2248
2303
  entityId: positiveId.describe("The party/opportunity/kase id."),
2249
- tagName: z13.string().min(1).describe(
2304
+ tagName: z14.string().min(1).describe(
2250
2305
  "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
2306
  )
2252
2307
  });
2253
2308
  async function addTag(input) {
2254
2309
  const { entity, entityId, tagName } = input;
2255
2310
  const wrapper = ENTITY_TO_WRAPPER[entity];
2256
- const result = await capsulePut(`/${entity}/${entityId}`, {
2311
+ const result = await capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
2257
2312
  [wrapper]: { tags: [{ name: tagName }] }
2258
2313
  });
2259
2314
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2260
2315
  return result;
2261
2316
  }
2262
- var removeTagByIdSchema = z13.object({
2317
+ var removeTagByIdSchema = z14.object({
2263
2318
  entity: TagEntity,
2264
2319
  entityId: positiveId.describe("The party/opportunity/kase id."),
2265
2320
  tagId: positiveId.describe(
@@ -2270,7 +2325,7 @@ async function removeTagById(input) {
2270
2325
  const { entity, entityId, tagId } = input;
2271
2326
  const wrapper = ENTITY_TO_WRAPPER[entity];
2272
2327
  const result = await idempotentWithResult(
2273
- () => capsulePut(`/${entity}/${entityId}`, {
2328
+ () => capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
2274
2329
  [wrapper]: { tags: [{ id: tagId, _delete: true }] }
2275
2330
  }),
2276
2331
  (result2) => ({
@@ -2290,7 +2345,7 @@ async function removeTagById(input) {
2290
2345
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2291
2346
  return result;
2292
2347
  }
2293
- var deleteTagDefinitionSchema = z13.object({
2348
+ var deleteTagDefinitionSchema = z14.object({
2294
2349
  entity: TagEntity,
2295
2350
  tagId: positiveId.describe(
2296
2351
  "The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
@@ -2305,7 +2360,7 @@ async function deleteTagDefinition(input) {
2305
2360
  throw new Error("delete_tag_definition requires confirm: true");
2306
2361
  }
2307
2362
  const result = await idempotent(
2308
- () => capsuleDelete(`/${entity}/tags/${tagId}`),
2363
+ () => capsuleDelete(`/${ENTITY_PATH[entity]}/tags/${tagId}`),
2309
2364
  () => ({ deleted: true, alreadyDeleted: false, entity, tagId }),
2310
2365
  () => ({ deleted: true, alreadyDeleted: true, entity, tagId })
2311
2366
  );
@@ -2326,44 +2381,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
2326
2381
  });
2327
2382
 
2328
2383
  // 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()
2384
+ import { z as z15 } from "zod";
2385
+ var listUsersSchema = z15.object({
2386
+ ...paginationFieldsNoDefaults
2333
2387
  });
2334
2388
  async function listUsers(input) {
2335
- const { data, nextPage } = await capsuleGetCached("/users", {
2389
+ return capsuleGetCachedList("/users", {
2336
2390
  page: input.page ?? 1,
2337
2391
  perPage: input.perPage ?? 100
2338
2392
  });
2339
- return { ...data, nextPage };
2340
2393
  }
2341
- var getCurrentUserSchema = z14.object({});
2394
+ var getCurrentUserSchema = z15.object({});
2342
2395
  async function getCurrentUser(_input) {
2343
2396
  const { data } = await capsuleGet("/users/current");
2344
2397
  return data;
2345
2398
  }
2346
2399
 
2347
2400
  // src/tools/filters.ts
2348
- import { z as z15 } from "zod";
2349
- var FilterConditionSchema = z15.object({
2350
- field: z15.string().describe(
2401
+ import { z as z16 } from "zod";
2402
+ var FilterConditionSchema = z16.object({
2403
+ field: z16.string().describe(
2351
2404
  "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
2405
  ),
2353
- operator: z15.string().describe(
2406
+ operator: z16.string().describe(
2354
2407
  "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
2408
  ),
2356
- value: z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]).describe(
2409
+ value: z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]).describe(
2357
2410
  "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
2411
  )
2359
2412
  });
2360
- var FilterInputSchema = z15.object({
2361
- conditions: z15.array(FilterConditionSchema).min(1).describe(
2413
+ var FilterInputSchema = z16.object({
2414
+ conditions: z16.array(FilterConditionSchema).min(1).describe(
2362
2415
  "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
2416
  ),
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)
2417
+ embed: embedParam(RECORD_EMBEDS),
2418
+ ...paginationFields
2367
2419
  });
2368
2420
  async function runFilter(entityPath, input) {
2369
2421
  const { data, nextPage } = await capsuleSearch(
@@ -2383,10 +2435,7 @@ async function filterParties(input) {
2383
2435
  }
2384
2436
  var filterOpportunitiesSchema = FilterInputSchema;
2385
2437
  async function filterOpportunities(input) {
2386
- return runFilter(
2387
- "opportunities",
2388
- input
2389
- );
2438
+ return runFilter("opportunities", input);
2390
2439
  }
2391
2440
  var filterProjectsSchema = FilterInputSchema;
2392
2441
  async function filterProjects(input) {
@@ -2394,139 +2443,129 @@ async function filterProjects(input) {
2394
2443
  }
2395
2444
 
2396
2445
  // 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.")
2446
+ import { z as z17 } from "zod";
2447
+ var paginationFields2 = {
2448
+ ...paginationFieldsNoDefaults,
2449
+ perPage: paginationFieldsNoDefaults.perPage.describe(
2450
+ "Page size, max 100. Defaults to 100 for reference data."
2451
+ )
2401
2452
  };
2402
- var listTeamsSchema = z16.object({ ...paginationFields3 });
2453
+ var listTeamsSchema = z17.object({ ...paginationFields2 });
2403
2454
  async function listTeams(input) {
2404
- const { data, nextPage } = await capsuleGetCached("/teams", {
2455
+ return capsuleGetCachedList("/teams", {
2405
2456
  page: input.page ?? 1,
2406
2457
  perPage: input.perPage ?? 100
2407
2458
  });
2408
- return { ...data, nextPage };
2409
2459
  }
2410
- var listLostReasonsSchema = z16.object({ ...paginationFields3 });
2460
+ var listLostReasonsSchema = z17.object({ ...paginationFields2 });
2411
2461
  async function listLostReasons(input) {
2412
- const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2462
+ return capsuleGetCachedList("/lostreasons", {
2413
2463
  page: input.page ?? 1,
2414
2464
  perPage: input.perPage ?? 100
2415
2465
  });
2416
- return { ...data, nextPage };
2417
2466
  }
2418
- var listActivityTypesSchema = z16.object({ ...paginationFields3 });
2467
+ var listActivityTypesSchema = z17.object({ ...paginationFields2 });
2419
2468
  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 };
2469
+ return capsuleGetCachedList("/activitytypes", {
2470
+ page: input.page ?? 1,
2471
+ perPage: input.perPage ?? 100
2472
+ });
2428
2473
  }
2429
- var getSiteSchema = z16.object({});
2474
+ var getSiteSchema = z17.object({});
2430
2475
  async function getSite(_input) {
2431
2476
  const { data } = await capsuleGetCached("/site");
2432
2477
  return data;
2433
2478
  }
2434
- var listTrackDefinitionsSchema = z16.object({ ...paginationFields3 });
2479
+ var listTrackDefinitionsSchema = z17.object({ ...paginationFields2 });
2435
2480
  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 };
2481
+ return capsuleGetCachedList("/trackdefinitions", {
2482
+ page: input.page ?? 1,
2483
+ perPage: input.perPage ?? 100
2484
+ });
2441
2485
  }
2442
- var listCategoriesSchema = z16.object({ ...paginationFields3 });
2486
+ var listCategoriesSchema = z17.object({ ...paginationFields2 });
2443
2487
  async function listCategories(input) {
2444
- const { data, nextPage } = await capsuleGetCached("/categories", {
2488
+ return capsuleGetCachedList("/categories", {
2445
2489
  page: input.page ?? 1,
2446
2490
  perPage: input.perPage ?? 100
2447
2491
  });
2448
- return { ...data, nextPage };
2449
2492
  }
2450
- var listGoalsSchema = z16.object({ ...paginationFields3 });
2493
+ var listGoalsSchema = z17.object({ ...paginationFields2 });
2451
2494
  async function listGoals(input) {
2452
- const { data, nextPage } = await capsuleGetCached("/goals", {
2495
+ return capsuleGetCachedList("/goals", {
2453
2496
  page: input.page ?? 1,
2454
2497
  perPage: input.perPage ?? 100
2455
2498
  });
2456
- return { ...data, nextPage };
2457
2499
  }
2458
2500
 
2459
2501
  // src/tools/audit.ts
2460
- import { z as z17 } from "zod";
2461
- var listEmployeesSchema = z17.object({
2502
+ import { z as z18 } from "zod";
2503
+ var listEmployeesSchema = z18.object({
2462
2504
  partyId: positiveId.describe(
2463
2505
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2464
2506
  ),
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)
2507
+ ...paginationFields,
2508
+ embed: embedParam(RECORD_EMBEDS)
2468
2509
  });
2469
2510
  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 };
2511
+ return capsuleGetList(`/parties/${input.partyId}/people`, {
2512
+ page: input.page,
2513
+ perPage: input.perPage,
2514
+ embed: input.embed
2515
+ });
2475
2516
  }
2476
- var DeletedSinceSchema = z17.string().describe(
2517
+ var DeletedSinceSchema = z18.string().describe(
2477
2518
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2478
2519
  );
2479
2520
  var DeletedPagination = {
2480
2521
  since: DeletedSinceSchema,
2481
- page: z17.number().int().positive().optional().default(1),
2482
- perPage: z17.number().int().min(1).max(100).optional().default(25)
2522
+ ...paginationFields
2483
2523
  };
2484
- var listDeletedPartiesSchema = z17.object(DeletedPagination);
2524
+ var listDeletedPartiesSchema = z18.object(DeletedPagination);
2485
2525
  async function listDeletedParties(input) {
2486
- const { data, nextPage } = await capsuleGet("/parties/deleted", {
2526
+ return capsuleGetList("/parties/deleted", {
2487
2527
  since: input.since,
2488
2528
  page: input.page,
2489
2529
  perPage: input.perPage
2490
2530
  });
2491
- return { ...data, nextPage };
2492
2531
  }
2493
- var listDeletedOpportunitiesSchema = z17.object(DeletedPagination);
2532
+ var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
2494
2533
  async function listDeletedOpportunities(input) {
2495
- const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2534
+ return capsuleGetList("/opportunities/deleted", {
2496
2535
  since: input.since,
2497
2536
  page: input.page,
2498
2537
  perPage: input.perPage
2499
2538
  });
2500
- return { ...data, nextPage };
2501
2539
  }
2502
- var listDeletedProjectsSchema = z17.object(DeletedPagination);
2540
+ var listDeletedProjectsSchema = z18.object(DeletedPagination);
2503
2541
  async function listDeletedProjects(input) {
2504
- const { data, nextPage } = await capsuleGet("/kases/deleted", {
2542
+ return capsuleGetList("/kases/deleted", {
2505
2543
  since: input.since,
2506
2544
  page: input.page,
2507
2545
  perPage: input.perPage
2508
2546
  });
2509
- return { ...data, nextPage };
2510
2547
  }
2511
2548
 
2512
2549
  // 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({
2550
+ import { z as z19 } from "zod";
2551
+ var RelationshipEntity = z19.enum(["opportunities", "projects"]).describe("Which entity has the additional-party links.");
2552
+ var listAdditionalPartiesSchema = z19.object({
2516
2553
  entity: RelationshipEntity,
2517
2554
  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)
2555
+ embed: embedParam(RECORD_EMBEDS),
2556
+ ...paginationFields
2521
2557
  });
2522
2558
  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 }
2559
+ return capsuleGetList(
2560
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/parties`,
2561
+ {
2562
+ embed: input.embed,
2563
+ page: input.page,
2564
+ perPage: input.perPage
2565
+ }
2526
2566
  );
2527
- return { ...data, nextPage };
2528
2567
  }
2529
- var addAdditionalPartySchema = z18.object({
2568
+ var addAdditionalPartySchema = z19.object({
2530
2569
  entity: RelationshipEntity,
2531
2570
  entityId: positiveId,
2532
2571
  partyId: positiveId.describe(
@@ -2535,7 +2574,9 @@ var addAdditionalPartySchema = z18.object({
2535
2574
  });
2536
2575
  async function addAdditionalParty(input) {
2537
2576
  try {
2538
- await capsulePostNoContent(`/${input.entity}/${input.entityId}/parties/${input.partyId}`);
2577
+ await capsulePostNoContent(
2578
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`
2579
+ );
2539
2580
  return {
2540
2581
  linked: true,
2541
2582
  alreadyLinked: false,
@@ -2559,7 +2600,7 @@ async function addAdditionalParty(input) {
2559
2600
  throw err;
2560
2601
  }
2561
2602
  }
2562
- var removeAdditionalPartySchema = z18.object({
2603
+ var removeAdditionalPartySchema = z19.object({
2563
2604
  entity: RelationshipEntity,
2564
2605
  entityId: positiveId,
2565
2606
  partyId: positiveId,
@@ -2572,7 +2613,7 @@ async function removeAdditionalParty(input) {
2572
2613
  throw new Error("remove_additional_party requires confirm: true");
2573
2614
  }
2574
2615
  return idempotent(
2575
- () => capsuleDelete(`/${input.entity}/${input.entityId}/parties/${input.partyId}`),
2616
+ () => capsuleDelete(`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`),
2576
2617
  () => ({
2577
2618
  removed: true,
2578
2619
  alreadyRemoved: false,
@@ -2589,70 +2630,69 @@ async function removeAdditionalParty(input) {
2589
2630
  })
2590
2631
  );
2591
2632
  }
2592
- var listAssociatedProjectsSchema = z18.object({
2633
+ var listAssociatedProjectsSchema = z19.object({
2593
2634
  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)
2635
+ embed: embedParam(RECORD_EMBEDS),
2636
+ ...paginationFields
2597
2637
  });
2598
2638
  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 };
2639
+ return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
2640
+ embed: input.embed,
2641
+ page: input.page,
2642
+ perPage: input.perPage
2643
+ });
2604
2644
  }
2605
2645
 
2606
2646
  // 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({
2647
+ import { z as z20 } from "zod";
2648
+ var CustomFieldEntity = z20.enum(["parties", "opportunities", "projects"]).describe("Which entity type's custom field schema to inspect.");
2649
+ var listCustomFieldsSchema = z20.object({
2610
2650
  entity: CustomFieldEntity
2611
2651
  });
2612
2652
  async function listCustomFields(input) {
2613
2653
  const { data } = await capsuleGetCached(
2614
- `/${input.entity}/fields/definitions`
2654
+ `/${ENTITY_PATH[input.entity]}/fields/definitions`
2615
2655
  );
2616
2656
  return data;
2617
2657
  }
2618
- var getCustomFieldSchema = z19.object({
2658
+ var getCustomFieldSchema = z20.object({
2619
2659
  entity: CustomFieldEntity,
2620
- fieldId: positiveId.describe("Custom field definition id.")
2660
+ id: positiveId.describe("Custom field definition id.")
2621
2661
  });
2622
2662
  async function getCustomField(input) {
2623
2663
  const { data } = await capsuleGetCached(
2624
- `/${input.entity}/fields/definitions/${input.fieldId}`
2664
+ `/${input.entity}/fields/definitions/${input.id}`
2625
2665
  );
2626
2666
  return data;
2627
2667
  }
2628
2668
 
2629
2669
  // 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({
2670
+ import { z as z21 } from "zod";
2671
+ var TrackEntity = z21.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
2672
+ var listEntityTracksSchema = z21.object({
2633
2673
  entity: TrackEntity,
2634
2674
  entityId: positiveId
2635
2675
  });
2636
2676
  async function listEntityTracks(input) {
2637
2677
  const { data } = await capsuleGet(
2638
- `/${input.entity}/${input.entityId}/tracks`
2678
+ `/${ENTITY_PATH[input.entity]}/${input.entityId}/tracks`
2639
2679
  );
2640
2680
  return data;
2641
2681
  }
2642
- var showTrackSchema = z20.object({
2643
- trackId: positiveId
2682
+ var getTrackSchema = z21.object({
2683
+ id: positiveId
2644
2684
  });
2645
- async function showTrack(input) {
2646
- const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2685
+ async function getTrack(input) {
2686
+ const { data } = await capsuleGet(`/tracks/${input.id}`);
2647
2687
  return data;
2648
2688
  }
2649
- var applyTrackSchema = z20.object({
2650
- entity: z20.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2689
+ var applyTrackSchema = z21.object({
2690
+ entity: z21.enum(["opportunities", "projects"]).describe("Which entity to apply the track to."),
2651
2691
  entityId: positiveId,
2652
2692
  trackDefinitionId: positiveId.describe(
2653
2693
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2654
2694
  ),
2655
- startDate: z20.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2695
+ startDate: z21.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2656
2696
  "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
2697
  )
2658
2698
  });
@@ -2665,9 +2705,9 @@ async function applyTrack(input) {
2665
2705
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2666
2706
  return capsulePost("/tracks", { track });
2667
2707
  }
2668
- var updateTrackSchema = z20.object({
2669
- trackId: positiveId,
2670
- fields: z20.record(z20.string(), z20.unknown()).describe(
2708
+ var updateTrackSchema = z21.object({
2709
+ id: positiveId,
2710
+ fields: z21.record(z21.string(), z21.unknown()).describe(
2671
2711
  "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
2712
  )
2673
2713
  });
@@ -2675,12 +2715,12 @@ async function updateTrack(input) {
2675
2715
  if (Object.keys(input.fields).length === 0) {
2676
2716
  throw new Error("update_track: provide at least one field in `fields`");
2677
2717
  }
2678
- return capsulePut(`/tracks/${input.trackId}`, {
2718
+ return capsulePut(`/tracks/${input.id}`, {
2679
2719
  track: input.fields
2680
2720
  });
2681
2721
  }
2682
- var removeTrackSchema = z20.object({
2683
- trackId: positiveId,
2722
+ var removeTrackSchema = z21.object({
2723
+ id: positiveId,
2684
2724
  confirm: confirmFlag().describe(
2685
2725
  "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."
2686
2726
  )
@@ -2690,20 +2730,20 @@ async function removeTrack(input) {
2690
2730
  throw new Error("remove_track requires confirm: true");
2691
2731
  }
2692
2732
  return idempotent(
2693
- () => capsuleDelete(`/tracks/${input.trackId}`),
2694
- () => ({ removed: true, alreadyRemoved: false, trackId: input.trackId }),
2695
- () => ({ removed: true, alreadyRemoved: true, trackId: input.trackId })
2733
+ () => capsuleDelete(`/tracks/${input.id}`),
2734
+ () => ({ removed: true, alreadyRemoved: false, id: input.id }),
2735
+ () => ({ removed: true, alreadyRemoved: true, id: input.id })
2696
2736
  );
2697
2737
  }
2698
2738
 
2699
2739
  // src/tools/attachments.ts
2700
- import { z as z21 } from "zod";
2740
+ import { z as z22 } from "zod";
2701
2741
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2702
2742
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2703
2743
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
2704
- var getAttachmentSchema = z21.object({
2744
+ var getAttachmentSchema = z22.object({
2705
2745
  id: positiveId.describe("Attachment ID."),
2706
- maxSizeBytes: z21.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2746
+ maxSizeBytes: z22.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2707
2747
  `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
2748
  )
2709
2749
  });
@@ -2718,17 +2758,17 @@ async function getAttachment(input) {
2718
2758
  }
2719
2759
  return { contentType, buffer, sizeBytes };
2720
2760
  }
2721
- var uploadAttachmentSchema = z21.object({
2722
- filename: z21.string().min(1).describe(
2761
+ var uploadAttachmentSchema = z22.object({
2762
+ filename: z22.string().min(1).describe(
2723
2763
  "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
2764
  ),
2725
- contentType: z21.string().min(1).describe(
2765
+ contentType: z22.string().min(1).describe(
2726
2766
  "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
2767
  ),
2728
- dataBase64: z21.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2768
+ dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2729
2769
  "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
2770
  ),
2731
- content: z21.string().optional().describe(
2771
+ content: z22.string().optional().describe(
2732
2772
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
2733
2773
  ),
2734
2774
  partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
@@ -2746,12 +2786,7 @@ function decodedBase64Size(s) {
2746
2786
  return s.length / 4 * 3 - padding;
2747
2787
  }
2748
2788
  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
- }
2789
+ assertSingleParentRef("upload_attachment", input, { required: true });
2755
2790
  if (!isValidBase64(input.dataBase64)) {
2756
2791
  throw new Error(
2757
2792
  "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 +2811,39 @@ async function uploadAttachment(input) {
2776
2811
  content: input.content ?? "[attachment]",
2777
2812
  attachments: [{ token }]
2778
2813
  };
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 };
2814
+ setRef(entryBody, "party", input.partyId);
2815
+ setRef(entryBody, "opportunity", input.opportunityId);
2816
+ setRef(entryBody, "kase", input.projectId);
2782
2817
  return capsulePost("/entries", { entry: entryBody });
2783
2818
  }
2784
2819
 
2785
2820
  // src/tools/saved-filters.ts
2786
- import { z as z22 } from "zod";
2787
- var EntitySchema = z22.enum(["parties", "opportunities", "kases"]).describe(
2788
- "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
2789
- );
2790
- var listSavedFiltersSchema = z22.object({
2821
+ import { z as z23 } from "zod";
2822
+ var EntitySchema = z23.enum(["parties", "opportunities", "projects"]).describe("Which entity type the filter operates over.");
2823
+ var listSavedFiltersSchema = z23.object({
2791
2824
  entity: EntitySchema
2792
2825
  });
2793
2826
  async function listSavedFilters(input) {
2794
- const { data } = await capsuleGetCached(`/${input.entity}/filters`);
2827
+ const { data } = await capsuleGetCached(
2828
+ `/${ENTITY_PATH[input.entity]}/filters`
2829
+ );
2795
2830
  return data;
2796
2831
  }
2797
- var runSavedFilterSchema = z22.object({
2832
+ var runSavedFilterSchema = z23.object({
2798
2833
  entity: EntitySchema,
2799
2834
  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)
2835
+ embed: embedParam(RECORD_EMBEDS),
2836
+ ...paginationFields
2803
2837
  });
2804
2838
  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 }
2839
+ return capsuleGetList(
2840
+ `/${ENTITY_PATH[input.entity]}/filters/${input.id}/results`,
2841
+ {
2842
+ page: input.page,
2843
+ perPage: input.perPage,
2844
+ embed: input.embed
2845
+ }
2808
2846
  );
2809
- return { ...data, nextPage };
2810
2847
  }
2811
2848
 
2812
2849
  // src/server.ts
@@ -2817,7 +2854,7 @@ function createCapsuleMcpServer(opts) {
2817
2854
  const server2 = new McpServer(
2818
2855
  {
2819
2856
  name: "capsulemcp",
2820
- version: "1.8.0",
2857
+ version: "2.0.0",
2821
2858
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
2822
2859
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
2823
2860
  icons: ICONS
@@ -2934,7 +2971,7 @@ function createCapsuleMcpServer(opts) {
2934
2971
  registerTool(
2935
2972
  server2,
2936
2973
  "delete_party",
2937
- "DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to 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. TRACK INSTANCES applied to cascaded opportunities/projects are NOT cleaned up either \u2014 they survive as orphan records reachable only by track id via show_track. Use remove_track on each track explicitly before deleting the parent party if orphan accumulation matters (rare in practice \u2014 orphans are unreachable from normal navigation). Requires confirm=true. Always read the party first with get_party and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the party was already gone (Capsule's 404 is caught internally so reconciliation loops can re-issue safely).",
2974
+ "DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects. 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. TRACK INSTANCES applied to cascaded opportunities/projects are NOT cleaned up either \u2014 they survive as orphan records reachable only by track id via get_track. Use remove_track on each track explicitly before deleting the parent party if orphan accumulation matters (rare in practice \u2014 orphans are unreachable from normal navigation). Requires confirm=true. Always read the party first with get_party and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the party was already gone (Capsule's 404 is caught internally so reconciliation loops can re-issue safely).",
2938
2975
  deletePartySchema,
2939
2976
  deleteParty
2940
2977
  );
@@ -3033,7 +3070,7 @@ function createCapsuleMcpServer(opts) {
3033
3070
  registerTool(
3034
3071
  server2,
3035
3072
  "list_additional_parties",
3036
- "List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or 'kases' (Capsule's term for projects).",
3073
+ "List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or 'projects'.",
3037
3074
  listAdditionalPartiesSchema,
3038
3075
  listAdditionalParties
3039
3076
  );
@@ -3074,10 +3111,17 @@ function createCapsuleMcpServer(opts) {
3074
3111
  deleteOpportunity
3075
3112
  );
3076
3113
  }
3114
+ registerTool(
3115
+ server2,
3116
+ "search_projects",
3117
+ "Free-text search projects in Capsule CRM (matches name and description). Returns results in Capsule's default order (no sort parameter is supported here). Omit `q` to list all projects. For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3118
+ searchProjectsSchema,
3119
+ searchProjects
3120
+ );
3077
3121
  registerTool(
3078
3122
  server2,
3079
3123
  "list_projects",
3080
- "List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3124
+ "List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For free-text matching use search_projects; for structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
3081
3125
  listProjectsSchema,
3082
3126
  listProjects
3083
3127
  );
@@ -3105,7 +3149,7 @@ function createCapsuleMcpServer(opts) {
3105
3149
  registerTool(
3106
3150
  server2,
3107
3151
  "list_deleted_projects",
3108
- "Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `restrictedKases` key for records the integration user can't read fully.",
3152
+ "Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `restrictedProjects` key for records the integration user can't read fully.",
3109
3153
  listDeletedProjectsSchema,
3110
3154
  listDeletedProjects
3111
3155
  );
@@ -3267,20 +3311,72 @@ function createCapsuleMcpServer(opts) {
3267
3311
  listEntriesSchema,
3268
3312
  listEntries
3269
3313
  );
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) {
3314
+ if (shouldRegister("get_attachment")) {
3315
+ server2.tool(
3316
+ "get_attachment",
3317
+ "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.",
3318
+ getAttachmentSchema.shape,
3319
+ // get_attachment is read-only downloads a binary, never mutates.
3320
+ // Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
3321
+ // false}` that `registerTool` applies to every other `get_*` tool.
3322
+ // Explicit destructiveHint: false is load-bearing MCP spec
3323
+ // defaults destructiveHint to `true`, so omitting it would (in
3324
+ // some client implementations) classify this read as destructive.
3325
+ { readOnlyHint: true, destructiveHint: false },
3326
+ async (input) => {
3327
+ const result = await getAttachment(input);
3328
+ if (result.truncated) {
3329
+ return {
3330
+ content: [
3331
+ {
3332
+ type: "text",
3333
+ text: JSON.stringify(
3334
+ {
3335
+ id: input.id,
3336
+ contentType: result.contentType,
3337
+ sizeBytes: result.sizeBytes,
3338
+ truncated: true,
3339
+ message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
3340
+ },
3341
+ null,
3342
+ 2
3343
+ )
3344
+ }
3345
+ ]
3346
+ };
3347
+ }
3348
+ const baseType = result.contentType.split(";")[0].trim().toLowerCase();
3349
+ if (baseType.startsWith("image/")) {
3350
+ return {
3351
+ content: [
3352
+ {
3353
+ type: "image",
3354
+ data: result.buffer.toString("base64"),
3355
+ mimeType: result.contentType
3356
+ }
3357
+ ]
3358
+ };
3359
+ }
3360
+ const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
3361
+ if (isText) {
3362
+ return {
3363
+ content: [
3364
+ {
3365
+ type: "text",
3366
+ text: JSON.stringify(
3367
+ {
3368
+ id: input.id,
3369
+ contentType: result.contentType,
3370
+ sizeBytes: result.sizeBytes
3371
+ },
3372
+ null,
3373
+ 2
3374
+ )
3375
+ },
3376
+ { type: "text", text: result.buffer.toString("utf8") }
3377
+ ]
3378
+ };
3379
+ }
3284
3380
  return {
3285
3381
  content: [
3286
3382
  {
@@ -3290,8 +3386,7 @@ function createCapsuleMcpServer(opts) {
3290
3386
  id: input.id,
3291
3387
  contentType: result.contentType,
3292
3388
  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.`
3389
+ base64: result.buffer.toString("base64")
3295
3390
  },
3296
3391
  null,
3297
3392
  2
@@ -3300,57 +3395,8 @@ function createCapsuleMcpServer(opts) {
3300
3395
  ]
3301
3396
  };
3302
3397
  }
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
- );
3398
+ );
3399
+ }
3354
3400
  if (!readOnly) {
3355
3401
  registerTool(
3356
3402
  server2,
@@ -3418,14 +3464,14 @@ function createCapsuleMcpServer(opts) {
3418
3464
  );
3419
3465
  registerTool(
3420
3466
  server2,
3421
- "list_lostreasons",
3467
+ "list_lost_reasons",
3422
3468
  "List all configured opportunity-loss reasons (e.g. 'Poor Qualification', 'Lost to competitor', 'Price too high'). Returns each reason's id and name; the set is account-configured rather than a fixed enum, so call this to discover valid ids before referencing a lostReason in update_opportunity when closing a deal as lost. Useful for analysing closed-lost opportunities by reason.",
3423
3469
  listLostReasonsSchema,
3424
3470
  listLostReasons
3425
3471
  );
3426
3472
  registerTool(
3427
3473
  server2,
3428
- "list_activitytypes",
3474
+ "list_activity_types",
3429
3475
  "List all configured activity types (e.g. Call, Meeting, Email). These are the categories used when logging timeline entries via add_note. Returns each type's id and name. The set is account-configured rather than a fixed enum, so call this to discover valid values before referencing an activityType in entry creation.",
3430
3476
  listActivityTypesSchema,
3431
3477
  listActivityTypes
@@ -3453,10 +3499,10 @@ function createCapsuleMcpServer(opts) {
3453
3499
  );
3454
3500
  registerTool(
3455
3501
  server2,
3456
- "show_track",
3502
+ "get_track",
3457
3503
  "Fetch a single track instance by id. Returns the minimal Capsule projection: id, description, trackDateOn, direction, and the array of tasks attached to the track. Capsule's GET /tracks/{id} does NOT include a trackDefinition link, an entity reference, or a completion field \u2014 to find the entity a track is applied to, use list_entity_tracks (which lists track instances by their parent entity); to check completion, the track-tasks' own statuses are the proxy.",
3458
- showTrackSchema,
3459
- showTrack
3504
+ getTrackSchema,
3505
+ getTrack
3460
3506
  );
3461
3507
  registerTool(
3462
3508
  server2,
@@ -3489,7 +3535,7 @@ function createCapsuleMcpServer(opts) {
3489
3535
  registerTool(
3490
3536
  server2,
3491
3537
  "list_tags",
3492
- "List all tags available for a given entity type (parties, opportunities, or kases). Returns each tag's id, name, and any data-tag field schema. Tags are entity-specific \u2014 a party tag is not interchangeable with an opportunity tag. Use this to discover valid tag ids before calling add_tag, or to display the tag catalogue to the user when they ask 'what tags do we use?'",
3538
+ "List all tags available for a given entity type (parties, opportunities, or projects). Returns each tag's id, name, and any data-tag field schema. Tags are entity-specific \u2014 a party tag is not interchangeable with an opportunity tag. Use this to discover valid tag ids before calling add_tag, or to display the tag catalogue to the user when they ask 'what tags do we use?'",
3493
3539
  listTagsSchema,
3494
3540
  listTags
3495
3541
  );
@@ -3511,7 +3557,7 @@ function createCapsuleMcpServer(opts) {
3511
3557
  registerTool(
3512
3558
  server2,
3513
3559
  "delete_tag_definition",
3514
- "DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities / kases). Unlike remove_tag_by_id \u2014 which detaches a tag from ONE record and leaves the definition intact for others \u2014 this removes the definition itself, so the tag disappears from EVERY record that shared it. Use it to clean up stray / mistyped / test tag definitions polluting the tenant-global list. Requires confirm=true. Always read the affected tag first via list_tags and confirm with the user; if you only want to untag one record, use remove_tag_by_id instead. Irreversible (re-creating by name via add_tag mints a brand-new id). Idempotent on retry: `{deleted: true, alreadyDeleted: false, entity, tagId}` on a fresh delete, or `{deleted: true, alreadyDeleted: true, entity, tagId}` if the definition was already gone (Capsule's 404 is caught). Endpoint verified empirically (DELETE /<entity>/tags/{id} \u2192 204).",
3560
+ "DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities / projects). Unlike remove_tag_by_id \u2014 which detaches a tag from ONE record and leaves the definition intact for others \u2014 this removes the definition itself, so the tag disappears from EVERY record that shared it. Use it to clean up stray / mistyped / test tag definitions polluting the tenant-global list. Requires confirm=true. Always read the affected tag first via list_tags and confirm with the user; if you only want to untag one record, use remove_tag_by_id instead. Irreversible (re-creating by name via add_tag mints a brand-new id). Idempotent on retry: `{deleted: true, alreadyDeleted: false, entity, tagId}` on a fresh delete, or `{deleted: true, alreadyDeleted: true, entity, tagId}` if the definition was already gone (Capsule's 404 is caught). Endpoint verified empirically (DELETE /<entity>/tags/{id} \u2192 204).",
3515
3561
  deleteTagDefinitionSchema,
3516
3562
  deleteTagDefinition
3517
3563
  );
@@ -3560,6 +3606,22 @@ var transport = new StdioServerTransport();
3560
3606
  if (isReadOnly()) {
3561
3607
  console.error("[capsulemcp] read-only mode: write/delete tools are not registered");
3562
3608
  }
3609
+ function exitOnDisconnect() {
3610
+ let exiting = false;
3611
+ const die = () => {
3612
+ if (exiting) return;
3613
+ exiting = true;
3614
+ process.exit(0);
3615
+ };
3616
+ process.stdin.on("end", die);
3617
+ process.stdin.on("close", die);
3618
+ process.stdin.on("error", die);
3619
+ process.stdout.on("error", die);
3620
+ const orphanCheck = setInterval(() => {
3621
+ if (process.ppid === 1) die();
3622
+ }, 3e4);
3623
+ orphanCheck.unref?.();
3624
+ }
3563
3625
  try {
3564
3626
  await server.connect(transport);
3565
3627
  } catch (err) {
@@ -3567,3 +3629,4 @@ try {
3567
3629
  console.error(`[capsulemcp] Failed to start: ${message}`);
3568
3630
  process.exit(1);
3569
3631
  }
3632
+ exitOnDisconnect();