capsulemcp 1.8.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/http.js +861 -841
- package/dist/index.js +842 -779
- package/package.json +7 -8
package/dist/http.js
CHANGED
|
@@ -156,6 +156,26 @@ function invalidateByPrefix(pathPrefix, trigger) {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
// src/capsule/normalize.ts
|
|
160
|
+
var KEY_RENAMES = {
|
|
161
|
+
kase: "project",
|
|
162
|
+
kases: "projects",
|
|
163
|
+
restrictedKases: "restrictedProjects"
|
|
164
|
+
};
|
|
165
|
+
function normalizeProjectKeys(value) {
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
return value.map(normalizeProjectKeys);
|
|
168
|
+
}
|
|
169
|
+
if (value !== null && typeof value === "object") {
|
|
170
|
+
const out = {};
|
|
171
|
+
for (const [key, v] of Object.entries(value)) {
|
|
172
|
+
out[KEY_RENAMES[key] ?? key] = normalizeProjectKeys(v);
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
|
|
159
179
|
// src/capsule/client.ts
|
|
160
180
|
var DEFAULT_BASE_URL = "https://api.capsulecrm.com/api/v2";
|
|
161
181
|
function baseUrl() {
|
|
@@ -271,38 +291,31 @@ async function parseErrorBody(res) {
|
|
|
271
291
|
}
|
|
272
292
|
}
|
|
273
293
|
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
|
-
};
|
|
294
|
+
function isTimeoutAbort(err) {
|
|
295
|
+
return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
|
|
296
|
+
// "TimeoutError"; plain aborts (and older undici paths) surface
|
|
297
|
+
// as "AbortError" or carry "aborted" in the message.
|
|
298
|
+
(err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
|
|
286
299
|
}
|
|
287
300
|
async function mapAbort(p) {
|
|
288
301
|
try {
|
|
289
302
|
return await p;
|
|
290
303
|
} catch (err) {
|
|
291
|
-
if (
|
|
304
|
+
if (isTimeoutAbort(err)) {
|
|
292
305
|
throw new CapsuleTimeoutError();
|
|
293
306
|
}
|
|
294
307
|
throw err;
|
|
295
308
|
}
|
|
296
309
|
}
|
|
297
310
|
async function fetchWithTimeout(url, options) {
|
|
298
|
-
const { options: opts, cleanup } = withTimeout(options);
|
|
299
311
|
const startedAt = Date.now();
|
|
300
312
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
313
|
+
return await fetch(url, {
|
|
314
|
+
...options ?? {},
|
|
315
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
316
|
+
});
|
|
303
317
|
} catch (err) {
|
|
304
|
-
|
|
305
|
-
const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
|
|
318
|
+
const isAbort = isTimeoutAbort(err);
|
|
306
319
|
emitCapsuleFailure(
|
|
307
320
|
options?.method ?? "GET",
|
|
308
321
|
url,
|
|
@@ -326,24 +339,22 @@ async function doFetch(url, options) {
|
|
|
326
339
|
const startedAt = Date.now();
|
|
327
340
|
const method = options?.method ?? "GET";
|
|
328
341
|
const first = await fetchWithTimeout(url, options);
|
|
329
|
-
if (first.
|
|
330
|
-
const delay = parseRateLimitDelay(first
|
|
331
|
-
first
|
|
332
|
-
await drainBody(first.res);
|
|
342
|
+
if (first.status === 429) {
|
|
343
|
+
const delay = parseRateLimitDelay(first);
|
|
344
|
+
await drainBody(first);
|
|
333
345
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
334
346
|
const retried = await fetchWithTimeout(url, options);
|
|
335
|
-
if (retried.
|
|
336
|
-
retried
|
|
337
|
-
await drainBody(retried.res);
|
|
347
|
+
if (retried.status === 429) {
|
|
348
|
+
await drainBody(retried);
|
|
338
349
|
emitCapsuleRateLimited(method, url, Date.now() - startedAt);
|
|
339
350
|
throw new CapsuleApiError(
|
|
340
351
|
429,
|
|
341
352
|
"Rate limit exceeded after one retry. Please slow down your requests."
|
|
342
353
|
);
|
|
343
354
|
}
|
|
344
|
-
return {
|
|
355
|
+
return { res: retried, startedAt, method, url, retriedAfter429: true };
|
|
345
356
|
}
|
|
346
|
-
return {
|
|
357
|
+
return { res: first, startedAt, method, url, retriedAfter429: false };
|
|
347
358
|
}
|
|
348
359
|
async function consumeBody(start, body) {
|
|
349
360
|
try {
|
|
@@ -436,7 +447,8 @@ async function throwForStatus(res) {
|
|
|
436
447
|
}
|
|
437
448
|
async function handleResponse(res) {
|
|
438
449
|
await throwForStatus(res);
|
|
439
|
-
|
|
450
|
+
const body = await mapAbort(res.json());
|
|
451
|
+
return normalizeProjectKeys(body);
|
|
440
452
|
}
|
|
441
453
|
function buildUrl(path, params) {
|
|
442
454
|
const url = new URL(`${baseUrl()}${path}`);
|
|
@@ -453,15 +465,19 @@ async function capsuleGet(path, params) {
|
|
|
453
465
|
const token = getToken();
|
|
454
466
|
const url = buildUrl(path, params);
|
|
455
467
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
468
|
+
return consumeBody(start, async () => {
|
|
469
|
+
const data = await handleResponse(start.res);
|
|
470
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
471
|
+
return { data, nextPage };
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
async function capsuleGetList(path, params) {
|
|
475
|
+
const { data, nextPage } = await capsuleGet(path, params);
|
|
476
|
+
return { ...data, nextPage };
|
|
477
|
+
}
|
|
478
|
+
async function capsuleGetCachedList(path, params) {
|
|
479
|
+
const { data, nextPage } = await capsuleGetCached(path, params);
|
|
480
|
+
return { ...data, nextPage };
|
|
465
481
|
}
|
|
466
482
|
async function capsuleGetCached(path, params) {
|
|
467
483
|
if (cacheDisabled()) return capsuleGet(path, params);
|
|
@@ -500,11 +516,7 @@ async function capsulePost(path, body) {
|
|
|
500
516
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
501
517
|
body: JSON.stringify(body)
|
|
502
518
|
});
|
|
503
|
-
|
|
504
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
505
|
-
} finally {
|
|
506
|
-
start.cleanup();
|
|
507
|
-
}
|
|
519
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
508
520
|
}
|
|
509
521
|
async function capsulePostNoContent(path) {
|
|
510
522
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -514,15 +526,11 @@ async function capsulePostNoContent(path) {
|
|
|
514
526
|
method: "POST",
|
|
515
527
|
headers: baseHeaders(token)
|
|
516
528
|
});
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
});
|
|
523
|
-
} finally {
|
|
524
|
-
start.cleanup();
|
|
525
|
-
}
|
|
529
|
+
await consumeBody(start, async () => {
|
|
530
|
+
if (start.res.status === 204) return;
|
|
531
|
+
await throwForStatus(start.res);
|
|
532
|
+
await mapAbort(start.res.text());
|
|
533
|
+
});
|
|
526
534
|
}
|
|
527
535
|
async function capsuleSearch(path, body, params) {
|
|
528
536
|
const token = getToken();
|
|
@@ -532,15 +540,11 @@ async function capsuleSearch(path, body, params) {
|
|
|
532
540
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
533
541
|
body: JSON.stringify(body)
|
|
534
542
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
});
|
|
541
|
-
} finally {
|
|
542
|
-
start.cleanup();
|
|
543
|
-
}
|
|
543
|
+
return consumeBody(start, async () => {
|
|
544
|
+
const data = await handleResponse(start.res);
|
|
545
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
546
|
+
return { data, nextPage };
|
|
547
|
+
});
|
|
544
548
|
}
|
|
545
549
|
async function capsulePut(path, body) {
|
|
546
550
|
if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
|
|
@@ -551,68 +555,60 @@ async function capsulePut(path, body) {
|
|
|
551
555
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
552
556
|
body: JSON.stringify(body)
|
|
553
557
|
});
|
|
554
|
-
|
|
555
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
556
|
-
} finally {
|
|
557
|
-
start.cleanup();
|
|
558
|
-
}
|
|
558
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
559
559
|
}
|
|
560
560
|
async function capsuleGetBinary(path, maxBytes) {
|
|
561
561
|
const token = getToken();
|
|
562
562
|
const url = buildUrl(path);
|
|
563
563
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (
|
|
572
|
-
|
|
573
|
-
|
|
564
|
+
return consumeBody(start, async () => {
|
|
565
|
+
const res = start.res;
|
|
566
|
+
await throwForStatus(res);
|
|
567
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
568
|
+
const declared = res.headers.get("Content-Length");
|
|
569
|
+
const declaredBytes = declared ? Number(declared) : NaN;
|
|
570
|
+
if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
571
|
+
if (res.body) await res.body.cancel().catch(() => {
|
|
572
|
+
});
|
|
573
|
+
return {
|
|
574
|
+
contentType,
|
|
575
|
+
buffer: Buffer.alloc(0),
|
|
576
|
+
truncated: true,
|
|
577
|
+
sizeBytes: declaredBytes
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (maxBytes !== void 0 && res.body) {
|
|
581
|
+
const reader = res.body.getReader();
|
|
582
|
+
const chunks = [];
|
|
583
|
+
let total = 0;
|
|
584
|
+
let truncated = false;
|
|
585
|
+
while (true) {
|
|
586
|
+
const { done, value } = await mapAbort(reader.read());
|
|
587
|
+
if (done) break;
|
|
588
|
+
total += value.byteLength;
|
|
589
|
+
if (total > maxBytes) {
|
|
590
|
+
truncated = true;
|
|
591
|
+
await reader.cancel().catch(() => {
|
|
592
|
+
});
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
chunks.push(value);
|
|
596
|
+
}
|
|
597
|
+
if (truncated) {
|
|
574
598
|
return {
|
|
575
599
|
contentType,
|
|
576
600
|
buffer: Buffer.alloc(0),
|
|
577
601
|
truncated: true,
|
|
578
|
-
sizeBytes:
|
|
602
|
+
sizeBytes: total
|
|
579
603
|
};
|
|
580
604
|
}
|
|
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
|
-
}
|
|
605
|
+
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
606
|
+
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
607
|
+
}
|
|
608
|
+
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
609
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
610
|
+
return { contentType, buffer, sizeBytes: buffer.length };
|
|
611
|
+
});
|
|
616
612
|
}
|
|
617
613
|
async function capsulePostBinary(path, body, contentType, filename) {
|
|
618
614
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -628,11 +624,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
|
|
|
628
624
|
},
|
|
629
625
|
body
|
|
630
626
|
});
|
|
631
|
-
|
|
632
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
633
|
-
} finally {
|
|
634
|
-
start.cleanup();
|
|
635
|
-
}
|
|
627
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
636
628
|
}
|
|
637
629
|
async function capsuleDelete(path) {
|
|
638
630
|
if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
|
|
@@ -642,15 +634,11 @@ async function capsuleDelete(path) {
|
|
|
642
634
|
method: "DELETE",
|
|
643
635
|
headers: baseHeaders(token)
|
|
644
636
|
});
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
});
|
|
651
|
-
} finally {
|
|
652
|
-
start.cleanup();
|
|
653
|
-
}
|
|
637
|
+
await consumeBody(start, async () => {
|
|
638
|
+
if (start.res.status === 204) return;
|
|
639
|
+
await throwForStatus(start.res);
|
|
640
|
+
await mapAbort(start.res.text());
|
|
641
|
+
});
|
|
654
642
|
}
|
|
655
643
|
|
|
656
644
|
// src/auth/provider.ts
|
|
@@ -1171,6 +1159,45 @@ var ICONS = [
|
|
|
1171
1159
|
}
|
|
1172
1160
|
];
|
|
1173
1161
|
|
|
1162
|
+
// src/server/tier.ts
|
|
1163
|
+
var CORE_TOOLS = /* @__PURE__ */ new Set([
|
|
1164
|
+
// Parties
|
|
1165
|
+
"search_parties",
|
|
1166
|
+
"filter_parties",
|
|
1167
|
+
"get_party",
|
|
1168
|
+
"create_party",
|
|
1169
|
+
"update_party",
|
|
1170
|
+
"list_party_entries",
|
|
1171
|
+
// Opportunities
|
|
1172
|
+
"search_opportunities",
|
|
1173
|
+
"filter_opportunities",
|
|
1174
|
+
"get_opportunity",
|
|
1175
|
+
"create_opportunity",
|
|
1176
|
+
"update_opportunity",
|
|
1177
|
+
// Projects
|
|
1178
|
+
"search_projects",
|
|
1179
|
+
"filter_projects",
|
|
1180
|
+
"list_projects",
|
|
1181
|
+
"get_project",
|
|
1182
|
+
"create_project",
|
|
1183
|
+
"update_project",
|
|
1184
|
+
// Tasks
|
|
1185
|
+
"list_tasks",
|
|
1186
|
+
"get_task",
|
|
1187
|
+
"create_task",
|
|
1188
|
+
"update_task",
|
|
1189
|
+
"complete_task",
|
|
1190
|
+
// Timeline + tags + identity
|
|
1191
|
+
"add_note",
|
|
1192
|
+
"list_tags",
|
|
1193
|
+
"add_tag",
|
|
1194
|
+
"get_current_user"
|
|
1195
|
+
]);
|
|
1196
|
+
function shouldRegister(name) {
|
|
1197
|
+
if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
|
|
1198
|
+
return CORE_TOOLS.has(name);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1174
1201
|
// src/tasks/store.ts
|
|
1175
1202
|
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
1176
1203
|
import {
|
|
@@ -1379,27 +1406,25 @@ function wrapAsText(result) {
|
|
|
1379
1406
|
};
|
|
1380
1407
|
}
|
|
1381
1408
|
function registerTool(server, name, description, schema, handler) {
|
|
1409
|
+
if (!shouldRegister(name)) return;
|
|
1382
1410
|
const registerWithSchema = server.registerTool.bind(server);
|
|
1383
1411
|
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
|
-
}
|
|
1412
|
+
registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
|
|
1413
|
+
const startedAt = Date.now();
|
|
1414
|
+
const argFields = argFieldNames(input);
|
|
1415
|
+
const clientId = getRequestContext()?.clientId;
|
|
1416
|
+
try {
|
|
1417
|
+
const result = await handler(input);
|
|
1418
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
|
|
1419
|
+
return wrapAsText(result);
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
1422
|
+
throw err;
|
|
1399
1423
|
}
|
|
1400
|
-
);
|
|
1424
|
+
});
|
|
1401
1425
|
}
|
|
1402
1426
|
function registerToolTask(server, name, description, schema, handler) {
|
|
1427
|
+
if (!shouldRegister(name)) return;
|
|
1403
1428
|
const registerWithSchema = server.experimental.tasks.registerToolTask.bind(
|
|
1404
1429
|
server.experimental.tasks
|
|
1405
1430
|
);
|
|
@@ -1410,7 +1435,7 @@ function registerToolTask(server, name, description, schema, handler) {
|
|
|
1410
1435
|
description,
|
|
1411
1436
|
inputSchema: schema,
|
|
1412
1437
|
execution: { taskSupport: "optional" },
|
|
1413
|
-
|
|
1438
|
+
annotations
|
|
1414
1439
|
},
|
|
1415
1440
|
{
|
|
1416
1441
|
createTask: async (input, extra) => {
|
|
@@ -1470,7 +1495,7 @@ function registerToolTask(server, name, description, schema, handler) {
|
|
|
1470
1495
|
}
|
|
1471
1496
|
|
|
1472
1497
|
// src/tools/parties.ts
|
|
1473
|
-
import { z as
|
|
1498
|
+
import { z as z8 } from "zod";
|
|
1474
1499
|
|
|
1475
1500
|
// src/tools/body-helpers.ts
|
|
1476
1501
|
function setRef(body, key, id) {
|
|
@@ -1480,9 +1505,22 @@ function setNullableRef(body, key, id) {
|
|
|
1480
1505
|
if (id === null) body[key] = null;
|
|
1481
1506
|
else if (id !== void 0) body[key] = { id };
|
|
1482
1507
|
}
|
|
1508
|
+
function assertSingleParentRef(toolName, refs, opts = {}) {
|
|
1509
|
+
const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
|
|
1510
|
+
(v) => typeof v === "number"
|
|
1511
|
+
).length;
|
|
1512
|
+
if (opts.required && set !== 1) {
|
|
1513
|
+
throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
|
|
1514
|
+
}
|
|
1515
|
+
if (set > 1) {
|
|
1516
|
+
throw new Error(
|
|
1517
|
+
`${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1483
1521
|
|
|
1484
1522
|
// src/tools/define-batch.ts
|
|
1485
|
-
import { z as
|
|
1523
|
+
import { z as z3 } from "zod";
|
|
1486
1524
|
|
|
1487
1525
|
// src/capsule/batch.ts
|
|
1488
1526
|
function chunk(arr, size) {
|
|
@@ -1501,37 +1539,43 @@ function getBatchConcurrency() {
|
|
|
1501
1539
|
MAX_CONCURRENCY
|
|
1502
1540
|
);
|
|
1503
1541
|
}
|
|
1504
|
-
async function
|
|
1505
|
-
const concurrency = getBatchConcurrency();
|
|
1542
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1506
1543
|
const results = new Array(items.length);
|
|
1507
|
-
const startedAt = Date.now();
|
|
1508
|
-
const signal = options.signal;
|
|
1509
1544
|
let cursor = 0;
|
|
1510
1545
|
async function worker() {
|
|
1511
1546
|
while (true) {
|
|
1512
1547
|
const i = cursor;
|
|
1513
1548
|
cursor += 1;
|
|
1514
1549
|
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
|
-
}
|
|
1550
|
+
results[i] = await fn(items[i], i);
|
|
1528
1551
|
}
|
|
1529
1552
|
}
|
|
1530
1553
|
const workers = [];
|
|
1531
|
-
for (let w = 0; w < Math.min(
|
|
1554
|
+
for (let w = 0; w < Math.min(limit, items.length); w++) {
|
|
1532
1555
|
workers.push(worker());
|
|
1533
1556
|
}
|
|
1534
1557
|
await Promise.all(workers);
|
|
1558
|
+
return results;
|
|
1559
|
+
}
|
|
1560
|
+
async function batchExecute(tool, items, action, options = {}) {
|
|
1561
|
+
const concurrency = getBatchConcurrency();
|
|
1562
|
+
const startedAt = Date.now();
|
|
1563
|
+
const signal = options.signal;
|
|
1564
|
+
const results = await mapWithConcurrency(
|
|
1565
|
+
items,
|
|
1566
|
+
concurrency,
|
|
1567
|
+
async (item, i) => {
|
|
1568
|
+
if (signal?.aborted) {
|
|
1569
|
+
return { ok: false, error: { message: "cancelled by tasks/cancel" } };
|
|
1570
|
+
}
|
|
1571
|
+
try {
|
|
1572
|
+
const result = await action(item, i);
|
|
1573
|
+
return { ok: true, result };
|
|
1574
|
+
} catch (err) {
|
|
1575
|
+
return { ok: false, error: extractError(err) };
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
);
|
|
1535
1579
|
const succeeded = results.filter((r) => r.ok).length;
|
|
1536
1580
|
const failed = results.length - succeeded;
|
|
1537
1581
|
const summary = { total: results.length, succeeded, failed };
|
|
@@ -1576,10 +1620,52 @@ function topFailureReasons(results, n) {
|
|
|
1576
1620
|
return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
|
|
1577
1621
|
}
|
|
1578
1622
|
|
|
1623
|
+
// src/tools/strip-descriptions.ts
|
|
1624
|
+
import { z as z2 } from "zod";
|
|
1625
|
+
function cloneWithDef(node, patch) {
|
|
1626
|
+
const def = node.def;
|
|
1627
|
+
return node.clone({ ...def, ...patch });
|
|
1628
|
+
}
|
|
1629
|
+
function stripDescriptions(schema) {
|
|
1630
|
+
let node = schema;
|
|
1631
|
+
if (node instanceof z2.ZodObject) {
|
|
1632
|
+
const shape = node.def.shape;
|
|
1633
|
+
const next = {};
|
|
1634
|
+
let changed = false;
|
|
1635
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
1636
|
+
next[key] = stripDescriptions(child);
|
|
1637
|
+
if (next[key] !== child) changed = true;
|
|
1638
|
+
}
|
|
1639
|
+
if (changed) node = cloneWithDef(node, { shape: next });
|
|
1640
|
+
} else if (node instanceof z2.ZodArray) {
|
|
1641
|
+
const element = stripDescriptions(node.def.element);
|
|
1642
|
+
if (element !== node.def.element) node = cloneWithDef(node, { element });
|
|
1643
|
+
} else if (node instanceof z2.ZodOptional || node instanceof z2.ZodNullable || node instanceof z2.ZodDefault || node instanceof z2.ZodReadonly) {
|
|
1644
|
+
const innerType = stripDescriptions(node.def.innerType);
|
|
1645
|
+
if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
|
|
1646
|
+
} else if (node instanceof z2.ZodUnion) {
|
|
1647
|
+
const options = node.def.options.map(stripDescriptions);
|
|
1648
|
+
if (options.some((o, i) => o !== node.def.options[i])) {
|
|
1649
|
+
node = cloneWithDef(node, { options });
|
|
1650
|
+
}
|
|
1651
|
+
} else if (node instanceof z2.ZodPipe) {
|
|
1652
|
+
const inSchema = stripDescriptions(node.def.in);
|
|
1653
|
+
const outSchema = stripDescriptions(node.def.out);
|
|
1654
|
+
if (inSchema !== node.def.in || outSchema !== node.def.out) {
|
|
1655
|
+
node = cloneWithDef(node, { in: inSchema, out: outSchema });
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (node.description !== void 0) {
|
|
1659
|
+
node = node.meta({ description: void 0 });
|
|
1660
|
+
}
|
|
1661
|
+
return node;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1579
1664
|
// src/tools/define-batch.ts
|
|
1580
1665
|
function defineBatch(args) {
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1666
|
+
const itemSchema = stripDescriptions(args.itemSchema);
|
|
1667
|
+
const schema = z3.object({
|
|
1668
|
+
items: z3.array(itemSchema).min(1).max(50).describe(args.itemDescription)
|
|
1583
1669
|
});
|
|
1584
1670
|
async function handler(input, opts = {}) {
|
|
1585
1671
|
return batchExecute(args.toolName, input.items, args.itemHandler, opts);
|
|
@@ -1587,27 +1673,51 @@ function defineBatch(args) {
|
|
|
1587
1673
|
return { schema, handler };
|
|
1588
1674
|
}
|
|
1589
1675
|
|
|
1590
|
-
// src/tools/descriptions.ts
|
|
1591
|
-
var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'";
|
|
1592
|
-
var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
|
|
1593
|
-
|
|
1594
1676
|
// src/tools/define-delete.ts
|
|
1595
|
-
import { z as
|
|
1677
|
+
import { z as z6 } from "zod";
|
|
1596
1678
|
|
|
1597
1679
|
// src/tools/confirm-flag.ts
|
|
1598
|
-
import { z as
|
|
1680
|
+
import { z as z4 } from "zod";
|
|
1599
1681
|
var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
|
|
1600
1682
|
function confirmFlag() {
|
|
1601
|
-
return
|
|
1683
|
+
return z4.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
|
|
1602
1684
|
}
|
|
1603
1685
|
|
|
1604
1686
|
// src/tools/shared-schemas.ts
|
|
1605
|
-
import { z as
|
|
1606
|
-
var positiveId =
|
|
1687
|
+
import { z as z5 } from "zod";
|
|
1688
|
+
var positiveId = z5.preprocess((input) => {
|
|
1607
1689
|
if (typeof input !== "string") return input;
|
|
1608
1690
|
const trimmed = input.trim();
|
|
1609
1691
|
return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
|
|
1610
|
-
},
|
|
1692
|
+
}, z5.number().int().positive());
|
|
1693
|
+
var paginationFields = {
|
|
1694
|
+
page: z5.number().int().positive().optional().default(1),
|
|
1695
|
+
perPage: z5.number().int().min(1).max(100).optional().default(25)
|
|
1696
|
+
};
|
|
1697
|
+
var paginationFieldsNoDefaults = {
|
|
1698
|
+
page: z5.number().int().positive().optional(),
|
|
1699
|
+
perPage: z5.number().int().min(1).max(100).optional()
|
|
1700
|
+
};
|
|
1701
|
+
var ENTITY_PATH = {
|
|
1702
|
+
parties: "parties",
|
|
1703
|
+
opportunities: "opportunities",
|
|
1704
|
+
projects: "kases"
|
|
1705
|
+
};
|
|
1706
|
+
function embedParam(allowed) {
|
|
1707
|
+
return z5.string().superRefine((value, ctx) => {
|
|
1708
|
+
const tokens = value.split(",").map((t) => t.trim());
|
|
1709
|
+
for (const token of tokens) {
|
|
1710
|
+
if (token === "" || !allowed.includes(token)) {
|
|
1711
|
+
ctx.addIssue({
|
|
1712
|
+
code: "custom",
|
|
1713
|
+
message: `Unknown embed token '${token}'. Valid tokens: ${allowed.join(", ")} (comma-separated). Capsule silently ignores unknown tokens, so this is rejected client-side to prevent silently-missing data.`
|
|
1714
|
+
});
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
}).describe(`Comma-separated embeds. Valid tokens: ${allowed.join(", ")}.`).optional();
|
|
1718
|
+
}
|
|
1719
|
+
var RECORD_EMBEDS = ["tags", "fields", "missingImportantFields"];
|
|
1720
|
+
var ENTRY_EMBEDS = ["attachments", "participants"];
|
|
1611
1721
|
|
|
1612
1722
|
// src/capsule/idempotent.ts
|
|
1613
1723
|
var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
|
|
@@ -1634,7 +1744,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
|
|
|
1634
1744
|
// src/tools/define-delete.ts
|
|
1635
1745
|
function defineDelete(args) {
|
|
1636
1746
|
const { toolName, pathPrefix, confirmHint, idDescription } = args;
|
|
1637
|
-
const schema =
|
|
1747
|
+
const schema = z6.object({
|
|
1638
1748
|
id: idDescription ? positiveId.describe(idDescription) : positiveId,
|
|
1639
1749
|
confirm: confirmFlag().describe(confirmHint)
|
|
1640
1750
|
});
|
|
@@ -1682,12 +1792,12 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
|
|
|
1682
1792
|
}
|
|
1683
1793
|
|
|
1684
1794
|
// src/tools/custom-field-helpers.ts
|
|
1685
|
-
import { z as
|
|
1686
|
-
var CustomFieldWriteSchema =
|
|
1795
|
+
import { z as z7 } from "zod";
|
|
1796
|
+
var CustomFieldWriteSchema = z7.object({
|
|
1687
1797
|
definitionId: positiveId.describe(
|
|
1688
1798
|
"The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
|
|
1689
1799
|
),
|
|
1690
|
-
value:
|
|
1800
|
+
value: z7.union([z7.string(), z7.number(), z7.boolean(), z7.null()]).describe(
|
|
1691
1801
|
"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
1802
|
)
|
|
1693
1803
|
});
|
|
@@ -1703,24 +1813,24 @@ function mapFieldsForBody(fields) {
|
|
|
1703
1813
|
}
|
|
1704
1814
|
|
|
1705
1815
|
// src/tools/parties.ts
|
|
1706
|
-
var EmailAddressSchema =
|
|
1707
|
-
address:
|
|
1708
|
-
type:
|
|
1816
|
+
var EmailAddressSchema = z8.object({
|
|
1817
|
+
address: z8.string().email(),
|
|
1818
|
+
type: z8.string().optional()
|
|
1709
1819
|
});
|
|
1710
|
-
var PhoneNumberSchema =
|
|
1820
|
+
var PhoneNumberSchema = z8.object({
|
|
1711
1821
|
// Capsule rejects empty strings with `phoneNumber.number: number is
|
|
1712
1822
|
// required`. Enforce at the schema layer to catch typos pre-call,
|
|
1713
1823
|
// matching how EmailAddressSchema's address field behaves.
|
|
1714
|
-
number:
|
|
1715
|
-
type:
|
|
1824
|
+
number: z8.string().min(1),
|
|
1825
|
+
type: z8.string().optional()
|
|
1716
1826
|
});
|
|
1717
1827
|
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:
|
|
1828
|
+
var AddressSchema = z8.object({
|
|
1829
|
+
street: z8.string().optional(),
|
|
1830
|
+
city: z8.string().optional(),
|
|
1831
|
+
state: z8.string().optional(),
|
|
1832
|
+
country: z8.string().optional().describe(CountryDescription),
|
|
1833
|
+
zip: z8.string().optional()
|
|
1724
1834
|
});
|
|
1725
1835
|
function validateWebsiteAddress(data, ctx) {
|
|
1726
1836
|
const isUrlService = data.service === void 0 || data.service === "URL";
|
|
@@ -1743,7 +1853,7 @@ function validateWebsiteAddress(data, ctx) {
|
|
|
1743
1853
|
});
|
|
1744
1854
|
}
|
|
1745
1855
|
}
|
|
1746
|
-
var WebsiteServiceEnum =
|
|
1856
|
+
var WebsiteServiceEnum = z8.enum([
|
|
1747
1857
|
"URL",
|
|
1748
1858
|
"SKYPE",
|
|
1749
1859
|
"TWITTER",
|
|
@@ -1762,33 +1872,31 @@ var WebsiteServiceEnum = z7.enum([
|
|
|
1762
1872
|
"BLUESKY",
|
|
1763
1873
|
"SNAPCHAT"
|
|
1764
1874
|
]);
|
|
1765
|
-
var WebsiteSchema =
|
|
1766
|
-
address:
|
|
1875
|
+
var WebsiteSchema = z8.object({
|
|
1876
|
+
address: z8.string().min(1).describe(
|
|
1767
1877
|
"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
1878
|
),
|
|
1769
1879
|
service: WebsiteServiceEnum.optional().describe(
|
|
1770
1880
|
"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
1881
|
)
|
|
1772
1882
|
}).superRefine(validateWebsiteAddress);
|
|
1773
|
-
var searchPartiesSchema =
|
|
1774
|
-
q:
|
|
1775
|
-
embed:
|
|
1776
|
-
|
|
1777
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1883
|
+
var searchPartiesSchema = z8.object({
|
|
1884
|
+
q: z8.string().optional().describe("Free-text search query"),
|
|
1885
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
1886
|
+
...paginationFields
|
|
1778
1887
|
});
|
|
1779
1888
|
async function searchParties(input) {
|
|
1780
1889
|
const path = input.q ? "/parties/search" : "/parties";
|
|
1781
|
-
|
|
1890
|
+
return capsuleGetList(path, {
|
|
1782
1891
|
q: input.q,
|
|
1783
1892
|
embed: input.embed,
|
|
1784
1893
|
page: input.page,
|
|
1785
1894
|
perPage: input.perPage
|
|
1786
1895
|
});
|
|
1787
|
-
return { ...data, nextPage };
|
|
1788
1896
|
}
|
|
1789
|
-
var getPartySchema =
|
|
1897
|
+
var getPartySchema = z8.object({
|
|
1790
1898
|
id: positiveId.describe("Party ID"),
|
|
1791
|
-
embed:
|
|
1899
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
1792
1900
|
});
|
|
1793
1901
|
async function getParty(input) {
|
|
1794
1902
|
const { data } = await capsuleGet(`/parties/${input.id}`, {
|
|
@@ -1796,51 +1904,47 @@ async function getParty(input) {
|
|
|
1796
1904
|
});
|
|
1797
1905
|
return data;
|
|
1798
1906
|
}
|
|
1799
|
-
var getPartiesSchema =
|
|
1800
|
-
ids:
|
|
1907
|
+
var getPartiesSchema = z8.object({
|
|
1908
|
+
ids: z8.array(positiveId).min(1).max(50).describe(
|
|
1801
1909
|
"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
1910
|
),
|
|
1803
|
-
embed:
|
|
1911
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
1804
1912
|
});
|
|
1805
1913
|
async function getParties(input) {
|
|
1806
1914
|
return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
|
|
1807
1915
|
}
|
|
1808
|
-
var listPartyOpportunitiesSchema =
|
|
1916
|
+
var listPartyOpportunitiesSchema = z8.object({
|
|
1809
1917
|
partyId: positiveId,
|
|
1810
|
-
|
|
1811
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1918
|
+
...paginationFields
|
|
1812
1919
|
});
|
|
1813
1920
|
async function listPartyOpportunities(input) {
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
);
|
|
1818
|
-
return { ...data, nextPage };
|
|
1921
|
+
return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
|
|
1922
|
+
page: input.page,
|
|
1923
|
+
perPage: input.perPage
|
|
1924
|
+
});
|
|
1819
1925
|
}
|
|
1820
|
-
var listPartyProjectsSchema =
|
|
1926
|
+
var listPartyProjectsSchema = z8.object({
|
|
1821
1927
|
partyId: positiveId,
|
|
1822
|
-
|
|
1823
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1928
|
+
...paginationFields
|
|
1824
1929
|
});
|
|
1825
1930
|
async function listPartyProjects(input) {
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
);
|
|
1830
|
-
return { ...data, nextPage };
|
|
1931
|
+
return capsuleGetList(`/parties/${input.partyId}/kases`, {
|
|
1932
|
+
page: input.page,
|
|
1933
|
+
perPage: input.perPage
|
|
1934
|
+
});
|
|
1831
1935
|
}
|
|
1832
1936
|
var PartyWriteBaseSchema = {
|
|
1833
|
-
about:
|
|
1834
|
-
emailAddresses:
|
|
1937
|
+
about: z8.string().optional(),
|
|
1938
|
+
emailAddresses: z8.array(EmailAddressSchema).optional().describe(
|
|
1835
1939
|
"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
1940
|
),
|
|
1837
|
-
phoneNumbers:
|
|
1941
|
+
phoneNumbers: z8.array(PhoneNumberSchema).optional().describe(
|
|
1838
1942
|
"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
1943
|
),
|
|
1840
|
-
addresses:
|
|
1944
|
+
addresses: z8.array(AddressSchema).optional().describe(
|
|
1841
1945
|
"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
1946
|
),
|
|
1843
|
-
websites:
|
|
1947
|
+
websites: z8.array(WebsiteSchema).optional().describe(
|
|
1844
1948
|
"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
1949
|
),
|
|
1846
1950
|
ownerId: positiveId.nullable().optional().describe(
|
|
@@ -1850,16 +1954,16 @@ var PartyWriteBaseSchema = {
|
|
|
1850
1954
|
"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
1955
|
)
|
|
1852
1956
|
};
|
|
1853
|
-
var createPartySchema =
|
|
1854
|
-
type:
|
|
1957
|
+
var createPartySchema = z8.object({
|
|
1958
|
+
type: z8.enum(["person", "organisation"]),
|
|
1855
1959
|
// person
|
|
1856
|
-
firstName:
|
|
1857
|
-
lastName:
|
|
1858
|
-
title:
|
|
1859
|
-
jobTitle:
|
|
1960
|
+
firstName: z8.string().optional(),
|
|
1961
|
+
lastName: z8.string().optional(),
|
|
1962
|
+
title: z8.string().optional(),
|
|
1963
|
+
jobTitle: z8.string().optional(),
|
|
1860
1964
|
organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
|
|
1861
1965
|
// organisation
|
|
1862
|
-
name:
|
|
1966
|
+
name: z8.string().optional(),
|
|
1863
1967
|
...PartyWriteBaseSchema,
|
|
1864
1968
|
ownerId: positiveId.optional().describe(
|
|
1865
1969
|
"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,9 +1971,24 @@ var createPartySchema = z7.object({
|
|
|
1867
1971
|
teamId: positiveId.optional().describe(
|
|
1868
1972
|
"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
1973
|
),
|
|
1870
|
-
fields:
|
|
1974
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(
|
|
1871
1975
|
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
1976
|
)
|
|
1977
|
+
}).superRefine((data, ctx) => {
|
|
1978
|
+
if (data.type === "person" && !data.firstName && !data.lastName) {
|
|
1979
|
+
ctx.addIssue({
|
|
1980
|
+
code: "custom",
|
|
1981
|
+
path: ["firstName"],
|
|
1982
|
+
message: "create_party: a person requires firstName and/or lastName"
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
if (data.type === "organisation" && !data.name) {
|
|
1986
|
+
ctx.addIssue({
|
|
1987
|
+
code: "custom",
|
|
1988
|
+
path: ["name"],
|
|
1989
|
+
message: "create_party: an organisation requires name"
|
|
1990
|
+
});
|
|
1991
|
+
}
|
|
1873
1992
|
});
|
|
1874
1993
|
async function createParty(input) {
|
|
1875
1994
|
const { ownerId, teamId, organisationId, fields, ...rest } = input;
|
|
@@ -1881,17 +2000,17 @@ async function createParty(input) {
|
|
|
1881
2000
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1882
2001
|
return capsulePost("/parties", { party: body });
|
|
1883
2002
|
}
|
|
1884
|
-
var updatePartySchema =
|
|
2003
|
+
var updatePartySchema = z8.object({
|
|
1885
2004
|
id: positiveId,
|
|
1886
|
-
firstName:
|
|
1887
|
-
lastName:
|
|
1888
|
-
title:
|
|
1889
|
-
jobTitle:
|
|
1890
|
-
name:
|
|
2005
|
+
firstName: z8.string().optional(),
|
|
2006
|
+
lastName: z8.string().optional(),
|
|
2007
|
+
title: z8.string().optional(),
|
|
2008
|
+
jobTitle: z8.string().optional(),
|
|
2009
|
+
name: z8.string().optional(),
|
|
1891
2010
|
organisationId: positiveId.nullable().optional().describe(
|
|
1892
2011
|
"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
2012
|
),
|
|
1894
|
-
fields:
|
|
2013
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
|
|
1895
2014
|
...PartyWriteBaseSchema
|
|
1896
2015
|
});
|
|
1897
2016
|
async function updateParty(input) {
|
|
@@ -1920,12 +2039,44 @@ var { schema: batchUpdatePartySchema, handler: batchUpdateParty } = defineBatch(
|
|
|
1920
2039
|
var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
|
|
1921
2040
|
toolName: "delete_party",
|
|
1922
2041
|
pathPrefix: "/parties",
|
|
1923
|
-
confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects
|
|
2042
|
+
confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects. Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
|
|
1924
2043
|
});
|
|
1925
|
-
|
|
2044
|
+
function definePartySubResourceRemove(opts) {
|
|
2045
|
+
const shape = {
|
|
2046
|
+
partyId: positiveId,
|
|
2047
|
+
[opts.idField]: positiveId.describe(
|
|
2048
|
+
`Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
|
|
2049
|
+
)
|
|
2050
|
+
};
|
|
2051
|
+
const schema = z8.object(shape);
|
|
2052
|
+
async function handler(input) {
|
|
2053
|
+
const partyId = input["partyId"];
|
|
2054
|
+
const rowId = input[opts.idField];
|
|
2055
|
+
return idempotentWithResult(
|
|
2056
|
+
() => capsulePut(`/parties/${partyId}`, {
|
|
2057
|
+
party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
|
|
2058
|
+
}),
|
|
2059
|
+
(result) => ({
|
|
2060
|
+
removed: true,
|
|
2061
|
+
alreadyRemoved: false,
|
|
2062
|
+
partyId,
|
|
2063
|
+
[opts.idField]: rowId,
|
|
2064
|
+
...result
|
|
2065
|
+
}),
|
|
2066
|
+
() => ({
|
|
2067
|
+
removed: true,
|
|
2068
|
+
alreadyRemoved: true,
|
|
2069
|
+
partyId,
|
|
2070
|
+
[opts.idField]: rowId
|
|
2071
|
+
})
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
return { schema, handler };
|
|
2075
|
+
}
|
|
2076
|
+
var addPartyEmailAddressSchema = z8.object({
|
|
1926
2077
|
partyId: positiveId,
|
|
1927
|
-
address:
|
|
1928
|
-
type:
|
|
2078
|
+
address: z8.string().email(),
|
|
2079
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
|
|
1929
2080
|
});
|
|
1930
2081
|
async function addPartyEmailAddress(input) {
|
|
1931
2082
|
const { partyId, address, type } = input;
|
|
@@ -1935,32 +2086,17 @@ async function addPartyEmailAddress(input) {
|
|
|
1935
2086
|
party: { emailAddresses: [item] }
|
|
1936
2087
|
});
|
|
1937
2088
|
}
|
|
1938
|
-
var
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
)
|
|
2089
|
+
var removePartyEmailAddress = definePartySubResourceRemove({
|
|
2090
|
+
arrayKey: "emailAddresses",
|
|
2091
|
+
idField: "emailAddressId",
|
|
2092
|
+
rowNoun: "email-address"
|
|
1943
2093
|
});
|
|
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({
|
|
2094
|
+
var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
|
|
2095
|
+
var removePartyEmailAddressById = removePartyEmailAddress.handler;
|
|
2096
|
+
var addPartyPhoneNumberSchema = z8.object({
|
|
1961
2097
|
partyId: positiveId,
|
|
1962
|
-
number:
|
|
1963
|
-
type:
|
|
2098
|
+
number: z8.string().min(1),
|
|
2099
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
|
|
1964
2100
|
});
|
|
1965
2101
|
async function addPartyPhoneNumber(input) {
|
|
1966
2102
|
const { partyId, number, type } = input;
|
|
@@ -1970,36 +2106,21 @@ async function addPartyPhoneNumber(input) {
|
|
|
1970
2106
|
party: { phoneNumbers: [item] }
|
|
1971
2107
|
});
|
|
1972
2108
|
}
|
|
1973
|
-
var
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
)
|
|
2109
|
+
var removePartyPhoneNumber = definePartySubResourceRemove({
|
|
2110
|
+
arrayKey: "phoneNumbers",
|
|
2111
|
+
idField: "phoneNumberId",
|
|
2112
|
+
rowNoun: "phone-number"
|
|
1978
2113
|
});
|
|
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({
|
|
2114
|
+
var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
|
|
2115
|
+
var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
|
|
2116
|
+
var addPartyAddressSchema = z8.object({
|
|
1996
2117
|
partyId: positiveId,
|
|
1997
|
-
street:
|
|
1998
|
-
city:
|
|
1999
|
-
state:
|
|
2000
|
-
country:
|
|
2001
|
-
zip:
|
|
2002
|
-
type:
|
|
2118
|
+
street: z8.string().optional(),
|
|
2119
|
+
city: z8.string().optional(),
|
|
2120
|
+
state: z8.string().optional(),
|
|
2121
|
+
country: z8.string().optional().describe(CountryDescription),
|
|
2122
|
+
zip: z8.string().optional(),
|
|
2123
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
|
|
2003
2124
|
});
|
|
2004
2125
|
async function addPartyAddress(input) {
|
|
2005
2126
|
const { partyId, ...rest } = input;
|
|
@@ -2011,31 +2132,16 @@ async function addPartyAddress(input) {
|
|
|
2011
2132
|
party: { addresses: [item] }
|
|
2012
2133
|
});
|
|
2013
2134
|
}
|
|
2014
|
-
var
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
)
|
|
2135
|
+
var removePartyAddress = definePartySubResourceRemove({
|
|
2136
|
+
arrayKey: "addresses",
|
|
2137
|
+
idField: "addressId",
|
|
2138
|
+
rowNoun: "address"
|
|
2019
2139
|
});
|
|
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({
|
|
2140
|
+
var removePartyAddressByIdSchema = removePartyAddress.schema;
|
|
2141
|
+
var removePartyAddressById = removePartyAddress.handler;
|
|
2142
|
+
var addPartyWebsiteSchema = z8.object({
|
|
2037
2143
|
partyId: positiveId,
|
|
2038
|
-
address:
|
|
2144
|
+
address: z8.string().min(1).describe(
|
|
2039
2145
|
"The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
|
|
2040
2146
|
),
|
|
2041
2147
|
service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
|
|
@@ -2048,58 +2154,41 @@ async function addPartyWebsite(input) {
|
|
|
2048
2154
|
party: { websites: [item] }
|
|
2049
2155
|
});
|
|
2050
2156
|
}
|
|
2051
|
-
var
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
)
|
|
2157
|
+
var removePartyWebsite = definePartySubResourceRemove({
|
|
2158
|
+
arrayKey: "websites",
|
|
2159
|
+
idField: "websiteId",
|
|
2160
|
+
rowNoun: "website"
|
|
2056
2161
|
});
|
|
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
|
-
}
|
|
2162
|
+
var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
|
|
2163
|
+
var removePartyWebsiteById = removePartyWebsite.handler;
|
|
2073
2164
|
|
|
2074
2165
|
// src/tools/opportunities.ts
|
|
2075
|
-
import { z as
|
|
2076
|
-
var OpportunityValueSchema =
|
|
2077
|
-
amount:
|
|
2078
|
-
currency:
|
|
2166
|
+
import { z as z9 } from "zod";
|
|
2167
|
+
var OpportunityValueSchema = z9.object({
|
|
2168
|
+
amount: z9.number().nonnegative(),
|
|
2169
|
+
currency: z9.string({
|
|
2079
2170
|
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
2171
|
}).length(3).describe(
|
|
2081
2172
|
"ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
|
|
2082
2173
|
)
|
|
2083
2174
|
});
|
|
2084
|
-
var searchOpportunitiesSchema =
|
|
2085
|
-
q:
|
|
2086
|
-
embed:
|
|
2087
|
-
|
|
2088
|
-
perPage: z8.number().int().min(1).max(100).optional().default(25)
|
|
2175
|
+
var searchOpportunitiesSchema = z9.object({
|
|
2176
|
+
q: z9.string().optional().describe("Free-text search query"),
|
|
2177
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
2178
|
+
...paginationFields
|
|
2089
2179
|
});
|
|
2090
2180
|
async function searchOpportunities(input) {
|
|
2091
2181
|
const path = input.q ? "/opportunities/search" : "/opportunities";
|
|
2092
|
-
|
|
2182
|
+
return capsuleGetList(path, {
|
|
2093
2183
|
q: input.q,
|
|
2094
2184
|
embed: input.embed,
|
|
2095
2185
|
page: input.page,
|
|
2096
2186
|
perPage: input.perPage
|
|
2097
2187
|
});
|
|
2098
|
-
return { ...data, nextPage };
|
|
2099
2188
|
}
|
|
2100
|
-
var getOpportunitySchema =
|
|
2189
|
+
var getOpportunitySchema = z9.object({
|
|
2101
2190
|
id: positiveId,
|
|
2102
|
-
embed:
|
|
2191
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
2103
2192
|
});
|
|
2104
2193
|
async function getOpportunity(input) {
|
|
2105
2194
|
const { data } = await capsuleGet(`/opportunities/${input.id}`, {
|
|
@@ -2107,32 +2196,32 @@ async function getOpportunity(input) {
|
|
|
2107
2196
|
});
|
|
2108
2197
|
return data;
|
|
2109
2198
|
}
|
|
2110
|
-
var getOpportunitiesSchema =
|
|
2111
|
-
ids:
|
|
2199
|
+
var getOpportunitiesSchema = z9.object({
|
|
2200
|
+
ids: z9.array(positiveId).min(1).max(50).describe(
|
|
2112
2201
|
"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
2202
|
),
|
|
2114
|
-
embed:
|
|
2203
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
2115
2204
|
});
|
|
2116
2205
|
async function getOpportunities(input) {
|
|
2117
2206
|
return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
|
|
2118
2207
|
}
|
|
2119
|
-
var createOpportunitySchema =
|
|
2120
|
-
name:
|
|
2208
|
+
var createOpportunitySchema = z9.object({
|
|
2209
|
+
name: z9.string().min(1),
|
|
2121
2210
|
partyId: positiveId.describe("ID of the party this opportunity belongs to"),
|
|
2122
2211
|
milestoneId: positiveId.describe(
|
|
2123
2212
|
"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
2213
|
),
|
|
2125
|
-
description:
|
|
2214
|
+
description: z9.string().optional(),
|
|
2126
2215
|
value: OpportunityValueSchema.optional(),
|
|
2127
|
-
expectedCloseOn:
|
|
2128
|
-
probability:
|
|
2216
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2217
|
+
probability: z9.number().int().min(0).max(100).optional(),
|
|
2129
2218
|
ownerId: positiveId.optional().describe(
|
|
2130
2219
|
"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
2220
|
),
|
|
2132
2221
|
teamId: positiveId.optional().describe(
|
|
2133
2222
|
"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
2223
|
),
|
|
2135
|
-
fields:
|
|
2224
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
2136
2225
|
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
2226
|
)
|
|
2138
2227
|
});
|
|
@@ -2149,23 +2238,23 @@ async function createOpportunity(input) {
|
|
|
2149
2238
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2150
2239
|
return capsulePost("/opportunities", { opportunity: body });
|
|
2151
2240
|
}
|
|
2152
|
-
var updateOpportunitySchema =
|
|
2241
|
+
var updateOpportunitySchema = z9.object({
|
|
2153
2242
|
id: positiveId,
|
|
2154
|
-
name:
|
|
2243
|
+
name: z9.string().min(1).optional(),
|
|
2155
2244
|
partyId: positiveId.optional().describe(
|
|
2156
2245
|
"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
2246
|
),
|
|
2158
2247
|
milestoneId: positiveId.optional().describe(
|
|
2159
2248
|
"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
2249
|
),
|
|
2161
|
-
description:
|
|
2250
|
+
description: z9.string().optional(),
|
|
2162
2251
|
value: OpportunityValueSchema.optional(),
|
|
2163
|
-
expectedCloseOn:
|
|
2164
|
-
probability:
|
|
2252
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
2253
|
+
probability: z9.number().int().min(0).max(100).optional().describe(
|
|
2165
2254
|
"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
2255
|
),
|
|
2167
2256
|
lostReasonId: positiveId.optional().describe(
|
|
2168
|
-
"Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via
|
|
2257
|
+
"Reason the opportunity was lost. Only meaningful when transitioning to a Lost milestone \u2014 Capsule silently drops it for other milestones. Without this set, a connector-driven Lost-close leaves `lostReason: null`. Discover IDs via list_lost_reasons."
|
|
2169
2258
|
),
|
|
2170
2259
|
ownerId: positiveId.nullable().optional().describe(
|
|
2171
2260
|
"Reassign owner: pass a user ID to set, or `null` to unassign (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `owner: null` on PUT /opportunities/:id, mirroring the v1.6.4 finding on /parties; brings update_opportunity into parity with update_party and update_project). When you supply `ownerId` and omit `teamId`, the connector fetches the opportunity's current team and includes it in the PUT body to preserve it across the owner change. Without this defensive read, Capsule's PUT would clear the existing team (see NOTES-ON-CAPSULE-API.md \xA727 \u2014 same asymmetric semantic as /kases). Supply `teamId` explicitly on the same call to change the team instead. Combine `ownerId: null` + `teamId: <T>` in one call to transfer an opportunity to team-ownership with no specific user (verified empirically in v1.6.5; the owner-clears-team semantic doesn't fire when owner is being cleared to null)."
|
|
@@ -2173,7 +2262,7 @@ var updateOpportunitySchema = z8.object({
|
|
|
2173
2262
|
teamId: positiveId.nullable().optional().describe(
|
|
2174
2263
|
"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
2264
|
),
|
|
2176
|
-
fields:
|
|
2265
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
|
|
2177
2266
|
});
|
|
2178
2267
|
async function updateOpportunity(input) {
|
|
2179
2268
|
const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
@@ -2209,25 +2298,37 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
|
|
|
2209
2298
|
});
|
|
2210
2299
|
|
|
2211
2300
|
// src/tools/projects.ts
|
|
2212
|
-
import { z as
|
|
2213
|
-
var
|
|
2214
|
-
|
|
2215
|
-
embed:
|
|
2216
|
-
|
|
2217
|
-
|
|
2301
|
+
import { z as z10 } from "zod";
|
|
2302
|
+
var searchProjectsSchema = z10.object({
|
|
2303
|
+
q: z10.string().optional().describe("Free-text search query"),
|
|
2304
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
2305
|
+
...paginationFields
|
|
2306
|
+
});
|
|
2307
|
+
async function searchProjects(input) {
|
|
2308
|
+
const path = input.q ? "/kases/search" : "/kases";
|
|
2309
|
+
return capsuleGetList(path, {
|
|
2310
|
+
q: input.q,
|
|
2311
|
+
embed: input.embed,
|
|
2312
|
+
page: input.page,
|
|
2313
|
+
perPage: input.perPage
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
var listProjectsSchema = z10.object({
|
|
2317
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional(),
|
|
2318
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
2319
|
+
...paginationFields
|
|
2218
2320
|
});
|
|
2219
2321
|
async function listProjects(input) {
|
|
2220
|
-
|
|
2322
|
+
return capsuleGetList("/kases", {
|
|
2221
2323
|
status: input.status,
|
|
2222
2324
|
embed: input.embed,
|
|
2223
2325
|
page: input.page,
|
|
2224
2326
|
perPage: input.perPage
|
|
2225
2327
|
});
|
|
2226
|
-
return { ...data, nextPage };
|
|
2227
2328
|
}
|
|
2228
|
-
var getProjectSchema =
|
|
2329
|
+
var getProjectSchema = z10.object({
|
|
2229
2330
|
id: positiveId,
|
|
2230
|
-
embed:
|
|
2331
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
2231
2332
|
});
|
|
2232
2333
|
async function getProject(input) {
|
|
2233
2334
|
const { data } = await capsuleGet(`/kases/${input.id}`, {
|
|
@@ -2235,20 +2336,20 @@ async function getProject(input) {
|
|
|
2235
2336
|
});
|
|
2236
2337
|
return data;
|
|
2237
2338
|
}
|
|
2238
|
-
var getProjectsSchema =
|
|
2239
|
-
ids:
|
|
2339
|
+
var getProjectsSchema = z10.object({
|
|
2340
|
+
ids: z10.array(positiveId).min(1).max(50).describe(
|
|
2240
2341
|
"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
2342
|
),
|
|
2242
|
-
embed:
|
|
2343
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
2243
2344
|
});
|
|
2244
2345
|
async function getProjects(input) {
|
|
2245
|
-
return chunkedMultiGet("/kases", "
|
|
2346
|
+
return chunkedMultiGet("/kases", "projects", input.ids, { embed: input.embed });
|
|
2246
2347
|
}
|
|
2247
|
-
var createProjectSchema =
|
|
2248
|
-
name:
|
|
2348
|
+
var createProjectSchema = z10.object({
|
|
2349
|
+
name: z10.string().min(1),
|
|
2249
2350
|
partyId: positiveId.describe("ID of the party linked to this project"),
|
|
2250
|
-
description:
|
|
2251
|
-
status:
|
|
2351
|
+
description: z10.string().optional(),
|
|
2352
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
|
|
2252
2353
|
ownerId: positiveId.optional().describe(
|
|
2253
2354
|
"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
2355
|
),
|
|
@@ -2258,8 +2359,8 @@ var createProjectSchema = z9.object({
|
|
|
2258
2359
|
stageId: positiveId.optional().describe(
|
|
2259
2360
|
"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
2361
|
),
|
|
2261
|
-
expectedCloseOn:
|
|
2262
|
-
fields:
|
|
2362
|
+
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2363
|
+
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2263
2364
|
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
2365
|
)
|
|
2265
2366
|
});
|
|
@@ -2277,11 +2378,11 @@ async function createProject(input) {
|
|
|
2277
2378
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2278
2379
|
return capsulePost("/kases", { kase: body });
|
|
2279
2380
|
}
|
|
2280
|
-
var updateProjectSchema =
|
|
2381
|
+
var updateProjectSchema = z10.object({
|
|
2281
2382
|
id: positiveId,
|
|
2282
|
-
name:
|
|
2283
|
-
description:
|
|
2284
|
-
status:
|
|
2383
|
+
name: z10.string().min(1).optional(),
|
|
2384
|
+
description: z10.string().optional(),
|
|
2385
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional(),
|
|
2285
2386
|
partyId: positiveId.optional().describe(
|
|
2286
2387
|
"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
2388
|
),
|
|
@@ -2294,8 +2395,8 @@ var updateProjectSchema = z9.object({
|
|
|
2294
2395
|
stageId: positiveId.nullable().optional().describe(
|
|
2295
2396
|
"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
2397
|
),
|
|
2297
|
-
expectedCloseOn:
|
|
2298
|
-
fields:
|
|
2398
|
+
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2399
|
+
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2299
2400
|
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
2401
|
)
|
|
2301
2402
|
});
|
|
@@ -2309,7 +2410,7 @@ async function updateProject(input) {
|
|
|
2309
2410
|
let resolvedTeamId = teamId;
|
|
2310
2411
|
let resolvedStageId = stageId;
|
|
2311
2412
|
if (ownerId !== void 0 && (teamId === void 0 || stageId === void 0)) {
|
|
2312
|
-
const current = await readEntityRefs(`/kases/${id}`, "
|
|
2413
|
+
const current = await readEntityRefs(`/kases/${id}`, "project");
|
|
2313
2414
|
if (teamId === void 0) resolvedTeamId = current.teamId;
|
|
2314
2415
|
if (stageId === void 0) resolvedStageId = current.stageId;
|
|
2315
2416
|
}
|
|
@@ -2334,23 +2435,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
|
2334
2435
|
});
|
|
2335
2436
|
|
|
2336
2437
|
// src/tools/tasks.ts
|
|
2337
|
-
import { z as
|
|
2338
|
-
var listTasksSchema =
|
|
2438
|
+
import { z as z11 } from "zod";
|
|
2439
|
+
var listTasksSchema = z11.object({
|
|
2339
2440
|
// Note: Capsule has a third internal status `PENDING` (a task that's
|
|
2340
2441
|
// part of an active track but not yet "open"), but it can only be
|
|
2341
2442
|
// reached via track machinery — it is NOT directly settable by
|
|
2342
2443
|
// /tasks PUT, and a list filter for it returns the same as OPEN
|
|
2343
2444
|
// anyway. We expose only the two values that are actually filterable
|
|
2344
2445
|
// by the v2 API.
|
|
2345
|
-
status:
|
|
2446
|
+
status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
2346
2447
|
"Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
|
|
2347
2448
|
),
|
|
2348
2449
|
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)
|
|
2450
|
+
...paginationFields
|
|
2351
2451
|
});
|
|
2352
2452
|
async function listTasks(input) {
|
|
2353
|
-
|
|
2453
|
+
return capsuleGetList("/tasks", {
|
|
2354
2454
|
// Default 'OPEN' applied here (not via zod .default()) so that
|
|
2355
2455
|
// z.infer keeps `status` optional for callers that omit it.
|
|
2356
2456
|
status: input.status ?? "OPEN",
|
|
@@ -2359,28 +2459,27 @@ async function listTasks(input) {
|
|
|
2359
2459
|
page: input.page,
|
|
2360
2460
|
perPage: input.perPage
|
|
2361
2461
|
});
|
|
2362
|
-
return { ...data, nextPage };
|
|
2363
2462
|
}
|
|
2364
|
-
var getTaskSchema =
|
|
2463
|
+
var getTaskSchema = z11.object({
|
|
2365
2464
|
id: positiveId.describe("Task ID")
|
|
2366
2465
|
});
|
|
2367
2466
|
async function getTask(input) {
|
|
2368
2467
|
const { data } = await capsuleGet(`/tasks/${input.id}`);
|
|
2369
2468
|
return data;
|
|
2370
2469
|
}
|
|
2371
|
-
var getTasksSchema =
|
|
2372
|
-
ids:
|
|
2470
|
+
var getTasksSchema = z11.object({
|
|
2471
|
+
ids: z11.array(positiveId).min(1).max(50).describe(
|
|
2373
2472
|
"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
2473
|
)
|
|
2375
2474
|
});
|
|
2376
2475
|
async function getTasks(input) {
|
|
2377
2476
|
return chunkedMultiGet("/tasks", "tasks", input.ids);
|
|
2378
2477
|
}
|
|
2379
|
-
var createTaskSchema =
|
|
2380
|
-
description:
|
|
2381
|
-
dueOn:
|
|
2382
|
-
dueTime:
|
|
2383
|
-
detail:
|
|
2478
|
+
var createTaskSchema = z11.object({
|
|
2479
|
+
description: z11.string().min(1),
|
|
2480
|
+
dueOn: z11.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
|
|
2481
|
+
dueTime: z11.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
2482
|
+
detail: z11.string().optional(),
|
|
2384
2483
|
ownerId: positiveId.optional().describe(
|
|
2385
2484
|
"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
2485
|
),
|
|
@@ -2389,10 +2488,7 @@ var createTaskSchema = z10.object({
|
|
|
2389
2488
|
projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
|
|
2390
2489
|
});
|
|
2391
2490
|
async function createTask(input) {
|
|
2392
|
-
|
|
2393
|
-
if (linked.length > 1) {
|
|
2394
|
-
throw new Error("Provide at most one of partyId, opportunityId, or projectId");
|
|
2395
|
-
}
|
|
2491
|
+
assertSingleParentRef("create_task", input);
|
|
2396
2492
|
const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
2397
2493
|
const body = { ...rest };
|
|
2398
2494
|
setRef(body, "owner", ownerId);
|
|
@@ -2401,16 +2497,16 @@ async function createTask(input) {
|
|
|
2401
2497
|
setRef(body, "kase", projectId);
|
|
2402
2498
|
return capsulePost("/tasks", { task: body });
|
|
2403
2499
|
}
|
|
2404
|
-
var updateTaskSchema =
|
|
2500
|
+
var updateTaskSchema = z11.object({
|
|
2405
2501
|
id: positiveId,
|
|
2406
|
-
description:
|
|
2407
|
-
dueOn:
|
|
2408
|
-
dueTime:
|
|
2409
|
-
detail:
|
|
2502
|
+
description: z11.string().min(1).optional(),
|
|
2503
|
+
dueOn: z11.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2504
|
+
dueTime: z11.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
2505
|
+
detail: z11.string().optional(),
|
|
2410
2506
|
// Capsule rejects direct sets of `PENDING` (which is a track-machinery
|
|
2411
2507
|
// internal state) with 422 "cannot set task status to PENDING".
|
|
2412
2508
|
// Only OPEN and COMPLETED are settable here.
|
|
2413
|
-
status:
|
|
2509
|
+
status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
2414
2510
|
"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
2511
|
),
|
|
2416
2512
|
ownerId: positiveId.optional().describe(
|
|
@@ -2428,12 +2524,7 @@ var updateTaskSchema = z10.object({
|
|
|
2428
2524
|
});
|
|
2429
2525
|
async function updateTask(input) {
|
|
2430
2526
|
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
|
-
}
|
|
2527
|
+
assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
|
|
2437
2528
|
const body = {};
|
|
2438
2529
|
for (const [k, v] of Object.entries(rest)) {
|
|
2439
2530
|
if (v !== void 0) body[k] = v;
|
|
@@ -2444,7 +2535,7 @@ async function updateTask(input) {
|
|
|
2444
2535
|
setNullableRef(body, "kase", projectId);
|
|
2445
2536
|
return capsulePut(`/tasks/${id}`, { task: body });
|
|
2446
2537
|
}
|
|
2447
|
-
var completeTaskSchema =
|
|
2538
|
+
var completeTaskSchema = z11.object({
|
|
2448
2539
|
id: positiveId
|
|
2449
2540
|
});
|
|
2450
2541
|
async function completeTask(input) {
|
|
@@ -2452,8 +2543,8 @@ async function completeTask(input) {
|
|
|
2452
2543
|
task: { status: "COMPLETED" }
|
|
2453
2544
|
});
|
|
2454
2545
|
}
|
|
2455
|
-
var batchCompleteTaskSchema =
|
|
2456
|
-
ids:
|
|
2546
|
+
var batchCompleteTaskSchema = z11.object({
|
|
2547
|
+
ids: z11.array(positiveId).min(1).max(50).describe(
|
|
2457
2548
|
"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
2549
|
)
|
|
2459
2550
|
});
|
|
@@ -2467,77 +2558,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
|
|
|
2467
2558
|
});
|
|
2468
2559
|
|
|
2469
2560
|
// src/tools/entries.ts
|
|
2470
|
-
import { z as
|
|
2561
|
+
import { z as z12 } from "zod";
|
|
2471
2562
|
var listEntriesPagination = {
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2563
|
+
...paginationFields,
|
|
2564
|
+
embed: embedParam(ENTRY_EMBEDS)
|
|
2475
2565
|
};
|
|
2476
|
-
var listPartyEntriesSchema =
|
|
2566
|
+
var listPartyEntriesSchema = z12.object({
|
|
2477
2567
|
partyId: positiveId,
|
|
2478
2568
|
...listEntriesPagination,
|
|
2479
|
-
includeLinkedPersons:
|
|
2569
|
+
includeLinkedPersons: z12.boolean().optional().describe(
|
|
2480
2570
|
"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
2571
|
)
|
|
2482
2572
|
});
|
|
2573
|
+
var PER_PARTY_FETCH_CAP = 100;
|
|
2483
2574
|
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;
|
|
2575
|
+
return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
|
|
2576
|
+
const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
|
|
2577
|
+
embed,
|
|
2578
|
+
page: 1,
|
|
2579
|
+
perPage
|
|
2580
|
+
});
|
|
2581
|
+
return { entries: data.entries, nextPage };
|
|
2582
|
+
});
|
|
2510
2583
|
}
|
|
2511
2584
|
function mergedTimelineCandidatePerParty(page, perPage) {
|
|
2512
|
-
return Math.min(page * perPage,
|
|
2585
|
+
return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
|
|
2513
2586
|
}
|
|
2514
2587
|
function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
|
|
2515
2588
|
const requestedWindowEnd = page * perPage;
|
|
2516
2589
|
if (mergedLength > requestedWindowEnd) return page + 1;
|
|
2517
|
-
const nextWindowWithinCap = requestedWindowEnd <
|
|
2590
|
+
const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
|
|
2518
2591
|
if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
|
|
2519
2592
|
return void 0;
|
|
2520
2593
|
}
|
|
2521
2594
|
async function listPartyEntries(input) {
|
|
2522
2595
|
const { partyId, embed, page, perPage, includeLinkedPersons } = input;
|
|
2523
2596
|
if (!includeLinkedPersons) {
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2597
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2598
|
+
embed,
|
|
2599
|
+
page,
|
|
2600
|
+
perPage
|
|
2601
|
+
});
|
|
2529
2602
|
}
|
|
2530
2603
|
const { data: peopleData } = await capsuleGet(
|
|
2531
2604
|
`/parties/${partyId}/people`,
|
|
2532
|
-
{ page: 1, perPage:
|
|
2605
|
+
{ page: 1, perPage: PER_PARTY_FETCH_CAP }
|
|
2533
2606
|
);
|
|
2534
2607
|
const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
|
|
2535
2608
|
if (peopleIds.length === 0) {
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2609
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2610
|
+
embed,
|
|
2611
|
+
page,
|
|
2612
|
+
perPage
|
|
2613
|
+
});
|
|
2541
2614
|
}
|
|
2542
2615
|
const targetIds = [partyId, ...peopleIds];
|
|
2543
2616
|
const perPartyPages = await fanOutPartyEntries(
|
|
@@ -2572,31 +2645,31 @@ async function listPartyEntries(input) {
|
|
|
2572
2645
|
);
|
|
2573
2646
|
return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
|
|
2574
2647
|
}
|
|
2575
|
-
var listOpportunityEntriesSchema =
|
|
2648
|
+
var listOpportunityEntriesSchema = z12.object({
|
|
2576
2649
|
opportunityId: positiveId,
|
|
2577
2650
|
...listEntriesPagination
|
|
2578
2651
|
});
|
|
2579
2652
|
async function listOpportunityEntries(input) {
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2653
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
|
|
2654
|
+
embed: input.embed,
|
|
2655
|
+
page: input.page,
|
|
2656
|
+
perPage: input.perPage
|
|
2657
|
+
});
|
|
2585
2658
|
}
|
|
2586
|
-
var listProjectEntriesSchema =
|
|
2659
|
+
var listProjectEntriesSchema = z12.object({
|
|
2587
2660
|
projectId: positiveId,
|
|
2588
2661
|
...listEntriesPagination
|
|
2589
2662
|
});
|
|
2590
2663
|
async function listProjectEntries(input) {
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2664
|
+
return capsuleGetList(`/kases/${input.projectId}/entries`, {
|
|
2665
|
+
embed: input.embed,
|
|
2666
|
+
page: input.page,
|
|
2667
|
+
perPage: input.perPage
|
|
2668
|
+
});
|
|
2596
2669
|
}
|
|
2597
|
-
var getEntrySchema =
|
|
2670
|
+
var getEntrySchema = z12.object({
|
|
2598
2671
|
id: positiveId,
|
|
2599
|
-
embed:
|
|
2672
|
+
embed: embedParam(ENTRY_EMBEDS)
|
|
2600
2673
|
});
|
|
2601
2674
|
async function getEntry(input) {
|
|
2602
2675
|
const { data } = await capsuleGet(`/entries/${input.id}`, {
|
|
@@ -2604,34 +2677,30 @@ async function getEntry(input) {
|
|
|
2604
2677
|
});
|
|
2605
2678
|
return data;
|
|
2606
2679
|
}
|
|
2607
|
-
var listEntriesSchema =
|
|
2680
|
+
var listEntriesSchema = z12.object({
|
|
2608
2681
|
...listEntriesPagination
|
|
2609
2682
|
});
|
|
2610
2683
|
async function listEntries(input) {
|
|
2611
|
-
|
|
2684
|
+
return capsuleGetList("/entries", {
|
|
2612
2685
|
embed: input.embed,
|
|
2613
2686
|
page: input.page,
|
|
2614
2687
|
perPage: input.perPage
|
|
2615
2688
|
});
|
|
2616
|
-
return { ...data, nextPage };
|
|
2617
2689
|
}
|
|
2618
|
-
var addNoteSchema =
|
|
2619
|
-
content:
|
|
2690
|
+
var addNoteSchema = z12.object({
|
|
2691
|
+
content: z12.string().min(1).describe(
|
|
2620
2692
|
"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
2693
|
),
|
|
2622
2694
|
partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
|
|
2623
2695
|
opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
|
|
2624
2696
|
projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
|
|
2625
|
-
entryAt:
|
|
2697
|
+
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
2698
|
"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
2699
|
)
|
|
2628
2700
|
});
|
|
2629
2701
|
async function addNote(input) {
|
|
2630
2702
|
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
|
-
}
|
|
2703
|
+
assertSingleParentRef("add_note", input, { required: true });
|
|
2635
2704
|
const body = { type: "note", content };
|
|
2636
2705
|
setRef(body, "party", partyId);
|
|
2637
2706
|
setRef(body, "opportunity", opportunityId);
|
|
@@ -2639,12 +2708,12 @@ async function addNote(input) {
|
|
|
2639
2708
|
if (entryAt !== void 0) body["entryAt"] = entryAt;
|
|
2640
2709
|
return capsulePost("/entries", { entry: body });
|
|
2641
2710
|
}
|
|
2642
|
-
var updateEntrySchema =
|
|
2711
|
+
var updateEntrySchema = z12.object({
|
|
2643
2712
|
id: positiveId.describe("Entry ID to update"),
|
|
2644
|
-
content:
|
|
2713
|
+
content: z12.string().min(1).optional().describe(
|
|
2645
2714
|
"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
2715
|
),
|
|
2647
|
-
subject:
|
|
2716
|
+
subject: z12.string().optional().describe(
|
|
2648
2717
|
"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
2718
|
)
|
|
2650
2719
|
});
|
|
@@ -2666,103 +2735,89 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
|
|
|
2666
2735
|
});
|
|
2667
2736
|
|
|
2668
2737
|
// 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 });
|
|
2738
|
+
import { z as z13 } from "zod";
|
|
2739
|
+
var listPipelinesSchema = z13.object({ ...paginationFieldsNoDefaults });
|
|
2675
2740
|
async function listPipelines(input) {
|
|
2676
|
-
|
|
2741
|
+
return capsuleGetCachedList("/pipelines", {
|
|
2677
2742
|
page: input.page ?? 1,
|
|
2678
2743
|
perPage: input.perPage ?? 100
|
|
2679
2744
|
});
|
|
2680
|
-
return { ...data, nextPage };
|
|
2681
2745
|
}
|
|
2682
|
-
var listMilestonesSchema =
|
|
2746
|
+
var listMilestonesSchema = z13.object({
|
|
2683
2747
|
pipelineId: positiveId,
|
|
2684
|
-
...
|
|
2748
|
+
...paginationFieldsNoDefaults
|
|
2685
2749
|
});
|
|
2686
2750
|
async function listMilestones(input) {
|
|
2687
|
-
|
|
2751
|
+
return capsuleGetCachedList(
|
|
2688
2752
|
`/pipelines/${input.pipelineId}/milestones`,
|
|
2689
2753
|
{ page: input.page ?? 1, perPage: input.perPage ?? 100 }
|
|
2690
2754
|
);
|
|
2691
|
-
return { ...data, nextPage };
|
|
2692
2755
|
}
|
|
2693
2756
|
|
|
2694
2757
|
// 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 });
|
|
2758
|
+
import { z as z14 } from "zod";
|
|
2759
|
+
var listBoardsSchema = z14.object({ ...paginationFieldsNoDefaults });
|
|
2701
2760
|
async function listBoards(input) {
|
|
2702
|
-
|
|
2761
|
+
return capsuleGetCachedList("/boards", {
|
|
2703
2762
|
page: input.page ?? 1,
|
|
2704
2763
|
perPage: input.perPage ?? 100
|
|
2705
2764
|
});
|
|
2706
|
-
return { ...data, nextPage };
|
|
2707
2765
|
}
|
|
2708
|
-
var listStagesSchema =
|
|
2766
|
+
var listStagesSchema = z14.object({
|
|
2709
2767
|
boardId: positiveId.optional().describe(
|
|
2710
2768
|
"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
2769
|
),
|
|
2712
|
-
...
|
|
2770
|
+
...paginationFieldsNoDefaults
|
|
2713
2771
|
});
|
|
2714
2772
|
async function listStages(input) {
|
|
2715
2773
|
const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
|
|
2716
|
-
|
|
2774
|
+
return capsuleGetCachedList(path, {
|
|
2717
2775
|
page: input.page ?? 1,
|
|
2718
2776
|
perPage: input.perPage ?? 100
|
|
2719
2777
|
});
|
|
2720
|
-
return { ...data, nextPage };
|
|
2721
2778
|
}
|
|
2722
2779
|
|
|
2723
2780
|
// src/tools/tags.ts
|
|
2724
|
-
import { z as
|
|
2781
|
+
import { z as z15 } from "zod";
|
|
2725
2782
|
var TAG_LIST_PATH = {
|
|
2726
2783
|
parties: "/parties/tags",
|
|
2727
2784
|
opportunities: "/opportunities/tags",
|
|
2728
|
-
|
|
2785
|
+
projects: "/kases/tags"
|
|
2729
2786
|
};
|
|
2730
2787
|
var ENTITY_TO_WRAPPER = {
|
|
2731
2788
|
parties: "party",
|
|
2732
2789
|
opportunities: "opportunity",
|
|
2733
|
-
|
|
2790
|
+
projects: "kase"
|
|
2734
2791
|
};
|
|
2735
|
-
var TagEntity =
|
|
2736
|
-
var listTagsSchema =
|
|
2737
|
-
entity:
|
|
2738
|
-
|
|
2739
|
-
perPage: z14.number().int().min(1).max(100).optional()
|
|
2792
|
+
var TagEntity = z15.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
|
|
2793
|
+
var listTagsSchema = z15.object({
|
|
2794
|
+
entity: z15.enum(["parties", "opportunities", "projects"]).describe("The resource type to list tags for"),
|
|
2795
|
+
...paginationFieldsNoDefaults
|
|
2740
2796
|
});
|
|
2741
2797
|
async function listTags(input) {
|
|
2742
2798
|
const path = TAG_LIST_PATH[input.entity];
|
|
2743
|
-
|
|
2799
|
+
return capsuleGetCachedList(path, {
|
|
2744
2800
|
page: input.page ?? 1,
|
|
2745
2801
|
perPage: input.perPage ?? 100
|
|
2746
2802
|
});
|
|
2747
|
-
return { ...data, nextPage };
|
|
2748
2803
|
}
|
|
2749
|
-
var addTagSchema =
|
|
2804
|
+
var addTagSchema = z15.object({
|
|
2750
2805
|
entity: TagEntity,
|
|
2751
2806
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2752
|
-
tagName:
|
|
2807
|
+
tagName: z15.string().min(1).describe(
|
|
2753
2808
|
"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
2809
|
)
|
|
2755
2810
|
});
|
|
2756
2811
|
async function addTag(input) {
|
|
2757
2812
|
const { entity, entityId, tagName } = input;
|
|
2758
2813
|
const wrapper = ENTITY_TO_WRAPPER[entity];
|
|
2759
|
-
const result = await capsulePut(`/${entity}/${entityId}`, {
|
|
2814
|
+
const result = await capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
|
|
2760
2815
|
[wrapper]: { tags: [{ name: tagName }] }
|
|
2761
2816
|
});
|
|
2762
2817
|
invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
|
|
2763
2818
|
return result;
|
|
2764
2819
|
}
|
|
2765
|
-
var removeTagByIdSchema =
|
|
2820
|
+
var removeTagByIdSchema = z15.object({
|
|
2766
2821
|
entity: TagEntity,
|
|
2767
2822
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2768
2823
|
tagId: positiveId.describe(
|
|
@@ -2773,7 +2828,7 @@ async function removeTagById(input) {
|
|
|
2773
2828
|
const { entity, entityId, tagId } = input;
|
|
2774
2829
|
const wrapper = ENTITY_TO_WRAPPER[entity];
|
|
2775
2830
|
const result = await idempotentWithResult(
|
|
2776
|
-
() => capsulePut(`/${entity}/${entityId}`, {
|
|
2831
|
+
() => capsulePut(`/${ENTITY_PATH[entity]}/${entityId}`, {
|
|
2777
2832
|
[wrapper]: { tags: [{ id: tagId, _delete: true }] }
|
|
2778
2833
|
}),
|
|
2779
2834
|
(result2) => ({
|
|
@@ -2793,7 +2848,7 @@ async function removeTagById(input) {
|
|
|
2793
2848
|
invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
|
|
2794
2849
|
return result;
|
|
2795
2850
|
}
|
|
2796
|
-
var deleteTagDefinitionSchema =
|
|
2851
|
+
var deleteTagDefinitionSchema = z15.object({
|
|
2797
2852
|
entity: TagEntity,
|
|
2798
2853
|
tagId: positiveId.describe(
|
|
2799
2854
|
"The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
|
|
@@ -2808,7 +2863,7 @@ async function deleteTagDefinition(input) {
|
|
|
2808
2863
|
throw new Error("delete_tag_definition requires confirm: true");
|
|
2809
2864
|
}
|
|
2810
2865
|
const result = await idempotent(
|
|
2811
|
-
() => capsuleDelete(`/${entity}/tags/${tagId}`),
|
|
2866
|
+
() => capsuleDelete(`/${ENTITY_PATH[entity]}/tags/${tagId}`),
|
|
2812
2867
|
() => ({ deleted: true, alreadyDeleted: false, entity, tagId }),
|
|
2813
2868
|
() => ({ deleted: true, alreadyDeleted: true, entity, tagId })
|
|
2814
2869
|
);
|
|
@@ -2829,44 +2884,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
|
|
|
2829
2884
|
});
|
|
2830
2885
|
|
|
2831
2886
|
// src/tools/users.ts
|
|
2832
|
-
import { z as
|
|
2833
|
-
var listUsersSchema =
|
|
2834
|
-
|
|
2835
|
-
perPage: z15.number().int().min(1).max(100).optional()
|
|
2887
|
+
import { z as z16 } from "zod";
|
|
2888
|
+
var listUsersSchema = z16.object({
|
|
2889
|
+
...paginationFieldsNoDefaults
|
|
2836
2890
|
});
|
|
2837
2891
|
async function listUsers(input) {
|
|
2838
|
-
|
|
2892
|
+
return capsuleGetCachedList("/users", {
|
|
2839
2893
|
page: input.page ?? 1,
|
|
2840
2894
|
perPage: input.perPage ?? 100
|
|
2841
2895
|
});
|
|
2842
|
-
return { ...data, nextPage };
|
|
2843
2896
|
}
|
|
2844
|
-
var getCurrentUserSchema =
|
|
2897
|
+
var getCurrentUserSchema = z16.object({});
|
|
2845
2898
|
async function getCurrentUser(_input) {
|
|
2846
2899
|
const { data } = await capsuleGet("/users/current");
|
|
2847
2900
|
return data;
|
|
2848
2901
|
}
|
|
2849
2902
|
|
|
2850
2903
|
// src/tools/filters.ts
|
|
2851
|
-
import { z as
|
|
2852
|
-
var FilterConditionSchema =
|
|
2853
|
-
field:
|
|
2904
|
+
import { z as z17 } from "zod";
|
|
2905
|
+
var FilterConditionSchema = z17.object({
|
|
2906
|
+
field: z17.string().describe(
|
|
2854
2907
|
"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
2908
|
),
|
|
2856
|
-
operator:
|
|
2909
|
+
operator: z17.string().describe(
|
|
2857
2910
|
"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
2911
|
),
|
|
2859
|
-
value:
|
|
2912
|
+
value: z17.union([z17.string(), z17.number(), z17.boolean(), z17.null()]).describe(
|
|
2860
2913
|
"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
2914
|
)
|
|
2862
2915
|
});
|
|
2863
|
-
var FilterInputSchema =
|
|
2864
|
-
conditions:
|
|
2916
|
+
var FilterInputSchema = z17.object({
|
|
2917
|
+
conditions: z17.array(FilterConditionSchema).min(1).describe(
|
|
2865
2918
|
"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
2919
|
),
|
|
2867
|
-
embed:
|
|
2868
|
-
|
|
2869
|
-
perPage: z16.number().int().min(1).max(100).optional().default(25)
|
|
2920
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
2921
|
+
...paginationFields
|
|
2870
2922
|
});
|
|
2871
2923
|
async function runFilter(entityPath, input) {
|
|
2872
2924
|
const { data, nextPage } = await capsuleSearch(
|
|
@@ -2886,10 +2938,7 @@ async function filterParties(input) {
|
|
|
2886
2938
|
}
|
|
2887
2939
|
var filterOpportunitiesSchema = FilterInputSchema;
|
|
2888
2940
|
async function filterOpportunities(input) {
|
|
2889
|
-
return runFilter(
|
|
2890
|
-
"opportunities",
|
|
2891
|
-
input
|
|
2892
|
-
);
|
|
2941
|
+
return runFilter("opportunities", input);
|
|
2893
2942
|
}
|
|
2894
2943
|
var filterProjectsSchema = FilterInputSchema;
|
|
2895
2944
|
async function filterProjects(input) {
|
|
@@ -2897,139 +2946,129 @@ async function filterProjects(input) {
|
|
|
2897
2946
|
}
|
|
2898
2947
|
|
|
2899
2948
|
// src/tools/metadata.ts
|
|
2900
|
-
import { z as
|
|
2901
|
-
var
|
|
2902
|
-
|
|
2903
|
-
perPage:
|
|
2949
|
+
import { z as z18 } from "zod";
|
|
2950
|
+
var paginationFields2 = {
|
|
2951
|
+
...paginationFieldsNoDefaults,
|
|
2952
|
+
perPage: paginationFieldsNoDefaults.perPage.describe(
|
|
2953
|
+
"Page size, max 100. Defaults to 100 for reference data."
|
|
2954
|
+
)
|
|
2904
2955
|
};
|
|
2905
|
-
var listTeamsSchema =
|
|
2956
|
+
var listTeamsSchema = z18.object({ ...paginationFields2 });
|
|
2906
2957
|
async function listTeams(input) {
|
|
2907
|
-
|
|
2958
|
+
return capsuleGetCachedList("/teams", {
|
|
2908
2959
|
page: input.page ?? 1,
|
|
2909
2960
|
perPage: input.perPage ?? 100
|
|
2910
2961
|
});
|
|
2911
|
-
return { ...data, nextPage };
|
|
2912
2962
|
}
|
|
2913
|
-
var listLostReasonsSchema =
|
|
2963
|
+
var listLostReasonsSchema = z18.object({ ...paginationFields2 });
|
|
2914
2964
|
async function listLostReasons(input) {
|
|
2915
|
-
|
|
2965
|
+
return capsuleGetCachedList("/lostreasons", {
|
|
2916
2966
|
page: input.page ?? 1,
|
|
2917
2967
|
perPage: input.perPage ?? 100
|
|
2918
2968
|
});
|
|
2919
|
-
return { ...data, nextPage };
|
|
2920
2969
|
}
|
|
2921
|
-
var listActivityTypesSchema =
|
|
2970
|
+
var listActivityTypesSchema = z18.object({ ...paginationFields2 });
|
|
2922
2971
|
async function listActivityTypes(input) {
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
perPage: input.perPage ?? 100
|
|
2928
|
-
}
|
|
2929
|
-
);
|
|
2930
|
-
return { ...data, nextPage };
|
|
2972
|
+
return capsuleGetCachedList("/activitytypes", {
|
|
2973
|
+
page: input.page ?? 1,
|
|
2974
|
+
perPage: input.perPage ?? 100
|
|
2975
|
+
});
|
|
2931
2976
|
}
|
|
2932
|
-
var getSiteSchema =
|
|
2977
|
+
var getSiteSchema = z18.object({});
|
|
2933
2978
|
async function getSite(_input) {
|
|
2934
2979
|
const { data } = await capsuleGetCached("/site");
|
|
2935
2980
|
return data;
|
|
2936
2981
|
}
|
|
2937
|
-
var listTrackDefinitionsSchema =
|
|
2982
|
+
var listTrackDefinitionsSchema = z18.object({ ...paginationFields2 });
|
|
2938
2983
|
async function listTrackDefinitions(input) {
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
);
|
|
2943
|
-
return { ...data, nextPage };
|
|
2984
|
+
return capsuleGetCachedList("/trackdefinitions", {
|
|
2985
|
+
page: input.page ?? 1,
|
|
2986
|
+
perPage: input.perPage ?? 100
|
|
2987
|
+
});
|
|
2944
2988
|
}
|
|
2945
|
-
var listCategoriesSchema =
|
|
2989
|
+
var listCategoriesSchema = z18.object({ ...paginationFields2 });
|
|
2946
2990
|
async function listCategories(input) {
|
|
2947
|
-
|
|
2991
|
+
return capsuleGetCachedList("/categories", {
|
|
2948
2992
|
page: input.page ?? 1,
|
|
2949
2993
|
perPage: input.perPage ?? 100
|
|
2950
2994
|
});
|
|
2951
|
-
return { ...data, nextPage };
|
|
2952
2995
|
}
|
|
2953
|
-
var listGoalsSchema =
|
|
2996
|
+
var listGoalsSchema = z18.object({ ...paginationFields2 });
|
|
2954
2997
|
async function listGoals(input) {
|
|
2955
|
-
|
|
2998
|
+
return capsuleGetCachedList("/goals", {
|
|
2956
2999
|
page: input.page ?? 1,
|
|
2957
3000
|
perPage: input.perPage ?? 100
|
|
2958
3001
|
});
|
|
2959
|
-
return { ...data, nextPage };
|
|
2960
3002
|
}
|
|
2961
3003
|
|
|
2962
3004
|
// src/tools/audit.ts
|
|
2963
|
-
import { z as
|
|
2964
|
-
var listEmployeesSchema =
|
|
3005
|
+
import { z as z19 } from "zod";
|
|
3006
|
+
var listEmployeesSchema = z19.object({
|
|
2965
3007
|
partyId: positiveId.describe(
|
|
2966
3008
|
"The organisation's party id. Returns the people whose `organisation` field links to this party."
|
|
2967
3009
|
),
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
3010
|
+
...paginationFields,
|
|
3011
|
+
embed: embedParam(RECORD_EMBEDS)
|
|
2971
3012
|
});
|
|
2972
3013
|
async function listEmployees(input) {
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
3014
|
+
return capsuleGetList(`/parties/${input.partyId}/people`, {
|
|
3015
|
+
page: input.page,
|
|
3016
|
+
perPage: input.perPage,
|
|
3017
|
+
embed: input.embed
|
|
3018
|
+
});
|
|
2978
3019
|
}
|
|
2979
|
-
var DeletedSinceSchema =
|
|
3020
|
+
var DeletedSinceSchema = z19.string().describe(
|
|
2980
3021
|
"REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
|
|
2981
3022
|
);
|
|
2982
3023
|
var DeletedPagination = {
|
|
2983
3024
|
since: DeletedSinceSchema,
|
|
2984
|
-
|
|
2985
|
-
perPage: z18.number().int().min(1).max(100).optional().default(25)
|
|
3025
|
+
...paginationFields
|
|
2986
3026
|
};
|
|
2987
|
-
var listDeletedPartiesSchema =
|
|
3027
|
+
var listDeletedPartiesSchema = z19.object(DeletedPagination);
|
|
2988
3028
|
async function listDeletedParties(input) {
|
|
2989
|
-
|
|
3029
|
+
return capsuleGetList("/parties/deleted", {
|
|
2990
3030
|
since: input.since,
|
|
2991
3031
|
page: input.page,
|
|
2992
3032
|
perPage: input.perPage
|
|
2993
3033
|
});
|
|
2994
|
-
return { ...data, nextPage };
|
|
2995
3034
|
}
|
|
2996
|
-
var listDeletedOpportunitiesSchema =
|
|
3035
|
+
var listDeletedOpportunitiesSchema = z19.object(DeletedPagination);
|
|
2997
3036
|
async function listDeletedOpportunities(input) {
|
|
2998
|
-
|
|
3037
|
+
return capsuleGetList("/opportunities/deleted", {
|
|
2999
3038
|
since: input.since,
|
|
3000
3039
|
page: input.page,
|
|
3001
3040
|
perPage: input.perPage
|
|
3002
3041
|
});
|
|
3003
|
-
return { ...data, nextPage };
|
|
3004
3042
|
}
|
|
3005
|
-
var listDeletedProjectsSchema =
|
|
3043
|
+
var listDeletedProjectsSchema = z19.object(DeletedPagination);
|
|
3006
3044
|
async function listDeletedProjects(input) {
|
|
3007
|
-
|
|
3045
|
+
return capsuleGetList("/kases/deleted", {
|
|
3008
3046
|
since: input.since,
|
|
3009
3047
|
page: input.page,
|
|
3010
3048
|
perPage: input.perPage
|
|
3011
3049
|
});
|
|
3012
|
-
return { ...data, nextPage };
|
|
3013
3050
|
}
|
|
3014
3051
|
|
|
3015
3052
|
// src/tools/relationships.ts
|
|
3016
|
-
import { z as
|
|
3017
|
-
var RelationshipEntity =
|
|
3018
|
-
var listAdditionalPartiesSchema =
|
|
3053
|
+
import { z as z20 } from "zod";
|
|
3054
|
+
var RelationshipEntity = z20.enum(["opportunities", "projects"]).describe("Which entity has the additional-party links.");
|
|
3055
|
+
var listAdditionalPartiesSchema = z20.object({
|
|
3019
3056
|
entity: RelationshipEntity,
|
|
3020
3057
|
entityId: positiveId.describe("ID of the opportunity or project."),
|
|
3021
|
-
embed:
|
|
3022
|
-
|
|
3023
|
-
perPage: z19.number().int().min(1).max(100).optional().default(25)
|
|
3058
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
3059
|
+
...paginationFields
|
|
3024
3060
|
});
|
|
3025
3061
|
async function listAdditionalParties(input) {
|
|
3026
|
-
|
|
3027
|
-
`/${input.entity}/${input.entityId}/parties`,
|
|
3028
|
-
{
|
|
3062
|
+
return capsuleGetList(
|
|
3063
|
+
`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties`,
|
|
3064
|
+
{
|
|
3065
|
+
embed: input.embed,
|
|
3066
|
+
page: input.page,
|
|
3067
|
+
perPage: input.perPage
|
|
3068
|
+
}
|
|
3029
3069
|
);
|
|
3030
|
-
return { ...data, nextPage };
|
|
3031
3070
|
}
|
|
3032
|
-
var addAdditionalPartySchema =
|
|
3071
|
+
var addAdditionalPartySchema = z20.object({
|
|
3033
3072
|
entity: RelationshipEntity,
|
|
3034
3073
|
entityId: positiveId,
|
|
3035
3074
|
partyId: positiveId.describe(
|
|
@@ -3038,7 +3077,9 @@ var addAdditionalPartySchema = z19.object({
|
|
|
3038
3077
|
});
|
|
3039
3078
|
async function addAdditionalParty(input) {
|
|
3040
3079
|
try {
|
|
3041
|
-
await capsulePostNoContent(
|
|
3080
|
+
await capsulePostNoContent(
|
|
3081
|
+
`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`
|
|
3082
|
+
);
|
|
3042
3083
|
return {
|
|
3043
3084
|
linked: true,
|
|
3044
3085
|
alreadyLinked: false,
|
|
@@ -3062,7 +3103,7 @@ async function addAdditionalParty(input) {
|
|
|
3062
3103
|
throw err;
|
|
3063
3104
|
}
|
|
3064
3105
|
}
|
|
3065
|
-
var removeAdditionalPartySchema =
|
|
3106
|
+
var removeAdditionalPartySchema = z20.object({
|
|
3066
3107
|
entity: RelationshipEntity,
|
|
3067
3108
|
entityId: positiveId,
|
|
3068
3109
|
partyId: positiveId,
|
|
@@ -3075,7 +3116,7 @@ async function removeAdditionalParty(input) {
|
|
|
3075
3116
|
throw new Error("remove_additional_party requires confirm: true");
|
|
3076
3117
|
}
|
|
3077
3118
|
return idempotent(
|
|
3078
|
-
() => capsuleDelete(`/${input.entity}/${input.entityId}/parties/${input.partyId}`),
|
|
3119
|
+
() => capsuleDelete(`/${ENTITY_PATH[input.entity]}/${input.entityId}/parties/${input.partyId}`),
|
|
3079
3120
|
() => ({
|
|
3080
3121
|
removed: true,
|
|
3081
3122
|
alreadyRemoved: false,
|
|
@@ -3092,70 +3133,69 @@ async function removeAdditionalParty(input) {
|
|
|
3092
3133
|
})
|
|
3093
3134
|
);
|
|
3094
3135
|
}
|
|
3095
|
-
var listAssociatedProjectsSchema =
|
|
3136
|
+
var listAssociatedProjectsSchema = z20.object({
|
|
3096
3137
|
opportunityId: positiveId,
|
|
3097
|
-
embed:
|
|
3098
|
-
|
|
3099
|
-
perPage: z19.number().int().min(1).max(100).optional().default(25)
|
|
3138
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
3139
|
+
...paginationFields
|
|
3100
3140
|
});
|
|
3101
3141
|
async function listAssociatedProjects(input) {
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3142
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
|
|
3143
|
+
embed: input.embed,
|
|
3144
|
+
page: input.page,
|
|
3145
|
+
perPage: input.perPage
|
|
3146
|
+
});
|
|
3107
3147
|
}
|
|
3108
3148
|
|
|
3109
3149
|
// src/tools/custom-fields.ts
|
|
3110
|
-
import { z as
|
|
3111
|
-
var CustomFieldEntity =
|
|
3112
|
-
var listCustomFieldsSchema =
|
|
3150
|
+
import { z as z21 } from "zod";
|
|
3151
|
+
var CustomFieldEntity = z21.enum(["parties", "opportunities", "projects"]).describe("Which entity type's custom field schema to inspect.");
|
|
3152
|
+
var listCustomFieldsSchema = z21.object({
|
|
3113
3153
|
entity: CustomFieldEntity
|
|
3114
3154
|
});
|
|
3115
3155
|
async function listCustomFields(input) {
|
|
3116
3156
|
const { data } = await capsuleGetCached(
|
|
3117
|
-
`/${input.entity}/fields/definitions`
|
|
3157
|
+
`/${ENTITY_PATH[input.entity]}/fields/definitions`
|
|
3118
3158
|
);
|
|
3119
3159
|
return data;
|
|
3120
3160
|
}
|
|
3121
|
-
var getCustomFieldSchema =
|
|
3161
|
+
var getCustomFieldSchema = z21.object({
|
|
3122
3162
|
entity: CustomFieldEntity,
|
|
3123
|
-
|
|
3163
|
+
id: positiveId.describe("Custom field definition id.")
|
|
3124
3164
|
});
|
|
3125
3165
|
async function getCustomField(input) {
|
|
3126
3166
|
const { data } = await capsuleGetCached(
|
|
3127
|
-
`/${input.entity}/fields/definitions/${input.
|
|
3167
|
+
`/${input.entity}/fields/definitions/${input.id}`
|
|
3128
3168
|
);
|
|
3129
3169
|
return data;
|
|
3130
3170
|
}
|
|
3131
3171
|
|
|
3132
3172
|
// src/tools/tracks.ts
|
|
3133
|
-
import { z as
|
|
3134
|
-
var TrackEntity =
|
|
3135
|
-
var listEntityTracksSchema =
|
|
3173
|
+
import { z as z22 } from "zod";
|
|
3174
|
+
var TrackEntity = z22.enum(["parties", "opportunities", "projects"]).describe("Which entity type.");
|
|
3175
|
+
var listEntityTracksSchema = z22.object({
|
|
3136
3176
|
entity: TrackEntity,
|
|
3137
3177
|
entityId: positiveId
|
|
3138
3178
|
});
|
|
3139
3179
|
async function listEntityTracks(input) {
|
|
3140
3180
|
const { data } = await capsuleGet(
|
|
3141
|
-
`/${input.entity}/${input.entityId}/tracks`
|
|
3181
|
+
`/${ENTITY_PATH[input.entity]}/${input.entityId}/tracks`
|
|
3142
3182
|
);
|
|
3143
3183
|
return data;
|
|
3144
3184
|
}
|
|
3145
|
-
var
|
|
3146
|
-
|
|
3185
|
+
var getTrackSchema = z22.object({
|
|
3186
|
+
id: positiveId
|
|
3147
3187
|
});
|
|
3148
|
-
async function
|
|
3149
|
-
const { data } = await capsuleGet(`/tracks/${input.
|
|
3188
|
+
async function getTrack(input) {
|
|
3189
|
+
const { data } = await capsuleGet(`/tracks/${input.id}`);
|
|
3150
3190
|
return data;
|
|
3151
3191
|
}
|
|
3152
|
-
var applyTrackSchema =
|
|
3153
|
-
entity:
|
|
3192
|
+
var applyTrackSchema = z22.object({
|
|
3193
|
+
entity: z22.enum(["opportunities", "projects"]).describe("Which entity to apply the track to."),
|
|
3154
3194
|
entityId: positiveId,
|
|
3155
3195
|
trackDefinitionId: positiveId.describe(
|
|
3156
3196
|
"The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
|
|
3157
3197
|
),
|
|
3158
|
-
startDate:
|
|
3198
|
+
startDate: z22.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
|
|
3159
3199
|
"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
3200
|
)
|
|
3161
3201
|
});
|
|
@@ -3168,9 +3208,9 @@ async function applyTrack(input) {
|
|
|
3168
3208
|
if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
|
|
3169
3209
|
return capsulePost("/tracks", { track });
|
|
3170
3210
|
}
|
|
3171
|
-
var updateTrackSchema =
|
|
3172
|
-
|
|
3173
|
-
fields:
|
|
3211
|
+
var updateTrackSchema = z22.object({
|
|
3212
|
+
id: positiveId,
|
|
3213
|
+
fields: z22.record(z22.string(), z22.unknown()).describe(
|
|
3174
3214
|
"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
3215
|
)
|
|
3176
3216
|
});
|
|
@@ -3178,12 +3218,12 @@ async function updateTrack(input) {
|
|
|
3178
3218
|
if (Object.keys(input.fields).length === 0) {
|
|
3179
3219
|
throw new Error("update_track: provide at least one field in `fields`");
|
|
3180
3220
|
}
|
|
3181
|
-
return capsulePut(`/tracks/${input.
|
|
3221
|
+
return capsulePut(`/tracks/${input.id}`, {
|
|
3182
3222
|
track: input.fields
|
|
3183
3223
|
});
|
|
3184
3224
|
}
|
|
3185
|
-
var removeTrackSchema =
|
|
3186
|
-
|
|
3225
|
+
var removeTrackSchema = z22.object({
|
|
3226
|
+
id: positiveId,
|
|
3187
3227
|
confirm: confirmFlag().describe(
|
|
3188
3228
|
"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."
|
|
3189
3229
|
)
|
|
@@ -3193,20 +3233,20 @@ async function removeTrack(input) {
|
|
|
3193
3233
|
throw new Error("remove_track requires confirm: true");
|
|
3194
3234
|
}
|
|
3195
3235
|
return idempotent(
|
|
3196
|
-
() => capsuleDelete(`/tracks/${input.
|
|
3197
|
-
() => ({ removed: true, alreadyRemoved: false,
|
|
3198
|
-
() => ({ removed: true, alreadyRemoved: true,
|
|
3236
|
+
() => capsuleDelete(`/tracks/${input.id}`),
|
|
3237
|
+
() => ({ removed: true, alreadyRemoved: false, id: input.id }),
|
|
3238
|
+
() => ({ removed: true, alreadyRemoved: true, id: input.id })
|
|
3199
3239
|
);
|
|
3200
3240
|
}
|
|
3201
3241
|
|
|
3202
3242
|
// src/tools/attachments.ts
|
|
3203
|
-
import { z as
|
|
3243
|
+
import { z as z23 } from "zod";
|
|
3204
3244
|
var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
|
|
3205
3245
|
var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
|
|
3206
3246
|
var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
|
|
3207
|
-
var getAttachmentSchema =
|
|
3247
|
+
var getAttachmentSchema = z23.object({
|
|
3208
3248
|
id: positiveId.describe("Attachment ID."),
|
|
3209
|
-
maxSizeBytes:
|
|
3249
|
+
maxSizeBytes: z23.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
|
|
3210
3250
|
`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
3251
|
)
|
|
3212
3252
|
});
|
|
@@ -3221,17 +3261,17 @@ async function getAttachment(input) {
|
|
|
3221
3261
|
}
|
|
3222
3262
|
return { contentType, buffer, sizeBytes };
|
|
3223
3263
|
}
|
|
3224
|
-
var uploadAttachmentSchema =
|
|
3225
|
-
filename:
|
|
3264
|
+
var uploadAttachmentSchema = z23.object({
|
|
3265
|
+
filename: z23.string().min(1).describe(
|
|
3226
3266
|
"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
3267
|
),
|
|
3228
|
-
contentType:
|
|
3268
|
+
contentType: z23.string().min(1).describe(
|
|
3229
3269
|
"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
3270
|
),
|
|
3231
|
-
dataBase64:
|
|
3271
|
+
dataBase64: z23.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
|
|
3232
3272
|
"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
3273
|
),
|
|
3234
|
-
content:
|
|
3274
|
+
content: z23.string().optional().describe(
|
|
3235
3275
|
"Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
|
|
3236
3276
|
),
|
|
3237
3277
|
partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
|
|
@@ -3249,12 +3289,7 @@ function decodedBase64Size(s) {
|
|
|
3249
3289
|
return s.length / 4 * 3 - padding;
|
|
3250
3290
|
}
|
|
3251
3291
|
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
|
-
}
|
|
3292
|
+
assertSingleParentRef("upload_attachment", input, { required: true });
|
|
3258
3293
|
if (!isValidBase64(input.dataBase64)) {
|
|
3259
3294
|
throw new Error(
|
|
3260
3295
|
"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 +3314,39 @@ async function uploadAttachment(input) {
|
|
|
3279
3314
|
content: input.content ?? "[attachment]",
|
|
3280
3315
|
attachments: [{ token }]
|
|
3281
3316
|
};
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3317
|
+
setRef(entryBody, "party", input.partyId);
|
|
3318
|
+
setRef(entryBody, "opportunity", input.opportunityId);
|
|
3319
|
+
setRef(entryBody, "kase", input.projectId);
|
|
3285
3320
|
return capsulePost("/entries", { entry: entryBody });
|
|
3286
3321
|
}
|
|
3287
3322
|
|
|
3288
3323
|
// src/tools/saved-filters.ts
|
|
3289
|
-
import { z as
|
|
3290
|
-
var EntitySchema =
|
|
3291
|
-
|
|
3292
|
-
);
|
|
3293
|
-
var listSavedFiltersSchema = z23.object({
|
|
3324
|
+
import { z as z24 } from "zod";
|
|
3325
|
+
var EntitySchema = z24.enum(["parties", "opportunities", "projects"]).describe("Which entity type the filter operates over.");
|
|
3326
|
+
var listSavedFiltersSchema = z24.object({
|
|
3294
3327
|
entity: EntitySchema
|
|
3295
3328
|
});
|
|
3296
3329
|
async function listSavedFilters(input) {
|
|
3297
|
-
const { data } = await capsuleGetCached(
|
|
3330
|
+
const { data } = await capsuleGetCached(
|
|
3331
|
+
`/${ENTITY_PATH[input.entity]}/filters`
|
|
3332
|
+
);
|
|
3298
3333
|
return data;
|
|
3299
3334
|
}
|
|
3300
|
-
var runSavedFilterSchema =
|
|
3335
|
+
var runSavedFilterSchema = z24.object({
|
|
3301
3336
|
entity: EntitySchema,
|
|
3302
3337
|
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)
|
|
3338
|
+
embed: embedParam(RECORD_EMBEDS),
|
|
3339
|
+
...paginationFields
|
|
3306
3340
|
});
|
|
3307
3341
|
async function runSavedFilter(input) {
|
|
3308
|
-
|
|
3309
|
-
`/${input.entity}/filters/${input.id}/results`,
|
|
3310
|
-
{
|
|
3342
|
+
return capsuleGetList(
|
|
3343
|
+
`/${ENTITY_PATH[input.entity]}/filters/${input.id}/results`,
|
|
3344
|
+
{
|
|
3345
|
+
page: input.page,
|
|
3346
|
+
perPage: input.perPage,
|
|
3347
|
+
embed: input.embed
|
|
3348
|
+
}
|
|
3311
3349
|
);
|
|
3312
|
-
return { ...data, nextPage };
|
|
3313
3350
|
}
|
|
3314
3351
|
|
|
3315
3352
|
// src/server.ts
|
|
@@ -3320,7 +3357,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3320
3357
|
const server = new McpServer(
|
|
3321
3358
|
{
|
|
3322
3359
|
name: "capsulemcp",
|
|
3323
|
-
version: "
|
|
3360
|
+
version: "2.0.0",
|
|
3324
3361
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
3325
3362
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
3326
3363
|
icons: ICONS
|
|
@@ -3437,7 +3474,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3437
3474
|
registerTool(
|
|
3438
3475
|
server,
|
|
3439
3476
|
"delete_party",
|
|
3440
|
-
"DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects
|
|
3477
|
+
"DESTRUCTIVE & IRREVERSIBLE: permanently delete a party (person or organisation). Cascades to all linked notes, tasks, opportunities, AND projects. Deleting an organisation does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. TRACK INSTANCES applied to cascaded opportunities/projects are NOT cleaned up either \u2014 they survive as orphan records reachable only by track id via get_track. Use remove_track on each track explicitly before deleting the parent party if orphan accumulation matters (rare in practice \u2014 orphans are unreachable from normal navigation). Requires confirm=true. Always read the party first with get_party and confirm with the user before calling. Idempotent on retry: response is `{deleted: true, alreadyDeleted: false, id}` on a fresh delete or `{deleted: true, alreadyDeleted: true, id}` if the party was already gone (Capsule's 404 is caught internally so reconciliation loops can re-issue safely).",
|
|
3441
3478
|
deletePartySchema,
|
|
3442
3479
|
deleteParty
|
|
3443
3480
|
);
|
|
@@ -3536,7 +3573,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3536
3573
|
registerTool(
|
|
3537
3574
|
server,
|
|
3538
3575
|
"list_additional_parties",
|
|
3539
|
-
"List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or '
|
|
3576
|
+
"List secondary party links on an opportunity or project. The 'main' party is on the entity itself (opportunity.party); additional parties are e.g. partners, consultants, or referrers also involved in the deal. Set entity to 'opportunities' or 'projects'.",
|
|
3540
3577
|
listAdditionalPartiesSchema,
|
|
3541
3578
|
listAdditionalParties
|
|
3542
3579
|
);
|
|
@@ -3577,10 +3614,17 @@ function createCapsuleMcpServer(opts) {
|
|
|
3577
3614
|
deleteOpportunity
|
|
3578
3615
|
);
|
|
3579
3616
|
}
|
|
3617
|
+
registerTool(
|
|
3618
|
+
server,
|
|
3619
|
+
"search_projects",
|
|
3620
|
+
"Free-text search projects in Capsule CRM (matches name and description). Returns results in Capsule's default order (no sort parameter is supported here). Omit `q` to list all projects. For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
|
|
3621
|
+
searchProjectsSchema,
|
|
3622
|
+
searchProjects
|
|
3623
|
+
);
|
|
3580
3624
|
registerTool(
|
|
3581
3625
|
server,
|
|
3582
3626
|
"list_projects",
|
|
3583
|
-
"List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
|
|
3627
|
+
"List projects (cases) in Capsule CRM, optionally filtered by status. Returns results in Capsule's default order (no sort parameter is supported here). For free-text matching use search_projects; for structured queries \u2014 'most recent project', 'projects opened this month', 'projects tagged X' \u2014 use filter_projects instead.",
|
|
3584
3628
|
listProjectsSchema,
|
|
3585
3629
|
listProjects
|
|
3586
3630
|
);
|
|
@@ -3608,7 +3652,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3608
3652
|
registerTool(
|
|
3609
3653
|
server,
|
|
3610
3654
|
"list_deleted_projects",
|
|
3611
|
-
"Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `
|
|
3655
|
+
"Audit feature: list projects deleted on or after a given timestamp. The `since` parameter is REQUIRED. Response also includes a `restrictedProjects` key for records the integration user can't read fully.",
|
|
3612
3656
|
listDeletedProjectsSchema,
|
|
3613
3657
|
listDeletedProjects
|
|
3614
3658
|
);
|
|
@@ -3770,20 +3814,72 @@ function createCapsuleMcpServer(opts) {
|
|
|
3770
3814
|
listEntriesSchema,
|
|
3771
3815
|
listEntries
|
|
3772
3816
|
);
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3817
|
+
if (shouldRegister("get_attachment")) {
|
|
3818
|
+
server.tool(
|
|
3819
|
+
"get_attachment",
|
|
3820
|
+
"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.",
|
|
3821
|
+
getAttachmentSchema.shape,
|
|
3822
|
+
// get_attachment is read-only — downloads a binary, never mutates.
|
|
3823
|
+
// Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
|
|
3824
|
+
// false}` that `registerTool` applies to every other `get_*` tool.
|
|
3825
|
+
// Explicit destructiveHint: false is load-bearing — MCP spec
|
|
3826
|
+
// defaults destructiveHint to `true`, so omitting it would (in
|
|
3827
|
+
// some client implementations) classify this read as destructive.
|
|
3828
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
3829
|
+
async (input) => {
|
|
3830
|
+
const result = await getAttachment(input);
|
|
3831
|
+
if (result.truncated) {
|
|
3832
|
+
return {
|
|
3833
|
+
content: [
|
|
3834
|
+
{
|
|
3835
|
+
type: "text",
|
|
3836
|
+
text: JSON.stringify(
|
|
3837
|
+
{
|
|
3838
|
+
id: input.id,
|
|
3839
|
+
contentType: result.contentType,
|
|
3840
|
+
sizeBytes: result.sizeBytes,
|
|
3841
|
+
truncated: true,
|
|
3842
|
+
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3843
|
+
},
|
|
3844
|
+
null,
|
|
3845
|
+
2
|
|
3846
|
+
)
|
|
3847
|
+
}
|
|
3848
|
+
]
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
const baseType = result.contentType.split(";")[0].trim().toLowerCase();
|
|
3852
|
+
if (baseType.startsWith("image/")) {
|
|
3853
|
+
return {
|
|
3854
|
+
content: [
|
|
3855
|
+
{
|
|
3856
|
+
type: "image",
|
|
3857
|
+
data: result.buffer.toString("base64"),
|
|
3858
|
+
mimeType: result.contentType
|
|
3859
|
+
}
|
|
3860
|
+
]
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3864
|
+
if (isText) {
|
|
3865
|
+
return {
|
|
3866
|
+
content: [
|
|
3867
|
+
{
|
|
3868
|
+
type: "text",
|
|
3869
|
+
text: JSON.stringify(
|
|
3870
|
+
{
|
|
3871
|
+
id: input.id,
|
|
3872
|
+
contentType: result.contentType,
|
|
3873
|
+
sizeBytes: result.sizeBytes
|
|
3874
|
+
},
|
|
3875
|
+
null,
|
|
3876
|
+
2
|
|
3877
|
+
)
|
|
3878
|
+
},
|
|
3879
|
+
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3880
|
+
]
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3787
3883
|
return {
|
|
3788
3884
|
content: [
|
|
3789
3885
|
{
|
|
@@ -3793,8 +3889,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3793
3889
|
id: input.id,
|
|
3794
3890
|
contentType: result.contentType,
|
|
3795
3891
|
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.`
|
|
3892
|
+
base64: result.buffer.toString("base64")
|
|
3798
3893
|
},
|
|
3799
3894
|
null,
|
|
3800
3895
|
2
|
|
@@ -3803,57 +3898,8 @@ function createCapsuleMcpServer(opts) {
|
|
|
3803
3898
|
]
|
|
3804
3899
|
};
|
|
3805
3900
|
}
|
|
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
|
-
);
|
|
3901
|
+
);
|
|
3902
|
+
}
|
|
3857
3903
|
if (!readOnly) {
|
|
3858
3904
|
registerTool(
|
|
3859
3905
|
server,
|
|
@@ -3921,14 +3967,14 @@ function createCapsuleMcpServer(opts) {
|
|
|
3921
3967
|
);
|
|
3922
3968
|
registerTool(
|
|
3923
3969
|
server,
|
|
3924
|
-
"
|
|
3970
|
+
"list_lost_reasons",
|
|
3925
3971
|
"List all configured opportunity-loss reasons (e.g. 'Poor Qualification', 'Lost to competitor', 'Price too high'). Returns each reason's id and name; the set is account-configured rather than a fixed enum, so call this to discover valid ids before referencing a lostReason in update_opportunity when closing a deal as lost. Useful for analysing closed-lost opportunities by reason.",
|
|
3926
3972
|
listLostReasonsSchema,
|
|
3927
3973
|
listLostReasons
|
|
3928
3974
|
);
|
|
3929
3975
|
registerTool(
|
|
3930
3976
|
server,
|
|
3931
|
-
"
|
|
3977
|
+
"list_activity_types",
|
|
3932
3978
|
"List all configured activity types (e.g. Call, Meeting, Email). These are the categories used when logging timeline entries via add_note. Returns each type's id and name. The set is account-configured rather than a fixed enum, so call this to discover valid values before referencing an activityType in entry creation.",
|
|
3933
3979
|
listActivityTypesSchema,
|
|
3934
3980
|
listActivityTypes
|
|
@@ -3956,10 +4002,10 @@ function createCapsuleMcpServer(opts) {
|
|
|
3956
4002
|
);
|
|
3957
4003
|
registerTool(
|
|
3958
4004
|
server,
|
|
3959
|
-
"
|
|
4005
|
+
"get_track",
|
|
3960
4006
|
"Fetch a single track instance by id. Returns the minimal Capsule projection: id, description, trackDateOn, direction, and the array of tasks attached to the track. Capsule's GET /tracks/{id} does NOT include a trackDefinition link, an entity reference, or a completion field \u2014 to find the entity a track is applied to, use list_entity_tracks (which lists track instances by their parent entity); to check completion, the track-tasks' own statuses are the proxy.",
|
|
3961
|
-
|
|
3962
|
-
|
|
4007
|
+
getTrackSchema,
|
|
4008
|
+
getTrack
|
|
3963
4009
|
);
|
|
3964
4010
|
registerTool(
|
|
3965
4011
|
server,
|
|
@@ -3992,7 +4038,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3992
4038
|
registerTool(
|
|
3993
4039
|
server,
|
|
3994
4040
|
"list_tags",
|
|
3995
|
-
"List all tags available for a given entity type (parties, opportunities, or
|
|
4041
|
+
"List all tags available for a given entity type (parties, opportunities, or projects). Returns each tag's id, name, and any data-tag field schema. Tags are entity-specific \u2014 a party tag is not interchangeable with an opportunity tag. Use this to discover valid tag ids before calling add_tag, or to display the tag catalogue to the user when they ask 'what tags do we use?'",
|
|
3996
4042
|
listTagsSchema,
|
|
3997
4043
|
listTags
|
|
3998
4044
|
);
|
|
@@ -4014,7 +4060,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
4014
4060
|
registerTool(
|
|
4015
4061
|
server,
|
|
4016
4062
|
"delete_tag_definition",
|
|
4017
|
-
"DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities /
|
|
4063
|
+
"DESTRUCTIVE & TENANT-WIDE: permanently delete a tag DEFINITION from an entity type's tag namespace (parties / opportunities / projects). Unlike remove_tag_by_id \u2014 which detaches a tag from ONE record and leaves the definition intact for others \u2014 this removes the definition itself, so the tag disappears from EVERY record that shared it. Use it to clean up stray / mistyped / test tag definitions polluting the tenant-global list. Requires confirm=true. Always read the affected tag first via list_tags and confirm with the user; if you only want to untag one record, use remove_tag_by_id instead. Irreversible (re-creating by name via add_tag mints a brand-new id). Idempotent on retry: `{deleted: true, alreadyDeleted: false, entity, tagId}` on a fresh delete, or `{deleted: true, alreadyDeleted: true, entity, tagId}` if the definition was already gone (Capsule's 404 is caught). Endpoint verified empirically (DELETE /<entity>/tags/{id} \u2192 204).",
|
|
4018
4064
|
deleteTagDefinitionSchema,
|
|
4019
4065
|
deleteTagDefinition
|
|
4020
4066
|
);
|
|
@@ -4229,77 +4275,51 @@ a{color:#1e3a8a}
|
|
|
4229
4275
|
}
|
|
4230
4276
|
next();
|
|
4231
4277
|
};
|
|
4232
|
-
|
|
4233
|
-
"/mcp",
|
|
4278
|
+
const mcpGuards = [
|
|
4234
4279
|
guardOrigin,
|
|
4235
4280
|
requireBearerAuth({
|
|
4236
4281
|
verifier: oauthProvider2,
|
|
4237
4282
|
resourceMetadataUrl: mcpResourceMetadataUrl
|
|
4238
4283
|
}),
|
|
4239
4284
|
mcpRateLimit,
|
|
4240
|
-
guardProtocolVersion
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
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",
|
|
4273
|
-
guardOrigin,
|
|
4274
|
-
requireBearerAuth({
|
|
4275
|
-
verifier: oauthProvider2,
|
|
4276
|
-
resourceMetadataUrl: mcpResourceMetadataUrl
|
|
4277
|
-
}),
|
|
4278
|
-
mcpRateLimit,
|
|
4279
|
-
guardProtocolVersion,
|
|
4280
|
-
(_req, res) => {
|
|
4281
|
-
res.set("Allow", "POST").status(405).json({
|
|
4282
|
-
error: "method_not_allowed",
|
|
4283
|
-
message: "Use POST for MCP requests; this server runs in stateless mode."
|
|
4285
|
+
guardProtocolVersion
|
|
4286
|
+
];
|
|
4287
|
+
const methodNotAllowed = (_req, res) => {
|
|
4288
|
+
res.set("Allow", "POST").status(405).json({
|
|
4289
|
+
error: "method_not_allowed",
|
|
4290
|
+
message: "Use POST for MCP requests; this server runs in stateless mode."
|
|
4291
|
+
});
|
|
4292
|
+
};
|
|
4293
|
+
app2.post("/mcp", ...mcpGuards, express.json({ limit: jsonLimit2 }), async (req, res) => {
|
|
4294
|
+
try {
|
|
4295
|
+
const clientId = req.auth?.clientId;
|
|
4296
|
+
const server = createCapsuleMcpServer({ clientId });
|
|
4297
|
+
const transport = new StreamableHTTPServerTransport({});
|
|
4298
|
+
res.on("close", () => {
|
|
4299
|
+
void transport.close();
|
|
4300
|
+
void server.close();
|
|
4284
4301
|
});
|
|
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."
|
|
4302
|
+
await withRequestContext({ clientId }, async () => {
|
|
4303
|
+
await server.connect(transport);
|
|
4304
|
+
await transport.handleRequest(req, res, req.body);
|
|
4300
4305
|
});
|
|
4306
|
+
} catch (err) {
|
|
4307
|
+
const name = err instanceof Error ? err.name : typeof err;
|
|
4308
|
+
const status = err && typeof err === "object" && "status" in err ? Number(err.status) : void 0;
|
|
4309
|
+
const summary = status !== void 0 ? `${name} ${status}` : name;
|
|
4310
|
+
if (process.env["MCP_HTTP_DEBUG"] === "1") {
|
|
4311
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4312
|
+
console.error(`[capsulemcp] /mcp error: ${summary} \u2014 ${message}`);
|
|
4313
|
+
} else {
|
|
4314
|
+
console.error(`[capsulemcp] /mcp error: ${summary}`);
|
|
4315
|
+
}
|
|
4316
|
+
if (!res.headersSent) {
|
|
4317
|
+
res.status(500).json({ error: "internal_error" });
|
|
4318
|
+
}
|
|
4301
4319
|
}
|
|
4302
|
-
);
|
|
4320
|
+
});
|
|
4321
|
+
app2.get("/mcp", ...mcpGuards, methodNotAllowed);
|
|
4322
|
+
app2.delete("/mcp", ...mcpGuards, methodNotAllowed);
|
|
4303
4323
|
return app2;
|
|
4304
4324
|
}
|
|
4305
4325
|
|