capsulemcp 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/http.js +854 -819
- package/dist/index.js +837 -758
- package/package.json +8 -9
package/dist/http.js
CHANGED
|
@@ -26,6 +26,22 @@ var chainHandlers = {
|
|
|
26
26
|
"capsule.request": (ctx) => {
|
|
27
27
|
ctx.capsuleCalls += 1;
|
|
28
28
|
},
|
|
29
|
+
// A timed-out or connection-failed call is still an attempt that
|
|
30
|
+
// never reaches the `capsule.request` emit (it throws at the fetch
|
|
31
|
+
// stage). Count it here so `tool.chain.capsuleCalls` stays honest and
|
|
32
|
+
// a chain whose duration ballooned is explained by a visible failure.
|
|
33
|
+
"capsule.timeout": (ctx) => {
|
|
34
|
+
ctx.capsuleCalls += 1;
|
|
35
|
+
},
|
|
36
|
+
"capsule.error": (ctx) => {
|
|
37
|
+
ctx.capsuleCalls += 1;
|
|
38
|
+
},
|
|
39
|
+
// A request that exhausted its 429 retry is a real (doubly-attempted)
|
|
40
|
+
// outbound call that throws before `capsule.request` fires — count it
|
|
41
|
+
// so a chain whose latency ballooned on rate-limit backoff is explained.
|
|
42
|
+
"capsule.ratelimit": (ctx) => {
|
|
43
|
+
ctx.capsuleCalls += 1;
|
|
44
|
+
},
|
|
29
45
|
// Cache-hit events feed the aggregate so the chain stat is right
|
|
30
46
|
// even on tools whose Capsule calls all hit the cache.
|
|
31
47
|
"cache.hit": (ctx) => {
|
|
@@ -184,6 +200,15 @@ var CapsuleApiError = class extends Error {
|
|
|
184
200
|
}
|
|
185
201
|
status;
|
|
186
202
|
};
|
|
203
|
+
var CapsuleTimeoutError = class extends CapsuleApiError {
|
|
204
|
+
constructor() {
|
|
205
|
+
super(
|
|
206
|
+
504,
|
|
207
|
+
`Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
|
|
208
|
+
);
|
|
209
|
+
this.name = "CapsuleTimeoutError";
|
|
210
|
+
}
|
|
211
|
+
};
|
|
187
212
|
function getToken() {
|
|
188
213
|
const token = process.env["CAPSULE_API_TOKEN"];
|
|
189
214
|
if (!token) {
|
|
@@ -246,72 +271,74 @@ async function parseErrorBody(res) {
|
|
|
246
271
|
}
|
|
247
272
|
}
|
|
248
273
|
var REQUEST_TIMEOUT_MS = 6e4;
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const controller = new AbortController();
|
|
255
|
-
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
256
|
-
timer.unref();
|
|
257
|
-
return {
|
|
258
|
-
options: { ...options ?? {}, signal: controller.signal },
|
|
259
|
-
cleanup: () => clearTimeout(timer)
|
|
260
|
-
};
|
|
274
|
+
function isTimeoutAbort(err) {
|
|
275
|
+
return err instanceof Error && // AbortSignal.timeout rejects with a DOMException named
|
|
276
|
+
// "TimeoutError"; plain aborts (and older undici paths) surface
|
|
277
|
+
// as "AbortError" or carry "aborted" in the message.
|
|
278
|
+
(err.name === "TimeoutError" || err.name === "AbortError" || /aborted/i.test(err.message));
|
|
261
279
|
}
|
|
262
280
|
async function mapAbort(p) {
|
|
263
281
|
try {
|
|
264
282
|
return await p;
|
|
265
283
|
} catch (err) {
|
|
266
|
-
if (
|
|
267
|
-
throw new
|
|
268
|
-
504,
|
|
269
|
-
`Capsule API request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s. The Capsule API may be slow or hung; retry after a short wait. If the failed call was a write/delete, read the entity first to see whether the change actually applied before retrying.`
|
|
270
|
-
);
|
|
284
|
+
if (isTimeoutAbort(err)) {
|
|
285
|
+
throw new CapsuleTimeoutError();
|
|
271
286
|
}
|
|
272
287
|
throw err;
|
|
273
288
|
}
|
|
274
289
|
}
|
|
275
290
|
async function fetchWithTimeout(url, options) {
|
|
276
|
-
const
|
|
291
|
+
const startedAt = Date.now();
|
|
277
292
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
293
|
+
return await fetch(url, {
|
|
294
|
+
...options ?? {},
|
|
295
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
296
|
+
});
|
|
280
297
|
} catch (err) {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
298
|
+
const isAbort = isTimeoutAbort(err);
|
|
299
|
+
emitCapsuleFailure(
|
|
300
|
+
options?.method ?? "GET",
|
|
301
|
+
url,
|
|
302
|
+
Date.now() - startedAt,
|
|
303
|
+
isAbort ? "timeout" : "network",
|
|
304
|
+
isAbort ? void 0 : err
|
|
305
|
+
);
|
|
306
|
+
if (isAbort) {
|
|
307
|
+
throw new CapsuleTimeoutError();
|
|
287
308
|
}
|
|
288
309
|
throw err;
|
|
289
310
|
}
|
|
290
311
|
}
|
|
312
|
+
async function drainBody(res) {
|
|
313
|
+
try {
|
|
314
|
+
await res.body?.cancel();
|
|
315
|
+
} catch {
|
|
316
|
+
}
|
|
317
|
+
}
|
|
291
318
|
async function doFetch(url, options) {
|
|
292
319
|
const startedAt = Date.now();
|
|
293
320
|
const method = options?.method ?? "GET";
|
|
294
321
|
const first = await fetchWithTimeout(url, options);
|
|
295
|
-
if (first.
|
|
296
|
-
const delay = parseRateLimitDelay(first
|
|
297
|
-
first
|
|
322
|
+
if (first.status === 429) {
|
|
323
|
+
const delay = parseRateLimitDelay(first);
|
|
324
|
+
await drainBody(first);
|
|
298
325
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
299
326
|
const retried = await fetchWithTimeout(url, options);
|
|
300
|
-
if (retried.
|
|
301
|
-
retried
|
|
327
|
+
if (retried.status === 429) {
|
|
328
|
+
await drainBody(retried);
|
|
329
|
+
emitCapsuleRateLimited(method, url, Date.now() - startedAt);
|
|
302
330
|
throw new CapsuleApiError(
|
|
303
331
|
429,
|
|
304
332
|
"Rate limit exceeded after one retry. Please slow down your requests."
|
|
305
333
|
);
|
|
306
334
|
}
|
|
307
|
-
return {
|
|
335
|
+
return { res: retried, startedAt, method, url, retriedAfter429: true };
|
|
308
336
|
}
|
|
309
|
-
return {
|
|
337
|
+
return { res: first, startedAt, method, url, retriedAfter429: false };
|
|
310
338
|
}
|
|
311
339
|
async function consumeBody(start, body) {
|
|
312
340
|
try {
|
|
313
|
-
|
|
314
|
-
} finally {
|
|
341
|
+
const result = await body();
|
|
315
342
|
emitCapsuleRequest(
|
|
316
343
|
start.method,
|
|
317
344
|
start.url,
|
|
@@ -319,15 +346,31 @@ async function consumeBody(start, body) {
|
|
|
319
346
|
Date.now() - start.startedAt,
|
|
320
347
|
start.retriedAfter429
|
|
321
348
|
);
|
|
349
|
+
return result;
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (err instanceof CapsuleTimeoutError) {
|
|
352
|
+
emitCapsuleFailure(start.method, start.url, Date.now() - start.startedAt, "timeout");
|
|
353
|
+
} else {
|
|
354
|
+
emitCapsuleRequest(
|
|
355
|
+
start.method,
|
|
356
|
+
start.url,
|
|
357
|
+
start.res,
|
|
358
|
+
Date.now() - start.startedAt,
|
|
359
|
+
start.retriedAfter429
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
throw err;
|
|
322
363
|
}
|
|
323
364
|
}
|
|
324
|
-
function
|
|
325
|
-
let path = "";
|
|
365
|
+
function redactedPath(url) {
|
|
326
366
|
try {
|
|
327
|
-
|
|
367
|
+
return redactPath(new URL(url).pathname);
|
|
328
368
|
} catch {
|
|
329
|
-
|
|
369
|
+
return "?";
|
|
330
370
|
}
|
|
371
|
+
}
|
|
372
|
+
function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
373
|
+
const path = redactedPath(url);
|
|
331
374
|
const lenHeader = res.headers.get("content-length");
|
|
332
375
|
const responseBytes = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
|
|
333
376
|
logEvent("capsule.request", {
|
|
@@ -339,6 +382,37 @@ function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
|
339
382
|
...retriedAfter429 ? { retriedAfter429: true } : {}
|
|
340
383
|
});
|
|
341
384
|
}
|
|
385
|
+
function emitCapsuleFailure(method, url, elapsedMs, reason, err) {
|
|
386
|
+
const path = redactedPath(url);
|
|
387
|
+
if (reason === "timeout") {
|
|
388
|
+
logEvent(
|
|
389
|
+
"capsule.timeout",
|
|
390
|
+
{ method, path, elapsedMs, timeoutMs: REQUEST_TIMEOUT_MS },
|
|
391
|
+
{ force: true }
|
|
392
|
+
);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const code = extractErrorCode(err);
|
|
396
|
+
logEvent(
|
|
397
|
+
"capsule.error",
|
|
398
|
+
{ method, path, elapsedMs, ...code ? { code } : {} },
|
|
399
|
+
{ force: true }
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
function emitCapsuleRateLimited(method, url, elapsedMs) {
|
|
403
|
+
logEvent(
|
|
404
|
+
"capsule.ratelimit",
|
|
405
|
+
{ method, path: redactedPath(url), elapsedMs, status: 429 },
|
|
406
|
+
{ force: true }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
function extractErrorCode(err) {
|
|
410
|
+
const e = err;
|
|
411
|
+
const code = e?.cause?.code ?? e?.code;
|
|
412
|
+
if (typeof code === "string") return code;
|
|
413
|
+
if (typeof e?.name === "string" && e.name !== "Error") return e.name;
|
|
414
|
+
return void 0;
|
|
415
|
+
}
|
|
342
416
|
async function throwForStatus(res) {
|
|
343
417
|
if (res.status === 401) {
|
|
344
418
|
const detail = await parseErrorBody(res);
|
|
@@ -370,15 +444,19 @@ async function capsuleGet(path, params) {
|
|
|
370
444
|
const token = getToken();
|
|
371
445
|
const url = buildUrl(path, params);
|
|
372
446
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
447
|
+
return consumeBody(start, async () => {
|
|
448
|
+
const data = await handleResponse(start.res);
|
|
449
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
450
|
+
return { data, nextPage };
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
async function capsuleGetList(path, params) {
|
|
454
|
+
const { data, nextPage } = await capsuleGet(path, params);
|
|
455
|
+
return { ...data, nextPage };
|
|
456
|
+
}
|
|
457
|
+
async function capsuleGetCachedList(path, params) {
|
|
458
|
+
const { data, nextPage } = await capsuleGetCached(path, params);
|
|
459
|
+
return { ...data, nextPage };
|
|
382
460
|
}
|
|
383
461
|
async function capsuleGetCached(path, params) {
|
|
384
462
|
if (cacheDisabled()) return capsuleGet(path, params);
|
|
@@ -417,11 +495,7 @@ async function capsulePost(path, body) {
|
|
|
417
495
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
418
496
|
body: JSON.stringify(body)
|
|
419
497
|
});
|
|
420
|
-
|
|
421
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
422
|
-
} finally {
|
|
423
|
-
start.cleanup();
|
|
424
|
-
}
|
|
498
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
425
499
|
}
|
|
426
500
|
async function capsulePostNoContent(path) {
|
|
427
501
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -431,15 +505,11 @@ async function capsulePostNoContent(path) {
|
|
|
431
505
|
method: "POST",
|
|
432
506
|
headers: baseHeaders(token)
|
|
433
507
|
});
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
});
|
|
440
|
-
} finally {
|
|
441
|
-
start.cleanup();
|
|
442
|
-
}
|
|
508
|
+
await consumeBody(start, async () => {
|
|
509
|
+
if (start.res.status === 204) return;
|
|
510
|
+
await throwForStatus(start.res);
|
|
511
|
+
await mapAbort(start.res.text());
|
|
512
|
+
});
|
|
443
513
|
}
|
|
444
514
|
async function capsuleSearch(path, body, params) {
|
|
445
515
|
const token = getToken();
|
|
@@ -449,15 +519,11 @@ async function capsuleSearch(path, body, params) {
|
|
|
449
519
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
450
520
|
body: JSON.stringify(body)
|
|
451
521
|
});
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
});
|
|
458
|
-
} finally {
|
|
459
|
-
start.cleanup();
|
|
460
|
-
}
|
|
522
|
+
return consumeBody(start, async () => {
|
|
523
|
+
const data = await handleResponse(start.res);
|
|
524
|
+
const nextPage = parseNextPage(start.res.headers.get("Link"));
|
|
525
|
+
return { data, nextPage };
|
|
526
|
+
});
|
|
461
527
|
}
|
|
462
528
|
async function capsulePut(path, body) {
|
|
463
529
|
if (isReadOnly()) throw new CapsuleReadOnlyError("PUT");
|
|
@@ -468,68 +534,60 @@ async function capsulePut(path, body) {
|
|
|
468
534
|
headers: { ...baseHeaders(token), "Content-Type": "application/json" },
|
|
469
535
|
body: JSON.stringify(body)
|
|
470
536
|
});
|
|
471
|
-
|
|
472
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
473
|
-
} finally {
|
|
474
|
-
start.cleanup();
|
|
475
|
-
}
|
|
537
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
476
538
|
}
|
|
477
539
|
async function capsuleGetBinary(path, maxBytes) {
|
|
478
540
|
const token = getToken();
|
|
479
541
|
const url = buildUrl(path);
|
|
480
542
|
const start = await doFetch(url, { headers: baseHeaders(token) });
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
490
|
-
|
|
543
|
+
return consumeBody(start, async () => {
|
|
544
|
+
const res = start.res;
|
|
545
|
+
await throwForStatus(res);
|
|
546
|
+
const contentType = res.headers.get("Content-Type") ?? "application/octet-stream";
|
|
547
|
+
const declared = res.headers.get("Content-Length");
|
|
548
|
+
const declaredBytes = declared ? Number(declared) : NaN;
|
|
549
|
+
if (maxBytes !== void 0 && Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
550
|
+
if (res.body) await res.body.cancel().catch(() => {
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
contentType,
|
|
554
|
+
buffer: Buffer.alloc(0),
|
|
555
|
+
truncated: true,
|
|
556
|
+
sizeBytes: declaredBytes
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
if (maxBytes !== void 0 && res.body) {
|
|
560
|
+
const reader = res.body.getReader();
|
|
561
|
+
const chunks = [];
|
|
562
|
+
let total = 0;
|
|
563
|
+
let truncated = false;
|
|
564
|
+
while (true) {
|
|
565
|
+
const { done, value } = await mapAbort(reader.read());
|
|
566
|
+
if (done) break;
|
|
567
|
+
total += value.byteLength;
|
|
568
|
+
if (total > maxBytes) {
|
|
569
|
+
truncated = true;
|
|
570
|
+
await reader.cancel().catch(() => {
|
|
571
|
+
});
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
chunks.push(value);
|
|
575
|
+
}
|
|
576
|
+
if (truncated) {
|
|
491
577
|
return {
|
|
492
578
|
contentType,
|
|
493
579
|
buffer: Buffer.alloc(0),
|
|
494
580
|
truncated: true,
|
|
495
|
-
sizeBytes:
|
|
581
|
+
sizeBytes: total
|
|
496
582
|
};
|
|
497
583
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (done) break;
|
|
506
|
-
total += value.byteLength;
|
|
507
|
-
if (total > maxBytes) {
|
|
508
|
-
truncated = true;
|
|
509
|
-
await reader.cancel().catch(() => {
|
|
510
|
-
});
|
|
511
|
-
break;
|
|
512
|
-
}
|
|
513
|
-
chunks.push(value);
|
|
514
|
-
}
|
|
515
|
-
if (truncated) {
|
|
516
|
-
return {
|
|
517
|
-
contentType,
|
|
518
|
-
buffer: Buffer.alloc(0),
|
|
519
|
-
truncated: true,
|
|
520
|
-
sizeBytes: total
|
|
521
|
-
};
|
|
522
|
-
}
|
|
523
|
-
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
524
|
-
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
525
|
-
}
|
|
526
|
-
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
527
|
-
const buffer = Buffer.from(arrayBuffer);
|
|
528
|
-
return { contentType, buffer, sizeBytes: buffer.length };
|
|
529
|
-
});
|
|
530
|
-
} finally {
|
|
531
|
-
start.cleanup();
|
|
532
|
-
}
|
|
584
|
+
const buffer2 = Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
585
|
+
return { contentType, buffer: buffer2, sizeBytes: buffer2.length };
|
|
586
|
+
}
|
|
587
|
+
const arrayBuffer = await mapAbort(res.arrayBuffer());
|
|
588
|
+
const buffer = Buffer.from(arrayBuffer);
|
|
589
|
+
return { contentType, buffer, sizeBytes: buffer.length };
|
|
590
|
+
});
|
|
533
591
|
}
|
|
534
592
|
async function capsulePostBinary(path, body, contentType, filename) {
|
|
535
593
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
@@ -545,11 +603,7 @@ async function capsulePostBinary(path, body, contentType, filename) {
|
|
|
545
603
|
},
|
|
546
604
|
body
|
|
547
605
|
});
|
|
548
|
-
|
|
549
|
-
return await consumeBody(start, () => handleResponse(start.res));
|
|
550
|
-
} finally {
|
|
551
|
-
start.cleanup();
|
|
552
|
-
}
|
|
606
|
+
return consumeBody(start, () => handleResponse(start.res));
|
|
553
607
|
}
|
|
554
608
|
async function capsuleDelete(path) {
|
|
555
609
|
if (isReadOnly()) throw new CapsuleReadOnlyError("DELETE");
|
|
@@ -559,15 +613,11 @@ async function capsuleDelete(path) {
|
|
|
559
613
|
method: "DELETE",
|
|
560
614
|
headers: baseHeaders(token)
|
|
561
615
|
});
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
});
|
|
568
|
-
} finally {
|
|
569
|
-
start.cleanup();
|
|
570
|
-
}
|
|
616
|
+
await consumeBody(start, async () => {
|
|
617
|
+
if (start.res.status === 204) return;
|
|
618
|
+
await throwForStatus(start.res);
|
|
619
|
+
await mapAbort(start.res.text());
|
|
620
|
+
});
|
|
571
621
|
}
|
|
572
622
|
|
|
573
623
|
// src/auth/provider.ts
|
|
@@ -1088,6 +1138,44 @@ var ICONS = [
|
|
|
1088
1138
|
}
|
|
1089
1139
|
];
|
|
1090
1140
|
|
|
1141
|
+
// src/server/tier.ts
|
|
1142
|
+
var CORE_TOOLS = /* @__PURE__ */ new Set([
|
|
1143
|
+
// Parties
|
|
1144
|
+
"search_parties",
|
|
1145
|
+
"filter_parties",
|
|
1146
|
+
"get_party",
|
|
1147
|
+
"create_party",
|
|
1148
|
+
"update_party",
|
|
1149
|
+
"list_party_entries",
|
|
1150
|
+
// Opportunities
|
|
1151
|
+
"search_opportunities",
|
|
1152
|
+
"filter_opportunities",
|
|
1153
|
+
"get_opportunity",
|
|
1154
|
+
"create_opportunity",
|
|
1155
|
+
"update_opportunity",
|
|
1156
|
+
// Projects
|
|
1157
|
+
"filter_projects",
|
|
1158
|
+
"list_projects",
|
|
1159
|
+
"get_project",
|
|
1160
|
+
"create_project",
|
|
1161
|
+
"update_project",
|
|
1162
|
+
// Tasks
|
|
1163
|
+
"list_tasks",
|
|
1164
|
+
"get_task",
|
|
1165
|
+
"create_task",
|
|
1166
|
+
"update_task",
|
|
1167
|
+
"complete_task",
|
|
1168
|
+
// Timeline + tags + identity
|
|
1169
|
+
"add_note",
|
|
1170
|
+
"list_tags",
|
|
1171
|
+
"add_tag",
|
|
1172
|
+
"get_current_user"
|
|
1173
|
+
]);
|
|
1174
|
+
function shouldRegister(name) {
|
|
1175
|
+
if (process.env["CAPSULE_MCP_TIER"] !== "core") return true;
|
|
1176
|
+
return CORE_TOOLS.has(name);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1091
1179
|
// src/tasks/store.ts
|
|
1092
1180
|
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
1093
1181
|
import {
|
|
@@ -1142,6 +1230,22 @@ var abortControllers = /* @__PURE__ */ new Map();
|
|
|
1142
1230
|
function registerAbortController(taskId, controller) {
|
|
1143
1231
|
abortControllers.set(taskId, controller);
|
|
1144
1232
|
}
|
|
1233
|
+
var evictionTimers = /* @__PURE__ */ new Map();
|
|
1234
|
+
var taskTtls = /* @__PURE__ */ new Map();
|
|
1235
|
+
function scheduleEviction(taskId, clientId, ttlMs) {
|
|
1236
|
+
const existing = evictionTimers.get(taskId);
|
|
1237
|
+
if (existing) clearTimeout(existing);
|
|
1238
|
+
taskTtls.set(taskId, ttlMs);
|
|
1239
|
+
const timer = setTimeout(() => {
|
|
1240
|
+
owners.delete(taskId);
|
|
1241
|
+
abortControllers.delete(taskId);
|
|
1242
|
+
evictionTimers.delete(taskId);
|
|
1243
|
+
taskTtls.delete(taskId);
|
|
1244
|
+
logEvent("task.evicted", { taskId, clientId, reason: "ttl" });
|
|
1245
|
+
}, ttlMs);
|
|
1246
|
+
timer.unref?.();
|
|
1247
|
+
evictionTimers.set(taskId, timer);
|
|
1248
|
+
}
|
|
1145
1249
|
function countPerClient(clientId) {
|
|
1146
1250
|
let n = 0;
|
|
1147
1251
|
for (const owner of owners.values()) {
|
|
@@ -1195,12 +1299,7 @@ function createScopedTaskStore(clientId) {
|
|
|
1195
1299
|
sessionId
|
|
1196
1300
|
);
|
|
1197
1301
|
owners.set(task.taskId, clientId);
|
|
1198
|
-
|
|
1199
|
-
owners.delete(task.taskId);
|
|
1200
|
-
abortControllers.delete(task.taskId);
|
|
1201
|
-
logEvent("task.evicted", { taskId: task.taskId, clientId, reason: "ttl" });
|
|
1202
|
-
}, clampedTtl);
|
|
1203
|
-
timer.unref?.();
|
|
1302
|
+
scheduleEviction(task.taskId, clientId, clampedTtl);
|
|
1204
1303
|
logEvent("task.created", {
|
|
1205
1304
|
taskId: task.taskId,
|
|
1206
1305
|
clientId,
|
|
@@ -1219,6 +1318,7 @@ function createScopedTaskStore(clientId) {
|
|
|
1219
1318
|
}
|
|
1220
1319
|
logEvent("task.transition", { taskId, clientId, status });
|
|
1221
1320
|
await global.storeTaskResult(taskId, status, result, sessionId);
|
|
1321
|
+
scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
|
|
1222
1322
|
},
|
|
1223
1323
|
async getTaskResult(taskId, sessionId) {
|
|
1224
1324
|
if (owners.get(taskId) !== clientId) {
|
|
@@ -1238,6 +1338,7 @@ function createScopedTaskStore(clientId) {
|
|
|
1238
1338
|
}
|
|
1239
1339
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
1240
1340
|
abortControllers.delete(taskId);
|
|
1341
|
+
scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
|
|
1241
1342
|
}
|
|
1242
1343
|
},
|
|
1243
1344
|
async listTasks(cursor, sessionId) {
|
|
@@ -1283,27 +1384,25 @@ function wrapAsText(result) {
|
|
|
1283
1384
|
};
|
|
1284
1385
|
}
|
|
1285
1386
|
function registerTool(server, name, description, schema, handler) {
|
|
1387
|
+
if (!shouldRegister(name)) return;
|
|
1286
1388
|
const registerWithSchema = server.registerTool.bind(server);
|
|
1287
1389
|
const annotations = inferAnnotations(name);
|
|
1288
|
-
registerWithSchema(
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
const
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
} catch (err) {
|
|
1300
|
-
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
1301
|
-
throw err;
|
|
1302
|
-
}
|
|
1390
|
+
registerWithSchema(name, { description, inputSchema: schema, annotations }, async (input) => {
|
|
1391
|
+
const startedAt = Date.now();
|
|
1392
|
+
const argFields = argFieldNames(input);
|
|
1393
|
+
const clientId = getRequestContext()?.clientId;
|
|
1394
|
+
try {
|
|
1395
|
+
const result = await handler(input);
|
|
1396
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
|
|
1397
|
+
return wrapAsText(result);
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
1400
|
+
throw err;
|
|
1303
1401
|
}
|
|
1304
|
-
);
|
|
1402
|
+
});
|
|
1305
1403
|
}
|
|
1306
1404
|
function registerToolTask(server, name, description, schema, handler) {
|
|
1405
|
+
if (!shouldRegister(name)) return;
|
|
1307
1406
|
const registerWithSchema = server.experimental.tasks.registerToolTask.bind(
|
|
1308
1407
|
server.experimental.tasks
|
|
1309
1408
|
);
|
|
@@ -1314,7 +1413,7 @@ function registerToolTask(server, name, description, schema, handler) {
|
|
|
1314
1413
|
description,
|
|
1315
1414
|
inputSchema: schema,
|
|
1316
1415
|
execution: { taskSupport: "optional" },
|
|
1317
|
-
|
|
1416
|
+
annotations
|
|
1318
1417
|
},
|
|
1319
1418
|
{
|
|
1320
1419
|
createTask: async (input, extra) => {
|
|
@@ -1374,7 +1473,7 @@ function registerToolTask(server, name, description, schema, handler) {
|
|
|
1374
1473
|
}
|
|
1375
1474
|
|
|
1376
1475
|
// src/tools/parties.ts
|
|
1377
|
-
import { z as
|
|
1476
|
+
import { z as z8 } from "zod";
|
|
1378
1477
|
|
|
1379
1478
|
// src/tools/body-helpers.ts
|
|
1380
1479
|
function setRef(body, key, id) {
|
|
@@ -1384,9 +1483,22 @@ function setNullableRef(body, key, id) {
|
|
|
1384
1483
|
if (id === null) body[key] = null;
|
|
1385
1484
|
else if (id !== void 0) body[key] = { id };
|
|
1386
1485
|
}
|
|
1486
|
+
function assertSingleParentRef(toolName, refs, opts = {}) {
|
|
1487
|
+
const set = [refs.partyId, refs.opportunityId, refs.projectId].filter(
|
|
1488
|
+
(v) => typeof v === "number"
|
|
1489
|
+
).length;
|
|
1490
|
+
if (opts.required && set !== 1) {
|
|
1491
|
+
throw new Error(`${toolName}: provide exactly one of partyId, opportunityId, or projectId`);
|
|
1492
|
+
}
|
|
1493
|
+
if (set > 1) {
|
|
1494
|
+
throw new Error(
|
|
1495
|
+
`${toolName}: provide at most one of partyId, opportunityId, or projectId \u2014 Capsule allows a record to be related to at most one entity`
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1387
1499
|
|
|
1388
1500
|
// src/tools/define-batch.ts
|
|
1389
|
-
import { z as
|
|
1501
|
+
import { z as z3 } from "zod";
|
|
1390
1502
|
|
|
1391
1503
|
// src/capsule/batch.ts
|
|
1392
1504
|
function chunk(arr, size) {
|
|
@@ -1405,37 +1517,43 @@ function getBatchConcurrency() {
|
|
|
1405
1517
|
MAX_CONCURRENCY
|
|
1406
1518
|
);
|
|
1407
1519
|
}
|
|
1408
|
-
async function
|
|
1409
|
-
const concurrency = getBatchConcurrency();
|
|
1520
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
1410
1521
|
const results = new Array(items.length);
|
|
1411
|
-
const startedAt = Date.now();
|
|
1412
|
-
const signal = options.signal;
|
|
1413
1522
|
let cursor = 0;
|
|
1414
1523
|
async function worker() {
|
|
1415
1524
|
while (true) {
|
|
1416
1525
|
const i = cursor;
|
|
1417
1526
|
cursor += 1;
|
|
1418
1527
|
if (i >= items.length) return;
|
|
1419
|
-
|
|
1420
|
-
results[i] = {
|
|
1421
|
-
ok: false,
|
|
1422
|
-
error: { message: "cancelled by tasks/cancel" }
|
|
1423
|
-
};
|
|
1424
|
-
continue;
|
|
1425
|
-
}
|
|
1426
|
-
try {
|
|
1427
|
-
const result = await action(items[i], i);
|
|
1428
|
-
results[i] = { ok: true, result };
|
|
1429
|
-
} catch (err) {
|
|
1430
|
-
results[i] = { ok: false, error: extractError(err) };
|
|
1431
|
-
}
|
|
1528
|
+
results[i] = await fn(items[i], i);
|
|
1432
1529
|
}
|
|
1433
1530
|
}
|
|
1434
1531
|
const workers = [];
|
|
1435
|
-
for (let w = 0; w < Math.min(
|
|
1532
|
+
for (let w = 0; w < Math.min(limit, items.length); w++) {
|
|
1436
1533
|
workers.push(worker());
|
|
1437
1534
|
}
|
|
1438
1535
|
await Promise.all(workers);
|
|
1536
|
+
return results;
|
|
1537
|
+
}
|
|
1538
|
+
async function batchExecute(tool, items, action, options = {}) {
|
|
1539
|
+
const concurrency = getBatchConcurrency();
|
|
1540
|
+
const startedAt = Date.now();
|
|
1541
|
+
const signal = options.signal;
|
|
1542
|
+
const results = await mapWithConcurrency(
|
|
1543
|
+
items,
|
|
1544
|
+
concurrency,
|
|
1545
|
+
async (item, i) => {
|
|
1546
|
+
if (signal?.aborted) {
|
|
1547
|
+
return { ok: false, error: { message: "cancelled by tasks/cancel" } };
|
|
1548
|
+
}
|
|
1549
|
+
try {
|
|
1550
|
+
const result = await action(item, i);
|
|
1551
|
+
return { ok: true, result };
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
return { ok: false, error: extractError(err) };
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
);
|
|
1439
1557
|
const succeeded = results.filter((r) => r.ok).length;
|
|
1440
1558
|
const failed = results.length - succeeded;
|
|
1441
1559
|
const summary = { total: results.length, succeeded, failed };
|
|
@@ -1480,10 +1598,52 @@ function topFailureReasons(results, n) {
|
|
|
1480
1598
|
return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
|
|
1481
1599
|
}
|
|
1482
1600
|
|
|
1601
|
+
// src/tools/strip-descriptions.ts
|
|
1602
|
+
import { z as z2 } from "zod";
|
|
1603
|
+
function cloneWithDef(node, patch) {
|
|
1604
|
+
const def = node.def;
|
|
1605
|
+
return node.clone({ ...def, ...patch });
|
|
1606
|
+
}
|
|
1607
|
+
function stripDescriptions(schema) {
|
|
1608
|
+
let node = schema;
|
|
1609
|
+
if (node instanceof z2.ZodObject) {
|
|
1610
|
+
const shape = node.def.shape;
|
|
1611
|
+
const next = {};
|
|
1612
|
+
let changed = false;
|
|
1613
|
+
for (const [key, child] of Object.entries(shape)) {
|
|
1614
|
+
next[key] = stripDescriptions(child);
|
|
1615
|
+
if (next[key] !== child) changed = true;
|
|
1616
|
+
}
|
|
1617
|
+
if (changed) node = cloneWithDef(node, { shape: next });
|
|
1618
|
+
} else if (node instanceof z2.ZodArray) {
|
|
1619
|
+
const element = stripDescriptions(node.def.element);
|
|
1620
|
+
if (element !== node.def.element) node = cloneWithDef(node, { element });
|
|
1621
|
+
} else if (node instanceof z2.ZodOptional || node instanceof z2.ZodNullable || node instanceof z2.ZodDefault || node instanceof z2.ZodReadonly) {
|
|
1622
|
+
const innerType = stripDescriptions(node.def.innerType);
|
|
1623
|
+
if (innerType !== node.def.innerType) node = cloneWithDef(node, { innerType });
|
|
1624
|
+
} else if (node instanceof z2.ZodUnion) {
|
|
1625
|
+
const options = node.def.options.map(stripDescriptions);
|
|
1626
|
+
if (options.some((o, i) => o !== node.def.options[i])) {
|
|
1627
|
+
node = cloneWithDef(node, { options });
|
|
1628
|
+
}
|
|
1629
|
+
} else if (node instanceof z2.ZodPipe) {
|
|
1630
|
+
const inSchema = stripDescriptions(node.def.in);
|
|
1631
|
+
const outSchema = stripDescriptions(node.def.out);
|
|
1632
|
+
if (inSchema !== node.def.in || outSchema !== node.def.out) {
|
|
1633
|
+
node = cloneWithDef(node, { in: inSchema, out: outSchema });
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
if (node.description !== void 0) {
|
|
1637
|
+
node = node.meta({ description: void 0 });
|
|
1638
|
+
}
|
|
1639
|
+
return node;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1483
1642
|
// src/tools/define-batch.ts
|
|
1484
1643
|
function defineBatch(args) {
|
|
1485
|
-
const
|
|
1486
|
-
|
|
1644
|
+
const itemSchema = stripDescriptions(args.itemSchema);
|
|
1645
|
+
const schema = z3.object({
|
|
1646
|
+
items: z3.array(itemSchema).min(1).max(50).describe(args.itemDescription)
|
|
1487
1647
|
});
|
|
1488
1648
|
async function handler(input, opts = {}) {
|
|
1489
1649
|
return batchExecute(args.toolName, input.items, args.itemHandler, opts);
|
|
@@ -1496,22 +1656,30 @@ var EMBED_TAGS_FIELDS_DESCRIPTION = "Comma-separated embeds, e.g. 'tags,fields'"
|
|
|
1496
1656
|
var EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION = "Comma-separated embeds, e.g. 'attachments,participants'";
|
|
1497
1657
|
|
|
1498
1658
|
// src/tools/define-delete.ts
|
|
1499
|
-
import { z as
|
|
1659
|
+
import { z as z6 } from "zod";
|
|
1500
1660
|
|
|
1501
1661
|
// src/tools/confirm-flag.ts
|
|
1502
|
-
import { z as
|
|
1662
|
+
import { z as z4 } from "zod";
|
|
1503
1663
|
var CONFIRM_REQUIRED_MESSAGE = "confirm: true is required to perform this destructive operation (set the parameter explicitly to acknowledge the destructive intent)";
|
|
1504
1664
|
function confirmFlag() {
|
|
1505
|
-
return
|
|
1665
|
+
return z4.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
|
|
1506
1666
|
}
|
|
1507
1667
|
|
|
1508
1668
|
// src/tools/shared-schemas.ts
|
|
1509
|
-
import { z as
|
|
1510
|
-
var positiveId =
|
|
1669
|
+
import { z as z5 } from "zod";
|
|
1670
|
+
var positiveId = z5.preprocess((input) => {
|
|
1511
1671
|
if (typeof input !== "string") return input;
|
|
1512
1672
|
const trimmed = input.trim();
|
|
1513
1673
|
return /^\d+$/.test(trimmed) ? Number(trimmed) : input;
|
|
1514
|
-
},
|
|
1674
|
+
}, z5.number().int().positive());
|
|
1675
|
+
var paginationFields = {
|
|
1676
|
+
page: z5.number().int().positive().optional().default(1),
|
|
1677
|
+
perPage: z5.number().int().min(1).max(100).optional().default(25)
|
|
1678
|
+
};
|
|
1679
|
+
var paginationFieldsNoDefaults = {
|
|
1680
|
+
page: z5.number().int().positive().optional(),
|
|
1681
|
+
perPage: z5.number().int().min(1).max(100).optional()
|
|
1682
|
+
};
|
|
1515
1683
|
|
|
1516
1684
|
// src/capsule/idempotent.ts
|
|
1517
1685
|
var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
|
|
@@ -1538,7 +1706,7 @@ async function idempotentWithResult(op, success, alreadyDone, isAlreadyDoneError
|
|
|
1538
1706
|
// src/tools/define-delete.ts
|
|
1539
1707
|
function defineDelete(args) {
|
|
1540
1708
|
const { toolName, pathPrefix, confirmHint, idDescription } = args;
|
|
1541
|
-
const schema =
|
|
1709
|
+
const schema = z6.object({
|
|
1542
1710
|
id: idDescription ? positiveId.describe(idDescription) : positiveId,
|
|
1543
1711
|
confirm: confirmFlag().describe(confirmHint)
|
|
1544
1712
|
});
|
|
@@ -1581,16 +1749,17 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
|
|
|
1581
1749
|
(chunkIds) => capsuleGet(`${base}/${chunkIds.join(",")}`, params)
|
|
1582
1750
|
)
|
|
1583
1751
|
);
|
|
1584
|
-
|
|
1752
|
+
const merged = responses.flatMap((r) => r.data[responseKey] ?? []);
|
|
1753
|
+
return { ...responses[0]?.data ?? {}, [responseKey]: merged };
|
|
1585
1754
|
}
|
|
1586
1755
|
|
|
1587
1756
|
// src/tools/custom-field-helpers.ts
|
|
1588
|
-
import { z as
|
|
1589
|
-
var CustomFieldWriteSchema =
|
|
1757
|
+
import { z as z7 } from "zod";
|
|
1758
|
+
var CustomFieldWriteSchema = z7.object({
|
|
1590
1759
|
definitionId: positiveId.describe(
|
|
1591
1760
|
"The custom-field definition id from list_custom_fields. Identifies which field on the entity to set."
|
|
1592
1761
|
),
|
|
1593
|
-
value:
|
|
1762
|
+
value: z7.union([z7.string(), z7.number(), z7.boolean(), z7.null()]).describe(
|
|
1594
1763
|
"The new value. String for TEXT / DATE / LIST / LARGE_TEXT / LINK fields, number for NUMBER fields, boolean for BOOLEAN fields. Clearing: pass null for TEXT / NUMBER / DATE / LIST (Capsule removes the row). BOOLEAN does NOT accept null (Capsule returns 422 'invalid type for field'); use `value: false` instead. Note BOOLEAN fields are observably **two-state**: a row exists with `value: true`, or no row exists. Setting `value: false` removes the row entirely \u2014 readers should treat absent BOOLEAN rows as equivalent to false. Tri-state BOOLEAN semantics (true / false / unknown) are not achievable through Capsule's API. Audit-log noise: sending value=null on a field that's already empty/cleared is accepted by Capsule but still bumps the parent entity's `updatedAt`. Read the current value via embed='fields' first if `updatedAt` is being used as a 'last meaningful change' signal. NUMBER quirks: Capsule stores numerics correctly but the read-back via embed=fields returns them as STRINGS (e.g. value=3 reads as '3'); callers comparing values must coerce. TEXT quirks: value='' has the same observable effect as value=null (row removed); empty-string and never-set are indistinguishable."
|
|
1595
1764
|
)
|
|
1596
1765
|
});
|
|
@@ -1606,24 +1775,24 @@ function mapFieldsForBody(fields) {
|
|
|
1606
1775
|
}
|
|
1607
1776
|
|
|
1608
1777
|
// src/tools/parties.ts
|
|
1609
|
-
var EmailAddressSchema =
|
|
1610
|
-
address:
|
|
1611
|
-
type:
|
|
1778
|
+
var EmailAddressSchema = z8.object({
|
|
1779
|
+
address: z8.string().email(),
|
|
1780
|
+
type: z8.string().optional()
|
|
1612
1781
|
});
|
|
1613
|
-
var PhoneNumberSchema =
|
|
1782
|
+
var PhoneNumberSchema = z8.object({
|
|
1614
1783
|
// Capsule rejects empty strings with `phoneNumber.number: number is
|
|
1615
1784
|
// required`. Enforce at the schema layer to catch typos pre-call,
|
|
1616
1785
|
// matching how EmailAddressSchema's address field behaves.
|
|
1617
|
-
number:
|
|
1618
|
-
type:
|
|
1786
|
+
number: z8.string().min(1),
|
|
1787
|
+
type: z8.string().optional()
|
|
1619
1788
|
});
|
|
1620
1789
|
var CountryDescription = "Country name. Capsule validates this against a small canonical-English-name dictionary; inputs not in the dictionary are REJECTED with 422 'address.country: unknown country' (NOT silently passed through or normalised). Probed examples \u2014 accepted: `United States`, `United Kingdom`, `Czechia`, `Germany`. Aliased: `USA \u2192 United States`. Rejected: `United States of America`, `Czech Republic` (use `Czechia`), `UK`/`Britain` (use `United Kingdom`), `Deutschland` (use `Germany`). Empty string is accepted and stored as `null` \u2014 a de-facto 'clear' shape. To discover an accepted name, read an existing party that already has the country set.";
|
|
1621
|
-
var AddressSchema =
|
|
1622
|
-
street:
|
|
1623
|
-
city:
|
|
1624
|
-
state:
|
|
1625
|
-
country:
|
|
1626
|
-
zip:
|
|
1790
|
+
var AddressSchema = z8.object({
|
|
1791
|
+
street: z8.string().optional(),
|
|
1792
|
+
city: z8.string().optional(),
|
|
1793
|
+
state: z8.string().optional(),
|
|
1794
|
+
country: z8.string().optional().describe(CountryDescription),
|
|
1795
|
+
zip: z8.string().optional()
|
|
1627
1796
|
});
|
|
1628
1797
|
function validateWebsiteAddress(data, ctx) {
|
|
1629
1798
|
const isUrlService = data.service === void 0 || data.service === "URL";
|
|
@@ -1646,7 +1815,7 @@ function validateWebsiteAddress(data, ctx) {
|
|
|
1646
1815
|
});
|
|
1647
1816
|
}
|
|
1648
1817
|
}
|
|
1649
|
-
var WebsiteServiceEnum =
|
|
1818
|
+
var WebsiteServiceEnum = z8.enum([
|
|
1650
1819
|
"URL",
|
|
1651
1820
|
"SKYPE",
|
|
1652
1821
|
"TWITTER",
|
|
@@ -1665,33 +1834,31 @@ var WebsiteServiceEnum = z7.enum([
|
|
|
1665
1834
|
"BLUESKY",
|
|
1666
1835
|
"SNAPCHAT"
|
|
1667
1836
|
]);
|
|
1668
|
-
var WebsiteSchema =
|
|
1669
|
-
address:
|
|
1837
|
+
var WebsiteSchema = z8.object({
|
|
1838
|
+
address: z8.string().min(1).describe(
|
|
1670
1839
|
"The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services like 'TWITTER', 'INSTAGRAM'. Capsule names this field `address` regardless of service type."
|
|
1671
1840
|
),
|
|
1672
1841
|
service: WebsiteServiceEnum.optional().describe(
|
|
1673
1842
|
"Service type. One of: URL, SKYPE, TWITTER, LINKED_IN, FACEBOOK, XING, FEED, GOOGLE_PLUS, FLICKR, GITHUB, YOUTUBE, INSTAGRAM, PINTEREST, TIKTOK, THREADS, BLUESKY, SNAPCHAT. Defaults to 'URL' if omitted."
|
|
1674
1843
|
)
|
|
1675
1844
|
}).superRefine(validateWebsiteAddress);
|
|
1676
|
-
var searchPartiesSchema =
|
|
1677
|
-
q:
|
|
1678
|
-
embed:
|
|
1679
|
-
|
|
1680
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1845
|
+
var searchPartiesSchema = z8.object({
|
|
1846
|
+
q: z8.string().optional().describe("Free-text search query"),
|
|
1847
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
1848
|
+
...paginationFields
|
|
1681
1849
|
});
|
|
1682
1850
|
async function searchParties(input) {
|
|
1683
1851
|
const path = input.q ? "/parties/search" : "/parties";
|
|
1684
|
-
|
|
1852
|
+
return capsuleGetList(path, {
|
|
1685
1853
|
q: input.q,
|
|
1686
1854
|
embed: input.embed,
|
|
1687
1855
|
page: input.page,
|
|
1688
1856
|
perPage: input.perPage
|
|
1689
1857
|
});
|
|
1690
|
-
return { ...data, nextPage };
|
|
1691
1858
|
}
|
|
1692
|
-
var getPartySchema =
|
|
1859
|
+
var getPartySchema = z8.object({
|
|
1693
1860
|
id: positiveId.describe("Party ID"),
|
|
1694
|
-
embed:
|
|
1861
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1695
1862
|
});
|
|
1696
1863
|
async function getParty(input) {
|
|
1697
1864
|
const { data } = await capsuleGet(`/parties/${input.id}`, {
|
|
@@ -1699,51 +1866,47 @@ async function getParty(input) {
|
|
|
1699
1866
|
});
|
|
1700
1867
|
return data;
|
|
1701
1868
|
}
|
|
1702
|
-
var getPartiesSchema =
|
|
1703
|
-
ids:
|
|
1869
|
+
var getPartiesSchema = z8.object({
|
|
1870
|
+
ids: z8.array(positiveId).min(1).max(50).describe(
|
|
1704
1871
|
"Array of party IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel. Result shape is identical regardless of input size."
|
|
1705
1872
|
),
|
|
1706
|
-
embed:
|
|
1873
|
+
embed: z8.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
1707
1874
|
});
|
|
1708
1875
|
async function getParties(input) {
|
|
1709
1876
|
return chunkedMultiGet("/parties", "parties", input.ids, { embed: input.embed });
|
|
1710
1877
|
}
|
|
1711
|
-
var listPartyOpportunitiesSchema =
|
|
1878
|
+
var listPartyOpportunitiesSchema = z8.object({
|
|
1712
1879
|
partyId: positiveId,
|
|
1713
|
-
|
|
1714
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1880
|
+
...paginationFields
|
|
1715
1881
|
});
|
|
1716
1882
|
async function listPartyOpportunities(input) {
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
);
|
|
1721
|
-
return { ...data, nextPage };
|
|
1883
|
+
return capsuleGetList(`/parties/${input.partyId}/opportunities`, {
|
|
1884
|
+
page: input.page,
|
|
1885
|
+
perPage: input.perPage
|
|
1886
|
+
});
|
|
1722
1887
|
}
|
|
1723
|
-
var listPartyProjectsSchema =
|
|
1888
|
+
var listPartyProjectsSchema = z8.object({
|
|
1724
1889
|
partyId: positiveId,
|
|
1725
|
-
|
|
1726
|
-
perPage: z7.number().int().min(1).max(100).optional().default(25)
|
|
1890
|
+
...paginationFields
|
|
1727
1891
|
});
|
|
1728
1892
|
async function listPartyProjects(input) {
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
);
|
|
1733
|
-
return { ...data, nextPage };
|
|
1893
|
+
return capsuleGetList(`/parties/${input.partyId}/kases`, {
|
|
1894
|
+
page: input.page,
|
|
1895
|
+
perPage: input.perPage
|
|
1896
|
+
});
|
|
1734
1897
|
}
|
|
1735
1898
|
var PartyWriteBaseSchema = {
|
|
1736
|
-
about:
|
|
1737
|
-
emailAddresses:
|
|
1899
|
+
about: z8.string().optional(),
|
|
1900
|
+
emailAddresses: z8.array(EmailAddressSchema).optional().describe(
|
|
1738
1901
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_email_address and remove_party_email_address_by_id. Passing `[]` here is a silent no-op (does not clear the list and does not advance updatedAt)."
|
|
1739
1902
|
),
|
|
1740
|
-
phoneNumbers:
|
|
1903
|
+
phoneNumbers: z8.array(PhoneNumberSchema).optional().describe(
|
|
1741
1904
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_phone_number and remove_party_phone_number_by_id."
|
|
1742
1905
|
),
|
|
1743
|
-
addresses:
|
|
1906
|
+
addresses: z8.array(AddressSchema).optional().describe(
|
|
1744
1907
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_address and remove_party_address_by_id. The `country` field is mapped through Capsule's country dictionary \u2014 see `add_party_address.country` for the dictionary edges (small canonical-English-name list; inputs not in the dictionary are REJECTED with 422, not silently dropped)."
|
|
1745
1908
|
),
|
|
1746
|
-
websites:
|
|
1909
|
+
websites: z8.array(WebsiteSchema).optional().describe(
|
|
1747
1910
|
"APPEND-ONLY: items are merged into the existing list, never replaced. For atomic add/remove/replace use add_party_website and remove_party_website_by_id."
|
|
1748
1911
|
),
|
|
1749
1912
|
ownerId: positiveId.nullable().optional().describe(
|
|
@@ -1753,16 +1916,16 @@ var PartyWriteBaseSchema = {
|
|
|
1753
1916
|
"Assign to team ID (discover via list_teams). Pass a team ID to set, or `null` to unassign. Capsule enforces the owner\u2208team membership constraint \u2014 passing a team the current owner doesn't belong to returns 422 'owner is not a member of the team'. Combine `ownerId: null` + `teamId: <T>` in one call to transfer a party to team-ownership with no specific user (verified empirically in v1.6.4 wire-trace; the membership rule doesn't fire when owner is null)."
|
|
1754
1917
|
)
|
|
1755
1918
|
};
|
|
1756
|
-
var createPartySchema =
|
|
1757
|
-
type:
|
|
1919
|
+
var createPartySchema = z8.object({
|
|
1920
|
+
type: z8.enum(["person", "organisation"]),
|
|
1758
1921
|
// person
|
|
1759
|
-
firstName:
|
|
1760
|
-
lastName:
|
|
1761
|
-
title:
|
|
1762
|
-
jobTitle:
|
|
1922
|
+
firstName: z8.string().optional(),
|
|
1923
|
+
lastName: z8.string().optional(),
|
|
1924
|
+
title: z8.string().optional(),
|
|
1925
|
+
jobTitle: z8.string().optional(),
|
|
1763
1926
|
organisationId: positiveId.optional().describe("Link person to an existing organisation ID"),
|
|
1764
1927
|
// organisation
|
|
1765
|
-
name:
|
|
1928
|
+
name: z8.string().optional(),
|
|
1766
1929
|
...PartyWriteBaseSchema,
|
|
1767
1930
|
ownerId: positiveId.optional().describe(
|
|
1768
1931
|
"Assign to user ID. Defaults to the API-token owner when omitted. To create a team-owned party with no specific user, first create the party, then call update_party with `ownerId: null` and `teamId`."
|
|
@@ -1770,7 +1933,7 @@ var createPartySchema = z7.object({
|
|
|
1770
1933
|
teamId: positiveId.optional().describe(
|
|
1771
1934
|
"Assign to team ID (discover via list_teams). Omit to leave team unset on create. To clear an existing team or create a team-owned party with no specific owner, use update_party after creation."
|
|
1772
1935
|
),
|
|
1773
|
-
fields:
|
|
1936
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(
|
|
1774
1937
|
fieldsArrayDescriptor("get_party") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /parties accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update."
|
|
1775
1938
|
)
|
|
1776
1939
|
});
|
|
@@ -1784,17 +1947,17 @@ async function createParty(input) {
|
|
|
1784
1947
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
1785
1948
|
return capsulePost("/parties", { party: body });
|
|
1786
1949
|
}
|
|
1787
|
-
var updatePartySchema =
|
|
1950
|
+
var updatePartySchema = z8.object({
|
|
1788
1951
|
id: positiveId,
|
|
1789
|
-
firstName:
|
|
1790
|
-
lastName:
|
|
1791
|
-
title:
|
|
1792
|
-
jobTitle:
|
|
1793
|
-
name:
|
|
1952
|
+
firstName: z8.string().optional(),
|
|
1953
|
+
lastName: z8.string().optional(),
|
|
1954
|
+
title: z8.string().optional(),
|
|
1955
|
+
jobTitle: z8.string().optional(),
|
|
1956
|
+
name: z8.string().optional(),
|
|
1794
1957
|
organisationId: positiveId.nullable().optional().describe(
|
|
1795
1958
|
"For PERSON parties: link to an organisation by id, or `null` to unlink (the person becomes an orphan / standalone record). Discover org IDs via search_parties / filter_parties with type=organisation. For ORGANISATION parties: silently ignored by Capsule's API \u2014 organisations don't have a parent organisation in the data model. Empirically verified in v1.6.3 wire-trace; no client-side type guard since the no-op is harmless."
|
|
1796
1959
|
),
|
|
1797
|
-
fields:
|
|
1960
|
+
fields: z8.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_party")),
|
|
1798
1961
|
...PartyWriteBaseSchema
|
|
1799
1962
|
});
|
|
1800
1963
|
async function updateParty(input) {
|
|
@@ -1825,10 +1988,42 @@ var { schema: deletePartySchema, handler: deleteParty } = defineDelete({
|
|
|
1825
1988
|
pathPrefix: "/parties",
|
|
1826
1989
|
confirmHint: "Must be set to true. Deletes the party AND all linked notes, tasks, opportunities, and projects (kases). Deleting an ORGANISATION does NOT delete people linked to it via organisationId \u2014 their `organisation` field is silently cleared to null and they survive as standalone records. Irreversible."
|
|
1827
1990
|
});
|
|
1828
|
-
|
|
1991
|
+
function definePartySubResourceRemove(opts) {
|
|
1992
|
+
const shape = {
|
|
1993
|
+
partyId: positiveId,
|
|
1994
|
+
[opts.idField]: positiveId.describe(
|
|
1995
|
+
`Capsule's id for the ${opts.rowNoun} row. Read it from get_party (each entry in ${opts.arrayKey} carries an id).`
|
|
1996
|
+
)
|
|
1997
|
+
};
|
|
1998
|
+
const schema = z8.object(shape);
|
|
1999
|
+
async function handler(input) {
|
|
2000
|
+
const partyId = input["partyId"];
|
|
2001
|
+
const rowId = input[opts.idField];
|
|
2002
|
+
return idempotentWithResult(
|
|
2003
|
+
() => capsulePut(`/parties/${partyId}`, {
|
|
2004
|
+
party: { [opts.arrayKey]: [{ id: rowId, _delete: true }] }
|
|
2005
|
+
}),
|
|
2006
|
+
(result) => ({
|
|
2007
|
+
removed: true,
|
|
2008
|
+
alreadyRemoved: false,
|
|
2009
|
+
partyId,
|
|
2010
|
+
[opts.idField]: rowId,
|
|
2011
|
+
...result
|
|
2012
|
+
}),
|
|
2013
|
+
() => ({
|
|
2014
|
+
removed: true,
|
|
2015
|
+
alreadyRemoved: true,
|
|
2016
|
+
partyId,
|
|
2017
|
+
[opts.idField]: rowId
|
|
2018
|
+
})
|
|
2019
|
+
);
|
|
2020
|
+
}
|
|
2021
|
+
return { schema, handler };
|
|
2022
|
+
}
|
|
2023
|
+
var addPartyEmailAddressSchema = z8.object({
|
|
1829
2024
|
partyId: positiveId,
|
|
1830
|
-
address:
|
|
1831
|
-
type:
|
|
2025
|
+
address: z8.string().email(),
|
|
2026
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Work', 'Home'.")
|
|
1832
2027
|
});
|
|
1833
2028
|
async function addPartyEmailAddress(input) {
|
|
1834
2029
|
const { partyId, address, type } = input;
|
|
@@ -1838,32 +2033,17 @@ async function addPartyEmailAddress(input) {
|
|
|
1838
2033
|
party: { emailAddresses: [item] }
|
|
1839
2034
|
});
|
|
1840
2035
|
}
|
|
1841
|
-
var
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
)
|
|
2036
|
+
var removePartyEmailAddress = definePartySubResourceRemove({
|
|
2037
|
+
arrayKey: "emailAddresses",
|
|
2038
|
+
idField: "emailAddressId",
|
|
2039
|
+
rowNoun: "email-address"
|
|
1846
2040
|
});
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1851
|
-
party: { emailAddresses: [{ id: emailAddressId, _delete: true }] }
|
|
1852
|
-
}),
|
|
1853
|
-
(result) => ({
|
|
1854
|
-
removed: true,
|
|
1855
|
-
alreadyRemoved: false,
|
|
1856
|
-
partyId,
|
|
1857
|
-
emailAddressId,
|
|
1858
|
-
...result
|
|
1859
|
-
}),
|
|
1860
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, emailAddressId })
|
|
1861
|
-
);
|
|
1862
|
-
}
|
|
1863
|
-
var addPartyPhoneNumberSchema = z7.object({
|
|
2041
|
+
var removePartyEmailAddressByIdSchema = removePartyEmailAddress.schema;
|
|
2042
|
+
var removePartyEmailAddressById = removePartyEmailAddress.handler;
|
|
2043
|
+
var addPartyPhoneNumberSchema = z8.object({
|
|
1864
2044
|
partyId: positiveId,
|
|
1865
|
-
number:
|
|
1866
|
-
type:
|
|
2045
|
+
number: z8.string().min(1),
|
|
2046
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Work', 'Mobile'.")
|
|
1867
2047
|
});
|
|
1868
2048
|
async function addPartyPhoneNumber(input) {
|
|
1869
2049
|
const { partyId, number, type } = input;
|
|
@@ -1873,36 +2053,21 @@ async function addPartyPhoneNumber(input) {
|
|
|
1873
2053
|
party: { phoneNumbers: [item] }
|
|
1874
2054
|
});
|
|
1875
2055
|
}
|
|
1876
|
-
var
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
)
|
|
2056
|
+
var removePartyPhoneNumber = definePartySubResourceRemove({
|
|
2057
|
+
arrayKey: "phoneNumbers",
|
|
2058
|
+
idField: "phoneNumberId",
|
|
2059
|
+
rowNoun: "phone-number"
|
|
1881
2060
|
});
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1886
|
-
party: { phoneNumbers: [{ id: phoneNumberId, _delete: true }] }
|
|
1887
|
-
}),
|
|
1888
|
-
(result) => ({
|
|
1889
|
-
removed: true,
|
|
1890
|
-
alreadyRemoved: false,
|
|
1891
|
-
partyId,
|
|
1892
|
-
phoneNumberId,
|
|
1893
|
-
...result
|
|
1894
|
-
}),
|
|
1895
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, phoneNumberId })
|
|
1896
|
-
);
|
|
1897
|
-
}
|
|
1898
|
-
var addPartyAddressSchema = z7.object({
|
|
2061
|
+
var removePartyPhoneNumberByIdSchema = removePartyPhoneNumber.schema;
|
|
2062
|
+
var removePartyPhoneNumberById = removePartyPhoneNumber.handler;
|
|
2063
|
+
var addPartyAddressSchema = z8.object({
|
|
1899
2064
|
partyId: positiveId,
|
|
1900
|
-
street:
|
|
1901
|
-
city:
|
|
1902
|
-
state:
|
|
1903
|
-
country:
|
|
1904
|
-
zip:
|
|
1905
|
-
type:
|
|
2065
|
+
street: z8.string().optional(),
|
|
2066
|
+
city: z8.string().optional(),
|
|
2067
|
+
state: z8.string().optional(),
|
|
2068
|
+
country: z8.string().optional().describe(CountryDescription),
|
|
2069
|
+
zip: z8.string().optional(),
|
|
2070
|
+
type: z8.string().optional().describe("Free-form label, e.g. 'Office', 'Home'.")
|
|
1906
2071
|
});
|
|
1907
2072
|
async function addPartyAddress(input) {
|
|
1908
2073
|
const { partyId, ...rest } = input;
|
|
@@ -1914,31 +2079,16 @@ async function addPartyAddress(input) {
|
|
|
1914
2079
|
party: { addresses: [item] }
|
|
1915
2080
|
});
|
|
1916
2081
|
}
|
|
1917
|
-
var
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
)
|
|
2082
|
+
var removePartyAddress = definePartySubResourceRemove({
|
|
2083
|
+
arrayKey: "addresses",
|
|
2084
|
+
idField: "addressId",
|
|
2085
|
+
rowNoun: "address"
|
|
1922
2086
|
});
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1927
|
-
party: { addresses: [{ id: addressId, _delete: true }] }
|
|
1928
|
-
}),
|
|
1929
|
-
(result) => ({
|
|
1930
|
-
removed: true,
|
|
1931
|
-
alreadyRemoved: false,
|
|
1932
|
-
partyId,
|
|
1933
|
-
addressId,
|
|
1934
|
-
...result
|
|
1935
|
-
}),
|
|
1936
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, addressId })
|
|
1937
|
-
);
|
|
1938
|
-
}
|
|
1939
|
-
var addPartyWebsiteSchema = z7.object({
|
|
2087
|
+
var removePartyAddressByIdSchema = removePartyAddress.schema;
|
|
2088
|
+
var removePartyAddressById = removePartyAddress.handler;
|
|
2089
|
+
var addPartyWebsiteSchema = z8.object({
|
|
1940
2090
|
partyId: positiveId,
|
|
1941
|
-
address:
|
|
2091
|
+
address: z8.string().min(1).describe(
|
|
1942
2092
|
"The website address. A URL when service='URL', or a handle (e.g. '@acmeco') for social services."
|
|
1943
2093
|
),
|
|
1944
2094
|
service: WebsiteServiceEnum.optional().describe("Defaults to 'URL' if omitted.")
|
|
@@ -1951,58 +2101,41 @@ async function addPartyWebsite(input) {
|
|
|
1951
2101
|
party: { websites: [item] }
|
|
1952
2102
|
});
|
|
1953
2103
|
}
|
|
1954
|
-
var
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
)
|
|
2104
|
+
var removePartyWebsite = definePartySubResourceRemove({
|
|
2105
|
+
arrayKey: "websites",
|
|
2106
|
+
idField: "websiteId",
|
|
2107
|
+
rowNoun: "website"
|
|
1959
2108
|
});
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
return idempotentWithResult(
|
|
1963
|
-
() => capsulePut(`/parties/${partyId}`, {
|
|
1964
|
-
party: { websites: [{ id: websiteId, _delete: true }] }
|
|
1965
|
-
}),
|
|
1966
|
-
(result) => ({
|
|
1967
|
-
removed: true,
|
|
1968
|
-
alreadyRemoved: false,
|
|
1969
|
-
partyId,
|
|
1970
|
-
websiteId,
|
|
1971
|
-
...result
|
|
1972
|
-
}),
|
|
1973
|
-
() => ({ removed: true, alreadyRemoved: true, partyId, websiteId })
|
|
1974
|
-
);
|
|
1975
|
-
}
|
|
2109
|
+
var removePartyWebsiteByIdSchema = removePartyWebsite.schema;
|
|
2110
|
+
var removePartyWebsiteById = removePartyWebsite.handler;
|
|
1976
2111
|
|
|
1977
2112
|
// src/tools/opportunities.ts
|
|
1978
|
-
import { z as
|
|
1979
|
-
var OpportunityValueSchema =
|
|
1980
|
-
amount:
|
|
1981
|
-
currency:
|
|
2113
|
+
import { z as z9 } from "zod";
|
|
2114
|
+
var OpportunityValueSchema = z9.object({
|
|
2115
|
+
amount: z9.number().nonnegative(),
|
|
2116
|
+
currency: z9.string({
|
|
1982
2117
|
error: (iss) => iss.code === "invalid_type" && iss.input === void 0 ? "currency is required when amount is set (3-letter ISO 4217 code, e.g. 'USD', 'EUR', 'GBP')" : void 0
|
|
1983
2118
|
}).length(3).describe(
|
|
1984
2119
|
"ISO 4217 currency code (3 letters), e.g. 'GBP', 'USD', 'EUR'. Required when amount is set."
|
|
1985
2120
|
)
|
|
1986
2121
|
});
|
|
1987
|
-
var searchOpportunitiesSchema =
|
|
1988
|
-
q:
|
|
1989
|
-
embed:
|
|
1990
|
-
|
|
1991
|
-
perPage: z8.number().int().min(1).max(100).optional().default(25)
|
|
2122
|
+
var searchOpportunitiesSchema = z9.object({
|
|
2123
|
+
q: z9.string().optional().describe("Free-text search query"),
|
|
2124
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2125
|
+
...paginationFields
|
|
1992
2126
|
});
|
|
1993
2127
|
async function searchOpportunities(input) {
|
|
1994
2128
|
const path = input.q ? "/opportunities/search" : "/opportunities";
|
|
1995
|
-
|
|
2129
|
+
return capsuleGetList(path, {
|
|
1996
2130
|
q: input.q,
|
|
1997
2131
|
embed: input.embed,
|
|
1998
2132
|
page: input.page,
|
|
1999
2133
|
perPage: input.perPage
|
|
2000
2134
|
});
|
|
2001
|
-
return { ...data, nextPage };
|
|
2002
2135
|
}
|
|
2003
|
-
var getOpportunitySchema =
|
|
2136
|
+
var getOpportunitySchema = z9.object({
|
|
2004
2137
|
id: positiveId,
|
|
2005
|
-
embed:
|
|
2138
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2006
2139
|
});
|
|
2007
2140
|
async function getOpportunity(input) {
|
|
2008
2141
|
const { data } = await capsuleGet(`/opportunities/${input.id}`, {
|
|
@@ -2010,32 +2143,32 @@ async function getOpportunity(input) {
|
|
|
2010
2143
|
});
|
|
2011
2144
|
return data;
|
|
2012
2145
|
}
|
|
2013
|
-
var getOpportunitiesSchema =
|
|
2014
|
-
ids:
|
|
2146
|
+
var getOpportunitiesSchema = z9.object({
|
|
2147
|
+
ids: z9.array(positiveId).min(1).max(50).describe(
|
|
2015
2148
|
"Array of opportunity IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
2016
2149
|
),
|
|
2017
|
-
embed:
|
|
2150
|
+
embed: z9.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2018
2151
|
});
|
|
2019
2152
|
async function getOpportunities(input) {
|
|
2020
2153
|
return chunkedMultiGet("/opportunities", "opportunities", input.ids, { embed: input.embed });
|
|
2021
2154
|
}
|
|
2022
|
-
var createOpportunitySchema =
|
|
2023
|
-
name:
|
|
2155
|
+
var createOpportunitySchema = z9.object({
|
|
2156
|
+
name: z9.string().min(1),
|
|
2024
2157
|
partyId: positiveId.describe("ID of the party this opportunity belongs to"),
|
|
2025
2158
|
milestoneId: positiveId.describe(
|
|
2026
2159
|
"ID of the pipeline milestone to place this opportunity at. The milestone implicitly determines the pipeline \u2014 there is no separate pipelineId parameter. Discover via list_pipelines / list_milestones. NOTE: some Capsule tenants configure **pipeline / milestone-reached automation rules** that mutate `owner` and/or `team` immediately after creation \u2014 e.g. an 'Assign to a Team' action that fires on entry to a specific milestone and has been observed to clear `owner` as an automation side-effect. If you observe a newly-created opp landing with `owner: null` despite passing `ownerId`, the cause is almost certainly a milestone automation on the destination pipeline rather than the connector. Documented workaround: follow `create_opportunity` with an immediate `batch_update_opportunity({items: [{id, ownerId, teamId}]})` carrying both fields \u2014 PUT does not re-fire milestone-reached triggers, so the owner sticks."
|
|
2027
2160
|
),
|
|
2028
|
-
description:
|
|
2161
|
+
description: z9.string().optional(),
|
|
2029
2162
|
value: OpportunityValueSchema.optional(),
|
|
2030
|
-
expectedCloseOn:
|
|
2031
|
-
probability:
|
|
2163
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2164
|
+
probability: z9.number().int().min(0).max(100).optional(),
|
|
2032
2165
|
ownerId: positiveId.optional().describe(
|
|
2033
2166
|
"Assign to user ID. Defaults to the API-token owner when omitted \u2014 note that opportunities do NOT inherit owner from the linked party, even though one might expect it. To clear owner later, call update_opportunity with `ownerId: null`. Discover IDs via list_users. WARNING: tenant pipeline / milestone-reached automation can mutate this field post-create \u2014 see the `milestoneId` description for details and the chained-PUT workaround."
|
|
2034
2167
|
),
|
|
2035
2168
|
teamId: positiveId.optional().describe(
|
|
2036
2169
|
"Assign to team ID (discover via list_teams). Independent from `ownerId` \u2014 setting one does NOT clear the other on create. Three ownership shapes are valid: owner alone, team alone, or owner+team (the owner must be a member of the team; users can belong to multiple teams \u2014 422 'owner is not a member of the team' otherwise)."
|
|
2037
2170
|
),
|
|
2038
|
-
fields:
|
|
2171
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(
|
|
2039
2172
|
fieldsArrayDescriptor("get_opportunity") + " Capsule's POST /opportunities accepts the same `fields[]` shape as PUT (inferred by symmetry with the v1.6.5 wire-trace findings on POST /parties and POST /kases \u2014 the tenant probed had no opportunity custom fields configured, so this is unverified empirically). Setting custom fields on creation removes the create-then-update ritual."
|
|
2040
2173
|
)
|
|
2041
2174
|
});
|
|
@@ -2052,19 +2185,19 @@ async function createOpportunity(input) {
|
|
|
2052
2185
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2053
2186
|
return capsulePost("/opportunities", { opportunity: body });
|
|
2054
2187
|
}
|
|
2055
|
-
var updateOpportunitySchema =
|
|
2188
|
+
var updateOpportunitySchema = z9.object({
|
|
2056
2189
|
id: positiveId,
|
|
2057
|
-
name:
|
|
2190
|
+
name: z9.string().min(1).optional(),
|
|
2058
2191
|
partyId: positiveId.optional().describe(
|
|
2059
2192
|
"Reassign the opportunity to a different primary party. Capsule requires every opportunity to have a party \u2014 passing `null` is rejected with 422 'party is required' (use Capsule's web UI if you need to dissolve the link entirely). Discover ids via search_parties / filter_parties. No defensive read-modify-write needed: this connector verified empirically (v1.6.3 wire-trace) that `party` is a standalone PUT field on /opportunities and does not interact with the asymmetric owner/team semantic from NOTES-ON-CAPSULE-API.md \xA727. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_project.partyId`."
|
|
2060
2193
|
),
|
|
2061
2194
|
milestoneId: positiveId.optional().describe(
|
|
2062
2195
|
"Move the opportunity to this milestone. Side effects depend on the target: closing milestones (Won/Lost) auto-set `closedOn` to today and `probability` to the milestone default (100/0), preserving `lastOpenMilestone` as the previous open stage; moving back to an open milestone clears `closedOn` and re-applies the milestone's default probability (Won/Lost is reversible \u2014 no separate reopen tool). WARNING: Capsule does NOT validate that the new milestone belongs to the opportunity's current pipeline. Passing a milestoneId from a different pipeline silently relocates the opportunity across pipelines, and `lastOpenMilestone` may then reference a milestone in the previous pipeline. Verify against the opportunity's current pipeline (read the opp first, list its pipeline's milestones via list_milestones) before passing a cross-pipeline id. NOTE: changing `milestoneId` can fire **pipeline / milestone-reached automations** that mutate `owner` / `team` on the destination milestone (same shape as `create_opportunity` \u2014 see its `milestoneId` description for the owner-clearing automation caveat). If a milestone-change-and-owner-set in the same call lands with `owner: null`, follow up with a second `update_opportunity` (or `batch_update_opportunity`) carrying both `ownerId` and `teamId` \u2014 milestone-reached triggers only fire on the transition, so a subsequent PUT preserves your values."
|
|
2063
2196
|
),
|
|
2064
|
-
description:
|
|
2197
|
+
description: z9.string().optional(),
|
|
2065
2198
|
value: OpportunityValueSchema.optional(),
|
|
2066
|
-
expectedCloseOn:
|
|
2067
|
-
probability:
|
|
2199
|
+
expectedCloseOn: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
2200
|
+
probability: z9.number().int().min(0).max(100).optional().describe(
|
|
2068
2201
|
"Win probability 0\u2013100. On an open milestone this overrides the milestone's default probability. CANNOT be set in the same call as a closing milestone (Won/Lost) \u2014 Capsule processes the milestone change first, the opportunity becomes closed, then the probability update is rejected as edit-on-closed-opp with 422 'probability can be updated only for open opportunity'. To close an opportunity, leave probability out of the call: it auto-snaps to 100% (Won) or 0% (Lost)."
|
|
2069
2202
|
),
|
|
2070
2203
|
lostReasonId: positiveId.optional().describe(
|
|
@@ -2076,7 +2209,7 @@ var updateOpportunitySchema = z8.object({
|
|
|
2076
2209
|
teamId: positiveId.nullable().optional().describe(
|
|
2077
2210
|
"Reassign team: pass a team ID (discover via list_teams) to set, or `null` to unassign. Capsule preserves the existing owner across a team change (server-side), so `update_opportunity { teamId }` alone is safe \u2014 the owner is carried through. Owner must be a member of the new team or Capsule returns 422 'owner is not a member of the team'. Independent from `ownerId` \u2014 setting `teamId` does NOT clear the owner."
|
|
2078
2211
|
),
|
|
2079
|
-
fields:
|
|
2212
|
+
fields: z9.array(CustomFieldWriteSchema).optional().describe(fieldsArrayDescriptor("get_opportunity"))
|
|
2080
2213
|
});
|
|
2081
2214
|
async function updateOpportunity(input) {
|
|
2082
2215
|
const { id, partyId, milestoneId, ownerId, teamId, lostReasonId, fields, ...rest } = input;
|
|
@@ -2112,25 +2245,23 @@ var { schema: deleteOpportunitySchema, handler: deleteOpportunity } = defineDele
|
|
|
2112
2245
|
});
|
|
2113
2246
|
|
|
2114
2247
|
// src/tools/projects.ts
|
|
2115
|
-
import { z as
|
|
2116
|
-
var listProjectsSchema =
|
|
2117
|
-
status:
|
|
2118
|
-
embed:
|
|
2119
|
-
|
|
2120
|
-
perPage: z9.number().int().min(1).max(100).optional().default(25)
|
|
2248
|
+
import { z as z10 } from "zod";
|
|
2249
|
+
var listProjectsSchema = z10.object({
|
|
2250
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional(),
|
|
2251
|
+
embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2252
|
+
...paginationFields
|
|
2121
2253
|
});
|
|
2122
2254
|
async function listProjects(input) {
|
|
2123
|
-
|
|
2255
|
+
return capsuleGetList("/kases", {
|
|
2124
2256
|
status: input.status,
|
|
2125
2257
|
embed: input.embed,
|
|
2126
2258
|
page: input.page,
|
|
2127
2259
|
perPage: input.perPage
|
|
2128
2260
|
});
|
|
2129
|
-
return { ...data, nextPage };
|
|
2130
2261
|
}
|
|
2131
|
-
var getProjectSchema =
|
|
2262
|
+
var getProjectSchema = z10.object({
|
|
2132
2263
|
id: positiveId,
|
|
2133
|
-
embed:
|
|
2264
|
+
embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2134
2265
|
});
|
|
2135
2266
|
async function getProject(input) {
|
|
2136
2267
|
const { data } = await capsuleGet(`/kases/${input.id}`, {
|
|
@@ -2138,20 +2269,20 @@ async function getProject(input) {
|
|
|
2138
2269
|
});
|
|
2139
2270
|
return data;
|
|
2140
2271
|
}
|
|
2141
|
-
var getProjectsSchema =
|
|
2142
|
-
ids:
|
|
2272
|
+
var getProjectsSchema = z10.object({
|
|
2273
|
+
ids: z10.array(positiveId).min(1).max(50).describe(
|
|
2143
2274
|
"Array of project IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
2144
2275
|
),
|
|
2145
|
-
embed:
|
|
2276
|
+
embed: z10.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2146
2277
|
});
|
|
2147
2278
|
async function getProjects(input) {
|
|
2148
2279
|
return chunkedMultiGet("/kases", "kases", input.ids, { embed: input.embed });
|
|
2149
2280
|
}
|
|
2150
|
-
var createProjectSchema =
|
|
2151
|
-
name:
|
|
2281
|
+
var createProjectSchema = z10.object({
|
|
2282
|
+
name: z10.string().min(1),
|
|
2152
2283
|
partyId: positiveId.describe("ID of the party linked to this project"),
|
|
2153
|
-
description:
|
|
2154
|
-
status:
|
|
2284
|
+
description: z10.string().optional(),
|
|
2285
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional().describe("Defaults to OPEN when omitted."),
|
|
2155
2286
|
ownerId: positiveId.optional().describe(
|
|
2156
2287
|
"Assign to user ID. Defaults to the API-token owner when omitted, same as create_party / create_opportunity / create_task. NOTE: some Capsule tenants configure board-level **automation rules** that mutate `owner` (and `team`) on project creation \u2014 e.g. an automation that clears `owner` when a project enters a particular board. If you observe a project landing with unexpected `owner: null` after a create_project with `ownerId`, check the target board's automation configuration. Capsule's API itself does not drop `ownerId` when `stageId` is also supplied."
|
|
2157
2288
|
),
|
|
@@ -2161,8 +2292,8 @@ var createProjectSchema = z9.object({
|
|
|
2161
2292
|
stageId: positiveId.optional().describe(
|
|
2162
2293
|
"Stage (board column) to place the project on. Discover IDs via list_stages \u2014 each stage belongs to one Board, so picking a stageId implicitly picks the board. If omitted, the project is created with no stage assignment (and won't appear on any board). NOTE: tenant-specific board automation rules may run on project creation and mutate `owner` / `team` fields. See `create_project.ownerId` / `create_project.teamId` for the automation caveat. Capsule's create endpoint itself preserves the `ownerId` / `teamId` you supply \u2014 any clearing you observe traces to board automations, not the API."
|
|
2163
2294
|
),
|
|
2164
|
-
expectedCloseOn:
|
|
2165
|
-
fields:
|
|
2295
|
+
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2296
|
+
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2166
2297
|
fieldsArrayDescriptor("get_project") + " Verified empirically in v1.6.5 wire-trace: Capsule's POST /kases accepts the same `fields[]` shape as PUT, so callers can set custom field values on creation without a follow-up update. Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
2167
2298
|
)
|
|
2168
2299
|
});
|
|
@@ -2180,11 +2311,11 @@ async function createProject(input) {
|
|
|
2180
2311
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
2181
2312
|
return capsulePost("/kases", { kase: body });
|
|
2182
2313
|
}
|
|
2183
|
-
var updateProjectSchema =
|
|
2314
|
+
var updateProjectSchema = z10.object({
|
|
2184
2315
|
id: positiveId,
|
|
2185
|
-
name:
|
|
2186
|
-
description:
|
|
2187
|
-
status:
|
|
2316
|
+
name: z10.string().min(1).optional(),
|
|
2317
|
+
description: z10.string().optional(),
|
|
2318
|
+
status: z10.enum(["OPEN", "CLOSED"]).optional(),
|
|
2188
2319
|
partyId: positiveId.optional().describe(
|
|
2189
2320
|
"Reassign the project to a different primary party. Capsule requires every project to have a party \u2014 passing `null` is rejected with 422 'party is required' (verified empirically in v1.6.3 wire-trace). Discover ids via search_parties / filter_parties. NOTE: parent-ref nullability differs by entity \u2014 `update_task.partyId` IS nullable (orphan task), but opportunities and projects must always have a parent party. The same applies to `update_opportunity.partyId`."
|
|
2190
2321
|
),
|
|
@@ -2197,8 +2328,8 @@ var updateProjectSchema = z9.object({
|
|
|
2197
2328
|
stageId: positiveId.nullable().optional().describe(
|
|
2198
2329
|
"Move the project to this stage (board column), or `null` to remove from all stages (verified empirically in v1.6.5 wire-trace \u2014 Capsule accepts `stage: null` on PUT /kases/:id and the project no longer appears on any board). Discover IDs via list_stages. Owner and team are preserved across stage-only updates (Capsule's PUT semantic). WARNING (cross-board): Capsule does NOT validate that the new stage belongs to the project's current board \u2014 passing a stageId from a different board silently relocates the project across boards. Team and other board-derived defaults are NOT updated to match the new board. Verify against the project's current board (read the project first, list its board's stages) before passing a cross-board id."
|
|
2199
2330
|
),
|
|
2200
|
-
expectedCloseOn:
|
|
2201
|
-
fields:
|
|
2331
|
+
expectedCloseOn: z10.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2332
|
+
fields: z10.array(CustomFieldWriteSchema).optional().describe(
|
|
2202
2333
|
fieldsArrayDescriptor("get_project") + " Project-specific: setting a field whose definition lives under a 'data tag' populates the row's internal tagId but does NOT auto-add the data tag to the project's tags array \u2014 use add_tag explicitly if you want it visible via embed=tags."
|
|
2203
2334
|
)
|
|
2204
2335
|
});
|
|
@@ -2237,23 +2368,22 @@ var { schema: deleteProjectSchema, handler: deleteProject } = defineDelete({
|
|
|
2237
2368
|
});
|
|
2238
2369
|
|
|
2239
2370
|
// src/tools/tasks.ts
|
|
2240
|
-
import { z as
|
|
2241
|
-
var listTasksSchema =
|
|
2371
|
+
import { z as z11 } from "zod";
|
|
2372
|
+
var listTasksSchema = z11.object({
|
|
2242
2373
|
// Note: Capsule has a third internal status `PENDING` (a task that's
|
|
2243
2374
|
// part of an active track but not yet "open"), but it can only be
|
|
2244
2375
|
// reached via track machinery — it is NOT directly settable by
|
|
2245
2376
|
// /tasks PUT, and a list filter for it returns the same as OPEN
|
|
2246
2377
|
// anyway. We expose only the two values that are actually filterable
|
|
2247
2378
|
// by the v2 API.
|
|
2248
|
-
status:
|
|
2379
|
+
status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
2249
2380
|
"Defaults to OPEN when omitted. Pass COMPLETED to filter to completed tasks, or 'OPEN' explicitly."
|
|
2250
2381
|
),
|
|
2251
2382
|
ownerId: positiveId.optional().describe("Filter to tasks owned by this user ID"),
|
|
2252
|
-
|
|
2253
|
-
perPage: z10.number().int().min(1).max(100).optional().default(25)
|
|
2383
|
+
...paginationFields
|
|
2254
2384
|
});
|
|
2255
2385
|
async function listTasks(input) {
|
|
2256
|
-
|
|
2386
|
+
return capsuleGetList("/tasks", {
|
|
2257
2387
|
// Default 'OPEN' applied here (not via zod .default()) so that
|
|
2258
2388
|
// z.infer keeps `status` optional for callers that omit it.
|
|
2259
2389
|
status: input.status ?? "OPEN",
|
|
@@ -2262,28 +2392,27 @@ async function listTasks(input) {
|
|
|
2262
2392
|
page: input.page,
|
|
2263
2393
|
perPage: input.perPage
|
|
2264
2394
|
});
|
|
2265
|
-
return { ...data, nextPage };
|
|
2266
2395
|
}
|
|
2267
|
-
var getTaskSchema =
|
|
2396
|
+
var getTaskSchema = z11.object({
|
|
2268
2397
|
id: positiveId.describe("Task ID")
|
|
2269
2398
|
});
|
|
2270
2399
|
async function getTask(input) {
|
|
2271
2400
|
const { data } = await capsuleGet(`/tasks/${input.id}`);
|
|
2272
2401
|
return data;
|
|
2273
2402
|
}
|
|
2274
|
-
var getTasksSchema =
|
|
2275
|
-
ids:
|
|
2403
|
+
var getTasksSchema = z11.object({
|
|
2404
|
+
ids: z11.array(positiveId).min(1).max(50).describe(
|
|
2276
2405
|
"Array of task IDs (1\u201350). Capsule's native batch-fetch endpoint caps at 10 per request; the connector transparently splits larger sets into 10-id chunks and fans out the Capsule calls in parallel."
|
|
2277
2406
|
)
|
|
2278
2407
|
});
|
|
2279
2408
|
async function getTasks(input) {
|
|
2280
2409
|
return chunkedMultiGet("/tasks", "tasks", input.ids);
|
|
2281
2410
|
}
|
|
2282
|
-
var createTaskSchema =
|
|
2283
|
-
description:
|
|
2284
|
-
dueOn:
|
|
2285
|
-
dueTime:
|
|
2286
|
-
detail:
|
|
2411
|
+
var createTaskSchema = z11.object({
|
|
2412
|
+
description: z11.string().min(1),
|
|
2413
|
+
dueOn: z11.string().regex(/^\d{4}-\d{2}-\d{2}$/).describe("YYYY-MM-DD"),
|
|
2414
|
+
dueTime: z11.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
2415
|
+
detail: z11.string().optional(),
|
|
2287
2416
|
ownerId: positiveId.optional().describe(
|
|
2288
2417
|
"Assign to user ID. Defaults to the API-token owner when omitted. Once set, this connector cannot clear the owner back to null \u2014 use Capsule's web UI for that."
|
|
2289
2418
|
),
|
|
@@ -2292,10 +2421,7 @@ var createTaskSchema = z10.object({
|
|
|
2292
2421
|
projectId: positiveId.optional().describe("Link task to a project (mutually exclusive with partyId/opportunityId)")
|
|
2293
2422
|
});
|
|
2294
2423
|
async function createTask(input) {
|
|
2295
|
-
|
|
2296
|
-
if (linked.length > 1) {
|
|
2297
|
-
throw new Error("Provide at most one of partyId, opportunityId, or projectId");
|
|
2298
|
-
}
|
|
2424
|
+
assertSingleParentRef("create_task", input);
|
|
2299
2425
|
const { ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
2300
2426
|
const body = { ...rest };
|
|
2301
2427
|
setRef(body, "owner", ownerId);
|
|
@@ -2304,16 +2430,16 @@ async function createTask(input) {
|
|
|
2304
2430
|
setRef(body, "kase", projectId);
|
|
2305
2431
|
return capsulePost("/tasks", { task: body });
|
|
2306
2432
|
}
|
|
2307
|
-
var updateTaskSchema =
|
|
2433
|
+
var updateTaskSchema = z11.object({
|
|
2308
2434
|
id: positiveId,
|
|
2309
|
-
description:
|
|
2310
|
-
dueOn:
|
|
2311
|
-
dueTime:
|
|
2312
|
-
detail:
|
|
2435
|
+
description: z11.string().min(1).optional(),
|
|
2436
|
+
dueOn: z11.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe("YYYY-MM-DD"),
|
|
2437
|
+
dueTime: z11.string().regex(/^\d{2}:\d{2}$/).optional().describe("HH:MM in user's timezone"),
|
|
2438
|
+
detail: z11.string().optional(),
|
|
2313
2439
|
// Capsule rejects direct sets of `PENDING` (which is a track-machinery
|
|
2314
2440
|
// internal state) with 422 "cannot set task status to PENDING".
|
|
2315
2441
|
// Only OPEN and COMPLETED are settable here.
|
|
2316
|
-
status:
|
|
2442
|
+
status: z11.enum(["OPEN", "COMPLETED"]).optional().describe(
|
|
2317
2443
|
"Set to OPEN or COMPLETED. (PENDING exists internally for track-driven tasks but cannot be set directly via this tool \u2014 Capsule rejects it.) Setting status: OPEN on an already-open task is a true no-op (does not advance updatedAt)."
|
|
2318
2444
|
),
|
|
2319
2445
|
ownerId: positiveId.optional().describe(
|
|
@@ -2331,12 +2457,7 @@ var updateTaskSchema = z10.object({
|
|
|
2331
2457
|
});
|
|
2332
2458
|
async function updateTask(input) {
|
|
2333
2459
|
const { id, ownerId, partyId, opportunityId, projectId, ...rest } = input;
|
|
2334
|
-
|
|
2335
|
-
if (setCount > 1) {
|
|
2336
|
-
throw new Error(
|
|
2337
|
-
"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')"
|
|
2338
|
-
);
|
|
2339
|
-
}
|
|
2460
|
+
assertSingleParentRef("update_task", { partyId, opportunityId, projectId });
|
|
2340
2461
|
const body = {};
|
|
2341
2462
|
for (const [k, v] of Object.entries(rest)) {
|
|
2342
2463
|
if (v !== void 0) body[k] = v;
|
|
@@ -2347,7 +2468,7 @@ async function updateTask(input) {
|
|
|
2347
2468
|
setNullableRef(body, "kase", projectId);
|
|
2348
2469
|
return capsulePut(`/tasks/${id}`, { task: body });
|
|
2349
2470
|
}
|
|
2350
|
-
var completeTaskSchema =
|
|
2471
|
+
var completeTaskSchema = z11.object({
|
|
2351
2472
|
id: positiveId
|
|
2352
2473
|
});
|
|
2353
2474
|
async function completeTask(input) {
|
|
@@ -2355,8 +2476,8 @@ async function completeTask(input) {
|
|
|
2355
2476
|
task: { status: "COMPLETED" }
|
|
2356
2477
|
});
|
|
2357
2478
|
}
|
|
2358
|
-
var batchCompleteTaskSchema =
|
|
2359
|
-
ids:
|
|
2479
|
+
var batchCompleteTaskSchema = z11.object({
|
|
2480
|
+
ids: z11.array(positiveId).min(1).max(50).describe(
|
|
2360
2481
|
"Array of 1\u201350 task ids to mark COMPLETED in parallel. Each id resolves to one PUT /tasks/{id}; failures (e.g. 404 for a deleted task) surface per-item in the result array, the rest still complete. Capped at 50."
|
|
2361
2482
|
)
|
|
2362
2483
|
});
|
|
@@ -2370,77 +2491,59 @@ var { schema: deleteTaskSchema, handler: deleteTask } = defineDelete({
|
|
|
2370
2491
|
});
|
|
2371
2492
|
|
|
2372
2493
|
// src/tools/entries.ts
|
|
2373
|
-
import { z as
|
|
2494
|
+
import { z as z12 } from "zod";
|
|
2374
2495
|
var listEntriesPagination = {
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
embed: z11.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2496
|
+
...paginationFields,
|
|
2497
|
+
embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2378
2498
|
};
|
|
2379
|
-
var listPartyEntriesSchema =
|
|
2499
|
+
var listPartyEntriesSchema = z12.object({
|
|
2380
2500
|
partyId: positiveId,
|
|
2381
2501
|
...listEntriesPagination,
|
|
2382
|
-
includeLinkedPersons:
|
|
2502
|
+
includeLinkedPersons: z12.boolean().optional().describe(
|
|
2383
2503
|
"When true AND `partyId` is an ORGANISATION, also include entries filed against the organisation's linked people (the persons whose `organisation` field references this org). The connector enumerates linked persons via `GET /parties/{orgId}/people`, fans out `GET /parties/{personId}/entries` in parallel (concurrency-capped, default 5 / configurable via `CAPSULE_MCP_BATCH_CONCURRENCY`), and merges into a single feed sorted by `entryAt` descending, deduped by entry id. Default is `false` \u2014 single GET, existing behaviour unchanged. WHY THIS FLAG EXISTS: Capsule's API files each entry against exactly one party row (verified v1.6.6 wire-trace probe 4 \u2014 POST /entries rejects multi-party bodies with 422 'entry must be linked to either a party, opportunity or kase'). For an organisation with multiple contacts, captured emails almost always land on a person row, not the org. As a result, `list_party_entries(orgId)` with `includeLinkedPersons: false` will miss recent customer-facing email \u2014 even though the org's own `lastContactedAt` is updated by the activity. This flag is the correct call for any 'what's new with $ORG?' question. WHEN `partyId` IS A PERSON: silently no-op \u2014 persons have no linked-people relationship in Capsule's data model, so the flag is functionally inert (the connector still issues a cheap `/people` check; the response is empty). LATENCY: 1 + N round trips for an org with N linked people, concurrency-capped (typical: 2-3 waves for N=10). Linked-person enumeration reads the first 100 linked people; use list_employees for explicit pagination when an organisation has more contacts than that. Use `includeLinkedPersons: false` for fast pre-screen reads where you only need the org-row entries (e.g. invoice/contract notes that are typically filed at the org level). PAGINATION CAVEAT: `page` and `perPage` apply to the MERGED window, and the merge has a hard ceiling \u2014 it reliably orders only the most-recent ~100 entries across the org + its people (each party is fetched at Capsule's per-party cap of 100, and a top-100-per-party merge is correct only up to global position 100). Windows that cross the ceiling are truncated to the entries still inside that top-100 set; windows starting beyond it return no entries and end the feed. It does NOT continue into older history. To read a specific contact's full timeline beyond the merged ceiling, call `list_party_entries` on that person's id directly (the default single-GET path paginates natively with no ceiling). For the LLM-driven 'what's the latest with $ORG' query this is the typical use of, the first page is exact and the ceiling is never reached."
|
|
2384
2504
|
)
|
|
2385
2505
|
});
|
|
2506
|
+
var PER_PARTY_FETCH_CAP = 100;
|
|
2386
2507
|
async function fanOutPartyEntries(partyIds, embed, perPage) {
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
const id = partyIds[i];
|
|
2396
|
-
const { data, nextPage } = await capsuleGet(
|
|
2397
|
-
`/parties/${id}/entries`,
|
|
2398
|
-
{
|
|
2399
|
-
embed,
|
|
2400
|
-
page: 1,
|
|
2401
|
-
perPage
|
|
2402
|
-
}
|
|
2403
|
-
);
|
|
2404
|
-
results[i] = { entries: data.entries, nextPage };
|
|
2405
|
-
}
|
|
2406
|
-
}
|
|
2407
|
-
const workers = [];
|
|
2408
|
-
for (let w = 0; w < Math.min(concurrency, partyIds.length); w++) {
|
|
2409
|
-
workers.push(worker());
|
|
2410
|
-
}
|
|
2411
|
-
await Promise.all(workers);
|
|
2412
|
-
return results;
|
|
2508
|
+
return mapWithConcurrency(partyIds, getBatchConcurrency(), async (id) => {
|
|
2509
|
+
const { data, nextPage } = await capsuleGet(`/parties/${id}/entries`, {
|
|
2510
|
+
embed,
|
|
2511
|
+
page: 1,
|
|
2512
|
+
perPage
|
|
2513
|
+
});
|
|
2514
|
+
return { entries: data.entries, nextPage };
|
|
2515
|
+
});
|
|
2413
2516
|
}
|
|
2414
2517
|
function mergedTimelineCandidatePerParty(page, perPage) {
|
|
2415
|
-
return Math.min(page * perPage,
|
|
2518
|
+
return Math.min(page * perPage, PER_PARTY_FETCH_CAP);
|
|
2416
2519
|
}
|
|
2417
2520
|
function mergedTimelineNextPage(page, perPage, mergedLength, upstreamHasNextPage) {
|
|
2418
2521
|
const requestedWindowEnd = page * perPage;
|
|
2419
2522
|
if (mergedLength > requestedWindowEnd) return page + 1;
|
|
2420
|
-
const nextWindowWithinCap = requestedWindowEnd <
|
|
2523
|
+
const nextWindowWithinCap = requestedWindowEnd < PER_PARTY_FETCH_CAP;
|
|
2421
2524
|
if (nextWindowWithinCap && upstreamHasNextPage) return page + 1;
|
|
2422
2525
|
return void 0;
|
|
2423
2526
|
}
|
|
2424
2527
|
async function listPartyEntries(input) {
|
|
2425
2528
|
const { partyId, embed, page, perPage, includeLinkedPersons } = input;
|
|
2426
2529
|
if (!includeLinkedPersons) {
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2530
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2531
|
+
embed,
|
|
2532
|
+
page,
|
|
2533
|
+
perPage
|
|
2534
|
+
});
|
|
2432
2535
|
}
|
|
2433
2536
|
const { data: peopleData } = await capsuleGet(
|
|
2434
2537
|
`/parties/${partyId}/people`,
|
|
2435
|
-
{ page: 1, perPage:
|
|
2538
|
+
{ page: 1, perPage: PER_PARTY_FETCH_CAP }
|
|
2436
2539
|
);
|
|
2437
2540
|
const peopleIds = (peopleData.parties ?? []).map((p) => p.id);
|
|
2438
2541
|
if (peopleIds.length === 0) {
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2542
|
+
return capsuleGetList(`/parties/${partyId}/entries`, {
|
|
2543
|
+
embed,
|
|
2544
|
+
page,
|
|
2545
|
+
perPage
|
|
2546
|
+
});
|
|
2444
2547
|
}
|
|
2445
2548
|
const targetIds = [partyId, ...peopleIds];
|
|
2446
2549
|
const perPartyPages = await fanOutPartyEntries(
|
|
@@ -2475,31 +2578,31 @@ async function listPartyEntries(input) {
|
|
|
2475
2578
|
);
|
|
2476
2579
|
return { entries: slice, ...nextPage !== void 0 ? { nextPage } : {} };
|
|
2477
2580
|
}
|
|
2478
|
-
var listOpportunityEntriesSchema =
|
|
2581
|
+
var listOpportunityEntriesSchema = z12.object({
|
|
2479
2582
|
opportunityId: positiveId,
|
|
2480
2583
|
...listEntriesPagination
|
|
2481
2584
|
});
|
|
2482
2585
|
async function listOpportunityEntries(input) {
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2586
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/entries`, {
|
|
2587
|
+
embed: input.embed,
|
|
2588
|
+
page: input.page,
|
|
2589
|
+
perPage: input.perPage
|
|
2590
|
+
});
|
|
2488
2591
|
}
|
|
2489
|
-
var listProjectEntriesSchema =
|
|
2592
|
+
var listProjectEntriesSchema = z12.object({
|
|
2490
2593
|
projectId: positiveId,
|
|
2491
2594
|
...listEntriesPagination
|
|
2492
2595
|
});
|
|
2493
2596
|
async function listProjectEntries(input) {
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2597
|
+
return capsuleGetList(`/kases/${input.projectId}/entries`, {
|
|
2598
|
+
embed: input.embed,
|
|
2599
|
+
page: input.page,
|
|
2600
|
+
perPage: input.perPage
|
|
2601
|
+
});
|
|
2499
2602
|
}
|
|
2500
|
-
var getEntrySchema =
|
|
2603
|
+
var getEntrySchema = z12.object({
|
|
2501
2604
|
id: positiveId,
|
|
2502
|
-
embed:
|
|
2605
|
+
embed: z12.string().optional().describe(EMBED_ATTACHMENTS_PARTICIPANTS_DESCRIPTION)
|
|
2503
2606
|
});
|
|
2504
2607
|
async function getEntry(input) {
|
|
2505
2608
|
const { data } = await capsuleGet(`/entries/${input.id}`, {
|
|
@@ -2507,34 +2610,30 @@ async function getEntry(input) {
|
|
|
2507
2610
|
});
|
|
2508
2611
|
return data;
|
|
2509
2612
|
}
|
|
2510
|
-
var listEntriesSchema =
|
|
2613
|
+
var listEntriesSchema = z12.object({
|
|
2511
2614
|
...listEntriesPagination
|
|
2512
2615
|
});
|
|
2513
2616
|
async function listEntries(input) {
|
|
2514
|
-
|
|
2617
|
+
return capsuleGetList("/entries", {
|
|
2515
2618
|
embed: input.embed,
|
|
2516
2619
|
page: input.page,
|
|
2517
2620
|
perPage: input.perPage
|
|
2518
2621
|
});
|
|
2519
|
-
return { ...data, nextPage };
|
|
2520
2622
|
}
|
|
2521
|
-
var addNoteSchema =
|
|
2522
|
-
content:
|
|
2623
|
+
var addNoteSchema = z12.object({
|
|
2624
|
+
content: z12.string().min(1).describe(
|
|
2523
2625
|
"Note body text. Stored verbatim and treated as MARKDOWN \u2014 Capsule's web UI renders the markdown when displaying. Pass markdown source ('# Heading', '**bold**', '- bullet'), not HTML."
|
|
2524
2626
|
),
|
|
2525
2627
|
partyId: positiveId.optional().describe("Link note to a party (mutually exclusive with opportunityId/projectId)"),
|
|
2526
2628
|
opportunityId: positiveId.optional().describe("Link note to an opportunity (mutually exclusive with partyId/projectId)"),
|
|
2527
2629
|
projectId: positiveId.optional().describe("Link note to a project (mutually exclusive with partyId/opportunityId)"),
|
|
2528
|
-
entryAt:
|
|
2630
|
+
entryAt: z12.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})$/).optional().describe(
|
|
2529
2631
|
"ISO-8601 timestamp for when this note actually happened (e.g. '2024-03-15T14:30:00Z'). Defaults to now. Use this for backdating historical notes when migrating from another system. `entryAt` is preserved across subsequent update_entry calls; only `updatedAt` advances on edits. Note attribution flows to the API-token owner \u2014 there is no way to record a note as authored by a different user via this connector (a `creatorId` parameter would enable audit-attribution spoofing on shared-connector deployments, so it is intentionally not exposed)."
|
|
2530
2632
|
)
|
|
2531
2633
|
});
|
|
2532
2634
|
async function addNote(input) {
|
|
2533
2635
|
const { content, partyId, opportunityId, projectId, entryAt } = input;
|
|
2534
|
-
|
|
2535
|
-
if (linked.length !== 1) {
|
|
2536
|
-
throw new Error("Provide exactly one of partyId, opportunityId, or projectId");
|
|
2537
|
-
}
|
|
2636
|
+
assertSingleParentRef("add_note", input, { required: true });
|
|
2538
2637
|
const body = { type: "note", content };
|
|
2539
2638
|
setRef(body, "party", partyId);
|
|
2540
2639
|
setRef(body, "opportunity", opportunityId);
|
|
@@ -2542,12 +2641,12 @@ async function addNote(input) {
|
|
|
2542
2641
|
if (entryAt !== void 0) body["entryAt"] = entryAt;
|
|
2543
2642
|
return capsulePost("/entries", { entry: body });
|
|
2544
2643
|
}
|
|
2545
|
-
var updateEntrySchema =
|
|
2644
|
+
var updateEntrySchema = z12.object({
|
|
2546
2645
|
id: positiveId.describe("Entry ID to update"),
|
|
2547
|
-
content:
|
|
2646
|
+
content: z12.string().min(1).optional().describe(
|
|
2548
2647
|
"New body text for the entry. For notes, this is the markdown content; for emails, the body. Provide only if you want to change it."
|
|
2549
2648
|
),
|
|
2550
|
-
subject:
|
|
2649
|
+
subject: z12.string().optional().describe(
|
|
2551
2650
|
"New subject line. Mostly meaningful on email-type entries; on plain notes Capsule accepts the call (HTTP 200) but **does not store the subject and does not advance `updatedAt`** \u2014 a true no-op for inapplicable fields. `entryAt` (when the note was authored) is preserved across edits; `updatedAt` advances only when an applicable field actually changes. To sort/filter by 'when did this happen', use `entryAt`; for 'last touched', use `updatedAt`."
|
|
2552
2651
|
)
|
|
2553
2652
|
});
|
|
@@ -2569,62 +2668,50 @@ var { schema: deleteEntrySchema, handler: deleteEntry } = defineDelete({
|
|
|
2569
2668
|
});
|
|
2570
2669
|
|
|
2571
2670
|
// src/tools/pipelines.ts
|
|
2572
|
-
import { z as
|
|
2573
|
-
var
|
|
2574
|
-
page: z12.number().int().positive().optional(),
|
|
2575
|
-
perPage: z12.number().int().min(1).max(100).optional()
|
|
2576
|
-
};
|
|
2577
|
-
var listPipelinesSchema = z12.object({ ...paginationFields });
|
|
2671
|
+
import { z as z13 } from "zod";
|
|
2672
|
+
var listPipelinesSchema = z13.object({ ...paginationFieldsNoDefaults });
|
|
2578
2673
|
async function listPipelines(input) {
|
|
2579
|
-
|
|
2674
|
+
return capsuleGetCachedList("/pipelines", {
|
|
2580
2675
|
page: input.page ?? 1,
|
|
2581
2676
|
perPage: input.perPage ?? 100
|
|
2582
2677
|
});
|
|
2583
|
-
return { ...data, nextPage };
|
|
2584
2678
|
}
|
|
2585
|
-
var listMilestonesSchema =
|
|
2679
|
+
var listMilestonesSchema = z13.object({
|
|
2586
2680
|
pipelineId: positiveId,
|
|
2587
|
-
...
|
|
2681
|
+
...paginationFieldsNoDefaults
|
|
2588
2682
|
});
|
|
2589
2683
|
async function listMilestones(input) {
|
|
2590
|
-
|
|
2684
|
+
return capsuleGetCachedList(
|
|
2591
2685
|
`/pipelines/${input.pipelineId}/milestones`,
|
|
2592
2686
|
{ page: input.page ?? 1, perPage: input.perPage ?? 100 }
|
|
2593
2687
|
);
|
|
2594
|
-
return { ...data, nextPage };
|
|
2595
2688
|
}
|
|
2596
2689
|
|
|
2597
2690
|
// src/tools/boards.ts
|
|
2598
|
-
import { z as
|
|
2599
|
-
var
|
|
2600
|
-
page: z13.number().int().positive().optional(),
|
|
2601
|
-
perPage: z13.number().int().min(1).max(100).optional()
|
|
2602
|
-
};
|
|
2603
|
-
var listBoardsSchema = z13.object({ ...paginationFields2 });
|
|
2691
|
+
import { z as z14 } from "zod";
|
|
2692
|
+
var listBoardsSchema = z14.object({ ...paginationFieldsNoDefaults });
|
|
2604
2693
|
async function listBoards(input) {
|
|
2605
|
-
|
|
2694
|
+
return capsuleGetCachedList("/boards", {
|
|
2606
2695
|
page: input.page ?? 1,
|
|
2607
2696
|
perPage: input.perPage ?? 100
|
|
2608
2697
|
});
|
|
2609
|
-
return { ...data, nextPage };
|
|
2610
2698
|
}
|
|
2611
|
-
var listStagesSchema =
|
|
2699
|
+
var listStagesSchema = z14.object({
|
|
2612
2700
|
boardId: positiveId.optional().describe(
|
|
2613
2701
|
"Optional. If provided, returns only the stages defined on that specific board (uses /boards/{id}/stages). Omit to get all stages across all boards in one call."
|
|
2614
2702
|
),
|
|
2615
|
-
...
|
|
2703
|
+
...paginationFieldsNoDefaults
|
|
2616
2704
|
});
|
|
2617
2705
|
async function listStages(input) {
|
|
2618
2706
|
const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
|
|
2619
|
-
|
|
2707
|
+
return capsuleGetCachedList(path, {
|
|
2620
2708
|
page: input.page ?? 1,
|
|
2621
2709
|
perPage: input.perPage ?? 100
|
|
2622
2710
|
});
|
|
2623
|
-
return { ...data, nextPage };
|
|
2624
2711
|
}
|
|
2625
2712
|
|
|
2626
2713
|
// src/tools/tags.ts
|
|
2627
|
-
import { z as
|
|
2714
|
+
import { z as z15 } from "zod";
|
|
2628
2715
|
var TAG_LIST_PATH = {
|
|
2629
2716
|
parties: "/parties/tags",
|
|
2630
2717
|
opportunities: "/opportunities/tags",
|
|
@@ -2635,24 +2722,22 @@ var ENTITY_TO_WRAPPER = {
|
|
|
2635
2722
|
opportunities: "opportunity",
|
|
2636
2723
|
kases: "kase"
|
|
2637
2724
|
};
|
|
2638
|
-
var TagEntity =
|
|
2639
|
-
var listTagsSchema =
|
|
2640
|
-
entity:
|
|
2641
|
-
|
|
2642
|
-
perPage: z14.number().int().min(1).max(100).optional()
|
|
2725
|
+
var TagEntity = z15.enum(["parties", "opportunities", "kases"]).describe("Which entity type. Use 'kases' for projects (Capsule's legacy path name).");
|
|
2726
|
+
var listTagsSchema = z15.object({
|
|
2727
|
+
entity: z15.enum(["parties", "opportunities", "kases"]).describe("The resource type to list tags for"),
|
|
2728
|
+
...paginationFieldsNoDefaults
|
|
2643
2729
|
});
|
|
2644
2730
|
async function listTags(input) {
|
|
2645
2731
|
const path = TAG_LIST_PATH[input.entity];
|
|
2646
|
-
|
|
2732
|
+
return capsuleGetCachedList(path, {
|
|
2647
2733
|
page: input.page ?? 1,
|
|
2648
2734
|
perPage: input.perPage ?? 100
|
|
2649
2735
|
});
|
|
2650
|
-
return { ...data, nextPage };
|
|
2651
2736
|
}
|
|
2652
|
-
var addTagSchema =
|
|
2737
|
+
var addTagSchema = z15.object({
|
|
2653
2738
|
entity: TagEntity,
|
|
2654
2739
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2655
|
-
tagName:
|
|
2740
|
+
tagName: z15.string().min(1).describe(
|
|
2656
2741
|
"Name of the tag to attach. Capsule resolves by name: if a tag with this name already exists in the tenant it is attached to the entity; if not, Capsule creates the tag and attaches it. Names are tenant-global. Capsule matches case-INSENSITIVELY when resolving (so 'VIP' and 'vip' attach the same tag), preserving the canonical casing from whichever variant was created first. To ensure consistent casing in your tag list, call list_tags first and reuse the exact name from there. Idempotent \u2014 re-attaching an already-attached tag is harmless."
|
|
2657
2742
|
)
|
|
2658
2743
|
});
|
|
@@ -2665,7 +2750,7 @@ async function addTag(input) {
|
|
|
2665
2750
|
invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
|
|
2666
2751
|
return result;
|
|
2667
2752
|
}
|
|
2668
|
-
var removeTagByIdSchema =
|
|
2753
|
+
var removeTagByIdSchema = z15.object({
|
|
2669
2754
|
entity: TagEntity,
|
|
2670
2755
|
entityId: positiveId.describe("The party/opportunity/kase id."),
|
|
2671
2756
|
tagId: positiveId.describe(
|
|
@@ -2696,7 +2781,7 @@ async function removeTagById(input) {
|
|
|
2696
2781
|
invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
|
|
2697
2782
|
return result;
|
|
2698
2783
|
}
|
|
2699
|
-
var deleteTagDefinitionSchema =
|
|
2784
|
+
var deleteTagDefinitionSchema = z15.object({
|
|
2700
2785
|
entity: TagEntity,
|
|
2701
2786
|
tagId: positiveId.describe(
|
|
2702
2787
|
"The tag definition's id (from list_tags, or embed='tags' on a record). NOT an entity id."
|
|
@@ -2732,44 +2817,41 @@ var { schema: batchRemoveTagByIdSchema, handler: batchRemoveTagById } = defineBa
|
|
|
2732
2817
|
});
|
|
2733
2818
|
|
|
2734
2819
|
// src/tools/users.ts
|
|
2735
|
-
import { z as
|
|
2736
|
-
var listUsersSchema =
|
|
2737
|
-
|
|
2738
|
-
perPage: z15.number().int().min(1).max(100).optional()
|
|
2820
|
+
import { z as z16 } from "zod";
|
|
2821
|
+
var listUsersSchema = z16.object({
|
|
2822
|
+
...paginationFieldsNoDefaults
|
|
2739
2823
|
});
|
|
2740
2824
|
async function listUsers(input) {
|
|
2741
|
-
|
|
2825
|
+
return capsuleGetCachedList("/users", {
|
|
2742
2826
|
page: input.page ?? 1,
|
|
2743
2827
|
perPage: input.perPage ?? 100
|
|
2744
2828
|
});
|
|
2745
|
-
return { ...data, nextPage };
|
|
2746
2829
|
}
|
|
2747
|
-
var getCurrentUserSchema =
|
|
2830
|
+
var getCurrentUserSchema = z16.object({});
|
|
2748
2831
|
async function getCurrentUser(_input) {
|
|
2749
2832
|
const { data } = await capsuleGet("/users/current");
|
|
2750
2833
|
return data;
|
|
2751
2834
|
}
|
|
2752
2835
|
|
|
2753
2836
|
// src/tools/filters.ts
|
|
2754
|
-
import { z as
|
|
2755
|
-
var FilterConditionSchema =
|
|
2756
|
-
field:
|
|
2837
|
+
import { z as z17 } from "zod";
|
|
2838
|
+
var FilterConditionSchema = z17.object({
|
|
2839
|
+
field: z17.string().describe(
|
|
2757
2840
|
"The Capsule filter-side field name (these differ from response field names \u2014 e.g. response.createdAt is filter-side 'addedOn', response.lastContactedAt is filter-side 'lastContactedOn'). Common: 'addedOn' (date created), 'updatedOn' (date last modified), 'lastContactedOn' (parties only), 'name', 'tag', 'owner', 'team', 'type' (parties: person|organisation), 'milestone' (opportunities), 'status' (opp/project: OPEN|CLOSED), 'closedOn' (opp/project), 'expectedCloseOn' (opp/project), 'hasTags', 'hasEmailAddress' (parties), 'isOpen', 'isStale' (opportunities), 'custom:{fieldId}'. Full per-entity list: https://developer.capsulecrm.com/v2/reference/filters"
|
|
2758
2841
|
),
|
|
2759
|
-
operator:
|
|
2842
|
+
operator: z17.string().describe(
|
|
2760
2843
|
"The filter operator. Common: 'is', 'is not' (use value=null to test for null), 'contains', 'does not contain', 'is greater than', 'is less than', 'is within last' (date fields, value=integer days), 'is more than' (date fields, value=integer days ago), 'starts with', 'ends with'. Operator validity depends on the field's type."
|
|
2761
2844
|
),
|
|
2762
|
-
value:
|
|
2845
|
+
value: z17.union([z17.string(), z17.number(), z17.boolean(), z17.null()]).describe(
|
|
2763
2846
|
"The value to compare against. For 'is within last' on date fields, pass an integer number of days. For tag filters, pass the tag name (string) or tag id (number). For 'is not' null tests, pass null literally."
|
|
2764
2847
|
)
|
|
2765
2848
|
});
|
|
2766
|
-
var FilterInputSchema =
|
|
2767
|
-
conditions:
|
|
2849
|
+
var FilterInputSchema = z17.object({
|
|
2850
|
+
conditions: z17.array(FilterConditionSchema).min(1).describe(
|
|
2768
2851
|
"Array of filter conditions. All conditions are ANDed together. To get newest records, use a date condition like {field: 'addedOn', operator: 'is within last', value: 7} and pick the highest-id row from the result (Capsule IDs are monotonic)."
|
|
2769
2852
|
),
|
|
2770
|
-
embed:
|
|
2771
|
-
|
|
2772
|
-
perPage: z16.number().int().min(1).max(100).optional().default(25)
|
|
2853
|
+
embed: z17.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2854
|
+
...paginationFields
|
|
2773
2855
|
});
|
|
2774
2856
|
async function runFilter(entityPath, input) {
|
|
2775
2857
|
const { data, nextPage } = await capsuleSearch(
|
|
@@ -2789,10 +2871,7 @@ async function filterParties(input) {
|
|
|
2789
2871
|
}
|
|
2790
2872
|
var filterOpportunitiesSchema = FilterInputSchema;
|
|
2791
2873
|
async function filterOpportunities(input) {
|
|
2792
|
-
return runFilter(
|
|
2793
|
-
"opportunities",
|
|
2794
|
-
input
|
|
2795
|
-
);
|
|
2874
|
+
return runFilter("opportunities", input);
|
|
2796
2875
|
}
|
|
2797
2876
|
var filterProjectsSchema = FilterInputSchema;
|
|
2798
2877
|
async function filterProjects(input) {
|
|
@@ -2800,139 +2879,126 @@ async function filterProjects(input) {
|
|
|
2800
2879
|
}
|
|
2801
2880
|
|
|
2802
2881
|
// src/tools/metadata.ts
|
|
2803
|
-
import { z as
|
|
2804
|
-
var
|
|
2805
|
-
|
|
2806
|
-
perPage:
|
|
2882
|
+
import { z as z18 } from "zod";
|
|
2883
|
+
var paginationFields2 = {
|
|
2884
|
+
...paginationFieldsNoDefaults,
|
|
2885
|
+
perPage: paginationFieldsNoDefaults.perPage.describe(
|
|
2886
|
+
"Page size, max 100. Defaults to 100 for reference data."
|
|
2887
|
+
)
|
|
2807
2888
|
};
|
|
2808
|
-
var listTeamsSchema =
|
|
2889
|
+
var listTeamsSchema = z18.object({ ...paginationFields2 });
|
|
2809
2890
|
async function listTeams(input) {
|
|
2810
|
-
|
|
2891
|
+
return capsuleGetCachedList("/teams", {
|
|
2811
2892
|
page: input.page ?? 1,
|
|
2812
2893
|
perPage: input.perPage ?? 100
|
|
2813
2894
|
});
|
|
2814
|
-
return { ...data, nextPage };
|
|
2815
2895
|
}
|
|
2816
|
-
var listLostReasonsSchema =
|
|
2896
|
+
var listLostReasonsSchema = z18.object({ ...paginationFields2 });
|
|
2817
2897
|
async function listLostReasons(input) {
|
|
2818
|
-
|
|
2898
|
+
return capsuleGetCachedList("/lostreasons", {
|
|
2819
2899
|
page: input.page ?? 1,
|
|
2820
2900
|
perPage: input.perPage ?? 100
|
|
2821
2901
|
});
|
|
2822
|
-
return { ...data, nextPage };
|
|
2823
2902
|
}
|
|
2824
|
-
var listActivityTypesSchema =
|
|
2903
|
+
var listActivityTypesSchema = z18.object({ ...paginationFields2 });
|
|
2825
2904
|
async function listActivityTypes(input) {
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
perPage: input.perPage ?? 100
|
|
2831
|
-
}
|
|
2832
|
-
);
|
|
2833
|
-
return { ...data, nextPage };
|
|
2905
|
+
return capsuleGetCachedList("/activitytypes", {
|
|
2906
|
+
page: input.page ?? 1,
|
|
2907
|
+
perPage: input.perPage ?? 100
|
|
2908
|
+
});
|
|
2834
2909
|
}
|
|
2835
|
-
var getSiteSchema =
|
|
2910
|
+
var getSiteSchema = z18.object({});
|
|
2836
2911
|
async function getSite(_input) {
|
|
2837
2912
|
const { data } = await capsuleGetCached("/site");
|
|
2838
2913
|
return data;
|
|
2839
2914
|
}
|
|
2840
|
-
var listTrackDefinitionsSchema =
|
|
2915
|
+
var listTrackDefinitionsSchema = z18.object({ ...paginationFields2 });
|
|
2841
2916
|
async function listTrackDefinitions(input) {
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
);
|
|
2846
|
-
return { ...data, nextPage };
|
|
2917
|
+
return capsuleGetCachedList("/trackdefinitions", {
|
|
2918
|
+
page: input.page ?? 1,
|
|
2919
|
+
perPage: input.perPage ?? 100
|
|
2920
|
+
});
|
|
2847
2921
|
}
|
|
2848
|
-
var listCategoriesSchema =
|
|
2922
|
+
var listCategoriesSchema = z18.object({ ...paginationFields2 });
|
|
2849
2923
|
async function listCategories(input) {
|
|
2850
|
-
|
|
2924
|
+
return capsuleGetCachedList("/categories", {
|
|
2851
2925
|
page: input.page ?? 1,
|
|
2852
2926
|
perPage: input.perPage ?? 100
|
|
2853
2927
|
});
|
|
2854
|
-
return { ...data, nextPage };
|
|
2855
2928
|
}
|
|
2856
|
-
var listGoalsSchema =
|
|
2929
|
+
var listGoalsSchema = z18.object({ ...paginationFields2 });
|
|
2857
2930
|
async function listGoals(input) {
|
|
2858
|
-
|
|
2931
|
+
return capsuleGetCachedList("/goals", {
|
|
2859
2932
|
page: input.page ?? 1,
|
|
2860
2933
|
perPage: input.perPage ?? 100
|
|
2861
2934
|
});
|
|
2862
|
-
return { ...data, nextPage };
|
|
2863
2935
|
}
|
|
2864
2936
|
|
|
2865
2937
|
// src/tools/audit.ts
|
|
2866
|
-
import { z as
|
|
2867
|
-
var listEmployeesSchema =
|
|
2938
|
+
import { z as z19 } from "zod";
|
|
2939
|
+
var listEmployeesSchema = z19.object({
|
|
2868
2940
|
partyId: positiveId.describe(
|
|
2869
2941
|
"The organisation's party id. Returns the people whose `organisation` field links to this party."
|
|
2870
2942
|
),
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
embed: z18.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2943
|
+
...paginationFields,
|
|
2944
|
+
embed: z19.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
2874
2945
|
});
|
|
2875
2946
|
async function listEmployees(input) {
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2947
|
+
return capsuleGetList(`/parties/${input.partyId}/people`, {
|
|
2948
|
+
page: input.page,
|
|
2949
|
+
perPage: input.perPage,
|
|
2950
|
+
embed: input.embed
|
|
2951
|
+
});
|
|
2881
2952
|
}
|
|
2882
|
-
var DeletedSinceSchema =
|
|
2953
|
+
var DeletedSinceSchema = z19.string().describe(
|
|
2883
2954
|
"REQUIRED. ISO-8601 timestamp; only deletions on or after this point are returned. Example: '2026-01-01T00:00:00Z'."
|
|
2884
2955
|
);
|
|
2885
2956
|
var DeletedPagination = {
|
|
2886
2957
|
since: DeletedSinceSchema,
|
|
2887
|
-
|
|
2888
|
-
perPage: z18.number().int().min(1).max(100).optional().default(25)
|
|
2958
|
+
...paginationFields
|
|
2889
2959
|
};
|
|
2890
|
-
var listDeletedPartiesSchema =
|
|
2960
|
+
var listDeletedPartiesSchema = z19.object(DeletedPagination);
|
|
2891
2961
|
async function listDeletedParties(input) {
|
|
2892
|
-
|
|
2962
|
+
return capsuleGetList("/parties/deleted", {
|
|
2893
2963
|
since: input.since,
|
|
2894
2964
|
page: input.page,
|
|
2895
2965
|
perPage: input.perPage
|
|
2896
2966
|
});
|
|
2897
|
-
return { ...data, nextPage };
|
|
2898
2967
|
}
|
|
2899
|
-
var listDeletedOpportunitiesSchema =
|
|
2968
|
+
var listDeletedOpportunitiesSchema = z19.object(DeletedPagination);
|
|
2900
2969
|
async function listDeletedOpportunities(input) {
|
|
2901
|
-
|
|
2970
|
+
return capsuleGetList("/opportunities/deleted", {
|
|
2902
2971
|
since: input.since,
|
|
2903
2972
|
page: input.page,
|
|
2904
2973
|
perPage: input.perPage
|
|
2905
2974
|
});
|
|
2906
|
-
return { ...data, nextPage };
|
|
2907
2975
|
}
|
|
2908
|
-
var listDeletedProjectsSchema =
|
|
2976
|
+
var listDeletedProjectsSchema = z19.object(DeletedPagination);
|
|
2909
2977
|
async function listDeletedProjects(input) {
|
|
2910
|
-
|
|
2978
|
+
return capsuleGetList("/kases/deleted", {
|
|
2911
2979
|
since: input.since,
|
|
2912
2980
|
page: input.page,
|
|
2913
2981
|
perPage: input.perPage
|
|
2914
2982
|
});
|
|
2915
|
-
return { ...data, nextPage };
|
|
2916
2983
|
}
|
|
2917
2984
|
|
|
2918
2985
|
// src/tools/relationships.ts
|
|
2919
|
-
import { z as
|
|
2920
|
-
var RelationshipEntity =
|
|
2921
|
-
var listAdditionalPartiesSchema =
|
|
2986
|
+
import { z as z20 } from "zod";
|
|
2987
|
+
var RelationshipEntity = z20.enum(["opportunities", "kases"]).describe("Which entity has the additional-party links. Use 'kases' for projects.");
|
|
2988
|
+
var listAdditionalPartiesSchema = z20.object({
|
|
2922
2989
|
entity: RelationshipEntity,
|
|
2923
2990
|
entityId: positiveId.describe("ID of the opportunity or project."),
|
|
2924
|
-
embed:
|
|
2925
|
-
|
|
2926
|
-
perPage: z19.number().int().min(1).max(100).optional().default(25)
|
|
2991
|
+
embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
2992
|
+
...paginationFields
|
|
2927
2993
|
});
|
|
2928
2994
|
async function listAdditionalParties(input) {
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2995
|
+
return capsuleGetList(`/${input.entity}/${input.entityId}/parties`, {
|
|
2996
|
+
embed: input.embed,
|
|
2997
|
+
page: input.page,
|
|
2998
|
+
perPage: input.perPage
|
|
2999
|
+
});
|
|
2934
3000
|
}
|
|
2935
|
-
var addAdditionalPartySchema =
|
|
3001
|
+
var addAdditionalPartySchema = z20.object({
|
|
2936
3002
|
entity: RelationshipEntity,
|
|
2937
3003
|
entityId: positiveId,
|
|
2938
3004
|
partyId: positiveId.describe(
|
|
@@ -2965,7 +3031,7 @@ async function addAdditionalParty(input) {
|
|
|
2965
3031
|
throw err;
|
|
2966
3032
|
}
|
|
2967
3033
|
}
|
|
2968
|
-
var removeAdditionalPartySchema =
|
|
3034
|
+
var removeAdditionalPartySchema = z20.object({
|
|
2969
3035
|
entity: RelationshipEntity,
|
|
2970
3036
|
entityId: positiveId,
|
|
2971
3037
|
partyId: positiveId,
|
|
@@ -2995,24 +3061,23 @@ async function removeAdditionalParty(input) {
|
|
|
2995
3061
|
})
|
|
2996
3062
|
);
|
|
2997
3063
|
}
|
|
2998
|
-
var listAssociatedProjectsSchema =
|
|
3064
|
+
var listAssociatedProjectsSchema = z20.object({
|
|
2999
3065
|
opportunityId: positiveId,
|
|
3000
|
-
embed:
|
|
3001
|
-
|
|
3002
|
-
perPage: z19.number().int().min(1).max(100).optional().default(25)
|
|
3066
|
+
embed: z20.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
3067
|
+
...paginationFields
|
|
3003
3068
|
});
|
|
3004
3069
|
async function listAssociatedProjects(input) {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3070
|
+
return capsuleGetList(`/opportunities/${input.opportunityId}/kases`, {
|
|
3071
|
+
embed: input.embed,
|
|
3072
|
+
page: input.page,
|
|
3073
|
+
perPage: input.perPage
|
|
3074
|
+
});
|
|
3010
3075
|
}
|
|
3011
3076
|
|
|
3012
3077
|
// src/tools/custom-fields.ts
|
|
3013
|
-
import { z as
|
|
3014
|
-
var CustomFieldEntity =
|
|
3015
|
-
var listCustomFieldsSchema =
|
|
3078
|
+
import { z as z21 } from "zod";
|
|
3079
|
+
var CustomFieldEntity = z21.enum(["parties", "opportunities", "kases"]).describe("Which entity type's custom field schema to inspect. Use 'kases' for projects.");
|
|
3080
|
+
var listCustomFieldsSchema = z21.object({
|
|
3016
3081
|
entity: CustomFieldEntity
|
|
3017
3082
|
});
|
|
3018
3083
|
async function listCustomFields(input) {
|
|
@@ -3021,7 +3086,7 @@ async function listCustomFields(input) {
|
|
|
3021
3086
|
);
|
|
3022
3087
|
return data;
|
|
3023
3088
|
}
|
|
3024
|
-
var getCustomFieldSchema =
|
|
3089
|
+
var getCustomFieldSchema = z21.object({
|
|
3025
3090
|
entity: CustomFieldEntity,
|
|
3026
3091
|
fieldId: positiveId.describe("Custom field definition id.")
|
|
3027
3092
|
});
|
|
@@ -3033,9 +3098,9 @@ async function getCustomField(input) {
|
|
|
3033
3098
|
}
|
|
3034
3099
|
|
|
3035
3100
|
// src/tools/tracks.ts
|
|
3036
|
-
import { z as
|
|
3037
|
-
var TrackEntity =
|
|
3038
|
-
var listEntityTracksSchema =
|
|
3101
|
+
import { z as z22 } from "zod";
|
|
3102
|
+
var TrackEntity = z22.enum(["parties", "opportunities", "kases"]).describe("Use 'kases' for projects.");
|
|
3103
|
+
var listEntityTracksSchema = z22.object({
|
|
3039
3104
|
entity: TrackEntity,
|
|
3040
3105
|
entityId: positiveId
|
|
3041
3106
|
});
|
|
@@ -3045,20 +3110,20 @@ async function listEntityTracks(input) {
|
|
|
3045
3110
|
);
|
|
3046
3111
|
return data;
|
|
3047
3112
|
}
|
|
3048
|
-
var showTrackSchema =
|
|
3113
|
+
var showTrackSchema = z22.object({
|
|
3049
3114
|
trackId: positiveId
|
|
3050
3115
|
});
|
|
3051
3116
|
async function showTrack(input) {
|
|
3052
3117
|
const { data } = await capsuleGet(`/tracks/${input.trackId}`);
|
|
3053
3118
|
return data;
|
|
3054
3119
|
}
|
|
3055
|
-
var applyTrackSchema =
|
|
3056
|
-
entity:
|
|
3120
|
+
var applyTrackSchema = z22.object({
|
|
3121
|
+
entity: z22.enum(["opportunities", "kases"]).describe("Which entity to apply the track to. Use 'kases' for projects."),
|
|
3057
3122
|
entityId: positiveId,
|
|
3058
3123
|
trackDefinitionId: positiveId.describe(
|
|
3059
3124
|
"The trackDefinition to apply (from list_track_definitions). Auto-creates task definitions on the target entity per the track's rules."
|
|
3060
3125
|
),
|
|
3061
|
-
startDate:
|
|
3126
|
+
startDate: z22.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional().describe(
|
|
3062
3127
|
"Optional ISO-8601 date (YYYY-MM-DD) the track should start from \u2014 drives task due-date calculations (each task's `dueOn` is computed as startDate + the track-definition's `daysAfter` offset). Defaults to today if omitted. Useful for scheduling a renewal-queue track against a future contract end-date, or backfilling tracks for historical projects."
|
|
3063
3128
|
)
|
|
3064
3129
|
});
|
|
@@ -3071,9 +3136,9 @@ async function applyTrack(input) {
|
|
|
3071
3136
|
if (input.startDate !== void 0) track["trackDateOn"] = input.startDate;
|
|
3072
3137
|
return capsulePost("/tracks", { track });
|
|
3073
3138
|
}
|
|
3074
|
-
var updateTrackSchema =
|
|
3139
|
+
var updateTrackSchema = z22.object({
|
|
3075
3140
|
trackId: positiveId,
|
|
3076
|
-
fields:
|
|
3141
|
+
fields: z22.record(z22.string(), z22.unknown()).describe(
|
|
3077
3142
|
"Object of fields to update on the track. Capsule's PUT semantics are partial \u2014 only the fields you provide are changed. Common: { complete: true } to mark a track completed. Capsule rejects unknown keys; consult Capsule's docs for the full updatable set."
|
|
3078
3143
|
)
|
|
3079
3144
|
});
|
|
@@ -3085,7 +3150,7 @@ async function updateTrack(input) {
|
|
|
3085
3150
|
track: input.fields
|
|
3086
3151
|
});
|
|
3087
3152
|
}
|
|
3088
|
-
var removeTrackSchema =
|
|
3153
|
+
var removeTrackSchema = z22.object({
|
|
3089
3154
|
trackId: positiveId,
|
|
3090
3155
|
confirm: confirmFlag().describe(
|
|
3091
3156
|
"Must be set to true. Removes the track instance from its entity. **Capsule also deletes the auto-tasks the track created when it was applied** \u2014 they go with the track and become unreachable (404 on GET /tasks/{id}, gone from list_tasks on the parent entity). If you need any of those tasks to outlive the track, copy their content into fresh tasks (or use the web UI) before calling remove_track."
|
|
@@ -3103,13 +3168,13 @@ async function removeTrack(input) {
|
|
|
3103
3168
|
}
|
|
3104
3169
|
|
|
3105
3170
|
// src/tools/attachments.ts
|
|
3106
|
-
import { z as
|
|
3171
|
+
import { z as z23 } from "zod";
|
|
3107
3172
|
var DEFAULT_MAX_SIZE_BYTES = 5 * 1024 * 1024;
|
|
3108
3173
|
var HARD_MAX_SIZE_BYTES = 25 * 1024 * 1024;
|
|
3109
3174
|
var HARD_MAX_BASE64_CHARS = Math.ceil(HARD_MAX_SIZE_BYTES / 3) * 4;
|
|
3110
|
-
var getAttachmentSchema =
|
|
3175
|
+
var getAttachmentSchema = z23.object({
|
|
3111
3176
|
id: positiveId.describe("Attachment ID."),
|
|
3112
|
-
maxSizeBytes:
|
|
3177
|
+
maxSizeBytes: z23.number().int().positive().max(HARD_MAX_SIZE_BYTES).optional().describe(
|
|
3113
3178
|
`Refuse to return content over this size (default ${DEFAULT_MAX_SIZE_BYTES} bytes \u2248 5MB; max ${HARD_MAX_SIZE_BYTES} bytes \u2248 25MB). Files exceeding the cap return metadata only with a 'truncated: true' flag.`
|
|
3114
3179
|
)
|
|
3115
3180
|
});
|
|
@@ -3124,17 +3189,17 @@ async function getAttachment(input) {
|
|
|
3124
3189
|
}
|
|
3125
3190
|
return { contentType, buffer, sizeBytes };
|
|
3126
3191
|
}
|
|
3127
|
-
var uploadAttachmentSchema =
|
|
3128
|
-
filename:
|
|
3192
|
+
var uploadAttachmentSchema = z23.object({
|
|
3193
|
+
filename: z23.string().min(1).describe(
|
|
3129
3194
|
"Filename Capsule should record (e.g. 'contract.pdf'). Capsule does NOT validate consistency between filename, contentType, and the actual bytes \u2014 a typo in either is accepted and the file is stored as labelled."
|
|
3130
3195
|
),
|
|
3131
|
-
contentType:
|
|
3196
|
+
contentType: z23.string().min(1).describe(
|
|
3132
3197
|
"MIME type of the file (e.g. 'application/pdf', 'image/png', 'text/plain'). Trusted by Capsule verbatim; not cross-checked against `filename` or the actual bytes."
|
|
3133
3198
|
),
|
|
3134
|
-
dataBase64:
|
|
3199
|
+
dataBase64: z23.string().min(1).max(HARD_MAX_BASE64_CHARS).describe(
|
|
3135
3200
|
"File contents, base64-encoded. Decoded server-side and uploaded as the request body. Maximum 25 MB per attachment (Capsule's documented limit); the connector rejects oversized base64 before uploading. The inbound HTTP body limit is ~35 MB which leaves room for the base64 expansion of a 25 MB binary."
|
|
3136
3201
|
),
|
|
3137
|
-
content:
|
|
3202
|
+
content: z23.string().optional().describe(
|
|
3138
3203
|
"Body text for the note that will hold the attachment. Defaults to '[attachment]' if omitted."
|
|
3139
3204
|
),
|
|
3140
3205
|
partyId: positiveId.optional().describe("Link the new note to a party (mutually exclusive with opportunityId / projectId)."),
|
|
@@ -3152,12 +3217,7 @@ function decodedBase64Size(s) {
|
|
|
3152
3217
|
return s.length / 4 * 3 - padding;
|
|
3153
3218
|
}
|
|
3154
3219
|
async function uploadAttachment(input) {
|
|
3155
|
-
|
|
3156
|
-
if (linked.length !== 1) {
|
|
3157
|
-
throw new Error(
|
|
3158
|
-
"upload_attachment: provide exactly one of partyId, opportunityId, or projectId"
|
|
3159
|
-
);
|
|
3160
|
-
}
|
|
3220
|
+
assertSingleParentRef("upload_attachment", input, { required: true });
|
|
3161
3221
|
if (!isValidBase64(input.dataBase64)) {
|
|
3162
3222
|
throw new Error(
|
|
3163
3223
|
"upload_attachment: dataBase64 is not valid base64 \u2014 Node's tolerant decoder would silently produce corrupt bytes. Verify the encoding (RFC 4648, padded with '=' to a multiple of 4 chars)."
|
|
@@ -3182,37 +3242,36 @@ async function uploadAttachment(input) {
|
|
|
3182
3242
|
content: input.content ?? "[attachment]",
|
|
3183
3243
|
attachments: [{ token }]
|
|
3184
3244
|
};
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3245
|
+
setRef(entryBody, "party", input.partyId);
|
|
3246
|
+
setRef(entryBody, "opportunity", input.opportunityId);
|
|
3247
|
+
setRef(entryBody, "kase", input.projectId);
|
|
3188
3248
|
return capsulePost("/entries", { entry: entryBody });
|
|
3189
3249
|
}
|
|
3190
3250
|
|
|
3191
3251
|
// src/tools/saved-filters.ts
|
|
3192
|
-
import { z as
|
|
3193
|
-
var EntitySchema =
|
|
3252
|
+
import { z as z24 } from "zod";
|
|
3253
|
+
var EntitySchema = z24.enum(["parties", "opportunities", "kases"]).describe(
|
|
3194
3254
|
"Which entity type the filter operates over. Use 'kases' for projects (Capsule's legacy name)."
|
|
3195
3255
|
);
|
|
3196
|
-
var listSavedFiltersSchema =
|
|
3256
|
+
var listSavedFiltersSchema = z24.object({
|
|
3197
3257
|
entity: EntitySchema
|
|
3198
3258
|
});
|
|
3199
3259
|
async function listSavedFilters(input) {
|
|
3200
3260
|
const { data } = await capsuleGetCached(`/${input.entity}/filters`);
|
|
3201
3261
|
return data;
|
|
3202
3262
|
}
|
|
3203
|
-
var runSavedFilterSchema =
|
|
3263
|
+
var runSavedFilterSchema = z24.object({
|
|
3204
3264
|
entity: EntitySchema,
|
|
3205
3265
|
id: positiveId.describe("The saved filter id (from list_saved_filters)."),
|
|
3206
|
-
embed:
|
|
3207
|
-
|
|
3208
|
-
perPage: z23.number().int().min(1).max(100).optional().default(25)
|
|
3266
|
+
embed: z24.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION),
|
|
3267
|
+
...paginationFields
|
|
3209
3268
|
});
|
|
3210
3269
|
async function runSavedFilter(input) {
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3270
|
+
return capsuleGetList(`/${input.entity}/filters/${input.id}/results`, {
|
|
3271
|
+
page: input.page,
|
|
3272
|
+
perPage: input.perPage,
|
|
3273
|
+
embed: input.embed
|
|
3274
|
+
});
|
|
3216
3275
|
}
|
|
3217
3276
|
|
|
3218
3277
|
// src/server.ts
|
|
@@ -3223,7 +3282,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3223
3282
|
const server = new McpServer(
|
|
3224
3283
|
{
|
|
3225
3284
|
name: "capsulemcp",
|
|
3226
|
-
version: "1.
|
|
3285
|
+
version: "1.8.1",
|
|
3227
3286
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
3228
3287
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
3229
3288
|
icons: ICONS
|
|
@@ -3673,20 +3732,72 @@ function createCapsuleMcpServer(opts) {
|
|
|
3673
3732
|
listEntriesSchema,
|
|
3674
3733
|
listEntries
|
|
3675
3734
|
);
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3735
|
+
if (shouldRegister("get_attachment")) {
|
|
3736
|
+
server.tool(
|
|
3737
|
+
"get_attachment",
|
|
3738
|
+
"Download an attachment by id. Returns image content for image/* types (Claude can describe it natively); decoded text for text/* and application/json (small files); JSON metadata + base64 payload for other binary types (PDF, Office docs, etc.). Files exceeding maxSizeBytes (default 5MB) return metadata only with a `truncated: true` flag.",
|
|
3739
|
+
getAttachmentSchema.shape,
|
|
3740
|
+
// get_attachment is read-only — downloads a binary, never mutates.
|
|
3741
|
+
// Mirrors the auto-inferred `{readOnlyHint: true, destructiveHint:
|
|
3742
|
+
// false}` that `registerTool` applies to every other `get_*` tool.
|
|
3743
|
+
// Explicit destructiveHint: false is load-bearing — MCP spec
|
|
3744
|
+
// defaults destructiveHint to `true`, so omitting it would (in
|
|
3745
|
+
// some client implementations) classify this read as destructive.
|
|
3746
|
+
{ readOnlyHint: true, destructiveHint: false },
|
|
3747
|
+
async (input) => {
|
|
3748
|
+
const result = await getAttachment(input);
|
|
3749
|
+
if (result.truncated) {
|
|
3750
|
+
return {
|
|
3751
|
+
content: [
|
|
3752
|
+
{
|
|
3753
|
+
type: "text",
|
|
3754
|
+
text: JSON.stringify(
|
|
3755
|
+
{
|
|
3756
|
+
id: input.id,
|
|
3757
|
+
contentType: result.contentType,
|
|
3758
|
+
sizeBytes: result.sizeBytes,
|
|
3759
|
+
truncated: true,
|
|
3760
|
+
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3761
|
+
},
|
|
3762
|
+
null,
|
|
3763
|
+
2
|
|
3764
|
+
)
|
|
3765
|
+
}
|
|
3766
|
+
]
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
const baseType = result.contentType.split(";")[0].trim().toLowerCase();
|
|
3770
|
+
if (baseType.startsWith("image/")) {
|
|
3771
|
+
return {
|
|
3772
|
+
content: [
|
|
3773
|
+
{
|
|
3774
|
+
type: "image",
|
|
3775
|
+
data: result.buffer.toString("base64"),
|
|
3776
|
+
mimeType: result.contentType
|
|
3777
|
+
}
|
|
3778
|
+
]
|
|
3779
|
+
};
|
|
3780
|
+
}
|
|
3781
|
+
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3782
|
+
if (isText) {
|
|
3783
|
+
return {
|
|
3784
|
+
content: [
|
|
3785
|
+
{
|
|
3786
|
+
type: "text",
|
|
3787
|
+
text: JSON.stringify(
|
|
3788
|
+
{
|
|
3789
|
+
id: input.id,
|
|
3790
|
+
contentType: result.contentType,
|
|
3791
|
+
sizeBytes: result.sizeBytes
|
|
3792
|
+
},
|
|
3793
|
+
null,
|
|
3794
|
+
2
|
|
3795
|
+
)
|
|
3796
|
+
},
|
|
3797
|
+
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3798
|
+
]
|
|
3799
|
+
};
|
|
3800
|
+
}
|
|
3690
3801
|
return {
|
|
3691
3802
|
content: [
|
|
3692
3803
|
{
|
|
@@ -3696,8 +3807,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3696
3807
|
id: input.id,
|
|
3697
3808
|
contentType: result.contentType,
|
|
3698
3809
|
sizeBytes: result.sizeBytes,
|
|
3699
|
-
|
|
3700
|
-
message: `File exceeds the size cap (${input.maxSizeBytes ?? "default"} bytes). Increase maxSizeBytes if you need the bytes; max is 25MB.`
|
|
3810
|
+
base64: result.buffer.toString("base64")
|
|
3701
3811
|
},
|
|
3702
3812
|
null,
|
|
3703
3813
|
2
|
|
@@ -3706,57 +3816,8 @@ function createCapsuleMcpServer(opts) {
|
|
|
3706
3816
|
]
|
|
3707
3817
|
};
|
|
3708
3818
|
}
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
return {
|
|
3712
|
-
content: [
|
|
3713
|
-
{
|
|
3714
|
-
type: "image",
|
|
3715
|
-
data: result.buffer.toString("base64"),
|
|
3716
|
-
mimeType: result.contentType
|
|
3717
|
-
}
|
|
3718
|
-
]
|
|
3719
|
-
};
|
|
3720
|
-
}
|
|
3721
|
-
const isText = baseType.startsWith("text/") || baseType === "application/json" || baseType === "application/xml";
|
|
3722
|
-
if (isText) {
|
|
3723
|
-
return {
|
|
3724
|
-
content: [
|
|
3725
|
-
{
|
|
3726
|
-
type: "text",
|
|
3727
|
-
text: JSON.stringify(
|
|
3728
|
-
{
|
|
3729
|
-
id: input.id,
|
|
3730
|
-
contentType: result.contentType,
|
|
3731
|
-
sizeBytes: result.sizeBytes
|
|
3732
|
-
},
|
|
3733
|
-
null,
|
|
3734
|
-
2
|
|
3735
|
-
)
|
|
3736
|
-
},
|
|
3737
|
-
{ type: "text", text: result.buffer.toString("utf8") }
|
|
3738
|
-
]
|
|
3739
|
-
};
|
|
3740
|
-
}
|
|
3741
|
-
return {
|
|
3742
|
-
content: [
|
|
3743
|
-
{
|
|
3744
|
-
type: "text",
|
|
3745
|
-
text: JSON.stringify(
|
|
3746
|
-
{
|
|
3747
|
-
id: input.id,
|
|
3748
|
-
contentType: result.contentType,
|
|
3749
|
-
sizeBytes: result.sizeBytes,
|
|
3750
|
-
base64: result.buffer.toString("base64")
|
|
3751
|
-
},
|
|
3752
|
-
null,
|
|
3753
|
-
2
|
|
3754
|
-
)
|
|
3755
|
-
}
|
|
3756
|
-
]
|
|
3757
|
-
};
|
|
3758
|
-
}
|
|
3759
|
-
);
|
|
3819
|
+
);
|
|
3820
|
+
}
|
|
3760
3821
|
if (!readOnly) {
|
|
3761
3822
|
registerTool(
|
|
3762
3823
|
server,
|
|
@@ -4132,77 +4193,51 @@ a{color:#1e3a8a}
|
|
|
4132
4193
|
}
|
|
4133
4194
|
next();
|
|
4134
4195
|
};
|
|
4135
|
-
|
|
4136
|
-
"/mcp",
|
|
4196
|
+
const mcpGuards = [
|
|
4137
4197
|
guardOrigin,
|
|
4138
4198
|
requireBearerAuth({
|
|
4139
4199
|
verifier: oauthProvider2,
|
|
4140
4200
|
resourceMetadataUrl: mcpResourceMetadataUrl
|
|
4141
4201
|
}),
|
|
4142
4202
|
mcpRateLimit,
|
|
4143
|
-
guardProtocolVersion
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
const name = err instanceof Error ? err.name : typeof err;
|
|
4160
|
-
const status = err && typeof err === "object" && "status" in err ? Number(err.status) : void 0;
|
|
4161
|
-
const summary = status !== void 0 ? `${name} ${status}` : name;
|
|
4162
|
-
if (process.env["MCP_HTTP_DEBUG"] === "1") {
|
|
4163
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4164
|
-
console.error(`[capsulemcp] /mcp error: ${summary} \u2014 ${message}`);
|
|
4165
|
-
} else {
|
|
4166
|
-
console.error(`[capsulemcp] /mcp error: ${summary}`);
|
|
4167
|
-
}
|
|
4168
|
-
if (!res.headersSent) {
|
|
4169
|
-
res.status(500).json({ error: "internal_error" });
|
|
4170
|
-
}
|
|
4171
|
-
}
|
|
4172
|
-
}
|
|
4173
|
-
);
|
|
4174
|
-
app2.get(
|
|
4175
|
-
"/mcp",
|
|
4176
|
-
guardOrigin,
|
|
4177
|
-
requireBearerAuth({
|
|
4178
|
-
verifier: oauthProvider2,
|
|
4179
|
-
resourceMetadataUrl: mcpResourceMetadataUrl
|
|
4180
|
-
}),
|
|
4181
|
-
mcpRateLimit,
|
|
4182
|
-
guardProtocolVersion,
|
|
4183
|
-
(_req, res) => {
|
|
4184
|
-
res.set("Allow", "POST").status(405).json({
|
|
4185
|
-
error: "method_not_allowed",
|
|
4186
|
-
message: "Use POST for MCP requests; this server runs in stateless mode."
|
|
4203
|
+
guardProtocolVersion
|
|
4204
|
+
];
|
|
4205
|
+
const methodNotAllowed = (_req, res) => {
|
|
4206
|
+
res.set("Allow", "POST").status(405).json({
|
|
4207
|
+
error: "method_not_allowed",
|
|
4208
|
+
message: "Use POST for MCP requests; this server runs in stateless mode."
|
|
4209
|
+
});
|
|
4210
|
+
};
|
|
4211
|
+
app2.post("/mcp", ...mcpGuards, express.json({ limit: jsonLimit2 }), async (req, res) => {
|
|
4212
|
+
try {
|
|
4213
|
+
const clientId = req.auth?.clientId;
|
|
4214
|
+
const server = createCapsuleMcpServer({ clientId });
|
|
4215
|
+
const transport = new StreamableHTTPServerTransport({});
|
|
4216
|
+
res.on("close", () => {
|
|
4217
|
+
void transport.close();
|
|
4218
|
+
void server.close();
|
|
4187
4219
|
});
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
"/mcp",
|
|
4192
|
-
guardOrigin,
|
|
4193
|
-
requireBearerAuth({
|
|
4194
|
-
verifier: oauthProvider2,
|
|
4195
|
-
resourceMetadataUrl: mcpResourceMetadataUrl
|
|
4196
|
-
}),
|
|
4197
|
-
mcpRateLimit,
|
|
4198
|
-
guardProtocolVersion,
|
|
4199
|
-
(_req, res) => {
|
|
4200
|
-
res.set("Allow", "POST").status(405).json({
|
|
4201
|
-
error: "method_not_allowed",
|
|
4202
|
-
message: "Use POST for MCP requests; this server runs in stateless mode."
|
|
4220
|
+
await withRequestContext({ clientId }, async () => {
|
|
4221
|
+
await server.connect(transport);
|
|
4222
|
+
await transport.handleRequest(req, res, req.body);
|
|
4203
4223
|
});
|
|
4224
|
+
} catch (err) {
|
|
4225
|
+
const name = err instanceof Error ? err.name : typeof err;
|
|
4226
|
+
const status = err && typeof err === "object" && "status" in err ? Number(err.status) : void 0;
|
|
4227
|
+
const summary = status !== void 0 ? `${name} ${status}` : name;
|
|
4228
|
+
if (process.env["MCP_HTTP_DEBUG"] === "1") {
|
|
4229
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4230
|
+
console.error(`[capsulemcp] /mcp error: ${summary} \u2014 ${message}`);
|
|
4231
|
+
} else {
|
|
4232
|
+
console.error(`[capsulemcp] /mcp error: ${summary}`);
|
|
4233
|
+
}
|
|
4234
|
+
if (!res.headersSent) {
|
|
4235
|
+
res.status(500).json({ error: "internal_error" });
|
|
4236
|
+
}
|
|
4204
4237
|
}
|
|
4205
|
-
);
|
|
4238
|
+
});
|
|
4239
|
+
app2.get("/mcp", ...mcpGuards, methodNotAllowed);
|
|
4240
|
+
app2.delete("/mcp", ...mcpGuards, methodNotAllowed);
|
|
4206
4241
|
return app2;
|
|
4207
4242
|
}
|
|
4208
4243
|
|