capsulemcp 1.7.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31,6 +31,22 @@ var chainHandlers = {
31
31
  "capsule.request": (ctx) => {
32
32
  ctx.capsuleCalls += 1;
33
33
  },
34
+ // A timed-out or connection-failed call is still an attempt that
35
+ // never reaches the `capsule.request` emit (it throws at the fetch
36
+ // stage). Count it here so `tool.chain.capsuleCalls` stays honest and
37
+ // a chain whose duration ballooned is explained by a visible failure.
38
+ "capsule.timeout": (ctx) => {
39
+ ctx.capsuleCalls += 1;
40
+ },
41
+ "capsule.error": (ctx) => {
42
+ ctx.capsuleCalls += 1;
43
+ },
44
+ // A request that exhausted its 429 retry is a real (doubly-attempted)
45
+ // outbound call that throws before `capsule.request` fires — count it
46
+ // so a chain whose latency ballooned on rate-limit backoff is explained.
47
+ "capsule.ratelimit": (ctx) => {
48
+ ctx.capsuleCalls += 1;
49
+ },
34
50
  // Cache-hit events feed the aggregate so the chain stat is right
35
51
  // even on tools whose Capsule calls all hit the cache.
36
52
  "cache.hit": (ctx) => {
@@ -166,6 +182,15 @@ var CapsuleApiError = class extends Error {
166
182
  }
167
183
  status;
168
184
  };
185
+ var CapsuleTimeoutError = class extends CapsuleApiError {
186
+ constructor() {
187
+ super(
188
+ 504,
189
+ `Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
190
+ );
191
+ this.name = "CapsuleTimeoutError";
192
+ }
193
+ };
169
194
  function getToken() {
170
195
  const token = process.env["CAPSULE_API_TOKEN"];
171
196
  if (!token) {
@@ -228,72 +253,74 @@ async function parseErrorBody(res) {
228
253
  }
229
254
  }
230
255
  var REQUEST_TIMEOUT_MS = 6e4;
231
- function withTimeout(options) {
232
- if (options && options.signal !== void 0) {
233
- return { options, cleanup: () => {
234
- } };
235
- }
236
- const controller = new AbortController();
237
- const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
238
- timer.unref();
239
- return {
240
- options: { ...options ?? {}, signal: controller.signal },
241
- cleanup: () => clearTimeout(timer)
242
- };
256
+ function isTimeoutAbort(err) {
257
+ return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
258
+ // "TimeoutError"; plain aborts (and older undici paths) surface
259
+ // as "AbortError" or carry "aborted" in the message.
260
+ (err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
243
261
  }
244
262
  async function mapAbort(p) {
245
263
  try {
246
264
  return await p;
247
265
  } catch (err) {
248
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
249
- throw new CapsuleApiError(
250
- 504,
251
- `Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
252
- );
266
+ if (isTimeoutAbort(err)) {
267
+ throw new CapsuleTimeoutError();
253
268
  }
254
269
  throw err;
255
270
  }
256
271
  }
257
272
  async function fetchWithTimeout(url, options) {
258
- const { options: opts, cleanup } = withTimeout(options);
273
+ const startedAt = Date.now();
259
274
  try {
260
- const res = await fetch(url, opts);
261
- return { res, cleanup };
275
+ return await fetch(url, {
276
+ ...options ?? {},
277
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
278
+ });
262
279
  } catch (err) {
263
- cleanup();
264
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
265
- throw new CapsuleApiError(
266
- 504,
267
- `Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
268
- );
280
+ const isAbort = isTimeoutAbort(err);
281
+ emitCapsuleFailure(
282
+ options?.method ?? "GET",
283
+ url,
284
+ Date.now() - startedAt,
285
+ isAbort ? "timeout" : "network",
286
+ isAbort ? void 0 : err
287
+ );
288
+ if (isAbort) {
289
+ throw new CapsuleTimeoutError();
269
290
  }
270
291
  throw err;
271
292
  }
272
293
  }
294
+ async function drainBody(res) {
295
+ try {
296
+ await res.body?.cancel();
297
+ } catch {
298
+ }
299
+ }
273
300
  async function doFetch(url, options) {
274
301
  const startedAt = Date.now();
275
302
  const method = options?.method ?? "GET";
276
303
  const first = await fetchWithTimeout(url, options);
277
- if (first.res.status === 429) {
278
- const delay = parseRateLimitDelay(first.res);
279
- first.cleanup();
304
+ if (first.status === 429) {
305
+ const delay = parseRateLimitDelay(first);
306
+ await drainBody(first);
280
307
  await new Promise((resolve) => setTimeout(resolve, delay));
281
308
  const retried = await fetchWithTimeout(url, options);
282
- if (retried.res.status === 429) {
283
- retried.cleanup();
309
+ if (retried.status === 429) {
310
+ await drainBody(retried);
311
+ emitCapsuleRateLimited(method, url, Date.now() - startedAt);
284
312
  throw new CapsuleApiError(
285
313
  429,
286
314
  "Rate limit exceeded after one retry. Please slow down your requests."
287
315
  );
288
316
  }
289
- return { ...retried, startedAt, method, url, retriedAfter429: true };
317
+ return { res: retried, startedAt, method, url, retriedAfter429: true };
290
318
  }
291
- return { ...first, startedAt, method, url, retriedAfter429: false };
319
+ return { res: first, startedAt, method, url, retriedAfter429: false };
292
320
  }
293
321
  async function consumeBody(start, body) {
294
322
  try {
295
- return await body();
296
- } finally {
323
+ const result = await body();
297
324
  emitCapsuleRequest(
298
325
  start.method,
299
326
  start.url,
@@ -301,15 +328,31 @@ async function consumeBody(start, body) {
301
328
  Date.now() - start.startedAt,
302
329
  start.retriedAfter429
303
330
  );
331
+ return result;
332
+ } catch (err) {
333
+ if (err instanceof CapsuleTimeoutError) {
334
+ emitCapsuleFailure(start.method, start.url, Date.now() - start.startedAt, "timeout");
335
+ } else {
336
+ emitCapsuleRequest(
337
+ start.method,
338
+ start.url,
339
+ start.res,
340
+ Date.now() - start.startedAt,
341
+ start.retriedAfter429
342
+ );
343
+ }
344
+ throw err;
304
345
  }
305
346
  }
306
- function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
307
- let path = "";
347
+ function redactedPath(url) {
308
348
  try {
309
- path = redactPath(new URL(url).pathname);
349
+ return redactPath(new URL(url).pathname);
310
350
  } catch {
311
- path = "?";
351
+ return "?";
312
352
  }
353
+ }
354
+ function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
355
+ const path = redactedPath(url);
313
356
  const lenHeader = res.headers.get("content-length");
314
357
  const responseBytes = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
315
358
  logEvent("capsule.request", {
@@ -321,6 +364,37 @@ function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
321
364
  ...retriedAfter429 ? { retriedAfter429: true } : {}
322
365
  });
323
366
  }
367
+ function emitCapsuleFailure(method, url, elapsedMs, reason, err) {
368
+ const path = redactedPath(url);
369
+ if (reason === "timeout") {
370
+ logEvent(
371
+ "capsule.timeout",
372
+ { method, path, elapsedMs, timeoutMs: REQUEST_TIMEOUT_MS },
373
+ { force: true }
374
+ );
375
+ return;
376
+ }
377
+ const code = extractErrorCode(err);
378
+ logEvent(
379
+ "capsule.error",
380
+ { method, path, elapsedMs, ...code ? { code } : {} },
381
+ { force: true }
382
+ );
383
+ }
384
+ function emitCapsuleRateLimited(method, url, elapsedMs) {
385
+ logEvent(
386
+ "capsule.ratelimit",
387
+ { method, path: redactedPath(url), elapsedMs, status: 429 },
388
+ { force: true }
389
+ );
390
+ }
391
+ function extractErrorCode(err) {
392
+ const e = err;
393
+ const code = e?.cause?.code ?? e?.code;
394
+ if (typeof code === "string") return code;
395
+ if (typeof e?.name === "string" && e.name !== "Error") return e.name;
396
+ return void 0;
397
+ }
324
398
  async function throwForStatus(res) {
325
399
  if (res.status === 401) {
326
400
  const detail = await parseErrorBody(res);
@@ -352,15 +426,19 @@ async function capsuleGet(path, params) {
352
426
  const token = getToken();
353
427
  const url = buildUrl(path, params);
354
428
  const start = await doFetch(url, { headers: baseHeaders(token) });
355
- try {
356
- return await consumeBody(start, async () => {
357
- const data = await handleResponse(start.res);
358
- const nextPage = parseNextPage(start.res.headers.get("Link"));
359
- return { data, nextPage };
360
- });
361
- } finally {
362
- start.cleanup();
363
- }
429
+ return consumeBody(start, async () => {
430
+ const data = await handleResponse(start.res);
431
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
432
+ return { data, nextPage };
433
+ });
434
+ }
435
+ async function capsuleGetList(path, params) {
436
+ const { data, nextPage } = await capsuleGet(path, params);
437
+ return { ...data, nextPage };
438
+ }
439
+ async function capsuleGetCachedList(path, params) {
440
+ const { data, nextPage } = await capsuleGetCached(path, params);
441
+ return { ...data, nextPage };
364
442
  }
365
443
  async function capsuleGetCached(path, params) {
366
444
  if (cacheDisabled()) return capsuleGet(path, params);
@@ -399,11 +477,7 @@ async function capsulePost(path, body) {
399
477
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
400
478
  body: JSON.stringify(body)
401
479
  });
402
- try {
403
- return await consumeBody(start, () => handleResponse(start.res));
404
- } finally {
405
- start.cleanup();
406
- }
480
+ return consumeBody(start, () => handleResponse(start.res));
407
481
  }
408
482
  async function capsulePostNoContent(path) {
409
483
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -413,15 +487,11 @@ async function capsulePostNoContent(path) {
413
487
  method: "POST",
414
488
  headers: baseHeaders(token)
415
489
  });
416
- try {
417
- await consumeBody(start, async () => {
418
- if (start.res.status === 204) return;
419
- await throwForStatus(start.res);
420
- await mapAbort(start.res.text());
421
- });
422
- } finally {
423
- start.cleanup();
424
- }
490
+ await consumeBody(start, async () => {
491
+ if (start.res.status === 204) return;
492
+ await throwForStatus(start.res);
493
+ await mapAbort(start.res.text());
494
+ });
425
495
  }
426
496
  async function capsuleSearch(path, body, params) {
427
497
  const token = getToken();
@@ -431,15 +501,11 @@ async function capsuleSearch(path, body, params) {
431
501
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
432
502
  body: JSON.stringify(body)
433
503
  });
434
- try {
435
- return await consumeBody(start, async () => {
436
- const data = await handleResponse(start.res);
437
- const nextPage = parseNextPage(start.res.headers.get("Link"));
438
- return { data, nextPage };
439
- });
440
- } finally {
441
- start.cleanup();
442
- }
504
+ return consumeBody(start, async () => {
505
+ const data = await handleResponse(start.res);
506
+ const nextPage = parseNextPage(start.res.headers.get("Link"));
507
+ return { data, nextPage };
508
+ });
443
509
  }
444
510
  async function capsulePut(path, body) {
445
511
  if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
@@ -450,68 +516,60 @@ async function capsulePut(path, body) {
450
516
  headers: { ...baseHeaders(token), "Content-Type": "application/json" },
451
517
  body: JSON.stringify(body)
452
518
  });
453
- try {
454
- return await consumeBody(start, () => handleResponse(start.res));
455
- } finally {
456
- start.cleanup();
457
- }
519
+ return consumeBody(start, () => handleResponse(start.res));
458
520
  }
459
521
  async function capsuleGetBinary(path, maxBytes) {
460
522
  const token = getToken();
461
523
  const url = buildUrl(path);
462
524
  const start = await doFetch(url, { headers: baseHeaders(token) });
463
- try {
464
- return await consumeBody(start, async () => {
465
- const res = start.res;
466
- await throwForStatus(res);
467
- const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
468
- const declared = res.headers.get("Content-Length");
469
- const declaredBytes = declared ? Number(declared) : NaN;
470
- if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
471
- if (res.body) await res.body.cancel().catch(() => {
472
- });
525
+ return consumeBody(start, async () => {
526
+ const res = start.res;
527
+ await throwForStatus(res);
528
+ const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
529
+ const declared = res.headers.get("Content-Length");
530
+ const declaredBytes = declared ? Number(declared) : NaN;
531
+ if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
532
+ if (res.body) await res.body.cancel().catch(() => {
533
+ });
534
+ return {
535
+ contentType,
536
+ buffer: Buffer.alloc(0),
537
+ truncated: true,
538
+ sizeBytes: declaredBytes
539
+ };
540
+ }
541
+ if (maxBytes !== void 0 && res.body) {
542
+ const reader = res.body.getReader();
543
+ const chunks = [];
544
+ let total = 0;
545
+ let truncated = false;
546
+ while (true) {
547
+ const { done, value } = await mapAbort(reader.read());
548
+ if (done) break;
549
+ total += value.byteLength;
550
+ if (total > maxBytes) {
551
+ truncated = true;
552
+ await reader.cancel().catch(() => {
553
+ });
554
+ break;
555
+ }
556
+ chunks.push(value);
557
+ }
558
+ if (truncated) {
473
559
  return {
474
560
  contentType,
475
561
  buffer: Buffer.alloc(0),
476
562
  truncated: true,
477
- sizeBytes: declaredBytes
563
+ sizeBytes: total
478
564
  };
479
565
  }
480
- if (maxBytes !== void 0 && res.body) {
481
- const reader = res.body.getReader();
482
- const chunks = [];
483
- let total = 0;
484
- let truncated = false;
485
- while (true) {
486
- const { done, value } = await mapAbort(reader.read());
487
- if (done) break;
488
- total += value.byteLength;
489
- if (total > maxBytes) {
490
- truncated = true;
491
- await reader.cancel().catch(() => {
492
- });
493
- break;
494
- }
495
- chunks.push(value);
496
- }
497
- if (truncated) {
498
- return {
499
- contentType,
500
- buffer: Buffer.alloc(0),
501
- truncated: true,
502
- sizeBytes: total
503
- };
504
- }
505
- const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
506
- return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
507
- }
508
- const arrayBuffer = await mapAbort(res.arrayBuffer());
509
- const buffer = Buffer.from(arrayBuffer);
510
- return { contentType, buffer, sizeBytes: buffer.length };
511
- });
512
- } finally {
513
- start.cleanup();
514
- }
566
+ const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
567
+ return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
568
+ }
569
+ const arrayBuffer = await mapAbort(res.arrayBuffer());
570
+ const buffer = Buffer.from(arrayBuffer);
571
+ return { contentType, buffer, sizeBytes: buffer.length };
572
+ });
515
573
  }
516
574
  async function capsulePostBinary(path, body, contentType, filename) {
517
575
  if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
@@ -527,11 +585,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
527
585
  },
528
586
  body
529
587
  });
530
- try {
531
- return await consumeBody(start, () => handleResponse(start.res));
532
- } finally {
533
- start.cleanup();
534
- }
588
+ return consumeBody(start, () => handleResponse(start.res));
535
589
  }
536
590
  async function capsuleDelete(path) {
537
591
  if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
@@ -541,15 +595,11 @@ async function capsuleDelete(path) {
541
595
  method: "DELETE",
542
596
  headers: baseHeaders(token)
543
597
  });
544
- try {
545
- await consumeBody(start, async () => {
546
- if (start.res.status === 204) return;
547
- await throwForStatus(start.res);
548
- await mapAbort(start.res.text());
549
- });
550
- } finally {
551
- start.cleanup();
552
- }
598
+ await consumeBody(start, async () => {
599
+ if (start.res.status === 204) return;
600
+ await throwForStatus(start.res);
601
+ await mapAbort(start.res.text());
602
+ });
553
603
  }
554
604
 
555
605
  // src/server.ts
@@ -585,6 +635,44 @@ var ICONS = [
585
635
  }
586
636
  ];
587
637
 
638
+ // src/server/tier.ts
639
+ var CORE_TOOLS = /* @__PURE__ */ new Set([
640
+ // Parties
641
+ "search_parties",
642
+ "filter_parties",
643
+ "get_party",
644
+ "create_party",
645
+ "update_party",
646
+ "list_party_entries",
647
+ // Opportunities
648
+ "search_opportunities",
649
+ "filter_opportunities",
650
+ "get_opportunity",
651
+ "create_opportunity",
652
+ "update_opportunity",
653
+ // Projects
654
+ "filter_projects",
655
+ "list_projects",
656
+ "get_project",
657
+ "create_project",
658
+ "update_project",
659
+ // Tasks
660
+ "list_tasks",
661
+ "get_task",
662
+ "create_task",
663
+ "update_task",
664
+ "complete_task",
665
+ // Timeline + tags + identity
666
+ "add_note",
667
+ "list_tags",
668
+ "add_tag",
669
+ "get_current_user"
670
+ ]);
671
+ function shouldRegister(name) {
672
+ if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
673
+ return CORE_TOOLS.has(name);
674
+ }
675
+
588
676
  // src/tasks/store.ts
589
677
  import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
590
678
  import {
@@ -639,6 +727,22 @@ var abortControllers = /* @__PURE__ */ new Map();
639
727
  function registerAbortController(taskId, controller) {
640
728
  abortControllers.set(taskId, controller);
641
729
  }
730
+ var evictionTimers = /* @__PURE__ */ new Map();
731
+ var taskTtls = /* @__PURE__ */ new Map();
732
+ function scheduleEviction(taskId, clientId, ttlMs) {
733
+ const existing = evictionTimers.get(taskId);
734
+ if (existing) clearTimeout(existing);
735
+ taskTtls.set(taskId, ttlMs);
736
+ const timer = setTimeout(() => {
737
+ owners.delete(taskId);
738
+ abortControllers.delete(taskId);
739
+ evictionTimers.delete(taskId);
740
+ taskTtls.delete(taskId);
741
+ logEvent("task.evicted", { taskId, clientId, reason: "ttl" });
742
+ }, ttlMs);
743
+ timer.unref?.();
744
+ evictionTimers.set(taskId, timer);
745
+ }
642
746
  function countPerClient(clientId) {
643
747
  let n = 0;
644
748
  for (const owner of owners.values()) {
@@ -692,12 +796,7 @@ function createScopedTaskStore(clientId) {
692
796
  sessionId
693
797
  );
694
798
  owners.set(task.taskId, clientId);
695
- const timer = setTimeout(() => {
696
- owners.delete(task.taskId);
697
- abortControllers.delete(task.taskId);
698
- logEvent("task.evicted", { taskId: task.taskId, clientId, reason: "ttl" });
699
- }, clampedTtl);
700
- timer.unref?.();
799
+ scheduleEviction(task.taskId, clientId, clampedTtl);
701
800
  logEvent("task.created", {
702
801
  taskId: task.taskId,
703
802
  clientId,
@@ -716,6 +815,7 @@ function createScopedTaskStore(clientId) {
716
815
  }
717
816
  logEvent("task.transition", { taskId, clientId, status });
718
817
  await global.storeTaskResult(taskId, status, result, sessionId);
818
+ scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
719
819
  },
720
820
  async getTaskResult(taskId, sessionId) {
721
821
  if (owners.get(taskId) !== clientId) {
@@ -735,6 +835,7 @@ function createScopedTaskStore(clientId) {
735
835
  }
736
836
  if (status === "completed" || status === "failed" || status === "cancelled") {
737
837
  abortControllers.delete(taskId);
838
+ scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
738
839
  }
739
840
  },
740
841
  async listTasks(cursor, sessionId) {
@@ -780,27 +881,25 @@ function wrapAsText(result) {
780
881
  };
781
882
  }
782
883
  function registerTool(server2, name, description, schema, handler) {
884
+ if (!shouldRegister(name)) return;
783
885
  const registerWithSchema = server2.registerTool.bind(server2);
784
886
  const annotations = inferAnnotations(name);
785
- registerWithSchema(
786
- name,
787
- { description, inputSchema: schema, ...annotations ? { annotations } : {} },
788
- async (input) => {
789
- const startedAt = Date.now();
790
- const argFields = argFieldNames(input);
791
- const clientId = getRequestContext()?.clientId;
792
- try {
793
- const result = await handler(input);
794
- emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
795
- return wrapAsText(result);
796
- } catch (err) {
797
- emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
798
- throw err;
799
- }
887
+ registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
888
+ const startedAt = Date.now();
889
+ const argFields = argFieldNames(input);
890
+ const clientId = getRequestContext()?.clientId;
891
+ try {
892
+ const result = await handler(input);
893
+ emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
894
+ return wrapAsText(result);
895
+ } catch (err) {
896
+ emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
897
+ throw err;
800
898
  }
801
- );
899
+ });
802
900
  }
803
901
  function registerToolTask(server2, name, description, schema, handler) {
902
+ if (!shouldRegister(name)) return;
804
903
  const registerWithSchema = server2.experimental.tasks.registerToolTask.bind(
805
904
  server2.experimental.tasks
806
905
  );
@@ -811,7 +910,7 @@ function registerToolTask(server2, name, description, schema, handler) {
811
910
  description,
812
911
  inputSchema: schema,
813
912
  execution: { taskSupport: "optional" },
814
- ...annotations ? { annotations } : {}
913
+ annotations
815
914
  },
816
915
  {
817
916
  createTask: async (input, extra) => {
@@ -871,7 +970,7 @@ function registerToolTask(server2, name, description, schema, handler) {
871
970
  }
872
971
 
873
972
  // src/tools/parties.ts
874
- import { z as z6 } from "zod";
973
+ import { z as z7 } from "zod";
875
974
 
876
975
  // src/tools/body-helpers.ts
877
976
  function setRef(body, key, id) {
@@ -881,9 +980,22 @@ function setNullableRef(body, key, id) {
881
980
  if (id === null) body[key] = null;
882
981
  else if (id !== void 0) body[key] = { id };
883
982
  }
983
+ function assertSingleParentRef(toolName, refs, opts = {}) {
984
+ const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
985
+ (v) => typeof v === "number"
986
+ ).length;
987
+ if (opts.required && set !== 1) {
988
+ throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
989
+ }
990
+ if (set > 1) {
991
+ throw new Error(
992
+ `${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
993
+ );
994
+ }
995
+ }
884
996
 
885
997
  // src/tools/define-batch.ts
886
- import { z } from "zod";
998
+ import { z as z2 } from "zod";
887
999
 
888
1000
  // src/capsule/batch.ts
889
1001
  function chunk(arr, size) {
@@ -902,37 +1014,43 @@ function getBatchConcurrency() {
902
1014
  MAX_CONCURRENCY
903
1015
  );
904
1016
  }
905
- async function batchExecute(tool, items, action, options = {}) {
906
- const concurrency = getBatchConcurrency();
1017
+ async function mapWithConcurrency(items, limit, fn) {
907
1018
  const results = new Array(items.length);
908
- const startedAt = Date.now();
909
- const signal = options.signal;
910
1019
  let cursor = 0;
911
1020
  async function worker() {
912
1021
  while (true) {
913
1022
  const i = cursor;
914
1023
  cursor += 1;
915
1024
  if (i >= items.length) return;
916
- if (signal?.aborted) {
917
- results[i] = {
918
- ok: false,
919
- error: { message: "cancelled by tasks/cancel" }
920
- };
921
- continue;
922
- }
923
- try {
924
- const result = await action(items[i], i);
925
- results[i] = { ok: true, result };
926
- } catch (err) {
927
- results[i] = { ok: false, error: extractError(err) };
928
- }
1025
+ results[i] = await fn(items[i], i);
929
1026
  }
930
1027
  }
931
1028
  const workers = [];
932
- for (let w = 0; w < Math.min(concurrency, items.length); w++) {
1029
+ for (let w = 0; w < Math.min(limit, items.length); w++) {
933
1030
  workers.push(worker());
934
1031
  }
935
1032
  await Promise.all(workers);
1033
+ return results;
1034
+ }
1035
+ async function batchExecute(tool, items, action, options = {}) {
1036
+ const concurrency = getBatchConcurrency();
1037
+ const startedAt = Date.now();
1038
+ const signal = options.signal;
1039
+ const results = await mapWithConcurrency(
1040
+ items,
1041
+ concurrency,
1042
+ async (item, i) => {
1043
+ if (signal?.aborted) {
1044
+ return { ok: false, error: { message: "cancelled by tasks/cancel" } };
1045
+ }
1046
+ try {
1047
+ const result = await action(item, i);
1048
+ return { ok: true, result };
1049
+ } catch (err) {
1050
+ return { ok: false, error: extractError(err) };
1051
+ }
1052
+ }
1053
+ );
936
1054
  const succeeded = results.filter((r) => r.ok).length;
937
1055
  const failed = results.length - succeeded;
938
1056
  const summary = { total: results.length, succeeded, failed };
@@ -977,10 +1095,52 @@ function topFailureReasons(results, n) {
977
1095
  return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
978
1096
  }
979
1097
 
1098
+ // src/tools/strip-descriptions.ts
1099
+ import { z } from "zod";
1100
+ function cloneWithDef(node, patch) {
1101
+ const def = node.def;
1102
+ return node.clone({ ...def, ...patch });
1103
+ }
1104
+ function stripDescriptions(schema) {
1105
+ let node = schema;
1106
+ if (node instanceof z.ZodObject) {
1107
+ const shape = node.def.shape;
1108
+ const next = {};
1109
+ let changed = false;
1110
+ for (const [key, child] of Object.entries(shape)) {
1111
+ next[key] = stripDescriptions(child);
1112
+ if (next[key] !== child) changed = true;
1113
+ }
1114
+ if (changed) node = cloneWithDef(node, { shape: next });
1115
+ } else if (node instanceof z.ZodArray) {
1116
+ const element = stripDescriptions(node.def.element);
1117
+ if (element !== node.def.element) node = cloneWithDef(node, { element });
1118
+ } else if (node instanceof z.ZodOptional || node instanceof z.ZodNullable || node instanceof z.ZodDefault || node instanceof z.ZodReadonly) {
1119
+ const innerType = stripDescriptions(node.def.innerType);
1120
+ if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
1121
+ } else if (node instanceof z.ZodUnion) {
1122
+ const options = node.def.options.map(stripDescriptions);
1123
+ if (options.some((o, i) => o !== node.def.options[i])) {
1124
+ node = cloneWithDef(node, { options });
1125
+ }
1126
+ } else if (node instanceof z.ZodPipe) {
1127
+ const inSchema = stripDescriptions(node.def.in);
1128
+ const outSchema = stripDescriptions(node.def.out);
1129
+ if (inSchema !== node.def.in || outSchema !== node.def.out) {
1130
+ node = cloneWithDef(node, { in: inSchema, out: outSchema });
1131
+ }
1132
+ }
1133
+ if (node.description !== void 0) {
1134
+ node = node.meta({ description: void 0 });
1135
+ }
1136
+ return node;
1137
+ }
1138
+
980
1139
  // src/tools/define-batch.ts
981
1140
  function defineBatch(args) {
982
- const schema = z.object({
983
- items: z.array(args.itemSchema).min(1).max(50).describe(args.itemDescription)
1141
+ const itemSchema = stripDescriptions(args.itemSchema);
1142
+ const schema = z2.object({
1143
+ items: z2.array(itemSchema).min(1).max(50).describe(args.itemDescription)
984
1144
  });
985
1145
  async function handler(input, opts = {}) {
986
1146
  return batchExecute(args.toolName, input.items, args.itemHandler, opts);
@@ -993,22 +1153,30 @@ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'"
993
1153
  var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
994
1154
 
995
1155
  // src/tools/define-delete.ts
996
- import { z as z4 } from "zod";
1156
+ import { z as z5 } from "zod";
997
1157
 
998
1158
  // src/tools/confirm-flag.ts
999
- import { z as z2 } from "zod";
1159
+ import { z as z3 } from "zod";
1000
1160
  var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
1001
1161
  function confirmFlag() {
1002
- return z2.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1162
+ return z3.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
1003
1163
  }
1004
1164
 
1005
1165
  // src/tools/shared-schemas.ts
1006
- import { z as z3 } from "zod";
1007
- var positiveId = z3.preprocess((input) => {
1166
+ import { z as z4 } from "zod";
1167
+ var positiveId = z4.preprocess((input) => {
1008
1168
  if (typeof input !== "string") return input;
1009
1169
  const trimmed = input.trim();
1010
1170
  return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
1011
- }, z3.number().int().positive());
1171
+ }, z4.number().int().positive());
1172
+ var paginationFields = {
1173
+ page: z4.number().int().positive().optional().default(1),
1174
+ perPage: z4.number().int().min(1).max(100).optional().default(25)
1175
+ };
1176
+ var paginationFieldsNoDefaults = {
1177
+ page: z4.number().int().positive().optional(),
1178
+ perPage: z4.number().int().min(1).max(100).optional()
1179
+ };
1012
1180
 
1013
1181
  // src/capsule/idempotent.ts
1014
1182
  var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
@@ -1035,7 +1203,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
1035
1203
  // src/tools/define-delete.ts
1036
1204
  function defineDelete(args) {
1037
1205
  const { toolName, pathPrefix, confirmHint, idDescription } = args;
1038
- const schema = z4.object({
1206
+ const schema = z5.object({
1039
1207
  id: idDescription ? positiveId.describe(idDescription) : positiveId,
1040
1208
  confirm: confirmFlag().describe(confirmHint)
1041
1209
  });
@@ -1078,16 +1246,17 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
1078
1246
  (chunkIds) => capsuleGet(`${base}/${chunkIds.join(",")}`, params)
1079
1247
  )
1080
1248
  );
1081
- return { [responseKey]: responses.flatMap((r) => r.data[responseKey] ?? []) };
1249
+ const merged = responses.flatMap((r) => r.data[responseKey] ?? []);
1250
+ return { ...responses[0]?.data ?? {}, [responseKey]: merged };
1082
1251
  }
1083
1252
 
1084
1253
  // src/tools/custom-field-helpers.ts
1085
- import { z as z5 } from "zod";
1086
- var CustomFieldWriteSchema = z5.object({
1254
+ import { z as z6 } from "zod";
1255
+ var CustomFieldWriteSchema = z6.object({
1087
1256
  definitionId: positiveId.describe(
1088
1257
  "The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
1089
1258
  ),
1090
- value: z5.union([z5.string(), z5.number(), z5.boolean(), z5.null()]).describe(
1259
+ value: z6.union([z6.string(), z6.number(), z6.boolean(), z6.null()]).describe(
1091
1260
  "The new value. String for TEXT / DATE / LIST / LARGE_TEXT / LINK fields, number for NUMBER fields, boolean for BOOLEAN fields. Clearing: pass null for TEXT / NUMBER / DATE / LIST (Capsule removes the row). BOOLEAN does NOT accept null (Capsule returns 422 'invalid type for field'); use `value: false` instead. Note BOOLEAN fields are observably **two-state**: a row exists with `value: true`, or no row exists. Setting `value: false` removes the row entirely \u2014 readers should treat absent BOOLEAN rows as equivalent to false. Tri-state BOOLEAN semantics (true / false / unknown) are not achievable through Capsule's API. Audit-log noise: sending value=null on a field that's already empty/cleared is accepted by Capsule but still bumps the parent entity's `updatedAt`. Read the current value via embed='fields' first if `updatedAt` is being used as a 'last meaningful change' signal. NUMBER quirks: Capsule stores numerics correctly but the read-back via embed=fields returns them as STRINGS (e.g. value=3 reads as '3'); callers comparing values must coerce. TEXT quirks: value='' has the same observable effect as value=null (row removed); empty-string and never-set are indistinguishable."
1092
1261
  )
1093
1262
  });
@@ -1103,24 +1272,24 @@ function mapFieldsForBody(fields) {
1103
1272
  }
1104
1273
 
1105
1274
  // src/tools/parties.ts
1106
- var EmailAddressSchema = z6.object({
1107
- address: z6.string().email(),
1108
- type: z6.string().optional()
1275
+ var EmailAddressSchema = z7.object({
1276
+ address: z7.string().email(),
1277
+ type: z7.string().optional()
1109
1278
  });
1110
- var PhoneNumberSchema = z6.object({
1279
+ var PhoneNumberSchema = z7.object({
1111
1280
  // Capsule rejects empty strings with `phoneNumber.number: number is
1112
1281
  // required`. Enforce at the schema layer to catch typos pre-call,
1113
1282
  // matching how EmailAddressSchema's address field behaves.
1114
- number: z6.string().min(1),
1115
- type: z6.string().optional()
1283
+ number: z7.string().min(1),
1284
+ type: z7.string().optional()
1116
1285
  });
1117
1286
  var CountryDescription = "Country name. Capsule validates this against a small canonical-English-name dictionary; inputs not in the dictionary are REJECTED with 422 'address.country: unknown country' (NOT silently passed through or normalised). Probed examples \u2014 accepted: `United States`, `United Kingdom`, `Czechia`, `Germany`. Aliased: `USA \u2192 United States`. Rejected: `United States of America`, `Czech Republic` (use `Czechia`), `UK`/`Britain` (use `United Kingdom`), `Deutschland` (use `Germany`). Empty string is accepted and stored as `null` \u2014 a de-facto 'clear' shape. To discover an accepted name, read an existing party that already has the country set.";
1118
- var AddressSchema = z6.object({
1119
- street: z6.string().optional(),
1120
- city: z6.string().optional(),
1121
- state: z6.string().optional(),
1122
- country: z6.string().optional().describe(CountryDescription),
1123
- zip: z6.string().optional()
1287
+ var AddressSchema = z7.object({
1288
+ street: z7.string().optional(),
1289
+ city: z7.string().optional(),
1290
+ state: z7.string().optional(),
1291
+ country: z7.string().optional().describe(CountryDescription),
1292
+ zip: z7.string().optional()
1124
1293
  });
1125
1294
  function validateWebsiteAddress(data, ctx) {
1126
1295
  const isUrlService = data.service === void 0 || data.service === "URL";
@@ -1143,7 +1312,7 @@ function validateWebsiteAddress(data, ctx) {
1143
1312
  });
1144
1313
  }
1145
1314
  }
1146
- var WebsiteServiceEnum = z6.enum([
1315
+ var WebsiteServiceEnum = z7.enum([
1147
1316
  "URL",
1148
1317
  "SKYPE",
1149
1318
  "TWITTER",
@@ -1162,33 +1331,31 @@ var WebsiteServiceEnum = z6.enum([
1162
1331
  "BLUESKY",
1163
1332
  "SNAPCHAT"
1164
1333
  ]);
1165
- var WebsiteSchema = z6.object({
1166
- address: z6.string().min(1).describe(
1334
+ var WebsiteSchema = z7.object({
1335
+ address: z7.string().min(1).describe(
1167
1336
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services like 'TWITTER', 'INSTAGRAM'. Capsule names this field `address` regardless of service type."
1168
1337
  ),
1169
1338
  service: WebsiteServiceEnum.optional().describe(
1170
1339
  "Service type. One of: URL, SKYPE, TWITTER, LINKED_IN, FACEBOOK, XING, FEED, GOOGLE_PLUS, FLICKR, GITHUB, YOUTUBE, INSTAGRAM, PINTEREST, TIKTOK, THREADS, BLUESKY, SNAPCHAT. Defaults to 'URL' if omitted."
1171
1340
  )
1172
1341
  }).superRefine(validateWebsiteAddress);
1173
- var searchPartiesSchema = z6.object({
1174
- q: z6.string().optional().describe("Free-text search query"),
1175
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1176
- page: z6.number().int().positive().optional().default(1),
1177
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1342
+ var searchPartiesSchema = z7.object({
1343
+ q: z7.string().optional().describe("Free-text search query"),
1344
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1345
+ ...paginationFields
1178
1346
  });
1179
1347
  async function searchParties(input) {
1180
1348
  const path = input.q ? "/parties/search" : "/parties";
1181
- const { data, nextPage } = await capsuleGet(path, {
1349
+ return capsuleGetList(path, {
1182
1350
  q: input.q,
1183
1351
  embed: input.embed,
1184
1352
  page: input.page,
1185
1353
  perPage: input.perPage
1186
1354
  });
1187
- return { ...data, nextPage };
1188
1355
  }
1189
- var getPartySchema = z6.object({
1356
+ var getPartySchema = z7.object({
1190
1357
  id: positiveId.describe("Party ID"),
1191
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1358
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1192
1359
  });
1193
1360
  async function getParty(input) {
1194
1361
  const { data } = await capsuleGet(`/parties/${input.id}`, {
@@ -1196,51 +1363,47 @@ async function getParty(input) {
1196
1363
  });
1197
1364
  return data;
1198
1365
  }
1199
- var getPartiesSchema = z6.object({
1200
- ids: z6.array(positiveId).min(1).max(50).describe(
1366
+ var getPartiesSchema = z7.object({
1367
+ ids: z7.array(positiveId).min(1).max(50).describe(
1201
1368
  "Array of party IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel. Result shape is identical regardless of input size."
1202
1369
  ),
1203
- embed: z6.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1370
+ embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1204
1371
  });
1205
1372
  async function getParties(input) {
1206
1373
  return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
1207
1374
  }
1208
- var listPartyOpportunitiesSchema = z6.object({
1375
+ var listPartyOpportunitiesSchema = z7.object({
1209
1376
  partyId: positiveId,
1210
- page: z6.number().int().positive().optional().default(1),
1211
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1377
+ ...paginationFields
1212
1378
  });
1213
1379
  async function listPartyOpportunities(input) {
1214
- const { data, nextPage } = await capsuleGet(
1215
- `/parties/${input.partyId}/opportunities`,
1216
- { page: input.page, perPage: input.perPage }
1217
- );
1218
- return { ...data, nextPage };
1380
+ return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
1381
+ page: input.page,
1382
+ perPage: input.perPage
1383
+ });
1219
1384
  }
1220
- var listPartyProjectsSchema = z6.object({
1385
+ var listPartyProjectsSchema = z7.object({
1221
1386
  partyId: positiveId,
1222
- page: z6.number().int().positive().optional().default(1),
1223
- perPage: z6.number().int().min(1).max(100).optional().default(25)
1387
+ ...paginationFields
1224
1388
  });
1225
1389
  async function listPartyProjects(input) {
1226
- const { data, nextPage } = await capsuleGet(
1227
- `/parties/${input.partyId}/kases`,
1228
- { page: input.page, perPage: input.perPage }
1229
- );
1230
- return { ...data, nextPage };
1390
+ return capsuleGetList(`/parties/${input.partyId}/kases`, {
1391
+ page: input.page,
1392
+ perPage: input.perPage
1393
+ });
1231
1394
  }
1232
1395
  var PartyWriteBaseSchema = {
1233
- about: z6.string().optional(),
1234
- emailAddresses: z6.array(EmailAddressSchema).optional().describe(
1396
+ about: z7.string().optional(),
1397
+ emailAddresses: z7.array(EmailAddressSchema).optional().describe(
1235
1398
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_email_address and remove_party_email_address_by_id. Passing `[]` here is a silent no-op (does not clear the list and does not advance updatedAt)."
1236
1399
  ),
1237
- phoneNumbers: z6.array(PhoneNumberSchema).optional().describe(
1400
+ phoneNumbers: z7.array(PhoneNumberSchema).optional().describe(
1238
1401
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_phone_number and remove_party_phone_number_by_id."
1239
1402
  ),
1240
- addresses: z6.array(AddressSchema).optional().describe(
1403
+ addresses: z7.array(AddressSchema).optional().describe(
1241
1404
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_address and remove_party_address_by_id. The `country` field is mapped through Capsule's country dictionary \u2014 see `add_party_address.country` for the dictionary edges (small canonical-English-name list; inputs not in the dictionary are REJECTED with 422, not silently dropped)."
1242
1405
  ),
1243
- websites: z6.array(WebsiteSchema).optional().describe(
1406
+ websites: z7.array(WebsiteSchema).optional().describe(
1244
1407
  "APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
1245
1408
  ),
1246
1409
  ownerId: positiveId.nullable().optional().describe(
@@ -1250,16 +1413,16 @@ var PartyWriteBaseSchema = {
1250
1413
  "Assign to team ID (discover via list_teams). Pass a team ID to set, or `null` to unassign. Capsule enforces the owner\u2208team membership constraint \u2014 passing a team the current owner doesn't belong to returns 422 'owner is not a member of the team'. Combine `ownerId: null` + `teamId: <T>` in one call to transfer a party to team-ownership with no specific user (verified empirically in v1.6.4 wire-trace; the membership rule doesn't fire when owner is null)."
1251
1414
  )
1252
1415
  };
1253
- var createPartySchema = z6.object({
1254
- type: z6.enum(["person", "organisation"]),
1416
+ var createPartySchema = z7.object({
1417
+ type: z7.enum(["person", "organisation"]),
1255
1418
  // person
1256
- firstName: z6.string().optional(),
1257
- lastName: z6.string().optional(),
1258
- title: z6.string().optional(),
1259
- jobTitle: z6.string().optional(),
1419
+ firstName: z7.string().optional(),
1420
+ lastName: z7.string().optional(),
1421
+ title: z7.string().optional(),
1422
+ jobTitle: z7.string().optional(),
1260
1423
  organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
1261
1424
  // organisation
1262
- name: z6.string().optional(),
1425
+ name: z7.string().optional(),
1263
1426
  ...PartyWriteBaseSchema,
1264
1427
  ownerId: positiveId.optional().describe(
1265
1428
  "Assign to user ID. Defaults to the API-token owner when omitted. To create a team-owned party with no specific user, first create the party, then call update_party with `ownerId: null` and `teamId`."
@@ -1267,7 +1430,7 @@ var createPartySchema = z6.object({
1267
1430
  teamId: positiveId.optional().describe(
1268
1431
  "Assign to team ID (discover via list_teams). Omit to leave team unset on create. To clear an existing team or create a team-owned party with no specific owner, use update_party after creation."
1269
1432
  ),
1270
- fields: z6.array(CustomFieldWriteSchema).optional().describe(
1433
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(
1271
1434
  fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
1272
1435
  )
1273
1436
  });
@@ -1281,17 +1444,17 @@ async function createParty(input) {
1281
1444
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1282
1445
  return capsulePost("/parties", { party: body });
1283
1446
  }
1284
- var updatePartySchema = z6.object({
1447
+ var updatePartySchema = z7.object({
1285
1448
  id: positiveId,
1286
- firstName: z6.string().optional(),
1287
- lastName: z6.string().optional(),
1288
- title: z6.string().optional(),
1289
- jobTitle: z6.string().optional(),
1290
- name: z6.string().optional(),
1449
+ firstName: z7.string().optional(),
1450
+ lastName: z7.string().optional(),
1451
+ title: z7.string().optional(),
1452
+ jobTitle: z7.string().optional(),
1453
+ name: z7.string().optional(),
1291
1454
  organisationId: positiveId.nullable().optional().describe(
1292
1455
  "For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
1293
1456
  ),
1294
- fields: z6.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1457
+ fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
1295
1458
  ...PartyWriteBaseSchema
1296
1459
  });
1297
1460
  async function updateParty(input) {
@@ -1322,10 +1485,42 @@ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
1322
1485
  pathPrefix: "/parties",
1323
1486
  confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
1324
1487
  });
1325
- var addPartyEmailAddressSchema = z6.object({
1488
+ function definePartySubResourceRemove(opts) {
1489
+ const shape = {
1490
+ partyId: positiveId,
1491
+ [opts.idField]: positiveId.describe(
1492
+ `Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
1493
+ )
1494
+ };
1495
+ const schema = z7.object(shape);
1496
+ async function handler(input) {
1497
+ const partyId = input["partyId"];
1498
+ const rowId = input[opts.idField];
1499
+ return idempotentWithResult(
1500
+ () => capsulePut(`/parties/${partyId}`, {
1501
+ party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
1502
+ }),
1503
+ (result) => ({
1504
+ removed: true,
1505
+ alreadyRemoved: false,
1506
+ partyId,
1507
+ [opts.idField]: rowId,
1508
+ ...result
1509
+ }),
1510
+ () => ({
1511
+ removed: true,
1512
+ alreadyRemoved: true,
1513
+ partyId,
1514
+ [opts.idField]: rowId
1515
+ })
1516
+ );
1517
+ }
1518
+ return { schema, handler };
1519
+ }
1520
+ var addPartyEmailAddressSchema = z7.object({
1326
1521
  partyId: positiveId,
1327
- address: z6.string().email(),
1328
- type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1522
+ address: z7.string().email(),
1523
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
1329
1524
  });
1330
1525
  async function addPartyEmailAddress(input) {
1331
1526
  const { partyId, address, type } = input;
@@ -1335,32 +1530,17 @@ async function addPartyEmailAddress(input) {
1335
1530
  party: { emailAddresses: [item] }
1336
1531
  });
1337
1532
  }
1338
- var removePartyEmailAddressByIdSchema = z6.object({
1339
- partyId: positiveId,
1340
- emailAddressId: positiveId.describe(
1341
- "Capsule's id for the email-address row. Read it from get_party (each entry in emailAddresses carries an id)."
1342
- )
1533
+ var removePartyEmailAddress = definePartySubResourceRemove({
1534
+ arrayKey: "emailAddresses",
1535
+ idField: "emailAddressId",
1536
+ rowNoun: "email-address"
1343
1537
  });
1344
- async function removePartyEmailAddressById(input) {
1345
- const { partyId, emailAddressId } = input;
1346
- return idempotentWithResult(
1347
- () => capsulePut(`/parties/${partyId}`, {
1348
- party: { emailAddresses: [{ id: emailAddressId, _delete: true }] }
1349
- }),
1350
- (result) => ({
1351
- removed: true,
1352
- alreadyRemoved: false,
1353
- partyId,
1354
- emailAddressId,
1355
- ...result
1356
- }),
1357
- () => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
1358
- );
1359
- }
1360
- var addPartyPhoneNumberSchema = z6.object({
1538
+ var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
1539
+ var removePartyEmailAddressById = removePartyEmailAddress.handler;
1540
+ var addPartyPhoneNumberSchema = z7.object({
1361
1541
  partyId: positiveId,
1362
- number: z6.string().min(1),
1363
- type: z6.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1542
+ number: z7.string().min(1),
1543
+ type: z7.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
1364
1544
  });
1365
1545
  async function addPartyPhoneNumber(input) {
1366
1546
  const { partyId, number, type } = input;
@@ -1370,36 +1550,21 @@ async function addPartyPhoneNumber(input) {
1370
1550
  party: { phoneNumbers: [item] }
1371
1551
  });
1372
1552
  }
1373
- var removePartyPhoneNumberByIdSchema = z6.object({
1374
- partyId: positiveId,
1375
- phoneNumberId: positiveId.describe(
1376
- "Capsule's id for the phone-number row. Read it from get_party (each entry in phoneNumbers carries an id)."
1377
- )
1553
+ var removePartyPhoneNumber = definePartySubResourceRemove({
1554
+ arrayKey: "phoneNumbers",
1555
+ idField: "phoneNumberId",
1556
+ rowNoun: "phone-number"
1378
1557
  });
1379
- async function removePartyPhoneNumberById(input) {
1380
- const { partyId, phoneNumberId } = input;
1381
- return idempotentWithResult(
1382
- () => capsulePut(`/parties/${partyId}`, {
1383
- party: { phoneNumbers: [{ id: phoneNumberId, _delete: true }] }
1384
- }),
1385
- (result) => ({
1386
- removed: true,
1387
- alreadyRemoved: false,
1388
- partyId,
1389
- phoneNumberId,
1390
- ...result
1391
- }),
1392
- () => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
1393
- );
1394
- }
1395
- var addPartyAddressSchema = z6.object({
1558
+ var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
1559
+ var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
1560
+ var addPartyAddressSchema = z7.object({
1396
1561
  partyId: positiveId,
1397
- street: z6.string().optional(),
1398
- city: z6.string().optional(),
1399
- state: z6.string().optional(),
1400
- country: z6.string().optional().describe(CountryDescription),
1401
- zip: z6.string().optional(),
1402
- type: z6.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1562
+ street: z7.string().optional(),
1563
+ city: z7.string().optional(),
1564
+ state: z7.string().optional(),
1565
+ country: z7.string().optional().describe(CountryDescription),
1566
+ zip: z7.string().optional(),
1567
+ type: z7.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
1403
1568
  });
1404
1569
  async function addPartyAddress(input) {
1405
1570
  const { partyId, ...rest } = input;
@@ -1411,31 +1576,16 @@ async function addPartyAddress(input) {
1411
1576
  party: { addresses: [item] }
1412
1577
  });
1413
1578
  }
1414
- var removePartyAddressByIdSchema = z6.object({
1415
- partyId: positiveId,
1416
- addressId: positiveId.describe(
1417
- "Capsule's id for the address row. Read it from get_party (each entry in addresses carries an id)."
1418
- )
1579
+ var removePartyAddress = definePartySubResourceRemove({
1580
+ arrayKey: "addresses",
1581
+ idField: "addressId",
1582
+ rowNoun: "address"
1419
1583
  });
1420
- async function removePartyAddressById(input) {
1421
- const { partyId, addressId } = input;
1422
- return idempotentWithResult(
1423
- () => capsulePut(`/parties/${partyId}`, {
1424
- party: { addresses: [{ id: addressId, _delete: true }] }
1425
- }),
1426
- (result) => ({
1427
- removed: true,
1428
- alreadyRemoved: false,
1429
- partyId,
1430
- addressId,
1431
- ...result
1432
- }),
1433
- () => ({ removed: true, alreadyRemoved: true, partyId, addressId })
1434
- );
1435
- }
1436
- var addPartyWebsiteSchema = z6.object({
1584
+ var removePartyAddressByIdSchema = removePartyAddress.schema;
1585
+ var removePartyAddressById = removePartyAddress.handler;
1586
+ var addPartyWebsiteSchema = z7.object({
1437
1587
  partyId: positiveId,
1438
- address: z6.string().min(1).describe(
1588
+ address: z7.string().min(1).describe(
1439
1589
  "The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
1440
1590
  ),
1441
1591
  service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
@@ -1448,58 +1598,41 @@ async function addPartyWebsite(input) {
1448
1598
  party: { websites: [item] }
1449
1599
  });
1450
1600
  }
1451
- var removePartyWebsiteByIdSchema = z6.object({
1452
- partyId: positiveId,
1453
- websiteId: positiveId.describe(
1454
- "Capsule's id for the website row. Read it from get_party (each entry in websites carries an id)."
1455
- )
1601
+ var removePartyWebsite = definePartySubResourceRemove({
1602
+ arrayKey: "websites",
1603
+ idField: "websiteId",
1604
+ rowNoun: "website"
1456
1605
  });
1457
- async function removePartyWebsiteById(input) {
1458
- const { partyId, websiteId } = input;
1459
- return idempotentWithResult(
1460
- () => capsulePut(`/parties/${partyId}`, {
1461
- party: { websites: [{ id: websiteId, _delete: true }] }
1462
- }),
1463
- (result) => ({
1464
- removed: true,
1465
- alreadyRemoved: false,
1466
- partyId,
1467
- websiteId,
1468
- ...result
1469
- }),
1470
- () => ({ removed: true, alreadyRemoved: true, partyId, websiteId })
1471
- );
1472
- }
1606
+ var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
1607
+ var removePartyWebsiteById = removePartyWebsite.handler;
1473
1608
 
1474
1609
  // src/tools/opportunities.ts
1475
- import { z as z7 } from "zod";
1476
- var OpportunityValueSchema = z7.object({
1477
- amount: z7.number().nonnegative(),
1478
- currency: z7.string({
1610
+ import { z as z8 } from "zod";
1611
+ var OpportunityValueSchema = z8.object({
1612
+ amount: z8.number().nonnegative(),
1613
+ currency: z8.string({
1479
1614
  error: (iss) => iss.code === "invalid_type" && iss.input === void 0 ? "currency is required when amount is set (3-letter ISO 4217 code, e.g. 'USD', 'EUR', 'GBP')" : void 0
1480
1615
  }).length(3).describe(
1481
1616
  "ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
1482
1617
  )
1483
1618
  });
1484
- var searchOpportunitiesSchema = z7.object({
1485
- q: z7.string().optional().describe("Free-text search query"),
1486
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1487
- page: z7.number().int().positive().optional().default(1),
1488
- perPage: z7.number().int().min(1).max(100).optional().default(25)
1619
+ var searchOpportunitiesSchema = z8.object({
1620
+ q: z8.string().optional().describe("Free-text search query"),
1621
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1622
+ ...paginationFields
1489
1623
  });
1490
1624
  async function searchOpportunities(input) {
1491
1625
  const path = input.q ? "/opportunities/search" : "/opportunities";
1492
- const { data, nextPage } = await capsuleGet(path, {
1626
+ return capsuleGetList(path, {
1493
1627
  q: input.q,
1494
1628
  embed: input.embed,
1495
1629
  page: input.page,
1496
1630
  perPage: input.perPage
1497
1631
  });
1498
- return { ...data, nextPage };
1499
1632
  }
1500
- var getOpportunitySchema = z7.object({
1633
+ var getOpportunitySchema = z8.object({
1501
1634
  id: positiveId,
1502
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1635
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1503
1636
  });
1504
1637
  async function getOpportunity(input) {
1505
1638
  const { data } = await capsuleGet(`/opportunities/${input.id}`, {
@@ -1507,32 +1640,32 @@ async function getOpportunity(input) {
1507
1640
  });
1508
1641
  return data;
1509
1642
  }
1510
- var getOpportunitiesSchema = z7.object({
1511
- ids: z7.array(positiveId).min(1).max(50).describe(
1643
+ var getOpportunitiesSchema = z8.object({
1644
+ ids: z8.array(positiveId).min(1).max(50).describe(
1512
1645
  "Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1513
1646
  ),
1514
- embed: z7.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1647
+ embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1515
1648
  });
1516
1649
  async function getOpportunities(input) {
1517
1650
  return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
1518
1651
  }
1519
- var createOpportunitySchema = z7.object({
1520
- name: z7.string().min(1),
1652
+ var createOpportunitySchema = z8.object({
1653
+ name: z8.string().min(1),
1521
1654
  partyId: positiveId.describe("ID of the party this opportunity belongs to"),
1522
1655
  milestoneId: positiveId.describe(
1523
1656
  "ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones. NOTE: some Capsule tenants configure **pipeline / milestone-reached automation rules** that mutate `owner` and/or `team` immediately after creation \u2014 e.g. an 'Assign to a Team' action that fires on entry to a specific milestone and has been observed to clear `owner` as an automation side-effect. If you observe a newly-created opp landing with `owner: null` despite passing `ownerId`, the cause is almost certainly a milestone automation on the destination pipeline rather than the connector. Documented workaround: follow `create_opportunity` with an immediate `batch_update_opportunity({items: [{id, ownerId, teamId}]})` carrying both fields \u2014 PUT does not re-fire milestone-reached triggers, so the owner sticks."
1524
1657
  ),
1525
- description: z7.string().optional(),
1658
+ description: z8.string().optional(),
1526
1659
  value: OpportunityValueSchema.optional(),
1527
- expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1528
- probability: z7.number().int().min(0).max(100).optional(),
1660
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1661
+ probability: z8.number().int().min(0).max(100).optional(),
1529
1662
  ownerId: positiveId.optional().describe(
1530
1663
  "Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. To clear owner later, call update_opportunity with `ownerId: null`. Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
1531
1664
  ),
1532
1665
  teamId: positiveId.optional().describe(
1533
1666
  "Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
1534
1667
  ),
1535
- fields: z7.array(CustomFieldWriteSchema).optional().describe(
1668
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(
1536
1669
  fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
1537
1670
  )
1538
1671
  });
@@ -1549,19 +1682,19 @@ async function createOpportunity(input) {
1549
1682
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1550
1683
  return capsulePost("/opportunities", { opportunity: body });
1551
1684
  }
1552
- var updateOpportunitySchema = z7.object({
1685
+ var updateOpportunitySchema = z8.object({
1553
1686
  id: positiveId,
1554
- name: z7.string().min(1).optional(),
1687
+ name: z8.string().min(1).optional(),
1555
1688
  partyId: positiveId.optional().describe(
1556
1689
  "Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_project.partyId`."
1557
1690
  ),
1558
1691
  milestoneId: positiveId.optional().describe(
1559
1692
  "Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
1560
1693
  ),
1561
- description: z7.string().optional(),
1694
+ description: z8.string().optional(),
1562
1695
  value: OpportunityValueSchema.optional(),
1563
- expectedCloseOn: z7.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1564
- probability: z7.number().int().min(0).max(100).optional().describe(
1696
+ expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
1697
+ probability: z8.number().int().min(0).max(100).optional().describe(
1565
1698
  "Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
1566
1699
  ),
1567
1700
  lostReasonId: positiveId.optional().describe(
@@ -1573,7 +1706,7 @@ var updateOpportunitySchema = z7.object({
1573
1706
  teamId: positiveId.nullable().optional().describe(
1574
1707
  "Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
1575
1708
  ),
1576
- fields: z7.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1709
+ fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
1577
1710
  });
1578
1711
  async function updateOpportunity(input) {
1579
1712
  const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
@@ -1609,25 +1742,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
1609
1742
  });
1610
1743
 
1611
1744
  // src/tools/projects.ts
1612
- import { z as z8 } from "zod";
1613
- var listProjectsSchema = z8.object({
1614
- status: z8.enum(["OPEN", "CLOSED"]).optional(),
1615
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1616
- page: z8.number().int().positive().optional().default(1),
1617
- perPage: z8.number().int().min(1).max(100).optional().default(25)
1745
+ import { z as z9 } from "zod";
1746
+ var listProjectsSchema = z9.object({
1747
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
1748
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
1749
+ ...paginationFields
1618
1750
  });
1619
1751
  async function listProjects(input) {
1620
- const { data, nextPage } = await capsuleGet("/kases", {
1752
+ return capsuleGetList("/kases", {
1621
1753
  status: input.status,
1622
1754
  embed: input.embed,
1623
1755
  page: input.page,
1624
1756
  perPage: input.perPage
1625
1757
  });
1626
- return { ...data, nextPage };
1627
1758
  }
1628
- var getProjectSchema = z8.object({
1759
+ var getProjectSchema = z9.object({
1629
1760
  id: positiveId,
1630
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1761
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1631
1762
  });
1632
1763
  async function getProject(input) {
1633
1764
  const { data } = await capsuleGet(`/kases/${input.id}`, {
@@ -1635,20 +1766,20 @@ async function getProject(input) {
1635
1766
  });
1636
1767
  return data;
1637
1768
  }
1638
- var getProjectsSchema = z8.object({
1639
- ids: z8.array(positiveId).min(1).max(50).describe(
1769
+ var getProjectsSchema = z9.object({
1770
+ ids: z9.array(positiveId).min(1).max(50).describe(
1640
1771
  "Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1641
1772
  ),
1642
- embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1773
+ embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
1643
1774
  });
1644
1775
  async function getProjects(input) {
1645
1776
  return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
1646
1777
  }
1647
- var createProjectSchema = z8.object({
1648
- name: z8.string().min(1),
1778
+ var createProjectSchema = z9.object({
1779
+ name: z9.string().min(1),
1649
1780
  partyId: positiveId.describe("ID of the party linked to this project"),
1650
- description: z8.string().optional(),
1651
- status: z8.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1781
+ description: z9.string().optional(),
1782
+ status: z9.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
1652
1783
  ownerId: positiveId.optional().describe(
1653
1784
  "Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation \u2014 e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied."
1654
1785
  ),
@@ -1658,8 +1789,8 @@ var createProjectSchema = z8.object({
1658
1789
  stageId: positiveId.optional().describe(
1659
1790
  "Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
1660
1791
  ),
1661
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1662
- fields: z8.array(CustomFieldWriteSchema).optional().describe(
1792
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1793
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
1663
1794
  fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
1664
1795
  )
1665
1796
  });
@@ -1677,11 +1808,11 @@ async function createProject(input) {
1677
1808
  if (mappedFields !== void 0) body["fields"] = mappedFields;
1678
1809
  return capsulePost("/kases", { kase: body });
1679
1810
  }
1680
- var updateProjectSchema = z8.object({
1811
+ var updateProjectSchema = z9.object({
1681
1812
  id: positiveId,
1682
- name: z8.string().min(1).optional(),
1683
- description: z8.string().optional(),
1684
- status: z8.enum(["OPEN", "CLOSED"]).optional(),
1813
+ name: z9.string().min(1).optional(),
1814
+ description: z9.string().optional(),
1815
+ status: z9.enum(["OPEN", "CLOSED"]).optional(),
1685
1816
  partyId: positiveId.optional().describe(
1686
1817
  "Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_opportunity.partyId`."
1687
1818
  ),
@@ -1694,8 +1825,8 @@ var updateProjectSchema = z8.object({
1694
1825
  stageId: positiveId.nullable().optional().describe(
1695
1826
  "Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
1696
1827
  ),
1697
- expectedCloseOn: z8.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1698
- fields: z8.array(CustomFieldWriteSchema).optional().describe(
1828
+ expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1829
+ fields: z9.array(CustomFieldWriteSchema).optional().describe(
1699
1830
  fieldsArrayDescriptor("get_project") + " Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
1700
1831
  )
1701
1832
  });
@@ -1734,23 +1865,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
1734
1865
  });
1735
1866
 
1736
1867
  // src/tools/tasks.ts
1737
- import { z as z9 } from "zod";
1738
- var listTasksSchema = z9.object({
1868
+ import { z as z10 } from "zod";
1869
+ var listTasksSchema = z10.object({
1739
1870
  // Note: Capsule has a third internal status `PENDING` (a task that's
1740
1871
  // part of an active track but not yet "open"), but it can only be
1741
1872
  // reached via track machinery — it is NOT directly settable by
1742
1873
  // /tasks PUT, and a list filter for it returns the same as OPEN
1743
1874
  // anyway. We expose only the two values that are actually filterable
1744
1875
  // by the v2 API.
1745
- status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1876
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
1746
1877
  "Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
1747
1878
  ),
1748
1879
  ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
1749
- page: z9.number().int().positive().optional().default(1),
1750
- perPage: z9.number().int().min(1).max(100).optional().default(25)
1880
+ ...paginationFields
1751
1881
  });
1752
1882
  async function listTasks(input) {
1753
- const { data, nextPage } = await capsuleGet("/tasks", {
1883
+ return capsuleGetList("/tasks", {
1754
1884
  // Default 'OPEN' applied here (not via zod .default()) so that
1755
1885
  // z.infer keeps `status` optional for callers that omit it.
1756
1886
  status: input.status ?? "OPEN",
@@ -1759,28 +1889,27 @@ async function listTasks(input) {
1759
1889
  page: input.page,
1760
1890
  perPage: input.perPage
1761
1891
  });
1762
- return { ...data, nextPage };
1763
1892
  }
1764
- var getTaskSchema = z9.object({
1893
+ var getTaskSchema = z10.object({
1765
1894
  id: positiveId.describe("Task ID")
1766
1895
  });
1767
1896
  async function getTask(input) {
1768
1897
  const { data } = await capsuleGet(`/tasks/${input.id}`);
1769
1898
  return data;
1770
1899
  }
1771
- var getTasksSchema = z9.object({
1772
- ids: z9.array(positiveId).min(1).max(50).describe(
1900
+ var getTasksSchema = z10.object({
1901
+ ids: z10.array(positiveId).min(1).max(50).describe(
1773
1902
  "Array of task IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
1774
1903
  )
1775
1904
  });
1776
1905
  async function getTasks(input) {
1777
1906
  return chunkedMultiGet("/tasks", "tasks", input.ids);
1778
1907
  }
1779
- var createTaskSchema = z9.object({
1780
- description: z9.string().min(1),
1781
- dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1782
- dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1783
- detail: z9.string().optional(),
1908
+ var createTaskSchema = z10.object({
1909
+ description: z10.string().min(1),
1910
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
1911
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1912
+ detail: z10.string().optional(),
1784
1913
  ownerId: positiveId.optional().describe(
1785
1914
  "Assign to user ID. Defaults to the API-token owner when omitted. Once set, this connector cannot clear the owner back to null \u2014 use Capsule's web UI for that."
1786
1915
  ),
@@ -1789,10 +1918,7 @@ var createTaskSchema = z9.object({
1789
1918
  projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
1790
1919
  });
1791
1920
  async function createTask(input) {
1792
- const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
1793
- if (linked.length > 1) {
1794
- throw new Error("Provide at most one of partyId, opportunityId, or projectId");
1795
- }
1921
+ assertSingleParentRef("create_task", input);
1796
1922
  const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
1797
1923
  const body = { ...rest };
1798
1924
  setRef(body, "owner", ownerId);
@@ -1801,16 +1927,16 @@ async function createTask(input) {
1801
1927
  setRef(body, "kase", projectId);
1802
1928
  return capsulePost("/tasks", { task: body });
1803
1929
  }
1804
- var updateTaskSchema = z9.object({
1930
+ var updateTaskSchema = z10.object({
1805
1931
  id: positiveId,
1806
- description: z9.string().min(1).optional(),
1807
- dueOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1808
- dueTime: z9.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1809
- detail: z9.string().optional(),
1932
+ description: z10.string().min(1).optional(),
1933
+ dueOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
1934
+ dueTime: z10.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
1935
+ detail: z10.string().optional(),
1810
1936
  // Capsule rejects direct sets of `PENDING` (which is a track-machinery
1811
1937
  // internal state) with 422 "cannot set task status to PENDING".
1812
1938
  // Only OPEN and COMPLETED are settable here.
1813
- status: z9.enum(["OPEN", "COMPLETED"]).optional().describe(
1939
+ status: z10.enum(["OPEN", "COMPLETED"]).optional().describe(
1814
1940
  "Set to OPEN or COMPLETED. (PENDING exists internally for track-driven tasks but cannot be set directly via this tool \u2014 Capsule rejects it.) Setting status: OPEN on an already-open task is a true no-op (does not advance updatedAt)."
1815
1941
  ),
1816
1942
  ownerId: positiveId.optional().describe(
@@ -1828,12 +1954,7 @@ var updateTaskSchema = z9.object({
1828
1954
  });
1829
1955
  async function updateTask(input) {
1830
1956
  const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
1831
- const setCount = [partyId, opportunityId, projectId].filter((v) => typeof v === "number").length;
1832
- if (setCount > 1) {
1833
- throw new Error(
1834
- "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')"
1835
- );
1836
- }
1957
+ assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
1837
1958
  const body = {};
1838
1959
  for (const [k, v] of Object.entries(rest)) {
1839
1960
  if (v !== void 0) body[k] = v;
@@ -1844,7 +1965,7 @@ async function updateTask(input) {
1844
1965
  setNullableRef(body, "kase", projectId);
1845
1966
  return capsulePut(`/tasks/${id}`, { task: body });
1846
1967
  }
1847
- var completeTaskSchema = z9.object({
1968
+ var completeTaskSchema = z10.object({
1848
1969
  id: positiveId
1849
1970
  });
1850
1971
  async function completeTask(input) {
@@ -1852,8 +1973,8 @@ async function completeTask(input) {
1852
1973
  task: { status: "COMPLETED" }
1853
1974
  });
1854
1975
  }
1855
- var batchCompleteTaskSchema = z9.object({
1856
- ids: z9.array(positiveId).min(1).max(50).describe(
1976
+ var batchCompleteTaskSchema = z10.object({
1977
+ ids: z10.array(positiveId).min(1).max(50).describe(
1857
1978
  "Array of 1\u201350 task ids to mark COMPLETED in parallel. Each id resolves to one PUT /tasks/{id}; failures (e.g. 404 for a deleted task) surface per-item in the result array, the rest still complete. Capped at 50."
1858
1979
  )
1859
1980
  });
@@ -1867,77 +1988,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
1867
1988
  });
1868
1989
 
1869
1990
  // src/tools/entries.ts
1870
- import { z as z10 } from "zod";
1991
+ import { z as z11 } from "zod";
1871
1992
  var listEntriesPagination = {
1872
- page: z10.number().int().positive().optional().default(1),
1873
- perPage: z10.number().int().min(1).max(100).optional().default(25),
1874
- embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1993
+ ...paginationFields,
1994
+ embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
1875
1995
  };
1876
- var listPartyEntriesSchema = z10.object({
1996
+ var listPartyEntriesSchema = z11.object({
1877
1997
  partyId: positiveId,
1878
1998
  ...listEntriesPagination,
1879
- includeLinkedPersons: z10.boolean().optional().describe(
1999
+ includeLinkedPersons: z11.boolean().optional().describe(
1880
2000
  "When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422 'entry must be linked to either a party, opportunity or kase'). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
1881
2001
  )
1882
2002
  });
2003
+ var PER_PARTY_FETCH_CAP = 100;
1883
2004
  async function fanOutPartyEntries(partyIds, embed, perPage) {
1884
- const concurrency = getBatchConcurrency();
1885
- const results = new Array(partyIds.length);
1886
- let cursor = 0;
1887
- async function worker() {
1888
- while (true) {
1889
- const i = cursor;
1890
- cursor += 1;
1891
- if (i >= partyIds.length) return;
1892
- const id = partyIds[i];
1893
- const { data, nextPage } = await capsuleGet(
1894
- `/parties/${id}/entries`,
1895
- {
1896
- embed,
1897
- page: 1,
1898
- perPage
1899
- }
1900
- );
1901
- results[i] = { entries: data.entries, nextPage };
1902
- }
1903
- }
1904
- const workers = [];
1905
- for (let w = 0; w < Math.min(concurrency, partyIds.length); w++) {
1906
- workers.push(worker());
1907
- }
1908
- await Promise.all(workers);
1909
- return results;
2005
+ return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
2006
+ const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
2007
+ embed,
2008
+ page: 1,
2009
+ perPage
2010
+ });
2011
+ return { entries: data.entries, nextPage };
2012
+ });
1910
2013
  }
1911
2014
  function mergedTimelineCandidatePerParty(page, perPage) {
1912
- return Math.min(page * perPage, 100);
2015
+ return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
1913
2016
  }
1914
2017
  function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
1915
2018
  const requestedWindowEnd = page * perPage;
1916
2019
  if (mergedLength > requestedWindowEnd) return page + 1;
1917
- const nextWindowWithinCap = requestedWindowEnd < 100;
2020
+ const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
1918
2021
  if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
1919
2022
  return void 0;
1920
2023
  }
1921
2024
  async function listPartyEntries(input) {
1922
2025
  const { partyId, embed, page, perPage, includeLinkedPersons } = input;
1923
2026
  if (!includeLinkedPersons) {
1924
- const { data, nextPage: nextPage2 } = await capsuleGet(
1925
- `/parties/${partyId}/entries`,
1926
- { embed, page, perPage }
1927
- );
1928
- return { ...data, nextPage: nextPage2 };
2027
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2028
+ embed,
2029
+ page,
2030
+ perPage
2031
+ });
1929
2032
  }
1930
2033
  const { data: peopleData } = await capsuleGet(
1931
2034
  `/parties/${partyId}/people`,
1932
- { page: 1, perPage: 100 }
2035
+ { page: 1, perPage: PER_PARTY_FETCH_CAP }
1933
2036
  );
1934
2037
  const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
1935
2038
  if (peopleIds.length === 0) {
1936
- const { data, nextPage: nextPage2 } = await capsuleGet(
1937
- `/parties/${partyId}/entries`,
1938
- { embed, page, perPage }
1939
- );
1940
- return { ...data, nextPage: nextPage2 };
2039
+ return capsuleGetList(`/parties/${partyId}/entries`, {
2040
+ embed,
2041
+ page,
2042
+ perPage
2043
+ });
1941
2044
  }
1942
2045
  const targetIds = [partyId, ...peopleIds];
1943
2046
  const perPartyPages = await fanOutPartyEntries(
@@ -1972,31 +2075,31 @@ async function listPartyEntries(input) {
1972
2075
  );
1973
2076
  return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
1974
2077
  }
1975
- var listOpportunityEntriesSchema = z10.object({
2078
+ var listOpportunityEntriesSchema = z11.object({
1976
2079
  opportunityId: positiveId,
1977
2080
  ...listEntriesPagination
1978
2081
  });
1979
2082
  async function listOpportunityEntries(input) {
1980
- const { data, nextPage } = await capsuleGet(
1981
- `/opportunities/${input.opportunityId}/entries`,
1982
- { embed: input.embed, page: input.page, perPage: input.perPage }
1983
- );
1984
- return { ...data, nextPage };
2083
+ return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
2084
+ embed: input.embed,
2085
+ page: input.page,
2086
+ perPage: input.perPage
2087
+ });
1985
2088
  }
1986
- var listProjectEntriesSchema = z10.object({
2089
+ var listProjectEntriesSchema = z11.object({
1987
2090
  projectId: positiveId,
1988
2091
  ...listEntriesPagination
1989
2092
  });
1990
2093
  async function listProjectEntries(input) {
1991
- const { data, nextPage } = await capsuleGet(
1992
- `/kases/${input.projectId}/entries`,
1993
- { embed: input.embed, page: input.page, perPage: input.perPage }
1994
- );
1995
- return { ...data, nextPage };
2094
+ return capsuleGetList(`/kases/${input.projectId}/entries`, {
2095
+ embed: input.embed,
2096
+ page: input.page,
2097
+ perPage: input.perPage
2098
+ });
1996
2099
  }
1997
- var getEntrySchema = z10.object({
2100
+ var getEntrySchema = z11.object({
1998
2101
  id: positiveId,
1999
- embed: z10.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2102
+ embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
2000
2103
  });
2001
2104
  async function getEntry(input) {
2002
2105
  const { data } = await capsuleGet(`/entries/${input.id}`, {
@@ -2004,34 +2107,30 @@ async function getEntry(input) {
2004
2107
  });
2005
2108
  return data;
2006
2109
  }
2007
- var listEntriesSchema = z10.object({
2110
+ var listEntriesSchema = z11.object({
2008
2111
  ...listEntriesPagination
2009
2112
  });
2010
2113
  async function listEntries(input) {
2011
- const { data, nextPage } = await capsuleGet("/entries", {
2114
+ return capsuleGetList("/entries", {
2012
2115
  embed: input.embed,
2013
2116
  page: input.page,
2014
2117
  perPage: input.perPage
2015
2118
  });
2016
- return { ...data, nextPage };
2017
2119
  }
2018
- var addNoteSchema = z10.object({
2019
- content: z10.string().min(1).describe(
2120
+ var addNoteSchema = z11.object({
2121
+ content: z11.string().min(1).describe(
2020
2122
  "Note body text. Stored verbatim and treated as MARKDOWN \u2014 Capsule's web UI renders the markdown when displaying. Pass markdown source ('# Heading', '**bold**', '- bullet'), not HTML."
2021
2123
  ),
2022
2124
  partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
2023
2125
  opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
2024
2126
  projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
2025
- entryAt: z10.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
2127
+ entryAt: z11.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
2026
2128
  "ISO-8601 timestamp for when this note actually happened (e.g. '2024-03-15T14:30:00Z'). Defaults to now. Use this for backdating historical notes when migrating from another system. `entryAt` is preserved across subsequent update_entry calls; only `updatedAt` advances on edits. Note attribution flows to the API-token owner \u2014 there is no way to record a note as authored by a different user via this connector (a `creatorId` parameter would enable audit-attribution spoofing on shared-connector deployments, so it is intentionally not exposed)."
2027
2129
  )
2028
2130
  });
2029
2131
  async function addNote(input) {
2030
2132
  const { content, partyId, opportunityId, projectId, entryAt } = input;
2031
- const linked = [partyId, opportunityId, projectId].filter(Boolean);
2032
- if (linked.length !== 1) {
2033
- throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
2034
- }
2133
+ assertSingleParentRef("add_note", input, { required: true });
2035
2134
  const body = { type: "note", content };
2036
2135
  setRef(body, "party", partyId);
2037
2136
  setRef(body, "opportunity", opportunityId);
@@ -2039,12 +2138,12 @@ async function addNote(input) {
2039
2138
  if (entryAt !== void 0) body["entryAt"] = entryAt;
2040
2139
  return capsulePost("/entries", { entry: body });
2041
2140
  }
2042
- var updateEntrySchema = z10.object({
2141
+ var updateEntrySchema = z11.object({
2043
2142
  id: positiveId.describe("Entry ID to update"),
2044
- content: z10.string().min(1).optional().describe(
2143
+ content: z11.string().min(1).optional().describe(
2045
2144
  "New body text for the entry. For notes, this is the markdown content; for emails, the body. Provide only if you want to change it."
2046
2145
  ),
2047
- subject: z10.string().optional().describe(
2146
+ subject: z11.string().optional().describe(
2048
2147
  "New subject line. Mostly meaningful on email-type entries; on plain notes Capsule accepts the call (HTTP 200) but **does not store the subject and does not advance `updatedAt`** \u2014 a true no-op for inapplicable fields. `entryAt` (when the note was authored) is preserved across edits; `updatedAt` advances only when an applicable field actually changes. To sort/filter by 'when did this happen', use `entryAt`; for 'last touched', use `updatedAt`."
2049
2148
  )
2050
2149
  });
@@ -2066,62 +2165,50 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
2066
2165
  });
2067
2166
 
2068
2167
  // src/tools/pipelines.ts
2069
- import { z as z11 } from "zod";
2070
- var paginationFields = {
2071
- page: z11.number().int().positive().optional(),
2072
- perPage: z11.number().int().min(1).max(100).optional()
2073
- };
2074
- var listPipelinesSchema = z11.object({ ...paginationFields });
2168
+ import { z as z12 } from "zod";
2169
+ var listPipelinesSchema = z12.object({ ...paginationFieldsNoDefaults });
2075
2170
  async function listPipelines(input) {
2076
- const { data, nextPage } = await capsuleGetCached("/pipelines", {
2171
+ return capsuleGetCachedList("/pipelines", {
2077
2172
  page: input.page ?? 1,
2078
2173
  perPage: input.perPage ?? 100
2079
2174
  });
2080
- return { ...data, nextPage };
2081
2175
  }
2082
- var listMilestonesSchema = z11.object({
2176
+ var listMilestonesSchema = z12.object({
2083
2177
  pipelineId: positiveId,
2084
- ...paginationFields
2178
+ ...paginationFieldsNoDefaults
2085
2179
  });
2086
2180
  async function listMilestones(input) {
2087
- const { data, nextPage } = await capsuleGetCached(
2181
+ return capsuleGetCachedList(
2088
2182
  `/pipelines/${input.pipelineId}/milestones`,
2089
2183
  { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2090
2184
  );
2091
- return { ...data, nextPage };
2092
2185
  }
2093
2186
 
2094
2187
  // src/tools/boards.ts
2095
- import { z as z12 } from "zod";
2096
- var paginationFields2 = {
2097
- page: z12.number().int().positive().optional(),
2098
- perPage: z12.number().int().min(1).max(100).optional()
2099
- };
2100
- var listBoardsSchema = z12.object({ ...paginationFields2 });
2188
+ import { z as z13 } from "zod";
2189
+ var listBoardsSchema = z13.object({ ...paginationFieldsNoDefaults });
2101
2190
  async function listBoards(input) {
2102
- const { data, nextPage } = await capsuleGetCached("/boards", {
2191
+ return capsuleGetCachedList("/boards", {
2103
2192
  page: input.page ?? 1,
2104
2193
  perPage: input.perPage ?? 100
2105
2194
  });
2106
- return { ...data, nextPage };
2107
2195
  }
2108
- var listStagesSchema = z12.object({
2196
+ var listStagesSchema = z13.object({
2109
2197
  boardId: positiveId.optional().describe(
2110
2198
  "Optional. If provided, returns only the stages defined on that specific board (uses /boards/{id}/stages). Omit to get all stages across all boards in one call."
2111
2199
  ),
2112
- ...paginationFields2
2200
+ ...paginationFieldsNoDefaults
2113
2201
  });
2114
2202
  async function listStages(input) {
2115
2203
  const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
2116
- const { data, nextPage } = await capsuleGetCached(path, {
2204
+ return capsuleGetCachedList(path, {
2117
2205
  page: input.page ?? 1,
2118
2206
  perPage: input.perPage ?? 100
2119
2207
  });
2120
- return { ...data, nextPage };
2121
2208
  }
2122
2209
 
2123
2210
  // src/tools/tags.ts
2124
- import { z as z13 } from "zod";
2211
+ import { z as z14 } from "zod";
2125
2212
  var TAG_LIST_PATH = {
2126
2213
  parties: "/parties/tags",
2127
2214
  opportunities: "/opportunities/tags",
@@ -2132,24 +2219,22 @@ var ENTITY_TO_WRAPPER = {
2132
2219
  opportunities: "opportunity",
2133
2220
  kases: "kase"
2134
2221
  };
2135
- var TagEntity = z13.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2136
- var listTagsSchema = z13.object({
2137
- entity: z13.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2138
- page: z13.number().int().positive().optional(),
2139
- perPage: z13.number().int().min(1).max(100).optional()
2222
+ var TagEntity = z14.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
2223
+ var listTagsSchema = z14.object({
2224
+ entity: z14.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
2225
+ ...paginationFieldsNoDefaults
2140
2226
  });
2141
2227
  async function listTags(input) {
2142
2228
  const path = TAG_LIST_PATH[input.entity];
2143
- const { data, nextPage } = await capsuleGetCached(path, {
2229
+ return capsuleGetCachedList(path, {
2144
2230
  page: input.page ?? 1,
2145
2231
  perPage: input.perPage ?? 100
2146
2232
  });
2147
- return { ...data, nextPage };
2148
2233
  }
2149
- var addTagSchema = z13.object({
2234
+ var addTagSchema = z14.object({
2150
2235
  entity: TagEntity,
2151
2236
  entityId: positiveId.describe("The party/opportunity/kase id."),
2152
- tagName: z13.string().min(1).describe(
2237
+ tagName: z14.string().min(1).describe(
2153
2238
  "Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
2154
2239
  )
2155
2240
  });
@@ -2162,7 +2247,7 @@ async function addTag(input) {
2162
2247
  invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
2163
2248
  return result;
2164
2249
  }
2165
- var removeTagByIdSchema = z13.object({
2250
+ var removeTagByIdSchema = z14.object({
2166
2251
  entity: TagEntity,
2167
2252
  entityId: positiveId.describe("The party/opportunity/kase id."),
2168
2253
  tagId: positiveId.describe(
@@ -2193,7 +2278,7 @@ async function removeTagById(input) {
2193
2278
  invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
2194
2279
  return result;
2195
2280
  }
2196
- var deleteTagDefinitionSchema = z13.object({
2281
+ var deleteTagDefinitionSchema = z14.object({
2197
2282
  entity: TagEntity,
2198
2283
  tagId: positiveId.describe(
2199
2284
  "The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
@@ -2229,44 +2314,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
2229
2314
  });
2230
2315
 
2231
2316
  // src/tools/users.ts
2232
- import { z as z14 } from "zod";
2233
- var listUsersSchema = z14.object({
2234
- page: z14.number().int().positive().optional(),
2235
- perPage: z14.number().int().min(1).max(100).optional()
2317
+ import { z as z15 } from "zod";
2318
+ var listUsersSchema = z15.object({
2319
+ ...paginationFieldsNoDefaults
2236
2320
  });
2237
2321
  async function listUsers(input) {
2238
- const { data, nextPage } = await capsuleGetCached("/users", {
2322
+ return capsuleGetCachedList("/users", {
2239
2323
  page: input.page ?? 1,
2240
2324
  perPage: input.perPage ?? 100
2241
2325
  });
2242
- return { ...data, nextPage };
2243
2326
  }
2244
- var getCurrentUserSchema = z14.object({});
2327
+ var getCurrentUserSchema = z15.object({});
2245
2328
  async function getCurrentUser(_input) {
2246
2329
  const { data } = await capsuleGet("/users/current");
2247
2330
  return data;
2248
2331
  }
2249
2332
 
2250
2333
  // src/tools/filters.ts
2251
- import { z as z15 } from "zod";
2252
- var FilterConditionSchema = z15.object({
2253
- field: z15.string().describe(
2334
+ import { z as z16 } from "zod";
2335
+ var FilterConditionSchema = z16.object({
2336
+ field: z16.string().describe(
2254
2337
  "The Capsule filter-side field name (these differ from response field names \u2014 e.g. response.createdAt is filter-side 'addedOn', response.lastContactedAt is filter-side 'lastContactedOn'). Common: 'addedOn' (date created), 'updatedOn' (date last modified), 'lastContactedOn' (parties only), 'name', 'tag', 'owner', 'team', 'type' (parties: person|organisation), 'milestone' (opportunities), 'status' (opp/project: OPEN|CLOSED), 'closedOn' (opp/project), 'expectedCloseOn' (opp/project), 'hasTags', 'hasEmailAddress' (parties), 'isOpen', 'isStale' (opportunities), 'custom:{fieldId}'. Full per-entity list: https://developer.capsulecrm.com/v2/reference/filters"
2255
2338
  ),
2256
- operator: z15.string().describe(
2339
+ operator: z16.string().describe(
2257
2340
  "The filter operator. Common: 'is', 'is not' (use value=null to test for null), 'contains', 'does not contain', 'is greater than', 'is less than', 'is within last' (date fields, value=integer days), 'is more than' (date fields, value=integer days ago), 'starts with', 'ends with'. Operator validity depends on the field's type."
2258
2341
  ),
2259
- value: z15.union([z15.string(), z15.number(), z15.boolean(), z15.null()]).describe(
2342
+ value: z16.union([z16.string(), z16.number(), z16.boolean(), z16.null()]).describe(
2260
2343
  "The value to compare against. For 'is within last' on date fields, pass an integer number of days. For tag filters, pass the tag name (string) or tag id (number). For 'is not' null tests, pass null literally."
2261
2344
  )
2262
2345
  });
2263
- var FilterInputSchema = z15.object({
2264
- conditions: z15.array(FilterConditionSchema).min(1).describe(
2346
+ var FilterInputSchema = z16.object({
2347
+ conditions: z16.array(FilterConditionSchema).min(1).describe(
2265
2348
  "Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
2266
2349
  ),
2267
- embed: z15.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2268
- page: z15.number().int().positive().optional().default(1),
2269
- perPage: z15.number().int().min(1).max(100).optional().default(25)
2350
+ embed: z16.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2351
+ ...paginationFields
2270
2352
  });
2271
2353
  async function runFilter(entityPath, input) {
2272
2354
  const { data, nextPage } = await capsuleSearch(
@@ -2286,10 +2368,7 @@ async function filterParties(input) {
2286
2368
  }
2287
2369
  var filterOpportunitiesSchema = FilterInputSchema;
2288
2370
  async function filterOpportunities(input) {
2289
- return runFilter(
2290
- "opportunities",
2291
- input
2292
- );
2371
+ return runFilter("opportunities", input);
2293
2372
  }
2294
2373
  var filterProjectsSchema = FilterInputSchema;
2295
2374
  async function filterProjects(input) {
@@ -2297,139 +2376,126 @@ async function filterProjects(input) {
2297
2376
  }
2298
2377
 
2299
2378
  // src/tools/metadata.ts
2300
- import { z as z16 } from "zod";
2301
- var paginationFields3 = {
2302
- page: z16.number().int().positive().optional(),
2303
- perPage: z16.number().int().min(1).max(100).optional().describe("Page size, max 100. Defaults to 100 for reference data.")
2379
+ import { z as z17 } from "zod";
2380
+ var paginationFields2 = {
2381
+ ...paginationFieldsNoDefaults,
2382
+ perPage: paginationFieldsNoDefaults.perPage.describe(
2383
+ "Page size, max 100. Defaults to 100 for reference data."
2384
+ )
2304
2385
  };
2305
- var listTeamsSchema = z16.object({ ...paginationFields3 });
2386
+ var listTeamsSchema = z17.object({ ...paginationFields2 });
2306
2387
  async function listTeams(input) {
2307
- const { data, nextPage } = await capsuleGetCached("/teams", {
2388
+ return capsuleGetCachedList("/teams", {
2308
2389
  page: input.page ?? 1,
2309
2390
  perPage: input.perPage ?? 100
2310
2391
  });
2311
- return { ...data, nextPage };
2312
2392
  }
2313
- var listLostReasonsSchema = z16.object({ ...paginationFields3 });
2393
+ var listLostReasonsSchema = z17.object({ ...paginationFields2 });
2314
2394
  async function listLostReasons(input) {
2315
- const { data, nextPage } = await capsuleGetCached("/lostreasons", {
2395
+ return capsuleGetCachedList("/lostreasons", {
2316
2396
  page: input.page ?? 1,
2317
2397
  perPage: input.perPage ?? 100
2318
2398
  });
2319
- return { ...data, nextPage };
2320
2399
  }
2321
- var listActivityTypesSchema = z16.object({ ...paginationFields3 });
2400
+ var listActivityTypesSchema = z17.object({ ...paginationFields2 });
2322
2401
  async function listActivityTypes(input) {
2323
- const { data, nextPage } = await capsuleGetCached(
2324
- "/activitytypes",
2325
- {
2326
- page: input.page ?? 1,
2327
- perPage: input.perPage ?? 100
2328
- }
2329
- );
2330
- return { ...data, nextPage };
2402
+ return capsuleGetCachedList("/activitytypes", {
2403
+ page: input.page ?? 1,
2404
+ perPage: input.perPage ?? 100
2405
+ });
2331
2406
  }
2332
- var getSiteSchema = z16.object({});
2407
+ var getSiteSchema = z17.object({});
2333
2408
  async function getSite(_input) {
2334
2409
  const { data } = await capsuleGetCached("/site");
2335
2410
  return data;
2336
2411
  }
2337
- var listTrackDefinitionsSchema = z16.object({ ...paginationFields3 });
2412
+ var listTrackDefinitionsSchema = z17.object({ ...paginationFields2 });
2338
2413
  async function listTrackDefinitions(input) {
2339
- const { data, nextPage } = await capsuleGetCached(
2340
- "/trackdefinitions",
2341
- { page: input.page ?? 1, perPage: input.perPage ?? 100 }
2342
- );
2343
- return { ...data, nextPage };
2414
+ return capsuleGetCachedList("/trackdefinitions", {
2415
+ page: input.page ?? 1,
2416
+ perPage: input.perPage ?? 100
2417
+ });
2344
2418
  }
2345
- var listCategoriesSchema = z16.object({ ...paginationFields3 });
2419
+ var listCategoriesSchema = z17.object({ ...paginationFields2 });
2346
2420
  async function listCategories(input) {
2347
- const { data, nextPage } = await capsuleGetCached("/categories", {
2421
+ return capsuleGetCachedList("/categories", {
2348
2422
  page: input.page ?? 1,
2349
2423
  perPage: input.perPage ?? 100
2350
2424
  });
2351
- return { ...data, nextPage };
2352
2425
  }
2353
- var listGoalsSchema = z16.object({ ...paginationFields3 });
2426
+ var listGoalsSchema = z17.object({ ...paginationFields2 });
2354
2427
  async function listGoals(input) {
2355
- const { data, nextPage } = await capsuleGetCached("/goals", {
2428
+ return capsuleGetCachedList("/goals", {
2356
2429
  page: input.page ?? 1,
2357
2430
  perPage: input.perPage ?? 100
2358
2431
  });
2359
- return { ...data, nextPage };
2360
2432
  }
2361
2433
 
2362
2434
  // src/tools/audit.ts
2363
- import { z as z17 } from "zod";
2364
- var listEmployeesSchema = z17.object({
2435
+ import { z as z18 } from "zod";
2436
+ var listEmployeesSchema = z18.object({
2365
2437
  partyId: positiveId.describe(
2366
2438
  "The organisation's party id. Returns the people whose `organisation` field links to this party."
2367
2439
  ),
2368
- page: z17.number().int().positive().optional().default(1),
2369
- perPage: z17.number().int().min(1).max(100).optional().default(25),
2370
- embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2440
+ ...paginationFields,
2441
+ embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
2371
2442
  });
2372
2443
  async function listEmployees(input) {
2373
- const { data, nextPage } = await capsuleGet(
2374
- `/parties/${input.partyId}/people`,
2375
- { page: input.page, perPage: input.perPage, embed: input.embed }
2376
- );
2377
- return { ...data, nextPage };
2444
+ return capsuleGetList(`/parties/${input.partyId}/people`, {
2445
+ page: input.page,
2446
+ perPage: input.perPage,
2447
+ embed: input.embed
2448
+ });
2378
2449
  }
2379
- var DeletedSinceSchema = z17.string().describe(
2450
+ var DeletedSinceSchema = z18.string().describe(
2380
2451
  "REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
2381
2452
  );
2382
2453
  var DeletedPagination = {
2383
2454
  since: DeletedSinceSchema,
2384
- page: z17.number().int().positive().optional().default(1),
2385
- perPage: z17.number().int().min(1).max(100).optional().default(25)
2455
+ ...paginationFields
2386
2456
  };
2387
- var listDeletedPartiesSchema = z17.object(DeletedPagination);
2457
+ var listDeletedPartiesSchema = z18.object(DeletedPagination);
2388
2458
  async function listDeletedParties(input) {
2389
- const { data, nextPage } = await capsuleGet("/parties/deleted", {
2459
+ return capsuleGetList("/parties/deleted", {
2390
2460
  since: input.since,
2391
2461
  page: input.page,
2392
2462
  perPage: input.perPage
2393
2463
  });
2394
- return { ...data, nextPage };
2395
2464
  }
2396
- var listDeletedOpportunitiesSchema = z17.object(DeletedPagination);
2465
+ var listDeletedOpportunitiesSchema = z18.object(DeletedPagination);
2397
2466
  async function listDeletedOpportunities(input) {
2398
- const { data, nextPage } = await capsuleGet("/opportunities/deleted", {
2467
+ return capsuleGetList("/opportunities/deleted", {
2399
2468
  since: input.since,
2400
2469
  page: input.page,
2401
2470
  perPage: input.perPage
2402
2471
  });
2403
- return { ...data, nextPage };
2404
2472
  }
2405
- var listDeletedProjectsSchema = z17.object(DeletedPagination);
2473
+ var listDeletedProjectsSchema = z18.object(DeletedPagination);
2406
2474
  async function listDeletedProjects(input) {
2407
- const { data, nextPage } = await capsuleGet("/kases/deleted", {
2475
+ return capsuleGetList("/kases/deleted", {
2408
2476
  since: input.since,
2409
2477
  page: input.page,
2410
2478
  perPage: input.perPage
2411
2479
  });
2412
- return { ...data, nextPage };
2413
2480
  }
2414
2481
 
2415
2482
  // src/tools/relationships.ts
2416
- import { z as z18 } from "zod";
2417
- var RelationshipEntity = z18.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2418
- var listAdditionalPartiesSchema = z18.object({
2483
+ import { z as z19 } from "zod";
2484
+ var RelationshipEntity = z19.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
2485
+ var listAdditionalPartiesSchema = z19.object({
2419
2486
  entity: RelationshipEntity,
2420
2487
  entityId: positiveId.describe("ID of the opportunity or project."),
2421
- embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2422
- page: z18.number().int().positive().optional().default(1),
2423
- perPage: z18.number().int().min(1).max(100).optional().default(25)
2488
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2489
+ ...paginationFields
2424
2490
  });
2425
2491
  async function listAdditionalParties(input) {
2426
- const { data, nextPage } = await capsuleGet(
2427
- `/${input.entity}/${input.entityId}/parties`,
2428
- { embed: input.embed, page: input.page, perPage: input.perPage }
2429
- );
2430
- return { ...data, nextPage };
2492
+ return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
2493
+ embed: input.embed,
2494
+ page: input.page,
2495
+ perPage: input.perPage
2496
+ });
2431
2497
  }
2432
- var addAdditionalPartySchema = z18.object({
2498
+ var addAdditionalPartySchema = z19.object({
2433
2499
  entity: RelationshipEntity,
2434
2500
  entityId: positiveId,
2435
2501
  partyId: positiveId.describe(
@@ -2462,7 +2528,7 @@ async function addAdditionalParty(input) {
2462
2528
  throw err;
2463
2529
  }
2464
2530
  }
2465
- var removeAdditionalPartySchema = z18.object({
2531
+ var removeAdditionalPartySchema = z19.object({
2466
2532
  entity: RelationshipEntity,
2467
2533
  entityId: positiveId,
2468
2534
  partyId: positiveId,
@@ -2492,24 +2558,23 @@ async function removeAdditionalParty(input) {
2492
2558
  })
2493
2559
  );
2494
2560
  }
2495
- var listAssociatedProjectsSchema = z18.object({
2561
+ var listAssociatedProjectsSchema = z19.object({
2496
2562
  opportunityId: positiveId,
2497
- embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2498
- page: z18.number().int().positive().optional().default(1),
2499
- perPage: z18.number().int().min(1).max(100).optional().default(25)
2563
+ embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2564
+ ...paginationFields
2500
2565
  });
2501
2566
  async function listAssociatedProjects(input) {
2502
- const { data, nextPage } = await capsuleGet(
2503
- `/opportunities/${input.opportunityId}/kases`,
2504
- { embed: input.embed, page: input.page, perPage: input.perPage }
2505
- );
2506
- return { ...data, nextPage };
2567
+ return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
2568
+ embed: input.embed,
2569
+ page: input.page,
2570
+ perPage: input.perPage
2571
+ });
2507
2572
  }
2508
2573
 
2509
2574
  // src/tools/custom-fields.ts
2510
- import { z as z19 } from "zod";
2511
- var CustomFieldEntity = z19.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2512
- var listCustomFieldsSchema = z19.object({
2575
+ import { z as z20 } from "zod";
2576
+ var CustomFieldEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
2577
+ var listCustomFieldsSchema = z20.object({
2513
2578
  entity: CustomFieldEntity
2514
2579
  });
2515
2580
  async function listCustomFields(input) {
@@ -2518,7 +2583,7 @@ async function listCustomFields(input) {
2518
2583
  );
2519
2584
  return data;
2520
2585
  }
2521
- var getCustomFieldSchema = z19.object({
2586
+ var getCustomFieldSchema = z20.object({
2522
2587
  entity: CustomFieldEntity,
2523
2588
  fieldId: positiveId.describe("Custom field definition id.")
2524
2589
  });
@@ -2530,9 +2595,9 @@ async function getCustomField(input) {
2530
2595
  }
2531
2596
 
2532
2597
  // src/tools/tracks.ts
2533
- import { z as z20 } from "zod";
2534
- var TrackEntity = z20.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2535
- var listEntityTracksSchema = z20.object({
2598
+ import { z as z21 } from "zod";
2599
+ var TrackEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
2600
+ var listEntityTracksSchema = z21.object({
2536
2601
  entity: TrackEntity,
2537
2602
  entityId: positiveId
2538
2603
  });
@@ -2542,20 +2607,20 @@ async function listEntityTracks(input) {
2542
2607
  );
2543
2608
  return data;
2544
2609
  }
2545
- var showTrackSchema = z20.object({
2610
+ var showTrackSchema = z21.object({
2546
2611
  trackId: positiveId
2547
2612
  });
2548
2613
  async function showTrack(input) {
2549
2614
  const { data } = await capsuleGet(`/tracks/${input.trackId}`);
2550
2615
  return data;
2551
2616
  }
2552
- var applyTrackSchema = z20.object({
2553
- entity: z20.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2617
+ var applyTrackSchema = z21.object({
2618
+ entity: z21.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
2554
2619
  entityId: positiveId,
2555
2620
  trackDefinitionId: positiveId.describe(
2556
2621
  "The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
2557
2622
  ),
2558
- startDate: z20.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2623
+ startDate: z21.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
2559
2624
  "Optional ISO-8601 date (YYYY-MM-DD) the track should start from \u2014 drives task due-date calculations (each task's `dueOn` is computed as startDate + the track-definition's `daysAfter` offset). Defaults to today if omitted. Useful for scheduling a renewal-queue track against a future contract end-date, or backfilling tracks for historical projects."
2560
2625
  )
2561
2626
  });
@@ -2568,9 +2633,9 @@ async function applyTrack(input) {
2568
2633
  if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
2569
2634
  return capsulePost("/tracks", { track });
2570
2635
  }
2571
- var updateTrackSchema = z20.object({
2636
+ var updateTrackSchema = z21.object({
2572
2637
  trackId: positiveId,
2573
- fields: z20.record(z20.string(), z20.unknown()).describe(
2638
+ fields: z21.record(z21.string(), z21.unknown()).describe(
2574
2639
  "Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
2575
2640
  )
2576
2641
  });
@@ -2582,7 +2647,7 @@ async function updateTrack(input) {
2582
2647
  track: input.fields
2583
2648
  });
2584
2649
  }
2585
- var removeTrackSchema = z20.object({
2650
+ var removeTrackSchema = z21.object({
2586
2651
  trackId: positiveId,
2587
2652
  confirm: confirmFlag().describe(
2588
2653
  "Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
@@ -2600,13 +2665,13 @@ async function removeTrack(input) {
2600
2665
  }
2601
2666
 
2602
2667
  // src/tools/attachments.ts
2603
- import { z as z21 } from "zod";
2668
+ import { z as z22 } from "zod";
2604
2669
  var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
2605
2670
  var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
2606
2671
  var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
2607
- var getAttachmentSchema = z21.object({
2672
+ var getAttachmentSchema = z22.object({
2608
2673
  id: positiveId.describe("Attachment ID."),
2609
- maxSizeBytes: z21.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2674
+ maxSizeBytes: z22.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
2610
2675
  `Refuse to return content over this size (default ${DEFAULT_MAX_SIZE_BYTES} bytes \u2248 5MB; max ${HARD_MAX_SIZE_BYTES} bytes \u2248 25MB). Files exceeding the cap return metadata only with a 'truncated: true' flag.`
2611
2676
  )
2612
2677
  });
@@ -2621,17 +2686,17 @@ async function getAttachment(input) {
2621
2686
  }
2622
2687
  return { contentType, buffer, sizeBytes };
2623
2688
  }
2624
- var uploadAttachmentSchema = z21.object({
2625
- filename: z21.string().min(1).describe(
2689
+ var uploadAttachmentSchema = z22.object({
2690
+ filename: z22.string().min(1).describe(
2626
2691
  "Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes \u2014 a typo in either is accepted and the file is stored as labelled."
2627
2692
  ),
2628
- contentType: z21.string().min(1).describe(
2693
+ contentType: z22.string().min(1).describe(
2629
2694
  "MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes."
2630
2695
  ),
2631
- dataBase64: z21.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2696
+ dataBase64: z22.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
2632
2697
  "File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary."
2633
2698
  ),
2634
- content: z21.string().optional().describe(
2699
+ content: z22.string().optional().describe(
2635
2700
  "Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
2636
2701
  ),
2637
2702
  partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
@@ -2649,12 +2714,7 @@ function decodedBase64Size(s) {
2649
2714
  return s.length / 4 * 3 - padding;
2650
2715
  }
2651
2716
  async function uploadAttachment(input) {
2652
- const linked = [input.partyId, input.opportunityId, input.projectId].filter(Boolean);
2653
- if (linked.length !== 1) {
2654
- throw new Error(
2655
- "upload_attachment: provide exactly one of partyId, opportunityId, or projectId"
2656
- );
2657
- }
2717
+ assertSingleParentRef("upload_attachment", input, { required: true });
2658
2718
  if (!isValidBase64(input.dataBase64)) {
2659
2719
  throw new Error(
2660
2720
  "upload_attachment: dataBase64 is not valid base64 \u2014 Node's tolerant decoder would silently produce corrupt bytes. Verify the encoding (RFC 4648, padded with '=' to a multiple of 4 chars)."
@@ -2679,37 +2739,36 @@ async function uploadAttachment(input) {
2679
2739
  content: input.content ?? "[attachment]",
2680
2740
  attachments: [{ token }]
2681
2741
  };
2682
- if (input.partyId) entryBody["party"] = { id: input.partyId };
2683
- if (input.opportunityId) entryBody["opportunity"] = { id: input.opportunityId };
2684
- if (input.projectId) entryBody["kase"] = { id: input.projectId };
2742
+ setRef(entryBody, "party", input.partyId);
2743
+ setRef(entryBody, "opportunity", input.opportunityId);
2744
+ setRef(entryBody, "kase", input.projectId);
2685
2745
  return capsulePost("/entries", { entry: entryBody });
2686
2746
  }
2687
2747
 
2688
2748
  // src/tools/saved-filters.ts
2689
- import { z as z22 } from "zod";
2690
- var EntitySchema = z22.enum(["parties", "opportunities", "kases"]).describe(
2749
+ import { z as z23 } from "zod";
2750
+ var EntitySchema = z23.enum(["parties", "opportunities", "kases"]).describe(
2691
2751
  "Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
2692
2752
  );
2693
- var listSavedFiltersSchema = z22.object({
2753
+ var listSavedFiltersSchema = z23.object({
2694
2754
  entity: EntitySchema
2695
2755
  });
2696
2756
  async function listSavedFilters(input) {
2697
2757
  const { data } = await capsuleGetCached(`/${input.entity}/filters`);
2698
2758
  return data;
2699
2759
  }
2700
- var runSavedFilterSchema = z22.object({
2760
+ var runSavedFilterSchema = z23.object({
2701
2761
  entity: EntitySchema,
2702
2762
  id: positiveId.describe("The saved filter id (from list_saved_filters)."),
2703
- embed: z22.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2704
- page: z22.number().int().positive().optional().default(1),
2705
- perPage: z22.number().int().min(1).max(100).optional().default(25)
2763
+ embed: z23.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
2764
+ ...paginationFields
2706
2765
  });
2707
2766
  async function runSavedFilter(input) {
2708
- const { data, nextPage } = await capsuleGet(
2709
- `/${input.entity}/filters/${input.id}/results`,
2710
- { page: input.page, perPage: input.perPage, embed: input.embed }
2711
- );
2712
- return { ...data, nextPage };
2767
+ return capsuleGetList(`/${input.entity}/filters/${input.id}/results`, {
2768
+ page: input.page,
2769
+ perPage: input.perPage,
2770
+ embed: input.embed
2771
+ });
2713
2772
  }
2714
2773
 
2715
2774
  // src/server.ts
@@ -2720,7 +2779,7 @@ function createCapsuleMcpServer(opts) {
2720
2779
  const server2 = new McpServer(
2721
2780
  {
2722
2781
  name: "capsulemcp",
2723
- version: "1.7.0",
2782
+ version: "1.8.1",
2724
2783
  description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
2725
2784
  websiteUrl: "https://github.com/soil-dev/capsulemcp",
2726
2785
  icons: ICONS
@@ -3170,20 +3229,72 @@ function createCapsuleMcpServer(opts) {
3170
3229
  listEntriesSchema,
3171
3230
  listEntries
3172
3231
  );
3173
- server2.tool(
3174
- "get_attachment",
3175
- "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.",
3176
- getAttachmentSchema.shape,
3177
- // get_attachment is read-only — downloads a binary, never mutates.
3178
- // Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
3179
- // false}` that `registerTool` applies to every other `get_*` tool.
3180
- // Explicit destructiveHint: false is load-bearing MCP spec
3181
- // defaults destructiveHint to `true`, so omitting it would (in
3182
- // some client implementations) classify this read as destructive.
3183
- { readOnlyHint: true, destructiveHint: false },
3184
- async (input) => {
3185
- const result = await getAttachment(input);
3186
- if (result.truncated) {
3232
+ if (shouldRegister("get_attachment")) {
3233
+ server2.tool(
3234
+ "get_attachment",
3235
+ "Download an attachment by id. Returns image content for image/* types (Claude can describe it natively); decoded text for text/* and application/json (small files); JSON metadata + base64 payload for other binary types (PDF, Office docs, etc.). Files exceeding maxSizeBytes (default 5MB) return metadata only with a `truncated: true` flag.",
3236
+ getAttachmentSchema.shape,
3237
+ // get_attachment is read-only downloads a binary, never mutates.
3238
+ // Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
3239
+ // false}` that `registerTool` applies to every other `get_*` tool.
3240
+ // Explicit destructiveHint: false is load-bearing MCP spec
3241
+ // defaults destructiveHint to `true`, so omitting it would (in
3242
+ // some client implementations) classify this read as destructive.
3243
+ { readOnlyHint: true, destructiveHint: false },
3244
+ async (input) => {
3245
+ const result = await getAttachment(input);
3246
+ if (result.truncated) {
3247
+ return {
3248
+ content: [
3249
+ {
3250
+ type: "text",
3251
+ text: JSON.stringify(
3252
+ {
3253
+ id: input.id,
3254
+ contentType: result.contentType,
3255
+ sizeBytes: result.sizeBytes,
3256
+ truncated: true,
3257
+ message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
3258
+ },
3259
+ null,
3260
+ 2
3261
+ )
3262
+ }
3263
+ ]
3264
+ };
3265
+ }
3266
+ const baseType = result.contentType.split(";")[0].trim().toLowerCase();
3267
+ if (baseType.startsWith("image/")) {
3268
+ return {
3269
+ content: [
3270
+ {
3271
+ type: "image",
3272
+ data: result.buffer.toString("base64"),
3273
+ mimeType: result.contentType
3274
+ }
3275
+ ]
3276
+ };
3277
+ }
3278
+ const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
3279
+ if (isText) {
3280
+ return {
3281
+ content: [
3282
+ {
3283
+ type: "text",
3284
+ text: JSON.stringify(
3285
+ {
3286
+ id: input.id,
3287
+ contentType: result.contentType,
3288
+ sizeBytes: result.sizeBytes
3289
+ },
3290
+ null,
3291
+ 2
3292
+ )
3293
+ },
3294
+ { type: "text", text: result.buffer.toString("utf8") }
3295
+ ]
3296
+ };
3297
+ }
3187
3298
  return {
3188
3299
  content: [
3189
3300
  {
@@ -3193,8 +3304,7 @@ function createCapsuleMcpServer(opts) {
3193
3304
  id: input.id,
3194
3305
  contentType: result.contentType,
3195
3306
  sizeBytes: result.sizeBytes,
3196
- truncated: true,
3197
- message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
3307
+ base64: result.buffer.toString("base64")
3198
3308
  },
3199
3309
  null,
3200
3310
  2
@@ -3203,57 +3313,8 @@ function createCapsuleMcpServer(opts) {
3203
3313
  ]
3204
3314
  };
3205
3315
  }
3206
- const baseType = result.contentType.split(";")[0].trim().toLowerCase();
3207
- if (baseType.startsWith("image/")) {
3208
- return {
3209
- content: [
3210
- {
3211
- type: "image",
3212
- data: result.buffer.toString("base64"),
3213
- mimeType: result.contentType
3214
- }
3215
- ]
3216
- };
3217
- }
3218
- const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
3219
- if (isText) {
3220
- return {
3221
- content: [
3222
- {
3223
- type: "text",
3224
- text: JSON.stringify(
3225
- {
3226
- id: input.id,
3227
- contentType: result.contentType,
3228
- sizeBytes: result.sizeBytes
3229
- },
3230
- null,
3231
- 2
3232
- )
3233
- },
3234
- { type: "text", text: result.buffer.toString("utf8") }
3235
- ]
3236
- };
3237
- }
3238
- return {
3239
- content: [
3240
- {
3241
- type: "text",
3242
- text: JSON.stringify(
3243
- {
3244
- id: input.id,
3245
- contentType: result.contentType,
3246
- sizeBytes: result.sizeBytes,
3247
- base64: result.buffer.toString("base64")
3248
- },
3249
- null,
3250
- 2
3251
- )
3252
- }
3253
- ]
3254
- };
3255
- }
3256
- );
3316
+ );
3317
+ }
3257
3318
  if (!readOnly) {
3258
3319
  registerTool(
3259
3320
  server2,
@@ -3457,11 +3518,28 @@ if (!process.env["CAPSULE_API_TOKEN"]) {
3457
3518
  );
3458
3519
  process.exit(1);
3459
3520
  }
3460
- var server = createCapsuleMcpServer();
3521
+ var STDIO_CLIENT_ID = "stdio-local";
3522
+ var server = createCapsuleMcpServer({ clientId: STDIO_CLIENT_ID });
3461
3523
  var transport = new StdioServerTransport();
3462
3524
  if (isReadOnly()) {
3463
3525
  console.error("[capsulemcp] read-only mode: write/delete tools are not registered");
3464
3526
  }
3527
+ function exitOnDisconnect() {
3528
+ let exiting = false;
3529
+ const die = () => {
3530
+ if (exiting) return;
3531
+ exiting = true;
3532
+ process.exit(0);
3533
+ };
3534
+ process.stdin.on("end", die);
3535
+ process.stdin.on("close", die);
3536
+ process.stdin.on("error", die);
3537
+ process.stdout.on("error", die);
3538
+ const orphanCheck = setInterval(() => {
3539
+ if (process.ppid === 1) die();
3540
+ }, 3e4);
3541
+ orphanCheck.unref?.();
3542
+ }
3465
3543
  try {
3466
3544
  await server.connect(transport);
3467
3545
  } catch (err) {
@@ -3469,3 +3547,4 @@ try {
3469
3547
  console.error(`[capsulemcp] Failed to start: ${message}`);
3470
3548
  process.exit(1);
3471
3549
  }
3550
+ exitOnDisconnect();