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/http.js
CHANGED
|
@@ -271,38 +271,31 @@ async function parseErrorBody(res) {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
274
|
-
function
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const controller = new AbortController();
|
|
280
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
281
|
-
timer.unref();
|
|
282
|
-
return {
|
|
283
|
-
options: { ...options ?? {}, signal: controller.signal },
|
|
284
|
-
cleanup: () => clearTimeout(timer)
|
|
285
|
-
};
|
|
274
|
+
function isTimeoutAbort(err) {
|
|
275
|
+
return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
|
|
276
|
+
// "TimeoutError"; plain aborts (and older undici paths) surface
|
|
277
|
+
// as "AbortError" or carry "aborted" in the message.
|
|
278
|
+
(err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
|
|
286
279
|
}
|
|
287
280
|
async function mapAbort(p) {
|
|
288
281
|
try {
|
|
289
282
|
return await p;
|
|
290
283
|
} catch (err) {
|
|
291
|
-
if (
|
|
284
|
+
if (isTimeoutAbort(err)) {
|
|
292
285
|
throw new CapsuleTimeoutError();
|
|
293
286
|
}
|
|
294
287
|
throw err;
|
|
295
288
|
}
|
|
296
289
|
}
|
|
297
290
|
async function fetchWithTimeout(url, options) {
|
|
298
|
-
const { options: opts, cleanup } = withTimeout(options);
|
|
299
291
|
const startedAt = Date.now();
|
|
300
292
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
293
|
+
return await fetch(url, {
|
|
294
|
+
...options ?? {},
|
|
295
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
296
|
+
});
|
|
303
297
|
} catch (err) {
|
|
304
|
-
|
|
305
|
-
const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
|
|
298
|
+
const isAbort = isTimeoutAbort(err);
|
|
306
299
|
emitCapsuleFailure(
|
|
307
300
|
options?.method ?? "GET",
|
|
308
301
|
url,
|
|
@@ -326,24 +319,22 @@ async function doFetch(url, options) {
|
|
|
326
319
|
const startedAt = Date.now();
|
|
327
320
|
const method = options?.method ?? "GET";
|
|
328
321
|
const first = await fetchWithTimeout(url, options);
|
|
329
|
-
if (first.
|
|
330
|
-
const delay = parseRateLimitDelay(first
|
|
331
|
-
first
|
|
332
|
-
await drainBody(first.res);
|
|
322
|
+
if (first.status === 429) {
|
|
323
|
+
const delay = parseRateLimitDelay(first);
|
|
324
|
+
await drainBody(first);
|
|
333
325
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
334
326
|
const retried = await fetchWithTimeout(url, options);
|
|
335
|
-
if (retried.
|
|
336
|
-
retried
|
|
337
|
-
await drainBody(retried.res);
|
|
327
|
+
if (retried.status === 429) {
|
|
328
|
+
await drainBody(retried);
|
|
338
329
|
emitCapsuleRateLimited(method, url, Date.now() - startedAt);
|
|
339
330
|
throw new CapsuleApiError(
|
|
340
331
|
429,
|
|
341
332
|
"Rate limit exceeded after one retry. Please slow down your requests."
|
|
342
333
|
);
|
|
343
334
|
}
|
|
344
|
-
return {
|
|
335
|
+
return { res: retried, startedAt, method, url, retriedAfter429: true };
|
|
345
336
|
}
|
|
346
|
-
return {
|
|
337
|
+
return { res: first, startedAt, method, url, retriedAfter429: false };
|
|
347
338
|
}
|
|
348
339
|
async function consumeBody(start, body) {
|
|
349
340
|
try {
|
|
@@ -453,15 +444,19 @@ async function capsuleGet(path, params) {
|
|
|
453
444
|
const token = getToken();
|
|
454
445
|
const url = buildUrl(path, params);
|
|
455
446
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
447
|
+
return consumeBody(start, async () => {
|
|
448
|
+
const data = await handleResponse(start.res);
|
|
449
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
450
|
+
return { data, nextPage };
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async function capsuleGetList(path, params) {
|
|
454
|
+
const { data, nextPage } = await capsuleGet(path, params);
|
|
455
|
+
return { ...data, nextPage };
|
|
456
|
+
}
|
|
457
|
+
async function capsuleGetCachedList(path, params) {
|
|
458
|
+
const { data, nextPage } = await capsuleGetCached(path, params);
|
|
459
|
+
return { ...data, nextPage };
|
|
465
460
|
}
|
|
466
461
|
async function capsuleGetCached(path, params) {
|
|
467
462
|
if (cacheDisabled()) return capsuleGet(path, params);
|
|
@@ -500,11 +495,7 @@ async function capsulePost(path, body) {
|
|
|
500
495
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
501
496
|
body: JSON.stringify(body)
|
|
502
497
|
});
|
|
503
|
-
|
|
504
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
505
|
-
} finally {
|
|
506
|
-
start.cleanup();
|
|
507
|
-
}
|
|
498
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
508
499
|
}
|
|
509
500
|
async function capsulePostNoContent(path) {
|
|
510
501
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -514,15 +505,11 @@ async function capsulePostNoContent(path) {
|
|
|
514
505
|
method: "POST",
|
|
515
506
|
headers: baseHeaders(token)
|
|
516
507
|
});
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
});
|
|
523
|
-
} finally {
|
|
524
|
-
start.cleanup();
|
|
525
|
-
}
|
|
508
|
+
await consumeBody(start, async () => {
|
|
509
|
+
if (start.res.status === 204) return;
|
|
510
|
+
await throwForStatus(start.res);
|
|
511
|
+
await mapAbort(start.res.text());
|
|
512
|
+
});
|
|
526
513
|
}
|
|
527
514
|
async function capsuleSearch(path, body, params) {
|
|
528
515
|
const token = getToken();
|
|
@@ -532,15 +519,11 @@ async function capsuleSearch(path, body, params) {
|
|
|
532
519
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
533
520
|
body: JSON.stringify(body)
|
|
534
521
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
});
|
|
541
|
-
} finally {
|
|
542
|
-
start.cleanup();
|
|
543
|
-
}
|
|
522
|
+
return consumeBody(start, async () => {
|
|
523
|
+
const data = await handleResponse(start.res);
|
|
524
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
525
|
+
return { data, nextPage };
|
|
526
|
+
});
|
|
544
527
|
}
|
|
545
528
|
async function capsulePut(path, body) {
|
|
546
529
|
if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
|
|
@@ -551,68 +534,60 @@ async function capsulePut(path, body) {
|
|
|
551
534
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
552
535
|
body: JSON.stringify(body)
|
|
553
536
|
});
|
|
554
|
-
|
|
555
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
556
|
-
} finally {
|
|
557
|
-
start.cleanup();
|
|
558
|
-
}
|
|
537
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
559
538
|
}
|
|
560
539
|
async function capsuleGetBinary(path, maxBytes) {
|
|
561
540
|
const token = getToken();
|
|
562
541
|
const url = buildUrl(path);
|
|
563
542
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
543
|
+
return consumeBody(start, async () => {
|
|
544
|
+
const res = start.res;
|
|
545
|
+
await throwForStatus(res);
|
|
546
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
547
|
+
const declared = res.headers.get("Content-Length");
|
|
548
|
+
const declaredBytes = declared ? Number(declared) : NaN;
|
|
549
|
+
if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
550
|
+
if (res.body) await res.body.cancel().catch(() => {
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
contentType,
|
|
554
|
+
buffer: Buffer.alloc(0),
|
|
555
|
+
truncated: true,
|
|
556
|
+
sizeBytes: declaredBytes
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (maxBytes !== void 0 && res.body) {
|
|
560
|
+
const reader = res.body.getReader();
|
|
561
|
+
const chunks = [];
|
|
562
|
+
let total = 0;
|
|
563
|
+
let truncated = false;
|
|
564
|
+
while (true) {
|
|
565
|
+
const { done, value } = await mapAbort(reader.read());
|
|
566
|
+
if (done) break;
|
|
567
|
+
total += value.byteLength;
|
|
568
|
+
if (total > maxBytes) {
|
|
569
|
+
truncated = true;
|
|
570
|
+
await reader.cancel().catch(() => {
|
|
571
|
+
});
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
chunks.push(value);
|
|
575
|
+
}
|
|
576
|
+
if (truncated) {
|
|
574
577
|
return {
|
|
575
578
|
contentType,
|
|
576
579
|
buffer: Buffer.alloc(0),
|
|
577
580
|
truncated: true,
|
|
578
|
-
sizeBytes:
|
|
581
|
+
sizeBytes: total
|
|
579
582
|
};
|
|
580
583
|
}
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (done) break;
|
|
589
|
-
total += value.byteLength;
|
|
590
|
-
if (total > maxBytes) {
|
|
591
|
-
truncated = true;
|
|
592
|
-
await reader.cancel().catch(() => {
|
|
593
|
-
});
|
|
594
|
-
break;
|
|
595
|
-
}
|
|
596
|
-
chunks.push(value);
|
|
597
|
-
}
|
|
598
|
-
if (truncated) {
|
|
599
|
-
return {
|
|
600
|
-
contentType,
|
|
601
|
-
buffer: Buffer.alloc(0),
|
|
602
|
-
truncated: true,
|
|
603
|
-
sizeBytes: total
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
607
|
-
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
608
|
-
}
|
|
609
|
-
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
610
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
611
|
-
return { contentType, buffer, sizeBytes: buffer.length };
|
|
612
|
-
});
|
|
613
|
-
} finally {
|
|
614
|
-
start.cleanup();
|
|
615
|
-
}
|
|
584
|
+
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
585
|
+
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
586
|
+
}
|
|
587
|
+
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
588
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
589
|
+
return { contentType, buffer, sizeBytes: buffer.length };
|
|
590
|
+
});
|
|
616
591
|
}
|
|
617
592
|
async function capsulePostBinary(path, body, contentType, filename) {
|
|
618
593
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -628,11 +603,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
|
|
|
628
603
|
},
|
|
629
604
|
body
|
|
630
605
|
});
|
|
631
|
-
|
|
632
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
633
|
-
} finally {
|
|
634
|
-
start.cleanup();
|
|
635
|
-
}
|
|
606
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
636
607
|
}
|
|
637
608
|
async function capsuleDelete(path) {
|
|
638
609
|
if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
|
|
@@ -642,15 +613,11 @@ async function capsuleDelete(path) {
|
|
|
642
613
|
method: "DELETE",
|
|
643
614
|
headers: baseHeaders(token)
|
|
644
615
|
});
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
});
|
|
651
|
-
} finally {
|
|
652
|
-
start.cleanup();
|
|
653
|
-
}
|
|
616
|
+
await consumeBody(start, async () => {
|
|
617
|
+
if (start.res.status === 204) return;
|
|
618
|
+
await throwForStatus(start.res);
|
|
619
|
+
await mapAbort(start.res.text());
|
|
620
|
+
});
|
|
654
621
|
}
|
|
655
622
|
|
|
656
623
|
// src/auth/provider.ts
|
|
@@ -1171,6 +1138,44 @@ var ICONS = [
|
|
|
1171
1138
|
}
|
|
1172
1139
|
];
|
|
1173
1140
|
|
|
1141
|
+
// src/server/tier.ts
|
|
1142
|
+
var CORE_TOOLS = /* @__PURE__ */ new Set([
|
|
1143
|
+
// Parties
|
|
1144
|
+
"search_parties",
|
|
1145
|
+
"filter_parties",
|
|
1146
|
+
"get_party",
|
|
1147
|
+
"create_party",
|
|
1148
|
+
"update_party",
|
|
1149
|
+
"list_party_entries",
|
|
1150
|
+
// Opportunities
|
|
1151
|
+
"search_opportunities",
|
|
1152
|
+
"filter_opportunities",
|
|
1153
|
+
"get_opportunity",
|
|
1154
|
+
"create_opportunity",
|
|
1155
|
+
"update_opportunity",
|
|
1156
|
+
// Projects
|
|
1157
|
+
"filter_projects",
|
|
1158
|
+
"list_projects",
|
|
1159
|
+
"get_project",
|
|
1160
|
+
"create_project",
|
|
1161
|
+
"update_project",
|
|
1162
|
+
// Tasks
|
|
1163
|
+
"list_tasks",
|
|
1164
|
+
"get_task",
|
|
1165
|
+
"create_task",
|
|
1166
|
+
"update_task",
|
|
1167
|
+
"complete_task",
|
|
1168
|
+
// Timeline + tags + identity
|
|
1169
|
+
"add_note",
|
|
1170
|
+
"list_tags",
|
|
1171
|
+
"add_tag",
|
|
1172
|
+
"get_current_user"
|
|
1173
|
+
]);
|
|
1174
|
+
function shouldRegister(name) {
|
|
1175
|
+
if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
|
|
1176
|
+
return CORE_TOOLS.has(name);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1174
1179
|
// src/tasks/store.ts
|
|
1175
1180
|
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
1176
1181
|
import {
|
|
@@ -1379,27 +1384,25 @@ function wrapAsText(result) {
|
|
|
1379
1384
|
};
|
|
1380
1385
|
}
|
|
1381
1386
|
function registerTool(server, name, description, schema, handler) {
|
|
1387
|
+
if (!shouldRegister(name)) return;
|
|
1382
1388
|
const registerWithSchema = server.registerTool.bind(server);
|
|
1383
1389
|
const annotations = inferAnnotations(name);
|
|
1384
|
-
registerWithSchema(
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
} catch (err) {
|
|
1396
|
-
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
1397
|
-
throw err;
|
|
1398
|
-
}
|
|
1390
|
+
registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
|
|
1391
|
+
const startedAt = Date.now();
|
|
1392
|
+
const argFields = argFieldNames(input);
|
|
1393
|
+
const clientId = getRequestContext()?.clientId;
|
|
1394
|
+
try {
|
|
1395
|
+
const result = await handler(input);
|
|
1396
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
|
|
1397
|
+
return wrapAsText(result);
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
1400
|
+
throw err;
|
|
1399
1401
|
}
|
|
1400
|
-
);
|
|
1402
|
+
});
|
|
1401
1403
|
}
|
|
1402
1404
|
function registerToolTask(server, name, description, schema, handler) {
|
|
1405
|
+
if (!shouldRegister(name)) return;
|
|
1403
1406
|
const registerWithSchema = server.experimental.tasks.registerToolTask.bind(
|
|
1404
1407
|
server.experimental.tasks
|
|
1405
1408
|
);
|
|
@@ -1410,7 +1413,7 @@ function registerToolTask(server, name, description, schema, handler) {
|
|
|
1410
1413
|
description,
|
|
1411
1414
|
inputSchema: schema,
|
|
1412
1415
|
execution: { taskSupport: "optional" },
|
|
1413
|
-
|
|
1416
|
+
annotations
|
|
1414
1417
|
},
|
|
1415
1418
|
{
|
|
1416
1419
|
createTask: async (input, extra) => {
|
|
@@ -1470,7 +1473,7 @@ function registerToolTask(server, name, description, schema, handler) {
|
|
|
1470
1473
|
}
|
|
1471
1474
|
|
|
1472
1475
|
// src/tools/parties.ts
|
|
1473
|
-
import { z as
|
|
1476
|
+
import { z as z8 } from "zod";
|
|
1474
1477
|
|
|
1475
1478
|
// src/tools/body-helpers.ts
|
|
1476
1479
|
function setRef(body, key, id) {
|
|
@@ -1480,9 +1483,22 @@ function setNullableRef(body, key, id) {
|
|
|
1480
1483
|
if (id === null) body[key] = null;
|
|
1481
1484
|
else if (id !== void 0) body[key] = { id };
|
|
1482
1485
|
}
|
|
1486
|
+
function assertSingleParentRef(toolName, refs, opts = {}) {
|
|
1487
|
+
const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
|
|
1488
|
+
(v) => typeof v === "number"
|
|
1489
|
+
).length;
|
|
1490
|
+
if (opts.required && set !== 1) {
|
|
1491
|
+
throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
|
|
1492
|
+
}
|
|
1493
|
+
if (set > 1) {
|
|
1494
|
+
throw new Error(
|
|
1495
|
+
`${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1483
1499
|
|
|
1484
1500
|
// src/tools/define-batch.ts
|
|
1485
|
-
import { z as
|
|
1501
|
+
import { z as z3 } from "zod";
|
|
1486
1502
|
|
|
1487
1503
|
// src/capsule/batch.ts
|
|
1488
1504
|
function chunk(arr, size) {
|
|
@@ -1501,37 +1517,43 @@ function getBatchConcurrency() {
|
|
|
1501
1517
|
MAX_CONCURRENCY
|
|
1502
1518
|
);
|
|
1503
1519
|
}
|
|
1504
|
-
async function
|
|
1505
|
-
const concurrency = getBatchConcurrency();
|
|
1520
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1506
1521
|
const results = new Array(items.length);
|
|
1507
|
-
const startedAt = Date.now();
|
|
1508
|
-
const signal = options.signal;
|
|
1509
1522
|
let cursor = 0;
|
|
1510
1523
|
async function worker() {
|
|
1511
1524
|
while (true) {
|
|
1512
1525
|
const i = cursor;
|
|
1513
1526
|
cursor += 1;
|
|
1514
1527
|
if (i >= items.length) return;
|
|
1515
|
-
|
|
1516
|
-
results[i] = {
|
|
1517
|
-
ok: false,
|
|
1518
|
-
error: { message: "cancelled by tasks/cancel" }
|
|
1519
|
-
};
|
|
1520
|
-
continue;
|
|
1521
|
-
}
|
|
1522
|
-
try {
|
|
1523
|
-
const result = await action(items[i], i);
|
|
1524
|
-
results[i] = { ok: true, result };
|
|
1525
|
-
} catch (err) {
|
|
1526
|
-
results[i] = { ok: false, error: extractError(err) };
|
|
1527
|
-
}
|
|
1528
|
+
results[i] = await fn(items[i], i);
|
|
1528
1529
|
}
|
|
1529
1530
|
}
|
|
1530
1531
|
const workers = [];
|
|
1531
|
-
for (let w = 0; w < Math.min(
|
|
1532
|
+
for (let w = 0; w < Math.min(limit, items.length); w++) {
|
|
1532
1533
|
workers.push(worker());
|
|
1533
1534
|
}
|
|
1534
1535
|
await Promise.all(workers);
|
|
1536
|
+
return results;
|
|
1537
|
+
}
|
|
1538
|
+
async function batchExecute(tool, items, action, options = {}) {
|
|
1539
|
+
const concurrency = getBatchConcurrency();
|
|
1540
|
+
const startedAt = Date.now();
|
|
1541
|
+
const signal = options.signal;
|
|
1542
|
+
const results = await mapWithConcurrency(
|
|
1543
|
+
items,
|
|
1544
|
+
concurrency,
|
|
1545
|
+
async (item, i) => {
|
|
1546
|
+
if (signal?.aborted) {
|
|
1547
|
+
return { ok: false, error: { message: "cancelled by tasks/cancel" } };
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const result = await action(item, i);
|
|
1551
|
+
return { ok: true, result };
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
return { ok: false, error: extractError(err) };
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1535
1557
|
const succeeded = results.filter((r) => r.ok).length;
|
|
1536
1558
|
const failed = results.length - succeeded;
|
|
1537
1559
|
const summary = { total: results.length, succeeded, failed };
|
|
@@ -1576,10 +1598,52 @@ function topFailureReasons(results, n) {
|
|
|
1576
1598
|
return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
|
|
1577
1599
|
}
|
|
1578
1600
|
|
|
1601
|
+
// src/tools/strip-descriptions.ts
|
|
1602
|
+
import { z as z2 } from "zod";
|
|
1603
|
+
function cloneWithDef(node, patch) {
|
|
1604
|
+
const def = node.def;
|
|
1605
|
+
return node.clone({ ...def, ...patch });
|
|
1606
|
+
}
|
|
1607
|
+
function stripDescriptions(schema) {
|
|
1608
|
+
let node = schema;
|
|
1609
|
+
if (node instanceof z2.ZodObject) {
|
|
1610
|
+
const shape = node.def.shape;
|
|
1611
|
+
const next = {};
|
|
1612
|
+
let changed = false;
|
|
1613
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
1614
|
+
next[key] = stripDescriptions(child);
|
|
1615
|
+
if (next[key] !== child) changed = true;
|
|
1616
|
+
}
|
|
1617
|
+
if (changed) node = cloneWithDef(node, { shape: next });
|
|
1618
|
+
} else if (node instanceof z2.ZodArray) {
|
|
1619
|
+
const element = stripDescriptions(node.def.element);
|
|
1620
|
+
if (element !== node.def.element) node = cloneWithDef(node, { element });
|
|
1621
|
+
} else if (node instanceof z2.ZodOptional || node instanceof z2.ZodNullable || node instanceof z2.ZodDefault || node instanceof z2.ZodReadonly) {
|
|
1622
|
+
const innerType = stripDescriptions(node.def.innerType);
|
|
1623
|
+
if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
|
|
1624
|
+
} else if (node instanceof z2.ZodUnion) {
|
|
1625
|
+
const options = node.def.options.map(stripDescriptions);
|
|
1626
|
+
if (options.some((o, i) => o !== node.def.options[i])) {
|
|
1627
|
+
node = cloneWithDef(node, { options });
|
|
1628
|
+
}
|
|
1629
|
+
} else if (node instanceof z2.ZodPipe) {
|
|
1630
|
+
const inSchema = stripDescriptions(node.def.in);
|
|
1631
|
+
const outSchema = stripDescriptions(node.def.out);
|
|
1632
|
+
if (inSchema !== node.def.in || outSchema !== node.def.out) {
|
|
1633
|
+
node = cloneWithDef(node, { in: inSchema, out: outSchema });
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (node.description !== void 0) {
|
|
1637
|
+
node = node.meta({ description: void 0 });
|
|
1638
|
+
}
|
|
1639
|
+
return node;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1579
1642
|
// src/tools/define-batch.ts
|
|
1580
1643
|
function defineBatch(args) {
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1644
|
+
const itemSchema = stripDescriptions(args.itemSchema);
|
|
1645
|
+
const schema = z3.object({
|
|
1646
|
+
items: z3.array(itemSchema).min(1).max(50).describe(args.itemDescription)
|
|
1583
1647
|
});
|
|
1584
1648
|
async function handler(input, opts = {}) {
|
|
1585
1649
|
return batchExecute(args.toolName, input.items, args.itemHandler, opts);
|
|
@@ -1592,22 +1656,30 @@ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'"
|
|
|
1592
1656
|
var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
|
|
1593
1657
|
|
|
1594
1658
|
// src/tools/define-delete.ts
|
|
1595
|
-
import { z as
|
|
1659
|
+
import { z as z6 } from "zod";
|
|
1596
1660
|
|
|
1597
1661
|
// src/tools/confirm-flag.ts
|
|
1598
|
-
import { z as
|
|
1662
|
+
import { z as z4 } from "zod";
|
|
1599
1663
|
var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
|
|
1600
1664
|
function confirmFlag() {
|
|
1601
|
-
return
|
|
1665
|
+
return z4.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
|
|
1602
1666
|
}
|
|
1603
1667
|
|
|
1604
1668
|
// src/tools/shared-schemas.ts
|
|
1605
|
-
import { z as
|
|
1606
|
-
var positiveId =
|
|
1669
|
+
import { z as z5 } from "zod";
|
|
1670
|
+
var positiveId = z5.preprocess((input) => {
|
|
1607
1671
|
if (typeof input !== "string") return input;
|
|
1608
1672
|
const trimmed = input.trim();
|
|
1609
1673
|
return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
|
|
1610
|
-
},
|
|
1674
|
+
}, z5.number().int().positive());
|
|
1675
|
+
var paginationFields = {
|
|
1676
|
+
page: z5.number().int().positive().optional().default(1),
|
|
1677
|
+
perPage: z5.number().int().min(1).max(100).optional().default(25)
|
|
1678
|
+
};
|
|
1679
|
+
var paginationFieldsNoDefaults = {
|
|
1680
|
+
page: z5.number().int().positive().optional(),
|
|
1681
|
+
perPage: z5.number().int().min(1).max(100).optional()
|
|
1682
|
+
};
|
|
1611
1683
|
|
|
1612
1684
|
// src/capsule/idempotent.ts
|
|
1613
1685
|
var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
|
|
@@ -1634,7 +1706,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
|
|
|
1634
1706
|
// src/tools/define-delete.ts
|
|
1635
1707
|
function defineDelete(args) {
|
|
1636
1708
|
const { toolName, pathPrefix, confirmHint, idDescription } = args;
|
|
1637
|
-
const schema =
|
|
1709
|
+
const schema = z6.object({
|
|
1638
1710
|
id: idDescription ? positiveId.describe(idDescription) : positiveId,
|
|
1639
1711
|
confirm: confirmFlag().describe(confirmHint)
|
|
1640
1712
|
});
|
|
@@ -1682,12 +1754,12 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
|
|
|
1682
1754
|
}
|
|
1683
1755
|
|
|
1684
1756
|
// src/tools/custom-field-helpers.ts
|
|
1685
|
-
import { z as
|
|
1686
|
-
var CustomFieldWriteSchema =
|
|
1757
|
+
import { z as z7 } from "zod";
|
|
1758
|
+
var CustomFieldWriteSchema = z7.object({
|
|
1687
1759
|
definitionId: positiveId.describe(
|
|
1688
1760
|
"The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
|
|
1689
1761
|
),
|
|
1690
|
-
value:
|
|
1762
|
+
value: z7.union([z7.string(), z7.number(), z7.boolean(), z7.null()]).describe(
|
|
1691
1763
|
"The new value. String for TEXT / DATE / LIST / LARGE_TEXT / LINK fields, number for NUMBER fields, boolean for BOOLEAN fields. Clearing: pass null for TEXT / NUMBER / DATE / LIST (Capsule removes the row). BOOLEAN does NOT accept null (Capsule returns 422 'invalid type for field'); use `value: false` instead. Note BOOLEAN fields are observably **two-state**: a row exists with `value: true`, or no row exists. Setting `value: false` removes the row entirely \u2014 readers should treat absent BOOLEAN rows as equivalent to false. Tri-state BOOLEAN semantics (true / false / unknown) are not achievable through Capsule's API. Audit-log noise: sending value=null on a field that's already empty/cleared is accepted by Capsule but still bumps the parent entity's `updatedAt`. Read the current value via embed='fields' first if `updatedAt` is being used as a 'last meaningful change' signal. NUMBER quirks: Capsule stores numerics correctly but the read-back via embed=fields returns them as STRINGS (e.g. value=3 reads as '3'); callers comparing values must coerce. TEXT quirks: value='' has the same observable effect as value=null (row removed); empty-string and never-set are indistinguishable."
|
|
1692
1764
|
)
|
|
1693
1765
|
});
|
|
@@ -1703,24 +1775,24 @@ function mapFieldsForBody(fields) {
|
|
|
1703
1775
|
}
|
|
1704
1776
|
|
|
1705
1777
|
// src/tools/parties.ts
|
|
1706
|
-
var EmailAddressSchema =
|
|
1707
|
-
address:
|
|
1708
|
-
type:
|
|
1778
|
+
var EmailAddressSchema = z8.object({
|
|
1779
|
+
address: z8.string().email(),
|
|
1780
|
+
type: z8.string().optional()
|
|
1709
1781
|
});
|
|
1710
|
-
var PhoneNumberSchema =
|
|
1782
|
+
var PhoneNumberSchema = z8.object({
|
|
1711
1783
|
// Capsule rejects empty strings with `phoneNumber.number: number is
|
|
1712
1784
|
// required`. Enforce at the schema layer to catch typos pre-call,
|
|
1713
1785
|
// matching how EmailAddressSchema's address field behaves.
|
|
1714
|
-
number:
|
|
1715
|
-
type:
|
|
1786
|
+
number: z8.string().min(1),
|
|
1787
|
+
type: z8.string().optional()
|
|
1716
1788
|
});
|
|
1717
1789
|
var CountryDescription = "Country name. Capsule validates this against a small canonical-English-name dictionary; inputs not in the dictionary are REJECTED with 422 'address.country: unknown country' (NOT silently passed through or normalised). Probed examples \u2014 accepted: `United States`, `United Kingdom`, `Czechia`, `Germany`. Aliased: `USA \u2192 United States`. Rejected: `United States of America`, `Czech Republic` (use `Czechia`), `UK`/`Britain` (use `United Kingdom`), `Deutschland` (use `Germany`). Empty string is accepted and stored as `null` \u2014 a de-facto 'clear' shape. To discover an accepted name, read an existing party that already has the country set.";
|
|
1718
|
-
var AddressSchema =
|
|
1719
|
-
street:
|
|
1720
|
-
city:
|
|
1721
|
-
state:
|
|
1722
|
-
country:
|
|
1723
|
-
zip:
|
|
1790
|
+
var AddressSchema = z8.object({
|
|
1791
|
+
street: z8.string().optional(),
|
|
1792
|
+
city: z8.string().optional(),
|
|
1793
|
+
state: z8.string().optional(),
|
|
1794
|
+
country: z8.string().optional().describe(CountryDescription),
|
|
1795
|
+
zip: z8.string().optional()
|
|
1724
1796
|
});
|
|
1725
1797
|
function validateWebsiteAddress(data, ctx) {
|
|
1726
1798
|
const isUrlService = data.service === void 0 || data.service === "URL";
|
|
@@ -1743,7 +1815,7 @@ function validateWebsiteAddress(data, ctx) {
|
|
|
1743
1815
|
});
|
|
1744
1816
|
}
|
|
1745
1817
|
}
|
|
1746
|
-
var WebsiteServiceEnum =
|
|
1818
|
+
var WebsiteServiceEnum = z8.enum([
|
|
1747
1819
|
"URL",
|
|
1748
1820
|
"SKYPE",
|
|
1749
1821
|
"TWITTER",
|
|
@@ -1762,33 +1834,31 @@ var WebsiteServiceEnum = z7.enum([
|
|
|
1762
1834
|
"BLUESKY",
|
|
1763
1835
|
"SNAPCHAT"
|
|
1764
1836
|
]);
|
|
1765
|
-
var WebsiteSchema =
|
|
1766
|
-
address:
|
|
1837
|
+
var WebsiteSchema = z8.object({
|
|
1838
|
+
address: z8.string().min(1).describe(
|
|
1767
1839
|
"The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services like 'TWITTER', 'INSTAGRAM'. Capsule names this field `address` regardless of service type."
|
|
1768
1840
|
),
|
|
1769
1841
|
service: WebsiteServiceEnum.optional().describe(
|
|
1770
1842
|
"Service type. One of: URL, SKYPE, TWITTER, LINKED_IN, FACEBOOK, XING, FEED, GOOGLE_PLUS, FLICKR, GITHUB, YOUTUBE, INSTAGRAM, PINTEREST, TIKTOK, THREADS, BLUESKY, SNAPCHAT. Defaults to 'URL' if omitted."
|
|
1771
1843
|
)
|
|
1772
1844
|
}).superRefine(validateWebsiteAddress);
|
|
1773
|
-
var searchPartiesSchema =
|
|
1774
|
-
q:
|
|
1775
|
-
embed:
|
|
1776
|
-
|
|
1777
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1845
|
+
var searchPartiesSchema = z8.object({
|
|
1846
|
+
q: z8.string().optional().describe("Free-text search query"),
|
|
1847
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
1848
|
+
...paginationFields
|
|
1778
1849
|
});
|
|
1779
1850
|
async function searchParties(input) {
|
|
1780
1851
|
const path = input.q ? "/parties/search" : "/parties";
|
|
1781
|
-
|
|
1852
|
+
return capsuleGetList(path, {
|
|
1782
1853
|
q: input.q,
|
|
1783
1854
|
embed: input.embed,
|
|
1784
1855
|
page: input.page,
|
|
1785
1856
|
perPage: input.perPage
|
|
1786
1857
|
});
|
|
1787
|
-
return { ...data, nextPage };
|
|
1788
1858
|
}
|
|
1789
|
-
var getPartySchema =
|
|
1859
|
+
var getPartySchema = z8.object({
|
|
1790
1860
|
id: positiveId.describe("Party ID"),
|
|
1791
|
-
embed:
|
|
1861
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1792
1862
|
});
|
|
1793
1863
|
async function getParty(input) {
|
|
1794
1864
|
const { data } = await capsuleGet(`/parties/${input.id}`, {
|
|
@@ -1796,51 +1866,47 @@ async function getParty(input) {
|
|
|
1796
1866
|
});
|
|
1797
1867
|
return data;
|
|
1798
1868
|
}
|
|
1799
|
-
var getPartiesSchema =
|
|
1800
|
-
ids:
|
|
1869
|
+
var getPartiesSchema = z8.object({
|
|
1870
|
+
ids: z8.array(positiveId).min(1).max(50).describe(
|
|
1801
1871
|
"Array of party IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel. Result shape is identical regardless of input size."
|
|
1802
1872
|
),
|
|
1803
|
-
embed:
|
|
1873
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1804
1874
|
});
|
|
1805
1875
|
async function getParties(input) {
|
|
1806
1876
|
return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
|
|
1807
1877
|
}
|
|
1808
|
-
var listPartyOpportunitiesSchema =
|
|
1878
|
+
var listPartyOpportunitiesSchema = z8.object({
|
|
1809
1879
|
partyId: positiveId,
|
|
1810
|
-
|
|
1811
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1880
|
+
...paginationFields
|
|
1812
1881
|
});
|
|
1813
1882
|
async function listPartyOpportunities(input) {
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
);
|
|
1818
|
-
return { ...data, nextPage };
|
|
1883
|
+
return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
|
|
1884
|
+
page: input.page,
|
|
1885
|
+
perPage: input.perPage
|
|
1886
|
+
});
|
|
1819
1887
|
}
|
|
1820
|
-
var listPartyProjectsSchema =
|
|
1888
|
+
var listPartyProjectsSchema = z8.object({
|
|
1821
1889
|
partyId: positiveId,
|
|
1822
|
-
|
|
1823
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1890
|
+
...paginationFields
|
|
1824
1891
|
});
|
|
1825
1892
|
async function listPartyProjects(input) {
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
);
|
|
1830
|
-
return { ...data, nextPage };
|
|
1893
|
+
return capsuleGetList(`/parties/${input.partyId}/kases`, {
|
|
1894
|
+
page: input.page,
|
|
1895
|
+
perPage: input.perPage
|
|
1896
|
+
});
|
|
1831
1897
|
}
|
|
1832
1898
|
var PartyWriteBaseSchema = {
|
|
1833
|
-
about:
|
|
1834
|
-
emailAddresses:
|
|
1899
|
+
about: z8.string().optional(),
|
|
1900
|
+
emailAddresses: z8.array(EmailAddressSchema).optional().describe(
|
|
1835
1901
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_email_address and remove_party_email_address_by_id. Passing `[]` here is a silent no-op (does not clear the list and does not advance updatedAt)."
|
|
1836
1902
|
),
|
|
1837
|
-
phoneNumbers:
|
|
1903
|
+
phoneNumbers: z8.array(PhoneNumberSchema).optional().describe(
|
|
1838
1904
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_phone_number and remove_party_phone_number_by_id."
|
|
1839
1905
|
),
|
|
1840
|
-
addresses:
|
|
1906
|
+
addresses: z8.array(AddressSchema).optional().describe(
|
|
1841
1907
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_address and remove_party_address_by_id. The `country` field is mapped through Capsule's country dictionary \u2014 see `add_party_address.country` for the dictionary edges (small canonical-English-name list; inputs not in the dictionary are REJECTED with 422, not silently dropped)."
|
|
1842
1908
|
),
|
|
1843
|
-
websites:
|
|
1909
|
+
websites: z8.array(WebsiteSchema).optional().describe(
|
|
1844
1910
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
|
|
1845
1911
|
),
|
|
1846
1912
|
ownerId: positiveId.nullable().optional().describe(
|
|
@@ -1850,16 +1916,16 @@ var PartyWriteBaseSchema = {
|
|
|
1850
1916
|
"Assign to team ID (discover via list_teams). Pass a team ID to set, or `null` to unassign. Capsule enforces the owner\u2208team membership constraint \u2014 passing a team the current owner doesn't belong to returns 422 'owner is not a member of the team'. Combine `ownerId: null` + `teamId: <T>` in one call to transfer a party to team-ownership with no specific user (verified empirically in v1.6.4 wire-trace; the membership rule doesn't fire when owner is null)."
|
|
1851
1917
|
)
|
|
1852
1918
|
};
|
|
1853
|
-
var createPartySchema =
|
|
1854
|
-
type:
|
|
1919
|
+
var createPartySchema = z8.object({
|
|
1920
|
+
type: z8.enum(["person", "organisation"]),
|
|
1855
1921
|
// person
|
|
1856
|
-
firstName:
|
|
1857
|
-
lastName:
|
|
1858
|
-
title:
|
|
1859
|
-
jobTitle:
|
|
1922
|
+
firstName: z8.string().optional(),
|
|
1923
|
+
lastName: z8.string().optional(),
|
|
1924
|
+
title: z8.string().optional(),
|
|
1925
|
+
jobTitle: z8.string().optional(),
|
|
1860
1926
|
organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
|
|
1861
1927
|
// organisation
|
|
1862
|
-
name:
|
|
1928
|
+
name: z8.string().optional(),
|
|
1863
1929
|
...PartyWriteBaseSchema,
|
|
1864
1930
|
ownerId: positiveId.optional().describe(
|
|
1865
1931
|
"Assign to user ID. Defaults to the API-token owner when omitted. To create a team-owned party with no specific user, first create the party, then call update_party with `ownerId: null` and `teamId`."
|
|
@@ -1867,7 +1933,7 @@ var createPartySchema = z7.object({
|
|
|
1867
1933
|
teamId: positiveId.optional().describe(
|
|
1868
1934
|
"Assign to team ID (discover via list_teams). Omit to leave team unset on create. To clear an existing team or create a team-owned party with no specific owner, use update_party after creation."
|
|
1869
1935
|
),
|
|
1870
|
-
fields:
|
|
1936
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(
|
|
1871
1937
|
fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
|
|
1872
1938
|
)
|
|
1873
1939
|
});
|
|
@@ -1881,17 +1947,17 @@ async function createParty(input) {
|
|
|
1881
1947
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1882
1948
|
return capsulePost("/parties", { party: body });
|
|
1883
1949
|
}
|
|
1884
|
-
var updatePartySchema =
|
|
1950
|
+
var updatePartySchema = z8.object({
|
|
1885
1951
|
id: positiveId,
|
|
1886
|
-
firstName:
|
|
1887
|
-
lastName:
|
|
1888
|
-
title:
|
|
1889
|
-
jobTitle:
|
|
1890
|
-
name:
|
|
1952
|
+
firstName: z8.string().optional(),
|
|
1953
|
+
lastName: z8.string().optional(),
|
|
1954
|
+
title: z8.string().optional(),
|
|
1955
|
+
jobTitle: z8.string().optional(),
|
|
1956
|
+
name: z8.string().optional(),
|
|
1891
1957
|
organisationId: positiveId.nullable().optional().describe(
|
|
1892
1958
|
"For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
|
|
1893
1959
|
),
|
|
1894
|
-
fields:
|
|
1960
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
|
|
1895
1961
|
...PartyWriteBaseSchema
|
|
1896
1962
|
});
|
|
1897
1963
|
async function updateParty(input) {
|
|
@@ -1922,10 +1988,42 @@ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
|
|
|
1922
1988
|
pathPrefix: "/parties",
|
|
1923
1989
|
confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
|
|
1924
1990
|
});
|
|
1925
|
-
|
|
1991
|
+
function definePartySubResourceRemove(opts) {
|
|
1992
|
+
const shape = {
|
|
1993
|
+
partyId: positiveId,
|
|
1994
|
+
[opts.idField]: positiveId.describe(
|
|
1995
|
+
`Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
|
|
1996
|
+
)
|
|
1997
|
+
};
|
|
1998
|
+
const schema = z8.object(shape);
|
|
1999
|
+
async function handler(input) {
|
|
2000
|
+
const partyId = input["partyId"];
|
|
2001
|
+
const rowId = input[opts.idField];
|
|
2002
|
+
return idempotentWithResult(
|
|
2003
|
+
() => capsulePut(`/parties/${partyId}`, {
|
|
2004
|
+
party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
|
|
2005
|
+
}),
|
|
2006
|
+
(result) => ({
|
|
2007
|
+
removed: true,
|
|
2008
|
+
alreadyRemoved: false,
|
|
2009
|
+
partyId,
|
|
2010
|
+
[opts.idField]: rowId,
|
|
2011
|
+
...result
|
|
2012
|
+
}),
|
|
2013
|
+
() => ({
|
|
2014
|
+
removed: true,
|
|
2015
|
+
alreadyRemoved: true,
|
|
2016
|
+
partyId,
|
|
2017
|
+
[opts.idField]: rowId
|
|
2018
|
+
})
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
return { schema, handler };
|
|
2022
|
+
}
|
|
2023
|
+
var addPartyEmailAddressSchema = z8.object({
|
|
1926
2024
|
partyId: positiveId,
|
|
1927
|
-
address:
|
|
1928
|
-
type:
|
|
2025
|
+
address: z8.string().email(),
|
|
2026
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
|
|
1929
2027
|
});
|
|
1930
2028
|
async function addPartyEmailAddress(input) {
|
|
1931
2029
|
const { partyId, address, type } = input;
|
|
@@ -1935,32 +2033,17 @@ async function addPartyEmailAddress(input) {
|
|
|
1935
2033
|
party: { emailAddresses: [item] }
|
|
1936
2034
|
});
|
|
1937
2035
|
}
|
|
1938
|
-
var
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
)
|
|
2036
|
+
var removePartyEmailAddress = definePartySubResourceRemove({
|
|
2037
|
+
arrayKey: "emailAddresses",
|
|
2038
|
+
idField: "emailAddressId",
|
|
2039
|
+
rowNoun: "email-address"
|
|
1943
2040
|
});
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1948
|
-
party: { emailAddresses: [{ id: emailAddressId, _delete: true }] }
|
|
1949
|
-
}),
|
|
1950
|
-
(result) => ({
|
|
1951
|
-
removed: true,
|
|
1952
|
-
alreadyRemoved: false,
|
|
1953
|
-
partyId,
|
|
1954
|
-
emailAddressId,
|
|
1955
|
-
...result
|
|
1956
|
-
}),
|
|
1957
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
|
|
1958
|
-
);
|
|
1959
|
-
}
|
|
1960
|
-
var addPartyPhoneNumberSchema = z7.object({
|
|
2041
|
+
var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
|
|
2042
|
+
var removePartyEmailAddressById = removePartyEmailAddress.handler;
|
|
2043
|
+
var addPartyPhoneNumberSchema = z8.object({
|
|
1961
2044
|
partyId: positiveId,
|
|
1962
|
-
number:
|
|
1963
|
-
type:
|
|
2045
|
+
number: z8.string().min(1),
|
|
2046
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
|
|
1964
2047
|
});
|
|
1965
2048
|
async function addPartyPhoneNumber(input) {
|
|
1966
2049
|
const { partyId, number, type } = input;
|
|
@@ -1970,36 +2053,21 @@ async function addPartyPhoneNumber(input) {
|
|
|
1970
2053
|
party: { phoneNumbers: [item] }
|
|
1971
2054
|
});
|
|
1972
2055
|
}
|
|
1973
|
-
var
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
)
|
|
2056
|
+
var removePartyPhoneNumber = definePartySubResourceRemove({
|
|
2057
|
+
arrayKey: "phoneNumbers",
|
|
2058
|
+
idField: "phoneNumberId",
|
|
2059
|
+
rowNoun: "phone-number"
|
|
1978
2060
|
});
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1983
|
-
party: { phoneNumbers: [{ id: phoneNumberId, _delete: true }] }
|
|
1984
|
-
}),
|
|
1985
|
-
(result) => ({
|
|
1986
|
-
removed: true,
|
|
1987
|
-
alreadyRemoved: false,
|
|
1988
|
-
partyId,
|
|
1989
|
-
phoneNumberId,
|
|
1990
|
-
...result
|
|
1991
|
-
}),
|
|
1992
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
|
|
1993
|
-
);
|
|
1994
|
-
}
|
|
1995
|
-
var addPartyAddressSchema = z7.object({
|
|
2061
|
+
var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
|
|
2062
|
+
var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
|
|
2063
|
+
var addPartyAddressSchema = z8.object({
|
|
1996
2064
|
partyId: positiveId,
|
|
1997
|
-
street:
|
|
1998
|
-
city:
|
|
1999
|
-
state:
|
|
2000
|
-
country:
|
|
2001
|
-
zip:
|
|
2002
|
-
type:
|
|
2065
|
+
street: z8.string().optional(),
|
|
2066
|
+
city: z8.string().optional(),
|
|
2067
|
+
state: z8.string().optional(),
|
|
2068
|
+
country: z8.string().optional().describe(CountryDescription),
|
|
2069
|
+
zip: z8.string().optional(),
|
|
2070
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
|
|
2003
2071
|
});
|
|
2004
2072
|
async function addPartyAddress(input) {
|
|
2005
2073
|
const { partyId, ...rest } = input;
|
|
@@ -2011,31 +2079,16 @@ async function addPartyAddress(input) {
|
|
|
2011
2079
|
party: { addresses: [item] }
|
|
2012
2080
|
});
|
|
2013
2081
|
}
|
|
2014
|
-
var
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
)
|
|
2082
|
+
var removePartyAddress = definePartySubResourceRemove({
|
|
2083
|
+
arrayKey: "addresses",
|
|
2084
|
+
idField: "addressId",
|
|
2085
|
+
rowNoun: "address"
|
|
2019
2086
|
});
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
2024
|
-
party: { addresses: [{ id: addressId, _delete: true }] }
|
|
2025
|
-
}),
|
|
2026
|
-
(result) => ({
|
|
2027
|
-
removed: true,
|
|
2028
|
-
alreadyRemoved: false,
|
|
2029
|
-
partyId,
|
|
2030
|
-
addressId,
|
|
2031
|
-
...result
|
|
2032
|
-
}),
|
|
2033
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, addressId })
|
|
2034
|
-
);
|
|
2035
|
-
}
|
|
2036
|
-
var addPartyWebsiteSchema = z7.object({
|
|
2087
|
+
var removePartyAddressByIdSchema = removePartyAddress.schema;
|
|
2088
|
+
var removePartyAddressById = removePartyAddress.handler;
|
|
2089
|
+
var addPartyWebsiteSchema = z8.object({
|
|
2037
2090
|
partyId: positiveId,
|
|
2038
|
-
address:
|
|
2091
|
+
address: z8.string().min(1).describe(
|
|
2039
2092
|
"The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
|
|
2040
2093
|
),
|
|
2041
2094
|
service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
|
|
@@ -2048,58 +2101,41 @@ async function addPartyWebsite(input) {
|
|
|
2048
2101
|
party: { websites: [item] }
|
|
2049
2102
|
});
|
|
2050
2103
|
}
|
|
2051
|
-
var
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
)
|
|
2104
|
+
var removePartyWebsite = definePartySubResourceRemove({
|
|
2105
|
+
arrayKey: "websites",
|
|
2106
|
+
idField: "websiteId",
|
|
2107
|
+
rowNoun: "website"
|
|
2056
2108
|
});
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
return idempotentWithResult(
|
|
2060
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
2061
|
-
party: { websites: [{ id: websiteId, _delete: true }] }
|
|
2062
|
-
}),
|
|
2063
|
-
(result) => ({
|
|
2064
|
-
removed: true,
|
|
2065
|
-
alreadyRemoved: false,
|
|
2066
|
-
partyId,
|
|
2067
|
-
websiteId,
|
|
2068
|
-
...result
|
|
2069
|
-
}),
|
|
2070
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, websiteId })
|
|
2071
|
-
);
|
|
2072
|
-
}
|
|
2109
|
+
var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
|
|
2110
|
+
var removePartyWebsiteById = removePartyWebsite.handler;
|
|
2073
2111
|
|
|
2074
2112
|
// src/tools/opportunities.ts
|
|
2075
|
-
import { z as
|
|
2076
|
-
var OpportunityValueSchema =
|
|
2077
|
-
amount:
|
|
2078
|
-
currency:
|
|
2113
|
+
import { z as z9 } from "zod";
|
|
2114
|
+
var OpportunityValueSchema = z9.object({
|
|
2115
|
+
amount: z9.number().nonnegative(),
|
|
2116
|
+
currency: z9.string({
|
|
2079
2117
|
error: (iss) => iss.code === "invalid_type" && iss.input === void 0 ? "currency is required when amount is set (3-letter ISO 4217 code, e.g. 'USD', 'EUR', 'GBP')" : void 0
|
|
2080
2118
|
}).length(3).describe(
|
|
2081
2119
|
"ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
|
|
2082
2120
|
)
|
|
2083
2121
|
});
|
|
2084
|
-
var searchOpportunitiesSchema =
|
|
2085
|
-
q:
|
|
2086
|
-
embed:
|
|
2087
|
-
|
|
2088
|
-
perPage: z8.number().int().min(1).max(100).optional().default(25)
|
|
2122
|
+
var searchOpportunitiesSchema = z9.object({
|
|
2123
|
+
q: z9.string().optional().describe("Free-text search query"),
|
|
2124
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2125
|
+
...paginationFields
|
|
2089
2126
|
});
|
|
2090
2127
|
async function searchOpportunities(input) {
|
|
2091
2128
|
const path = input.q ? "/opportunities/search" : "/opportunities";
|
|
2092
|
-
|
|
2129
|
+
return capsuleGetList(path, {
|
|
2093
2130
|
q: input.q,
|
|
2094
2131
|
embed: input.embed,
|
|
2095
2132
|
page: input.page,
|
|
2096
2133
|
perPage: input.perPage
|
|
2097
2134
|
});
|
|
2098
|
-
return { ...data, nextPage };
|
|
2099
2135
|
}
|
|
2100
|
-
var getOpportunitySchema =
|
|
2136
|
+
var getOpportunitySchema = z9.object({
|
|
2101
2137
|
id: positiveId,
|
|
2102
|
-
embed:
|
|
2138
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2103
2139
|
});
|
|
2104
2140
|
async function getOpportunity(input) {
|
|
2105
2141
|
const { data } = await capsuleGet(`/opportunities/${input.id}`, {
|
|
@@ -2107,32 +2143,32 @@ async function getOpportunity(input) {
|
|
|
2107
2143
|
});
|
|
2108
2144
|
return data;
|
|
2109
2145
|
}
|
|
2110
|
-
var getOpportunitiesSchema =
|
|
2111
|
-
ids:
|
|
2146
|
+
var getOpportunitiesSchema = z9.object({
|
|
2147
|
+
ids: z9.array(positiveId).min(1).max(50).describe(
|
|
2112
2148
|
"Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
2113
2149
|
),
|
|
2114
|
-
embed:
|
|
2150
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2115
2151
|
});
|
|
2116
2152
|
async function getOpportunities(input) {
|
|
2117
2153
|
return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
|
|
2118
2154
|
}
|
|
2119
|
-
var createOpportunitySchema =
|
|
2120
|
-
name:
|
|
2155
|
+
var createOpportunitySchema = z9.object({
|
|
2156
|
+
name: z9.string().min(1),
|
|
2121
2157
|
partyId: positiveId.describe("ID of the party this opportunity belongs to"),
|
|
2122
2158
|
milestoneId: positiveId.describe(
|
|
2123
2159
|
"ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones. NOTE: some Capsule tenants configure **pipeline / milestone-reached automation rules** that mutate `owner` and/or `team` immediately after creation \u2014 e.g. an 'Assign to a Team' action that fires on entry to a specific milestone and has been observed to clear `owner` as an automation side-effect. If you observe a newly-created opp landing with `owner: null` despite passing `ownerId`, the cause is almost certainly a milestone automation on the destination pipeline rather than the connector. Documented workaround: follow `create_opportunity` with an immediate `batch_update_opportunity({items: [{id, ownerId, teamId}]})` carrying both fields \u2014 PUT does not re-fire milestone-reached triggers, so the owner sticks."
|
|
2124
2160
|
),
|
|
2125
|
-
description:
|
|
2161
|
+
description: z9.string().optional(),
|
|
2126
2162
|
value: OpportunityValueSchema.optional(),
|
|
2127
|
-
expectedCloseOn:
|
|
2128
|
-
probability:
|
|
2163
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2164
|
+
probability: z9.number().int().min(0).max(100).optional(),
|
|
2129
2165
|
ownerId: positiveId.optional().describe(
|
|
2130
2166
|
"Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. To clear owner later, call update_opportunity with `ownerId: null`. Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
|
|
2131
2167
|
),
|
|
2132
2168
|
teamId: positiveId.optional().describe(
|
|
2133
2169
|
"Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
|
|
2134
2170
|
),
|
|
2135
|
-
fields:
|
|
2171
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
2136
2172
|
fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
|
|
2137
2173
|
)
|
|
2138
2174
|
});
|
|
@@ -2149,19 +2185,19 @@ async function createOpportunity(input) {
|
|
|
2149
2185
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2150
2186
|
return capsulePost("/opportunities", { opportunity: body });
|
|
2151
2187
|
}
|
|
2152
|
-
var updateOpportunitySchema =
|
|
2188
|
+
var updateOpportunitySchema = z9.object({
|
|
2153
2189
|
id: positiveId,
|
|
2154
|
-
name:
|
|
2190
|
+
name: z9.string().min(1).optional(),
|
|
2155
2191
|
partyId: positiveId.optional().describe(
|
|
2156
2192
|
"Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_project.partyId`."
|
|
2157
2193
|
),
|
|
2158
2194
|
milestoneId: positiveId.optional().describe(
|
|
2159
2195
|
"Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
|
|
2160
2196
|
),
|
|
2161
|
-
description:
|
|
2197
|
+
description: z9.string().optional(),
|
|
2162
2198
|
value: OpportunityValueSchema.optional(),
|
|
2163
|
-
expectedCloseOn:
|
|
2164
|
-
probability:
|
|
2199
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
2200
|
+
probability: z9.number().int().min(0).max(100).optional().describe(
|
|
2165
2201
|
"Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
|
|
2166
2202
|
),
|
|
2167
2203
|
lostReasonId: positiveId.optional().describe(
|
|
@@ -2173,7 +2209,7 @@ var updateOpportunitySchema = z8.object({
|
|
|
2173
2209
|
teamId: positiveId.nullable().optional().describe(
|
|
2174
2210
|
"Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
|
|
2175
2211
|
),
|
|
2176
|
-
fields:
|
|
2212
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
|
|
2177
2213
|
});
|
|
2178
2214
|
async function updateOpportunity(input) {
|
|
2179
2215
|
const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
@@ -2209,25 +2245,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
|
|
|
2209
2245
|
});
|
|
2210
2246
|
|
|
2211
2247
|
// src/tools/projects.ts
|
|
2212
|
-
import { z as
|
|
2213
|
-
var listProjectsSchema =
|
|
2214
|
-
status:
|
|
2215
|
-
embed:
|
|
2216
|
-
|
|
2217
|
-
perPage: z9.number().int().min(1).max(100).optional().default(25)
|
|
2248
|
+
import { z as z10 } from "zod";
|
|
2249
|
+
var listProjectsSchema = z10.object({
|
|
2250
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional(),
|
|
2251
|
+
embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2252
|
+
...paginationFields
|
|
2218
2253
|
});
|
|
2219
2254
|
async function listProjects(input) {
|
|
2220
|
-
|
|
2255
|
+
return capsuleGetList("/kases", {
|
|
2221
2256
|
status: input.status,
|
|
2222
2257
|
embed: input.embed,
|
|
2223
2258
|
page: input.page,
|
|
2224
2259
|
perPage: input.perPage
|
|
2225
2260
|
});
|
|
2226
|
-
return { ...data, nextPage };
|
|
2227
2261
|
}
|
|
2228
|
-
var getProjectSchema =
|
|
2262
|
+
var getProjectSchema = z10.object({
|
|
2229
2263
|
id: positiveId,
|
|
2230
|
-
embed:
|
|
2264
|
+
embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2231
2265
|
});
|
|
2232
2266
|
async function getProject(input) {
|
|
2233
2267
|
const { data } = await capsuleGet(`/kases/${input.id}`, {
|
|
@@ -2235,20 +2269,20 @@ async function getProject(input) {
|
|
|
2235
2269
|
});
|
|
2236
2270
|
return data;
|
|
2237
2271
|
}
|
|
2238
|
-
var getProjectsSchema =
|
|
2239
|
-
ids:
|
|
2272
|
+
var getProjectsSchema = z10.object({
|
|
2273
|
+
ids: z10.array(positiveId).min(1).max(50).describe(
|
|
2240
2274
|
"Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
2241
2275
|
),
|
|
2242
|
-
embed:
|
|
2276
|
+
embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2243
2277
|
});
|
|
2244
2278
|
async function getProjects(input) {
|
|
2245
2279
|
return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
|
|
2246
2280
|
}
|
|
2247
|
-
var createProjectSchema =
|
|
2248
|
-
name:
|
|
2281
|
+
var createProjectSchema = z10.object({
|
|
2282
|
+
name: z10.string().min(1),
|
|
2249
2283
|
partyId: positiveId.describe("ID of the party linked to this project"),
|
|
2250
|
-
description:
|
|
2251
|
-
status:
|
|
2284
|
+
description: z10.string().optional(),
|
|
2285
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
|
|
2252
2286
|
ownerId: positiveId.optional().describe(
|
|
2253
2287
|
"Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation \u2014 e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied."
|
|
2254
2288
|
),
|
|
@@ -2258,8 +2292,8 @@ var createProjectSchema = z9.object({
|
|
|
2258
2292
|
stageId: positiveId.optional().describe(
|
|
2259
2293
|
"Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
|
|
2260
2294
|
),
|
|
2261
|
-
expectedCloseOn:
|
|
2262
|
-
fields:
|
|
2295
|
+
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2296
|
+
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2263
2297
|
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
2264
2298
|
)
|
|
2265
2299
|
});
|
|
@@ -2277,11 +2311,11 @@ async function createProject(input) {
|
|
|
2277
2311
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2278
2312
|
return capsulePost("/kases", { kase: body });
|
|
2279
2313
|
}
|
|
2280
|
-
var updateProjectSchema =
|
|
2314
|
+
var updateProjectSchema = z10.object({
|
|
2281
2315
|
id: positiveId,
|
|
2282
|
-
name:
|
|
2283
|
-
description:
|
|
2284
|
-
status:
|
|
2316
|
+
name: z10.string().min(1).optional(),
|
|
2317
|
+
description: z10.string().optional(),
|
|
2318
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional(),
|
|
2285
2319
|
partyId: positiveId.optional().describe(
|
|
2286
2320
|
"Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_opportunity.partyId`."
|
|
2287
2321
|
),
|
|
@@ -2294,8 +2328,8 @@ var updateProjectSchema = z9.object({
|
|
|
2294
2328
|
stageId: positiveId.nullable().optional().describe(
|
|
2295
2329
|
"Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
|
|
2296
2330
|
),
|
|
2297
|
-
expectedCloseOn:
|
|
2298
|
-
fields:
|
|
2331
|
+
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2332
|
+
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2299
2333
|
fieldsArrayDescriptor("get_project") + " Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
2300
2334
|
)
|
|
2301
2335
|
});
|
|
@@ -2334,23 +2368,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
|
2334
2368
|
});
|
|
2335
2369
|
|
|
2336
2370
|
// src/tools/tasks.ts
|
|
2337
|
-
import { z as
|
|
2338
|
-
var listTasksSchema =
|
|
2371
|
+
import { z as z11 } from "zod";
|
|
2372
|
+
var listTasksSchema = z11.object({
|
|
2339
2373
|
// Note: Capsule has a third internal status `PENDING` (a task that's
|
|
2340
2374
|
// part of an active track but not yet "open"), but it can only be
|
|
2341
2375
|
// reached via track machinery — it is NOT directly settable by
|
|
2342
2376
|
// /tasks PUT, and a list filter for it returns the same as OPEN
|
|
2343
2377
|
// anyway. We expose only the two values that are actually filterable
|
|
2344
2378
|
// by the v2 API.
|
|
2345
|
-
status:
|
|
2379
|
+
status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
2346
2380
|
"Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
|
|
2347
2381
|
),
|
|
2348
2382
|
ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
|
|
2349
|
-
|
|
2350
|
-
perPage: z10.number().int().min(1).max(100).optional().default(25)
|
|
2383
|
+
...paginationFields
|
|
2351
2384
|
});
|
|
2352
2385
|
async function listTasks(input) {
|
|
2353
|
-
|
|
2386
|
+
return capsuleGetList("/tasks", {
|
|
2354
2387
|
// Default 'OPEN' applied here (not via zod .default()) so that
|
|
2355
2388
|
// z.infer keeps `status` optional for callers that omit it.
|
|
2356
2389
|
status: input.status ?? "OPEN",
|
|
@@ -2359,28 +2392,27 @@ async function listTasks(input) {
|
|
|
2359
2392
|
page: input.page,
|
|
2360
2393
|
perPage: input.perPage
|
|
2361
2394
|
});
|
|
2362
|
-
return { ...data, nextPage };
|
|
2363
2395
|
}
|
|
2364
|
-
var getTaskSchema =
|
|
2396
|
+
var getTaskSchema = z11.object({
|
|
2365
2397
|
id: positiveId.describe("Task ID")
|
|
2366
2398
|
});
|
|
2367
2399
|
async function getTask(input) {
|
|
2368
2400
|
const { data } = await capsuleGet(`/tasks/${input.id}`);
|
|
2369
2401
|
return data;
|
|
2370
2402
|
}
|
|
2371
|
-
var getTasksSchema =
|
|
2372
|
-
ids:
|
|
2403
|
+
var getTasksSchema = z11.object({
|
|
2404
|
+
ids: z11.array(positiveId).min(1).max(50).describe(
|
|
2373
2405
|
"Array of task IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
2374
2406
|
)
|
|
2375
2407
|
});
|
|
2376
2408
|
async function getTasks(input) {
|
|
2377
2409
|
return chunkedMultiGet("/tasks", "tasks", input.ids);
|
|
2378
2410
|
}
|
|
2379
|
-
var createTaskSchema =
|
|
2380
|
-
description:
|
|
2381
|
-
dueOn:
|
|
2382
|
-
dueTime:
|
|
2383
|
-
detail:
|
|
2411
|
+
var createTaskSchema = z11.object({
|
|
2412
|
+
description: z11.string().min(1),
|
|
2413
|
+
dueOn: z11.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
|
|
2414
|
+
dueTime: z11.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
2415
|
+
detail: z11.string().optional(),
|
|
2384
2416
|
ownerId: positiveId.optional().describe(
|
|
2385
2417
|
"Assign to user ID. Defaults to the API-token owner when omitted. Once set, this connector cannot clear the owner back to null \u2014 use Capsule's web UI for that."
|
|
2386
2418
|
),
|
|
@@ -2389,10 +2421,7 @@ var createTaskSchema = z10.object({
|
|
|
2389
2421
|
projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
|
|
2390
2422
|
});
|
|
2391
2423
|
async function createTask(input) {
|
|
2392
|
-
|
|
2393
|
-
if (linked.length > 1) {
|
|
2394
|
-
throw new Error("Provide at most one of partyId, opportunityId, or projectId");
|
|
2395
|
-
}
|
|
2424
|
+
assertSingleParentRef("create_task", input);
|
|
2396
2425
|
const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
2397
2426
|
const body = { ...rest };
|
|
2398
2427
|
setRef(body, "owner", ownerId);
|
|
@@ -2401,16 +2430,16 @@ async function createTask(input) {
|
|
|
2401
2430
|
setRef(body, "kase", projectId);
|
|
2402
2431
|
return capsulePost("/tasks", { task: body });
|
|
2403
2432
|
}
|
|
2404
|
-
var updateTaskSchema =
|
|
2433
|
+
var updateTaskSchema = z11.object({
|
|
2405
2434
|
id: positiveId,
|
|
2406
|
-
description:
|
|
2407
|
-
dueOn:
|
|
2408
|
-
dueTime:
|
|
2409
|
-
detail:
|
|
2435
|
+
description: z11.string().min(1).optional(),
|
|
2436
|
+
dueOn: z11.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2437
|
+
dueTime: z11.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
2438
|
+
detail: z11.string().optional(),
|
|
2410
2439
|
// Capsule rejects direct sets of `PENDING` (which is a track-machinery
|
|
2411
2440
|
// internal state) with 422 "cannot set task status to PENDING".
|
|
2412
2441
|
// Only OPEN and COMPLETED are settable here.
|
|
2413
|
-
status:
|
|
2442
|
+
status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
2414
2443
|
"Set to OPEN or COMPLETED. (PENDING exists internally for track-driven tasks but cannot be set directly via this tool \u2014 Capsule rejects it.) Setting status: OPEN on an already-open task is a true no-op (does not advance updatedAt)."
|
|
2415
2444
|
),
|
|
2416
2445
|
ownerId: positiveId.optional().describe(
|
|
@@ -2428,12 +2457,7 @@ var updateTaskSchema = z10.object({
|
|
|
2428
2457
|
});
|
|
2429
2458
|
async function updateTask(input) {
|
|
2430
2459
|
const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
2431
|
-
|
|
2432
|
-
if (setCount > 1) {
|
|
2433
|
-
throw new Error(
|
|
2434
|
-
"update_task: provide at most one of partyId, opportunityId, or projectId (Capsule rejects multi-parent tasks with 422 'task can be related to at most one entity')"
|
|
2435
|
-
);
|
|
2436
|
-
}
|
|
2460
|
+
assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
|
|
2437
2461
|
const body = {};
|
|
2438
2462
|
for (const [k, v] of Object.entries(rest)) {
|
|
2439
2463
|
if (v !== void 0) body[k] = v;
|
|
@@ -2444,7 +2468,7 @@ async function updateTask(input) {
|
|
|
2444
2468
|
setNullableRef(body, "kase", projectId);
|
|
2445
2469
|
return capsulePut(`/tasks/${id}`, { task: body });
|
|
2446
2470
|
}
|
|
2447
|
-
var completeTaskSchema =
|
|
2471
|
+
var completeTaskSchema = z11.object({
|
|
2448
2472
|
id: positiveId
|
|
2449
2473
|
});
|
|
2450
2474
|
async function completeTask(input) {
|
|
@@ -2452,8 +2476,8 @@ async function completeTask(input) {
|
|
|
2452
2476
|
task: { status: "COMPLETED" }
|
|
2453
2477
|
});
|
|
2454
2478
|
}
|
|
2455
|
-
var batchCompleteTaskSchema =
|
|
2456
|
-
ids:
|
|
2479
|
+
var batchCompleteTaskSchema = z11.object({
|
|
2480
|
+
ids: z11.array(positiveId).min(1).max(50).describe(
|
|
2457
2481
|
"Array of 1\u201350 task ids to mark COMPLETED in parallel. Each id resolves to one PUT /tasks/{id}; failures (e.g. 404 for a deleted task) surface per-item in the result array, the rest still complete. Capped at 50."
|
|
2458
2482
|
)
|
|
2459
2483
|
});
|
|
@@ -2467,77 +2491,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
|
|
|
2467
2491
|
});
|
|
2468
2492
|
|
|
2469
2493
|
// src/tools/entries.ts
|
|
2470
|
-
import { z as
|
|
2494
|
+
import { z as z12 } from "zod";
|
|
2471
2495
|
var listEntriesPagination = {
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2496
|
+
...paginationFields,
|
|
2497
|
+
embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2475
2498
|
};
|
|
2476
|
-
var listPartyEntriesSchema =
|
|
2499
|
+
var listPartyEntriesSchema = z12.object({
|
|
2477
2500
|
partyId: positiveId,
|
|
2478
2501
|
...listEntriesPagination,
|
|
2479
|
-
includeLinkedPersons:
|
|
2502
|
+
includeLinkedPersons: z12.boolean().optional().describe(
|
|
2480
2503
|
"When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422 'entry must be linked to either a party, opportunity or kase'). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
|
|
2481
2504
|
)
|
|
2482
2505
|
});
|
|
2506
|
+
var PER_PARTY_FETCH_CAP = 100;
|
|
2483
2507
|
async function fanOutPartyEntries(partyIds, embed, perPage) {
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
const id = partyIds[i];
|
|
2493
|
-
const { data, nextPage } = await capsuleGet(
|
|
2494
|
-
`/parties/${id}/entries`,
|
|
2495
|
-
{
|
|
2496
|
-
embed,
|
|
2497
|
-
page: 1,
|
|
2498
|
-
perPage
|
|
2499
|
-
}
|
|
2500
|
-
);
|
|
2501
|
-
results[i] = { entries: data.entries, nextPage };
|
|
2502
|
-
}
|
|
2503
|
-
}
|
|
2504
|
-
const workers = [];
|
|
2505
|
-
for (let w = 0; w < Math.min(concurrency, partyIds.length); w++) {
|
|
2506
|
-
workers.push(worker());
|
|
2507
|
-
}
|
|
2508
|
-
await Promise.all(workers);
|
|
2509
|
-
return results;
|
|
2508
|
+
return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
|
|
2509
|
+
const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
|
|
2510
|
+
embed,
|
|
2511
|
+
page: 1,
|
|
2512
|
+
perPage
|
|
2513
|
+
});
|
|
2514
|
+
return { entries: data.entries, nextPage };
|
|
2515
|
+
});
|
|
2510
2516
|
}
|
|
2511
2517
|
function mergedTimelineCandidatePerParty(page, perPage) {
|
|
2512
|
-
return Math.min(page * perPage,
|
|
2518
|
+
return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
|
|
2513
2519
|
}
|
|
2514
2520
|
function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
|
|
2515
2521
|
const requestedWindowEnd = page * perPage;
|
|
2516
2522
|
if (mergedLength > requestedWindowEnd) return page + 1;
|
|
2517
|
-
const nextWindowWithinCap = requestedWindowEnd <
|
|
2523
|
+
const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
|
|
2518
2524
|
if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
|
|
2519
2525
|
return void 0;
|
|
2520
2526
|
}
|
|
2521
2527
|
async function listPartyEntries(input) {
|
|
2522
2528
|
const { partyId, embed, page, perPage, includeLinkedPersons } = input;
|
|
2523
2529
|
if (!includeLinkedPersons) {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2530
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2531
|
+
embed,
|
|
2532
|
+
page,
|
|
2533
|
+
perPage
|
|
2534
|
+
});
|
|
2529
2535
|
}
|
|
2530
2536
|
const { data: peopleData } = await capsuleGet(
|
|
2531
2537
|
`/parties/${partyId}/people`,
|
|
2532
|
-
{ page: 1, perPage:
|
|
2538
|
+
{ page: 1, perPage: PER_PARTY_FETCH_CAP }
|
|
2533
2539
|
);
|
|
2534
2540
|
const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
|
|
2535
2541
|
if (peopleIds.length === 0) {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2542
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2543
|
+
embed,
|
|
2544
|
+
page,
|
|
2545
|
+
perPage
|
|
2546
|
+
});
|
|
2541
2547
|
}
|
|
2542
2548
|
const targetIds = [partyId, ...peopleIds];
|
|
2543
2549
|
const perPartyPages = await fanOutPartyEntries(
|
|
@@ -2572,31 +2578,31 @@ async function listPartyEntries(input) {
|
|
|
2572
2578
|
);
|
|
2573
2579
|
return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
|
|
2574
2580
|
}
|
|
2575
|
-
var listOpportunityEntriesSchema =
|
|
2581
|
+
var listOpportunityEntriesSchema = z12.object({
|
|
2576
2582
|
opportunityId: positiveId,
|
|
2577
2583
|
...listEntriesPagination
|
|
2578
2584
|
});
|
|
2579
2585
|
async function listOpportunityEntries(input) {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2586
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
|
|
2587
|
+
embed: input.embed,
|
|
2588
|
+
page: input.page,
|
|
2589
|
+
perPage: input.perPage
|
|
2590
|
+
});
|
|
2585
2591
|
}
|
|
2586
|
-
var listProjectEntriesSchema =
|
|
2592
|
+
var listProjectEntriesSchema = z12.object({
|
|
2587
2593
|
projectId: positiveId,
|
|
2588
2594
|
...listEntriesPagination
|
|
2589
2595
|
});
|
|
2590
2596
|
async function listProjectEntries(input) {
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2597
|
+
return capsuleGetList(`/kases/${input.projectId}/entries`, {
|
|
2598
|
+
embed: input.embed,
|
|
2599
|
+
page: input.page,
|
|
2600
|
+
perPage: input.perPage
|
|
2601
|
+
});
|
|
2596
2602
|
}
|
|
2597
|
-
var getEntrySchema =
|
|
2603
|
+
var getEntrySchema = z12.object({
|
|
2598
2604
|
id: positiveId,
|
|
2599
|
-
embed:
|
|
2605
|
+
embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2600
2606
|
});
|
|
2601
2607
|
async function getEntry(input) {
|
|
2602
2608
|
const { data } = await capsuleGet(`/entries/${input.id}`, {
|
|
@@ -2604,34 +2610,30 @@ async function getEntry(input) {
|
|
|
2604
2610
|
});
|
|
2605
2611
|
return data;
|
|
2606
2612
|
}
|
|
2607
|
-
var listEntriesSchema =
|
|
2613
|
+
var listEntriesSchema = z12.object({
|
|
2608
2614
|
...listEntriesPagination
|
|
2609
2615
|
});
|
|
2610
2616
|
async function listEntries(input) {
|
|
2611
|
-
|
|
2617
|
+
return capsuleGetList("/entries", {
|
|
2612
2618
|
embed: input.embed,
|
|
2613
2619
|
page: input.page,
|
|
2614
2620
|
perPage: input.perPage
|
|
2615
2621
|
});
|
|
2616
|
-
return { ...data, nextPage };
|
|
2617
2622
|
}
|
|
2618
|
-
var addNoteSchema =
|
|
2619
|
-
content:
|
|
2623
|
+
var addNoteSchema = z12.object({
|
|
2624
|
+
content: z12.string().min(1).describe(
|
|
2620
2625
|
"Note body text. Stored verbatim and treated as MARKDOWN \u2014 Capsule's web UI renders the markdown when displaying. Pass markdown source ('# Heading', '**bold**', '- bullet'), not HTML."
|
|
2621
2626
|
),
|
|
2622
2627
|
partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
|
|
2623
2628
|
opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
|
|
2624
2629
|
projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
|
|
2625
|
-
entryAt:
|
|
2630
|
+
entryAt: z12.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
|
|
2626
2631
|
"ISO-8601 timestamp for when this note actually happened (e.g. '2024-03-15T14:30:00Z'). Defaults to now. Use this for backdating historical notes when migrating from another system. `entryAt` is preserved across subsequent update_entry calls; only `updatedAt` advances on edits. Note attribution flows to the API-token owner \u2014 there is no way to record a note as authored by a different user via this connector (a `creatorId` parameter would enable audit-attribution spoofing on shared-connector deployments, so it is intentionally not exposed)."
|
|
2627
2632
|
)
|
|
2628
2633
|
});
|
|
2629
2634
|
async function addNote(input) {
|
|
2630
2635
|
const { content, partyId, opportunityId, projectId, entryAt } = input;
|
|
2631
|
-
|
|
2632
|
-
if (linked.length !== 1) {
|
|
2633
|
-
throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
|
|
2634
|
-
}
|
|
2636
|
+
assertSingleParentRef("add_note", input, { required: true });
|
|
2635
2637
|
const body = { type: "note", content };
|
|
2636
2638
|
setRef(body, "party", partyId);
|
|
2637
2639
|
setRef(body, "opportunity", opportunityId);
|
|
@@ -2639,12 +2641,12 @@ async function addNote(input) {
|
|
|
2639
2641
|
if (entryAt !== void 0) body["entryAt"] = entryAt;
|
|
2640
2642
|
return capsulePost("/entries", { entry: body });
|
|
2641
2643
|
}
|
|
2642
|
-
var updateEntrySchema =
|
|
2644
|
+
var updateEntrySchema = z12.object({
|
|
2643
2645
|
id: positiveId.describe("Entry ID to update"),
|
|
2644
|
-
content:
|
|
2646
|
+
content: z12.string().min(1).optional().describe(
|
|
2645
2647
|
"New body text for the entry. For notes, this is the markdown content; for emails, the body. Provide only if you want to change it."
|
|
2646
2648
|
),
|
|
2647
|
-
subject:
|
|
2649
|
+
subject: z12.string().optional().describe(
|
|
2648
2650
|
"New subject line. Mostly meaningful on email-type entries; on plain notes Capsule accepts the call (HTTP 200) but **does not store the subject and does not advance `updatedAt`** \u2014 a true no-op for inapplicable fields. `entryAt` (when the note was authored) is preserved across edits; `updatedAt` advances only when an applicable field actually changes. To sort/filter by 'when did this happen', use `entryAt`; for 'last touched', use `updatedAt`."
|
|
2649
2651
|
)
|
|
2650
2652
|
});
|
|
@@ -2666,62 +2668,50 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
|
|
|
2666
2668
|
});
|
|
2667
2669
|
|
|
2668
2670
|
// src/tools/pipelines.ts
|
|
2669
|
-
import { z as
|
|
2670
|
-
var
|
|
2671
|
-
page: z12.number().int().positive().optional(),
|
|
2672
|
-
perPage: z12.number().int().min(1).max(100).optional()
|
|
2673
|
-
};
|
|
2674
|
-
var listPipelinesSchema = z12.object({ ...paginationFields });
|
|
2671
|
+
import { z as z13 } from "zod";
|
|
2672
|
+
var listPipelinesSchema = z13.object({ ...paginationFieldsNoDefaults });
|
|
2675
2673
|
async function listPipelines(input) {
|
|
2676
|
-
|
|
2674
|
+
return capsuleGetCachedList("/pipelines", {
|
|
2677
2675
|
page: input.page ?? 1,
|
|
2678
2676
|
perPage: input.perPage ?? 100
|
|
2679
2677
|
});
|
|
2680
|
-
return { ...data, nextPage };
|
|
2681
2678
|
}
|
|
2682
|
-
var listMilestonesSchema =
|
|
2679
|
+
var listMilestonesSchema = z13.object({
|
|
2683
2680
|
pipelineId: positiveId,
|
|
2684
|
-
...
|
|
2681
|
+
...paginationFieldsNoDefaults
|
|
2685
2682
|
});
|
|
2686
2683
|
async function listMilestones(input) {
|
|
2687
|
-
|
|
2684
|
+
return capsuleGetCachedList(
|
|
2688
2685
|
`/pipelines/${input.pipelineId}/milestones`,
|
|
2689
2686
|
{ page: input.page ?? 1, perPage: input.perPage ?? 100 }
|
|
2690
2687
|
);
|
|
2691
|
-
return { ...data, nextPage };
|
|
2692
2688
|
}
|
|
2693
2689
|
|
|
2694
2690
|
// src/tools/boards.ts
|
|
2695
|
-
import { z as
|
|
2696
|
-
var
|
|
2697
|
-
page: z13.number().int().positive().optional(),
|
|
2698
|
-
perPage: z13.number().int().min(1).max(100).optional()
|
|
2699
|
-
};
|
|
2700
|
-
var listBoardsSchema = z13.object({ ...paginationFields2 });
|
|
2691
|
+
import { z as z14 } from "zod";
|
|
2692
|
+
var listBoardsSchema = z14.object({ ...paginationFieldsNoDefaults });
|
|
2701
2693
|
async function listBoards(input) {
|
|
2702
|
-
|
|
2694
|
+
return capsuleGetCachedList("/boards", {
|
|
2703
2695
|
page: input.page ?? 1,
|
|
2704
2696
|
perPage: input.perPage ?? 100
|
|
2705
2697
|
});
|
|
2706
|
-
return { ...data, nextPage };
|
|
2707
2698
|
}
|
|
2708
|
-
var listStagesSchema =
|
|
2699
|
+
var listStagesSchema = z14.object({
|
|
2709
2700
|
boardId: positiveId.optional().describe(
|
|
2710
2701
|
"Optional. If provided, returns only the stages defined on that specific board (uses /boards/{id}/stages). Omit to get all stages across all boards in one call."
|
|
2711
2702
|
),
|
|
2712
|
-
...
|
|
2703
|
+
...paginationFieldsNoDefaults
|
|
2713
2704
|
});
|
|
2714
2705
|
async function listStages(input) {
|
|
2715
2706
|
const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
|
|
2716
|
-
|
|
2707
|
+
return capsuleGetCachedList(path, {
|
|
2717
2708
|
page: input.page ?? 1,
|
|
2718
2709
|
perPage: input.perPage ?? 100
|
|
2719
2710
|
});
|
|
2720
|
-
return { ...data, nextPage };
|
|
2721
2711
|
}
|
|
2722
2712
|
|
|
2723
2713
|
// src/tools/tags.ts
|
|
2724
|
-
import { z as
|
|
2714
|
+
import { z as z15 } from "zod";
|
|
2725
2715
|
var TAG_LIST_PATH = {
|
|
2726
2716
|
parties: "/parties/tags",
|
|
2727
2717
|
opportunities: "/opportunities/tags",
|
|
@@ -2732,24 +2722,22 @@ var ENTITY_TO_WRAPPER = {
|
|
|
2732
2722
|
opportunities: "opportunity",
|
|
2733
2723
|
kases: "kase"
|
|
2734
2724
|
};
|
|
2735
|
-
var TagEntity =
|
|
2736
|
-
var listTagsSchema =
|
|
2737
|
-
entity:
|
|
2738
|
-
|
|
2739
|
-
perPage: z14.number().int().min(1).max(100).optional()
|
|
2725
|
+
var TagEntity = z15.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
|
|
2726
|
+
var listTagsSchema = z15.object({
|
|
2727
|
+
entity: z15.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
|
|
2728
|
+
...paginationFieldsNoDefaults
|
|
2740
2729
|
});
|
|
2741
2730
|
async function listTags(input) {
|
|
2742
2731
|
const path = TAG_LIST_PATH[input.entity];
|
|
2743
|
-
|
|
2732
|
+
return capsuleGetCachedList(path, {
|
|
2744
2733
|
page: input.page ?? 1,
|
|
2745
2734
|
perPage: input.perPage ?? 100
|
|
2746
2735
|
});
|
|
2747
|
-
return { ...data, nextPage };
|
|
2748
2736
|
}
|
|
2749
|
-
var addTagSchema =
|
|
2737
|
+
var addTagSchema = z15.object({
|
|
2750
2738
|
entity: TagEntity,
|
|
2751
2739
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2752
|
-
tagName:
|
|
2740
|
+
tagName: z15.string().min(1).describe(
|
|
2753
2741
|
"Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
|
|
2754
2742
|
)
|
|
2755
2743
|
});
|
|
@@ -2762,7 +2750,7 @@ async function addTag(input) {
|
|
|
2762
2750
|
invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
|
|
2763
2751
|
return result;
|
|
2764
2752
|
}
|
|
2765
|
-
var removeTagByIdSchema =
|
|
2753
|
+
var removeTagByIdSchema = z15.object({
|
|
2766
2754
|
entity: TagEntity,
|
|
2767
2755
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2768
2756
|
tagId: positiveId.describe(
|
|
@@ -2793,7 +2781,7 @@ async function removeTagById(input) {
|
|
|
2793
2781
|
invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
|
|
2794
2782
|
return result;
|
|
2795
2783
|
}
|
|
2796
|
-
var deleteTagDefinitionSchema =
|
|
2784
|
+
var deleteTagDefinitionSchema = z15.object({
|
|
2797
2785
|
entity: TagEntity,
|
|
2798
2786
|
tagId: positiveId.describe(
|
|
2799
2787
|
"The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
|
|
@@ -2829,44 +2817,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
|
|
|
2829
2817
|
});
|
|
2830
2818
|
|
|
2831
2819
|
// src/tools/users.ts
|
|
2832
|
-
import { z as
|
|
2833
|
-
var listUsersSchema =
|
|
2834
|
-
|
|
2835
|
-
perPage: z15.number().int().min(1).max(100).optional()
|
|
2820
|
+
import { z as z16 } from "zod";
|
|
2821
|
+
var listUsersSchema = z16.object({
|
|
2822
|
+
...paginationFieldsNoDefaults
|
|
2836
2823
|
});
|
|
2837
2824
|
async function listUsers(input) {
|
|
2838
|
-
|
|
2825
|
+
return capsuleGetCachedList("/users", {
|
|
2839
2826
|
page: input.page ?? 1,
|
|
2840
2827
|
perPage: input.perPage ?? 100
|
|
2841
2828
|
});
|
|
2842
|
-
return { ...data, nextPage };
|
|
2843
2829
|
}
|
|
2844
|
-
var getCurrentUserSchema =
|
|
2830
|
+
var getCurrentUserSchema = z16.object({});
|
|
2845
2831
|
async function getCurrentUser(_input) {
|
|
2846
2832
|
const { data } = await capsuleGet("/users/current");
|
|
2847
2833
|
return data;
|
|
2848
2834
|
}
|
|
2849
2835
|
|
|
2850
2836
|
// src/tools/filters.ts
|
|
2851
|
-
import { z as
|
|
2852
|
-
var FilterConditionSchema =
|
|
2853
|
-
field:
|
|
2837
|
+
import { z as z17 } from "zod";
|
|
2838
|
+
var FilterConditionSchema = z17.object({
|
|
2839
|
+
field: z17.string().describe(
|
|
2854
2840
|
"The Capsule filter-side field name (these differ from response field names \u2014 e.g. response.createdAt is filter-side 'addedOn', response.lastContactedAt is filter-side 'lastContactedOn'). Common: 'addedOn' (date created), 'updatedOn' (date last modified), 'lastContactedOn' (parties only), 'name', 'tag', 'owner', 'team', 'type' (parties: person|organisation), 'milestone' (opportunities), 'status' (opp/project: OPEN|CLOSED), 'closedOn' (opp/project), 'expectedCloseOn' (opp/project), 'hasTags', 'hasEmailAddress' (parties), 'isOpen', 'isStale' (opportunities), 'custom:{fieldId}'. Full per-entity list: https://developer.capsulecrm.com/v2/reference/filters"
|
|
2855
2841
|
),
|
|
2856
|
-
operator:
|
|
2842
|
+
operator: z17.string().describe(
|
|
2857
2843
|
"The filter operator. Common: 'is', 'is not' (use value=null to test for null), 'contains', 'does not contain', 'is greater than', 'is less than', 'is within last' (date fields, value=integer days), 'is more than' (date fields, value=integer days ago), 'starts with', 'ends with'. Operator validity depends on the field's type."
|
|
2858
2844
|
),
|
|
2859
|
-
value:
|
|
2845
|
+
value: z17.union([z17.string(), z17.number(), z17.boolean(), z17.null()]).describe(
|
|
2860
2846
|
"The value to compare against. For 'is within last' on date fields, pass an integer number of days. For tag filters, pass the tag name (string) or tag id (number). For 'is not' null tests, pass null literally."
|
|
2861
2847
|
)
|
|
2862
2848
|
});
|
|
2863
|
-
var FilterInputSchema =
|
|
2864
|
-
conditions:
|
|
2849
|
+
var FilterInputSchema = z17.object({
|
|
2850
|
+
conditions: z17.array(FilterConditionSchema).min(1).describe(
|
|
2865
2851
|
"Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
|
|
2866
2852
|
),
|
|
2867
|
-
embed:
|
|
2868
|
-
|
|
2869
|
-
perPage: z16.number().int().min(1).max(100).optional().default(25)
|
|
2853
|
+
embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2854
|
+
...paginationFields
|
|
2870
2855
|
});
|
|
2871
2856
|
async function runFilter(entityPath, input) {
|
|
2872
2857
|
const { data, nextPage } = await capsuleSearch(
|
|
@@ -2886,10 +2871,7 @@ async function filterParties(input) {
|
|
|
2886
2871
|
}
|
|
2887
2872
|
var filterOpportunitiesSchema = FilterInputSchema;
|
|
2888
2873
|
async function filterOpportunities(input) {
|
|
2889
|
-
return runFilter(
|
|
2890
|
-
"opportunities",
|
|
2891
|
-
input
|
|
2892
|
-
);
|
|
2874
|
+
return runFilter("opportunities", input);
|
|
2893
2875
|
}
|
|
2894
2876
|
var filterProjectsSchema = FilterInputSchema;
|
|
2895
2877
|
async function filterProjects(input) {
|
|
@@ -2897,139 +2879,126 @@ async function filterProjects(input) {
|
|
|
2897
2879
|
}
|
|
2898
2880
|
|
|
2899
2881
|
// src/tools/metadata.ts
|
|
2900
|
-
import { z as
|
|
2901
|
-
var
|
|
2902
|
-
|
|
2903
|
-
perPage:
|
|
2882
|
+
import { z as z18 } from "zod";
|
|
2883
|
+
var paginationFields2 = {
|
|
2884
|
+
...paginationFieldsNoDefaults,
|
|
2885
|
+
perPage: paginationFieldsNoDefaults.perPage.describe(
|
|
2886
|
+
"Page size, max 100. Defaults to 100 for reference data."
|
|
2887
|
+
)
|
|
2904
2888
|
};
|
|
2905
|
-
var listTeamsSchema =
|
|
2889
|
+
var listTeamsSchema = z18.object({ ...paginationFields2 });
|
|
2906
2890
|
async function listTeams(input) {
|
|
2907
|
-
|
|
2891
|
+
return capsuleGetCachedList("/teams", {
|
|
2908
2892
|
page: input.page ?? 1,
|
|
2909
2893
|
perPage: input.perPage ?? 100
|
|
2910
2894
|
});
|
|
2911
|
-
return { ...data, nextPage };
|
|
2912
2895
|
}
|
|
2913
|
-
var listLostReasonsSchema =
|
|
2896
|
+
var listLostReasonsSchema = z18.object({ ...paginationFields2 });
|
|
2914
2897
|
async function listLostReasons(input) {
|
|
2915
|
-
|
|
2898
|
+
return capsuleGetCachedList("/lostreasons", {
|
|
2916
2899
|
page: input.page ?? 1,
|
|
2917
2900
|
perPage: input.perPage ?? 100
|
|
2918
2901
|
});
|
|
2919
|
-
return { ...data, nextPage };
|
|
2920
2902
|
}
|
|
2921
|
-
var listActivityTypesSchema =
|
|
2903
|
+
var listActivityTypesSchema = z18.object({ ...paginationFields2 });
|
|
2922
2904
|
async function listActivityTypes(input) {
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
perPage: input.perPage ?? 100
|
|
2928
|
-
}
|
|
2929
|
-
);
|
|
2930
|
-
return { ...data, nextPage };
|
|
2905
|
+
return capsuleGetCachedList("/activitytypes", {
|
|
2906
|
+
page: input.page ?? 1,
|
|
2907
|
+
perPage: input.perPage ?? 100
|
|
2908
|
+
});
|
|
2931
2909
|
}
|
|
2932
|
-
var getSiteSchema =
|
|
2910
|
+
var getSiteSchema = z18.object({});
|
|
2933
2911
|
async function getSite(_input) {
|
|
2934
2912
|
const { data } = await capsuleGetCached("/site");
|
|
2935
2913
|
return data;
|
|
2936
2914
|
}
|
|
2937
|
-
var listTrackDefinitionsSchema =
|
|
2915
|
+
var listTrackDefinitionsSchema = z18.object({ ...paginationFields2 });
|
|
2938
2916
|
async function listTrackDefinitions(input) {
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
);
|
|
2943
|
-
return { ...data, nextPage };
|
|
2917
|
+
return capsuleGetCachedList("/trackdefinitions", {
|
|
2918
|
+
page: input.page ?? 1,
|
|
2919
|
+
perPage: input.perPage ?? 100
|
|
2920
|
+
});
|
|
2944
2921
|
}
|
|
2945
|
-
var listCategoriesSchema =
|
|
2922
|
+
var listCategoriesSchema = z18.object({ ...paginationFields2 });
|
|
2946
2923
|
async function listCategories(input) {
|
|
2947
|
-
|
|
2924
|
+
return capsuleGetCachedList("/categories", {
|
|
2948
2925
|
page: input.page ?? 1,
|
|
2949
2926
|
perPage: input.perPage ?? 100
|
|
2950
2927
|
});
|
|
2951
|
-
return { ...data, nextPage };
|
|
2952
2928
|
}
|
|
2953
|
-
var listGoalsSchema =
|
|
2929
|
+
var listGoalsSchema = z18.object({ ...paginationFields2 });
|
|
2954
2930
|
async function listGoals(input) {
|
|
2955
|
-
|
|
2931
|
+
return capsuleGetCachedList("/goals", {
|
|
2956
2932
|
page: input.page ?? 1,
|
|
2957
2933
|
perPage: input.perPage ?? 100
|
|
2958
2934
|
});
|
|
2959
|
-
return { ...data, nextPage };
|
|
2960
2935
|
}
|
|
2961
2936
|
|
|
2962
2937
|
// src/tools/audit.ts
|
|
2963
|
-
import { z as
|
|
2964
|
-
var listEmployeesSchema =
|
|
2938
|
+
import { z as z19 } from "zod";
|
|
2939
|
+
var listEmployeesSchema = z19.object({
|
|
2965
2940
|
partyId: positiveId.describe(
|
|
2966
2941
|
"The organisation's party id. Returns the people whose `organisation` field links to this party."
|
|
2967
2942
|
),
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2943
|
+
...paginationFields,
|
|
2944
|
+
embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2971
2945
|
});
|
|
2972
2946
|
async function listEmployees(input) {
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2947
|
+
return capsuleGetList(`/parties/${input.partyId}/people`, {
|
|
2948
|
+
page: input.page,
|
|
2949
|
+
perPage: input.perPage,
|
|
2950
|
+
embed: input.embed
|
|
2951
|
+
});
|
|
2978
2952
|
}
|
|
2979
|
-
var DeletedSinceSchema =
|
|
2953
|
+
var DeletedSinceSchema = z19.string().describe(
|
|
2980
2954
|
"REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
|
|
2981
2955
|
);
|
|
2982
2956
|
var DeletedPagination = {
|
|
2983
2957
|
since: DeletedSinceSchema,
|
|
2984
|
-
|
|
2985
|
-
perPage: z18.number().int().min(1).max(100).optional().default(25)
|
|
2958
|
+
...paginationFields
|
|
2986
2959
|
};
|
|
2987
|
-
var listDeletedPartiesSchema =
|
|
2960
|
+
var listDeletedPartiesSchema = z19.object(DeletedPagination);
|
|
2988
2961
|
async function listDeletedParties(input) {
|
|
2989
|
-
|
|
2962
|
+
return capsuleGetList("/parties/deleted", {
|
|
2990
2963
|
since: input.since,
|
|
2991
2964
|
page: input.page,
|
|
2992
2965
|
perPage: input.perPage
|
|
2993
2966
|
});
|
|
2994
|
-
return { ...data, nextPage };
|
|
2995
2967
|
}
|
|
2996
|
-
var listDeletedOpportunitiesSchema =
|
|
2968
|
+
var listDeletedOpportunitiesSchema = z19.object(DeletedPagination);
|
|
2997
2969
|
async function listDeletedOpportunities(input) {
|
|
2998
|
-
|
|
2970
|
+
return capsuleGetList("/opportunities/deleted", {
|
|
2999
2971
|
since: input.since,
|
|
3000
2972
|
page: input.page,
|
|
3001
2973
|
perPage: input.perPage
|
|
3002
2974
|
});
|
|
3003
|
-
return { ...data, nextPage };
|
|
3004
2975
|
}
|
|
3005
|
-
var listDeletedProjectsSchema =
|
|
2976
|
+
var listDeletedProjectsSchema = z19.object(DeletedPagination);
|
|
3006
2977
|
async function listDeletedProjects(input) {
|
|
3007
|
-
|
|
2978
|
+
return capsuleGetList("/kases/deleted", {
|
|
3008
2979
|
since: input.since,
|
|
3009
2980
|
page: input.page,
|
|
3010
2981
|
perPage: input.perPage
|
|
3011
2982
|
});
|
|
3012
|
-
return { ...data, nextPage };
|
|
3013
2983
|
}
|
|
3014
2984
|
|
|
3015
2985
|
// src/tools/relationships.ts
|
|
3016
|
-
import { z as
|
|
3017
|
-
var RelationshipEntity =
|
|
3018
|
-
var listAdditionalPartiesSchema =
|
|
2986
|
+
import { z as z20 } from "zod";
|
|
2987
|
+
var RelationshipEntity = z20.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
|
|
2988
|
+
var listAdditionalPartiesSchema = z20.object({
|
|
3019
2989
|
entity: RelationshipEntity,
|
|
3020
2990
|
entityId: positiveId.describe("ID of the opportunity or project."),
|
|
3021
|
-
embed:
|
|
3022
|
-
|
|
3023
|
-
perPage: z19.number().int().min(1).max(100).optional().default(25)
|
|
2991
|
+
embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2992
|
+
...paginationFields
|
|
3024
2993
|
});
|
|
3025
2994
|
async function listAdditionalParties(input) {
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
2995
|
+
return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
|
|
2996
|
+
embed: input.embed,
|
|
2997
|
+
page: input.page,
|
|
2998
|
+
perPage: input.perPage
|
|
2999
|
+
});
|
|
3031
3000
|
}
|
|
3032
|
-
var addAdditionalPartySchema =
|
|
3001
|
+
var addAdditionalPartySchema = z20.object({
|
|
3033
3002
|
entity: RelationshipEntity,
|
|
3034
3003
|
entityId: positiveId,
|
|
3035
3004
|
partyId: positiveId.describe(
|
|
@@ -3062,7 +3031,7 @@ async function addAdditionalParty(input) {
|
|
|
3062
3031
|
throw err;
|
|
3063
3032
|
}
|
|
3064
3033
|
}
|
|
3065
|
-
var removeAdditionalPartySchema =
|
|
3034
|
+
var removeAdditionalPartySchema = z20.object({
|
|
3066
3035
|
entity: RelationshipEntity,
|
|
3067
3036
|
entityId: positiveId,
|
|
3068
3037
|
partyId: positiveId,
|
|
@@ -3092,24 +3061,23 @@ async function removeAdditionalParty(input) {
|
|
|
3092
3061
|
})
|
|
3093
3062
|
);
|
|
3094
3063
|
}
|
|
3095
|
-
var listAssociatedProjectsSchema =
|
|
3064
|
+
var listAssociatedProjectsSchema = z20.object({
|
|
3096
3065
|
opportunityId: positiveId,
|
|
3097
|
-
embed:
|
|
3098
|
-
|
|
3099
|
-
perPage: z19.number().int().min(1).max(100).optional().default(25)
|
|
3066
|
+
embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
3067
|
+
...paginationFields
|
|
3100
3068
|
});
|
|
3101
3069
|
async function listAssociatedProjects(input) {
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3070
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
|
|
3071
|
+
embed: input.embed,
|
|
3072
|
+
page: input.page,
|
|
3073
|
+
perPage: input.perPage
|
|
3074
|
+
});
|
|
3107
3075
|
}
|
|
3108
3076
|
|
|
3109
3077
|
// src/tools/custom-fields.ts
|
|
3110
|
-
import { z as
|
|
3111
|
-
var CustomFieldEntity =
|
|
3112
|
-
var listCustomFieldsSchema =
|
|
3078
|
+
import { z as z21 } from "zod";
|
|
3079
|
+
var CustomFieldEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
|
|
3080
|
+
var listCustomFieldsSchema = z21.object({
|
|
3113
3081
|
entity: CustomFieldEntity
|
|
3114
3082
|
});
|
|
3115
3083
|
async function listCustomFields(input) {
|
|
@@ -3118,7 +3086,7 @@ async function listCustomFields(input) {
|
|
|
3118
3086
|
);
|
|
3119
3087
|
return data;
|
|
3120
3088
|
}
|
|
3121
|
-
var getCustomFieldSchema =
|
|
3089
|
+
var getCustomFieldSchema = z21.object({
|
|
3122
3090
|
entity: CustomFieldEntity,
|
|
3123
3091
|
fieldId: positiveId.describe("Custom field definition id.")
|
|
3124
3092
|
});
|
|
@@ -3130,9 +3098,9 @@ async function getCustomField(input) {
|
|
|
3130
3098
|
}
|
|
3131
3099
|
|
|
3132
3100
|
// src/tools/tracks.ts
|
|
3133
|
-
import { z as
|
|
3134
|
-
var TrackEntity =
|
|
3135
|
-
var listEntityTracksSchema =
|
|
3101
|
+
import { z as z22 } from "zod";
|
|
3102
|
+
var TrackEntity = z22.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
|
|
3103
|
+
var listEntityTracksSchema = z22.object({
|
|
3136
3104
|
entity: TrackEntity,
|
|
3137
3105
|
entityId: positiveId
|
|
3138
3106
|
});
|
|
@@ -3142,20 +3110,20 @@ async function listEntityTracks(input) {
|
|
|
3142
3110
|
);
|
|
3143
3111
|
return data;
|
|
3144
3112
|
}
|
|
3145
|
-
var showTrackSchema =
|
|
3113
|
+
var showTrackSchema = z22.object({
|
|
3146
3114
|
trackId: positiveId
|
|
3147
3115
|
});
|
|
3148
3116
|
async function showTrack(input) {
|
|
3149
3117
|
const { data } = await capsuleGet(`/tracks/${input.trackId}`);
|
|
3150
3118
|
return data;
|
|
3151
3119
|
}
|
|
3152
|
-
var applyTrackSchema =
|
|
3153
|
-
entity:
|
|
3120
|
+
var applyTrackSchema = z22.object({
|
|
3121
|
+
entity: z22.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
|
|
3154
3122
|
entityId: positiveId,
|
|
3155
3123
|
trackDefinitionId: positiveId.describe(
|
|
3156
3124
|
"The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
|
|
3157
3125
|
),
|
|
3158
|
-
startDate:
|
|
3126
|
+
startDate: z22.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
|
|
3159
3127
|
"Optional ISO-8601 date (YYYY-MM-DD) the track should start from \u2014 drives task due-date calculations (each task's `dueOn` is computed as startDate + the track-definition's `daysAfter` offset). Defaults to today if omitted. Useful for scheduling a renewal-queue track against a future contract end-date, or backfilling tracks for historical projects."
|
|
3160
3128
|
)
|
|
3161
3129
|
});
|
|
@@ -3168,9 +3136,9 @@ async function applyTrack(input) {
|
|
|
3168
3136
|
if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
|
|
3169
3137
|
return capsulePost("/tracks", { track });
|
|
3170
3138
|
}
|
|
3171
|
-
var updateTrackSchema =
|
|
3139
|
+
var updateTrackSchema = z22.object({
|
|
3172
3140
|
trackId: positiveId,
|
|
3173
|
-
fields:
|
|
3141
|
+
fields: z22.record(z22.string(), z22.unknown()).describe(
|
|
3174
3142
|
"Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
|
|
3175
3143
|
)
|
|
3176
3144
|
});
|
|
@@ -3182,7 +3150,7 @@ async function updateTrack(input) {
|
|
|
3182
3150
|
track: input.fields
|
|
3183
3151
|
});
|
|
3184
3152
|
}
|
|
3185
|
-
var removeTrackSchema =
|
|
3153
|
+
var removeTrackSchema = z22.object({
|
|
3186
3154
|
trackId: positiveId,
|
|
3187
3155
|
confirm: confirmFlag().describe(
|
|
3188
3156
|
"Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
|
|
@@ -3200,13 +3168,13 @@ async function removeTrack(input) {
|
|
|
3200
3168
|
}
|
|
3201
3169
|
|
|
3202
3170
|
// src/tools/attachments.ts
|
|
3203
|
-
import { z as
|
|
3171
|
+
import { z as z23 } from "zod";
|
|
3204
3172
|
var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
|
|
3205
3173
|
var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
|
|
3206
3174
|
var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
|
|
3207
|
-
var getAttachmentSchema =
|
|
3175
|
+
var getAttachmentSchema = z23.object({
|
|
3208
3176
|
id: positiveId.describe("Attachment ID."),
|
|
3209
|
-
maxSizeBytes:
|
|
3177
|
+
maxSizeBytes: z23.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
|
|
3210
3178
|
`Refuse to return content over this size (default ${DEFAULT_MAX_SIZE_BYTES} bytes \u2248 5MB; max ${HARD_MAX_SIZE_BYTES} bytes \u2248 25MB). Files exceeding the cap return metadata only with a 'truncated: true' flag.`
|
|
3211
3179
|
)
|
|
3212
3180
|
});
|
|
@@ -3221,17 +3189,17 @@ async function getAttachment(input) {
|
|
|
3221
3189
|
}
|
|
3222
3190
|
return { contentType, buffer, sizeBytes };
|
|
3223
3191
|
}
|
|
3224
|
-
var uploadAttachmentSchema =
|
|
3225
|
-
filename:
|
|
3192
|
+
var uploadAttachmentSchema = z23.object({
|
|
3193
|
+
filename: z23.string().min(1).describe(
|
|
3226
3194
|
"Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes \u2014 a typo in either is accepted and the file is stored as labelled."
|
|
3227
3195
|
),
|
|
3228
|
-
contentType:
|
|
3196
|
+
contentType: z23.string().min(1).describe(
|
|
3229
3197
|
"MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes."
|
|
3230
3198
|
),
|
|
3231
|
-
dataBase64:
|
|
3199
|
+
dataBase64: z23.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
|
|
3232
3200
|
"File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary."
|
|
3233
3201
|
),
|
|
3234
|
-
content:
|
|
3202
|
+
content: z23.string().optional().describe(
|
|
3235
3203
|
"Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
|
|
3236
3204
|
),
|
|
3237
3205
|
partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
|
|
@@ -3249,12 +3217,7 @@ function decodedBase64Size(s) {
|
|
|
3249
3217
|
return s.length / 4 * 3 - padding;
|
|
3250
3218
|
}
|
|
3251
3219
|
async function uploadAttachment(input) {
|
|
3252
|
-
|
|
3253
|
-
if (linked.length !== 1) {
|
|
3254
|
-
throw new Error(
|
|
3255
|
-
"upload_attachment: provide exactly one of partyId, opportunityId, or projectId"
|
|
3256
|
-
);
|
|
3257
|
-
}
|
|
3220
|
+
assertSingleParentRef("upload_attachment", input, { required: true });
|
|
3258
3221
|
if (!isValidBase64(input.dataBase64)) {
|
|
3259
3222
|
throw new Error(
|
|
3260
3223
|
"upload_attachment: dataBase64 is not valid base64 \u2014 Node's tolerant decoder would silently produce corrupt bytes. Verify the encoding (RFC 4648, padded with '=' to a multiple of 4 chars)."
|
|
@@ -3279,37 +3242,36 @@ async function uploadAttachment(input) {
|
|
|
3279
3242
|
content: input.content ?? "[attachment]",
|
|
3280
3243
|
attachments: [{ token }]
|
|
3281
3244
|
};
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3245
|
+
setRef(entryBody, "party", input.partyId);
|
|
3246
|
+
setRef(entryBody, "opportunity", input.opportunityId);
|
|
3247
|
+
setRef(entryBody, "kase", input.projectId);
|
|
3285
3248
|
return capsulePost("/entries", { entry: entryBody });
|
|
3286
3249
|
}
|
|
3287
3250
|
|
|
3288
3251
|
// src/tools/saved-filters.ts
|
|
3289
|
-
import { z as
|
|
3290
|
-
var EntitySchema =
|
|
3252
|
+
import { z as z24 } from "zod";
|
|
3253
|
+
var EntitySchema = z24.enum(["parties", "opportunities", "kases"]).describe(
|
|
3291
3254
|
"Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
|
|
3292
3255
|
);
|
|
3293
|
-
var listSavedFiltersSchema =
|
|
3256
|
+
var listSavedFiltersSchema = z24.object({
|
|
3294
3257
|
entity: EntitySchema
|
|
3295
3258
|
});
|
|
3296
3259
|
async function listSavedFilters(input) {
|
|
3297
3260
|
const { data } = await capsuleGetCached(`/${input.entity}/filters`);
|
|
3298
3261
|
return data;
|
|
3299
3262
|
}
|
|
3300
|
-
var runSavedFilterSchema =
|
|
3263
|
+
var runSavedFilterSchema = z24.object({
|
|
3301
3264
|
entity: EntitySchema,
|
|
3302
3265
|
id: positiveId.describe("The saved filter id (from list_saved_filters)."),
|
|
3303
|
-
embed:
|
|
3304
|
-
|
|
3305
|
-
perPage: z23.number().int().min(1).max(100).optional().default(25)
|
|
3266
|
+
embed: z24.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
3267
|
+
...paginationFields
|
|
3306
3268
|
});
|
|
3307
3269
|
async function runSavedFilter(input) {
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3270
|
+
return capsuleGetList(`/${input.entity}/filters/${input.id}/results`, {
|
|
3271
|
+
page: input.page,
|
|
3272
|
+
perPage: input.perPage,
|
|
3273
|
+
embed: input.embed
|
|
3274
|
+
});
|
|
3313
3275
|
}
|
|
3314
3276
|
|
|
3315
3277
|
// src/server.ts
|
|
@@ -3320,7 +3282,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3320
3282
|
const server = new McpServer(
|
|
3321
3283
|
{
|
|
3322
3284
|
name: "capsulemcp",
|
|
3323
|
-
version: "1.8.
|
|
3285
|
+
version: "1.8.1",
|
|
3324
3286
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
3325
3287
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
3326
3288
|
icons: ICONS
|
|
@@ -3770,20 +3732,72 @@ function createCapsuleMcpServer(opts) {
|
|
|
3770
3732
|
listEntriesSchema,
|
|
3771
3733
|
listEntries
|
|
3772
3734
|
);
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3735
|
+
if (shouldRegister("get_attachment")) {
|
|
3736
|
+
server.tool(
|
|
3737
|
+
"get_attachment",
|
|
3738
|
+
"Download an attachment by id. Returns image content for image/* types (Claude can describe it natively); decoded text for text/* and application/json (small files); JSON metadata + base64 payload for other binary types (PDF, Office docs, etc.). Files exceeding maxSizeBytes (default 5MB) return metadata only with a `truncated: true` flag.",
|
|
3739
|
+
getAttachmentSchema.shape,
|
|
3740
|
+
// get_attachment is read-only — downloads a binary, never mutates.
|
|
3741
|
+
// Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
|
|
3742
|
+
// false}` that `registerTool` applies to every other `get_*` tool.
|
|
3743
|
+
// Explicit destructiveHint: false is load-bearing — MCP spec
|
|
3744
|
+
// defaults destructiveHint to `true`, so omitting it would (in
|
|
3745
|
+
// some client implementations) classify this read as destructive.
|
|
3746
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
3747
|
+
async (input) => {
|
|
3748
|
+
const result = await getAttachment(input);
|
|
3749
|
+
if (result.truncated) {
|
|
3750
|
+
return {
|
|
3751
|
+
content: [
|
|
3752
|
+
{
|
|
3753
|
+
type: "text",
|
|
3754
|
+
text: JSON.stringify(
|
|
3755
|
+
{
|
|
3756
|
+
id: input.id,
|
|
3757
|
+
contentType: result.contentType,
|
|
3758
|
+
sizeBytes: result.sizeBytes,
|
|
3759
|
+
truncated: true,
|
|
3760
|
+
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3761
|
+
},
|
|
3762
|
+
null,
|
|
3763
|
+
2
|
|
3764
|
+
)
|
|
3765
|
+
}
|
|
3766
|
+
]
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
const baseType = result.contentType.split(";")[0].trim().toLowerCase();
|
|
3770
|
+
if (baseType.startsWith("image/")) {
|
|
3771
|
+
return {
|
|
3772
|
+
content: [
|
|
3773
|
+
{
|
|
3774
|
+
type: "image",
|
|
3775
|
+
data: result.buffer.toString("base64"),
|
|
3776
|
+
mimeType: result.contentType
|
|
3777
|
+
}
|
|
3778
|
+
]
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3782
|
+
if (isText) {
|
|
3783
|
+
return {
|
|
3784
|
+
content: [
|
|
3785
|
+
{
|
|
3786
|
+
type: "text",
|
|
3787
|
+
text: JSON.stringify(
|
|
3788
|
+
{
|
|
3789
|
+
id: input.id,
|
|
3790
|
+
contentType: result.contentType,
|
|
3791
|
+
sizeBytes: result.sizeBytes
|
|
3792
|
+
},
|
|
3793
|
+
null,
|
|
3794
|
+
2
|
|
3795
|
+
)
|
|
3796
|
+
},
|
|
3797
|
+
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3798
|
+
]
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3787
3801
|
return {
|
|
3788
3802
|
content: [
|
|
3789
3803
|
{
|
|
@@ -3793,8 +3807,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3793
3807
|
id: input.id,
|
|
3794
3808
|
contentType: result.contentType,
|
|
3795
3809
|
sizeBytes: result.sizeBytes,
|
|
3796
|
-
|
|
3797
|
-
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3810
|
+
base64: result.buffer.toString("base64")
|
|
3798
3811
|
},
|
|
3799
3812
|
null,
|
|
3800
3813
|
2
|
|
@@ -3803,57 +3816,8 @@ function createCapsuleMcpServer(opts) {
|
|
|
3803
3816
|
]
|
|
3804
3817
|
};
|
|
3805
3818
|
}
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
return {
|
|
3809
|
-
content: [
|
|
3810
|
-
{
|
|
3811
|
-
type: "image",
|
|
3812
|
-
data: result.buffer.toString("base64"),
|
|
3813
|
-
mimeType: result.contentType
|
|
3814
|
-
}
|
|
3815
|
-
]
|
|
3816
|
-
};
|
|
3817
|
-
}
|
|
3818
|
-
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3819
|
-
if (isText) {
|
|
3820
|
-
return {
|
|
3821
|
-
content: [
|
|
3822
|
-
{
|
|
3823
|
-
type: "text",
|
|
3824
|
-
text: JSON.stringify(
|
|
3825
|
-
{
|
|
3826
|
-
id: input.id,
|
|
3827
|
-
contentType: result.contentType,
|
|
3828
|
-
sizeBytes: result.sizeBytes
|
|
3829
|
-
},
|
|
3830
|
-
null,
|
|
3831
|
-
2
|
|
3832
|
-
)
|
|
3833
|
-
},
|
|
3834
|
-
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3835
|
-
]
|
|
3836
|
-
};
|
|
3837
|
-
}
|
|
3838
|
-
return {
|
|
3839
|
-
content: [
|
|
3840
|
-
{
|
|
3841
|
-
type: "text",
|
|
3842
|
-
text: JSON.stringify(
|
|
3843
|
-
{
|
|
3844
|
-
id: input.id,
|
|
3845
|
-
contentType: result.contentType,
|
|
3846
|
-
sizeBytes: result.sizeBytes,
|
|
3847
|
-
base64: result.buffer.toString("base64")
|
|
3848
|
-
},
|
|
3849
|
-
null,
|
|
3850
|
-
2
|
|
3851
|
-
)
|
|
3852
|
-
}
|
|
3853
|
-
]
|
|
3854
|
-
};
|
|
3855
|
-
}
|
|
3856
|
-
);
|
|
3819
|
+
);
|
|
3820
|
+
}
|
|
3857
3821
|
if (!readOnly) {
|
|
3858
3822
|
registerTool(
|
|
3859
3823
|
server,
|
|
@@ -4229,77 +4193,51 @@ a{color:#1e3a8a}
|
|
|
4229
4193
|
}
|
|
4230
4194
|
next();
|
|
4231
4195
|
};
|
|
4232
|
-
|
|
4233
|
-
"/mcp",
|
|
4234
|
-
guardOrigin,
|
|
4235
|
-
requireBearerAuth({
|
|
4236
|
-
verifier: oauthProvider2,
|
|
4237
|
-
resourceMetadataUrl: mcpResourceMetadataUrl
|
|
4238
|
-
}),
|
|
4239
|
-
mcpRateLimit,
|
|
4240
|
-
guardProtocolVersion,
|
|
4241
|
-
express.json({ limit: jsonLimit2 }),
|
|
4242
|
-
async (req, res) => {
|
|
4243
|
-
try {
|
|
4244
|
-
const clientId = req.auth?.clientId;
|
|
4245
|
-
const server = createCapsuleMcpServer({ clientId });
|
|
4246
|
-
const transport = new StreamableHTTPServerTransport({});
|
|
4247
|
-
res.on("close", () => {
|
|
4248
|
-
void transport.close();
|
|
4249
|
-
void server.close();
|
|
4250
|
-
});
|
|
4251
|
-
await withRequestContext({ clientId }, async () => {
|
|
4252
|
-
await server.connect(transport);
|
|
4253
|
-
await transport.handleRequest(req, res, req.body);
|
|
4254
|
-
});
|
|
4255
|
-
} catch (err) {
|
|
4256
|
-
const name = err instanceof Error ? err.name : typeof err;
|
|
4257
|
-
const status = err && typeof err === "object" && "status" in err ? Number(err.status) : void 0;
|
|
4258
|
-
const summary = status !== void 0 ? `${name} ${status}` : name;
|
|
4259
|
-
if (process.env["MCP_HTTP_DEBUG"] === "1") {
|
|
4260
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4261
|
-
console.error(`[capsulemcp] /mcp error: ${summary} \u2014 ${message}`);
|
|
4262
|
-
} else {
|
|
4263
|
-
console.error(`[capsulemcp] /mcp error: ${summary}`);
|
|
4264
|
-
}
|
|
4265
|
-
if (!res.headersSent) {
|
|
4266
|
-
res.status(500).json({ error: "internal_error" });
|
|
4267
|
-
}
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
4270
|
-
);
|
|
4271
|
-
app2.get(
|
|
4272
|
-
"/mcp",
|
|
4196
|
+
const mcpGuards = [
|
|
4273
4197
|
guardOrigin,
|
|
4274
4198
|
requireBearerAuth({
|
|
4275
4199
|
verifier: oauthProvider2,
|
|
4276
4200
|
resourceMetadataUrl: mcpResourceMetadataUrl
|
|
4277
4201
|
}),
|
|
4278
4202
|
mcpRateLimit,
|
|
4279
|
-
guardProtocolVersion
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4203
|
+
guardProtocolVersion
|
|
4204
|
+
];
|
|
4205
|
+
const methodNotAllowed = (_req, res) => {
|
|
4206
|
+
res.set("Allow", "POST").status(405).json({
|
|
4207
|
+
error: "method_not_allowed",
|
|
4208
|
+
message: "Use POST for MCP requests; this server runs in stateless mode."
|
|
4209
|
+
});
|
|
4210
|
+
};
|
|
4211
|
+
app2.post("/mcp", ...mcpGuards, express.json({ limit: jsonLimit2 }), async (req, res) => {
|
|
4212
|
+
try {
|
|
4213
|
+
const clientId = req.auth?.clientId;
|
|
4214
|
+
const server = createCapsuleMcpServer({ clientId });
|
|
4215
|
+
const transport = new StreamableHTTPServerTransport({});
|
|
4216
|
+
res.on("close", () => {
|
|
4217
|
+
void transport.close();
|
|
4218
|
+
void server.close();
|
|
4284
4219
|
});
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
"/mcp",
|
|
4289
|
-
guardOrigin,
|
|
4290
|
-
requireBearerAuth({
|
|
4291
|
-
verifier: oauthProvider2,
|
|
4292
|
-
resourceMetadataUrl: mcpResourceMetadataUrl
|
|
4293
|
-
}),
|
|
4294
|
-
mcpRateLimit,
|
|
4295
|
-
guardProtocolVersion,
|
|
4296
|
-
(_req, res) => {
|
|
4297
|
-
res.set("Allow", "POST").status(405).json({
|
|
4298
|
-
error: "method_not_allowed",
|
|
4299
|
-
message: "Use POST for MCP requests; this server runs in stateless mode."
|
|
4220
|
+
await withRequestContext({ clientId }, async () => {
|
|
4221
|
+
await server.connect(transport);
|
|
4222
|
+
await transport.handleRequest(req, res, req.body);
|
|
4300
4223
|
});
|
|
4224
|
+
} catch (err) {
|
|
4225
|
+
const name = err instanceof Error ? err.name : typeof err;
|
|
4226
|
+
const status = err && typeof err === "object" && "status" in err ? Number(err.status) : void 0;
|
|
4227
|
+
const summary = status !== void 0 ? `${name} ${status}` : name;
|
|
4228
|
+
if (process.env["MCP_HTTP_DEBUG"] === "1") {
|
|
4229
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4230
|
+
console.error(`[capsulemcp] /mcp error: ${summary} \u2014 ${message}`);
|
|
4231
|
+
} else {
|
|
4232
|
+
console.error(`[capsulemcp] /mcp error: ${summary}`);
|
|
4233
|
+
}
|
|
4234
|
+
if (!res.headersSent) {
|
|
4235
|
+
res.status(500).json({ error: "internal_error" });
|
|
4236
|
+
}
|
|
4301
4237
|
}
|
|
4302
|
-
);
|
|
4238
|
+
});
|
|
4239
|
+
app2.get("/mcp", ...mcpGuards, methodNotAllowed);
|
|
4240
|
+
app2.delete("/mcp", ...mcpGuards, methodNotAllowed);
|
|
4303
4241
|
return app2;
|
|
4304
4242
|
}
|
|
4305
4243
|
|