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