capsulemcp 1.0.1 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -3
- package/dist/http.js +809 -77
- package/dist/index.js +758 -69
- package/package.json +10 -5
package/dist/index.js
CHANGED
|
@@ -5,6 +5,124 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
|
|
6
6
|
// src/capsule/client.ts
|
|
7
7
|
import { fetch } from "undici";
|
|
8
|
+
|
|
9
|
+
// src/env.ts
|
|
10
|
+
function readBool(name) {
|
|
11
|
+
const raw = process.env[name]?.toLowerCase();
|
|
12
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
13
|
+
}
|
|
14
|
+
function readPositiveInt(name, fallback, min = 1) {
|
|
15
|
+
const raw = process.env[name];
|
|
16
|
+
if (raw === void 0 || raw === "") return fallback;
|
|
17
|
+
const n = Number(raw);
|
|
18
|
+
if (!Number.isFinite(n) || n < min) return fallback;
|
|
19
|
+
return Math.floor(n);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/log.ts
|
|
23
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
24
|
+
function logVerbose() {
|
|
25
|
+
return readBool("CAPSULE_MCP_LOG_VERBOSE");
|
|
26
|
+
}
|
|
27
|
+
var chainHandlers = {
|
|
28
|
+
"tool.call": (ctx, f) => {
|
|
29
|
+
if (typeof f["tool"] === "string") ctx.tools.push(f["tool"]);
|
|
30
|
+
},
|
|
31
|
+
"capsule.request": (ctx) => {
|
|
32
|
+
ctx.capsuleCalls += 1;
|
|
33
|
+
},
|
|
34
|
+
// Cache-hit events feed the aggregate so the chain stat is right
|
|
35
|
+
// even on tools whose Capsule calls all hit the cache.
|
|
36
|
+
"cache.hit": (ctx) => {
|
|
37
|
+
ctx.cacheHits += 1;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
function logEvent(event, fields, opts = {}) {
|
|
41
|
+
const ctx = requestContext.getStore();
|
|
42
|
+
if (ctx) chainHandlers[event]?.(ctx, fields);
|
|
43
|
+
if (!opts.force && !logVerbose()) return;
|
|
44
|
+
process.stderr.write(
|
|
45
|
+
`${JSON.stringify({ event, ...fields, timestamp: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
46
|
+
`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
function redactPath(path) {
|
|
50
|
+
const noQuery = path.split("?")[0] ?? path;
|
|
51
|
+
return noQuery.replace(/\/\d+(?:,\d+)*/g, "/:id");
|
|
52
|
+
}
|
|
53
|
+
var requestContext = new AsyncLocalStorage();
|
|
54
|
+
function getRequestContext() {
|
|
55
|
+
return requestContext.getStore();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/capsule/cache.ts
|
|
59
|
+
var cache = /* @__PURE__ */ new Map();
|
|
60
|
+
var MAX_ENTRIES = 64;
|
|
61
|
+
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
62
|
+
function getCacheTtlMs() {
|
|
63
|
+
return readPositiveInt("CAPSULE_MCP_CACHE_TTL_MS", DEFAULT_TTL_MS, 0);
|
|
64
|
+
}
|
|
65
|
+
function explicitlyDisabled() {
|
|
66
|
+
return readBool("CAPSULE_MCP_CACHE_DISABLED");
|
|
67
|
+
}
|
|
68
|
+
function cacheDisabled() {
|
|
69
|
+
return explicitlyDisabled() || getCacheTtlMs() === 0;
|
|
70
|
+
}
|
|
71
|
+
function cacheKey(path, params) {
|
|
72
|
+
if (!params) return `GET ${path}`;
|
|
73
|
+
const entries = Object.entries(params).filter(([, v]) => v !== void 0);
|
|
74
|
+
if (entries.length === 0) return `GET ${path}`;
|
|
75
|
+
entries.sort(([a], [b]) => a.localeCompare(b));
|
|
76
|
+
const qs = entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join("&");
|
|
77
|
+
return `GET ${path}?${qs}`;
|
|
78
|
+
}
|
|
79
|
+
function cacheLookup(key) {
|
|
80
|
+
const entry = cache.get(key);
|
|
81
|
+
if (!entry) return { hit: false, reason: "empty" };
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
if (entry.expiresAt < now) {
|
|
84
|
+
cache.delete(key);
|
|
85
|
+
return { hit: false, reason: "expired" };
|
|
86
|
+
}
|
|
87
|
+
return { hit: true, result: entry.result, ageMs: now - entry.storedAt };
|
|
88
|
+
}
|
|
89
|
+
function cacheSet(key, result) {
|
|
90
|
+
if (cacheDisabled()) return;
|
|
91
|
+
const ttl = getCacheTtlMs();
|
|
92
|
+
while (cache.size >= MAX_ENTRIES) {
|
|
93
|
+
const oldest = cache.keys().next().value;
|
|
94
|
+
if (oldest === void 0) break;
|
|
95
|
+
cache.delete(oldest);
|
|
96
|
+
const evictedKey = `GET ${redactPath(oldest.replace(/^GET /, ""))}`;
|
|
97
|
+
logEvent("cache.evict", { evictedKey, cacheSize: cache.size, reason: "cap" });
|
|
98
|
+
}
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
cache.set(key, {
|
|
101
|
+
result,
|
|
102
|
+
storedAt: now,
|
|
103
|
+
expiresAt: now + ttl
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function invalidateByPrefix(pathPrefix, trigger) {
|
|
107
|
+
const needle = `GET ${pathPrefix}`;
|
|
108
|
+
let droppedCount = 0;
|
|
109
|
+
for (const k of cache.keys()) {
|
|
110
|
+
if (k === needle || k.startsWith(`${needle}?`) || k.startsWith(`${needle}/`)) {
|
|
111
|
+
cache.delete(k);
|
|
112
|
+
droppedCount++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (droppedCount > 0) {
|
|
116
|
+
logEvent("cache.invalidate", {
|
|
117
|
+
prefix: pathPrefix,
|
|
118
|
+
droppedCount,
|
|
119
|
+
cacheSize: cache.size,
|
|
120
|
+
...trigger ? { trigger } : {}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/capsule/client.ts
|
|
8
126
|
var DEFAULT_BASE_URL = "https://api.capsulecrm.com/api/v2";
|
|
9
127
|
function baseUrl() {
|
|
10
128
|
const override = process.env["CAPSULE_API_BASE_URL"];
|
|
@@ -24,8 +142,7 @@ function baseUrl() {
|
|
|
24
142
|
return override;
|
|
25
143
|
}
|
|
26
144
|
function isReadOnly() {
|
|
27
|
-
|
|
28
|
-
return v === "1" || v === "true" || v === "yes";
|
|
145
|
+
return readBool("CAPSULE_MCP_READONLY");
|
|
29
146
|
}
|
|
30
147
|
var CapsuleReadOnlyError = class extends Error {
|
|
31
148
|
constructor(method) {
|
|
@@ -154,6 +271,8 @@ async function fetchWithTimeout(url, options) {
|
|
|
154
271
|
}
|
|
155
272
|
}
|
|
156
273
|
async function doFetch(url, options) {
|
|
274
|
+
const startedAt = Date.now();
|
|
275
|
+
const method = options?.method ?? "GET";
|
|
157
276
|
const first = await fetchWithTimeout(url, options);
|
|
158
277
|
if (first.res.status === 429) {
|
|
159
278
|
const delay = parseRateLimitDelay(first.res);
|
|
@@ -167,10 +286,30 @@ async function doFetch(url, options) {
|
|
|
167
286
|
"Rate limit exceeded after one retry. Please slow down your requests."
|
|
168
287
|
);
|
|
169
288
|
}
|
|
289
|
+
emitCapsuleRequest(method, url, retried.res, Date.now() - startedAt, true);
|
|
170
290
|
return retried;
|
|
171
291
|
}
|
|
292
|
+
emitCapsuleRequest(method, url, first.res, Date.now() - startedAt, false);
|
|
172
293
|
return first;
|
|
173
294
|
}
|
|
295
|
+
function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
296
|
+
let path = "";
|
|
297
|
+
try {
|
|
298
|
+
path = redactPath(new URL(url).pathname);
|
|
299
|
+
} catch {
|
|
300
|
+
path = "?";
|
|
301
|
+
}
|
|
302
|
+
const lenHeader = res.headers.get("content-length");
|
|
303
|
+
const responseBytes = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
|
|
304
|
+
logEvent("capsule.request", {
|
|
305
|
+
method,
|
|
306
|
+
path,
|
|
307
|
+
status: res.status,
|
|
308
|
+
durationMs,
|
|
309
|
+
responseBytes: Number.isFinite(responseBytes) ? responseBytes : 0,
|
|
310
|
+
...retriedAfter429 ? { retriedAfter429: true } : {}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
174
313
|
async function throwForStatus(res) {
|
|
175
314
|
if (res.status === 401) {
|
|
176
315
|
const detail = await parseErrorBody(res);
|
|
@@ -210,6 +349,34 @@ async function capsuleGet(path, params) {
|
|
|
210
349
|
cleanup();
|
|
211
350
|
}
|
|
212
351
|
}
|
|
352
|
+
async function capsuleGetCached(path, params) {
|
|
353
|
+
if (cacheDisabled()) return capsuleGet(path, params);
|
|
354
|
+
const key = cacheKey(path, params);
|
|
355
|
+
const lookup = cacheLookup(key);
|
|
356
|
+
if (lookup.hit) {
|
|
357
|
+
if (logVerbose()) {
|
|
358
|
+
logEvent("cache.hit", {
|
|
359
|
+
path: redactPath(path),
|
|
360
|
+
...params ? { paramFields: Object.keys(params) } : {},
|
|
361
|
+
ageMs: lookup.ageMs
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return lookup.result;
|
|
365
|
+
}
|
|
366
|
+
const fetchStart = Date.now();
|
|
367
|
+
const result = await capsuleGet(path, params);
|
|
368
|
+
const latencyMs = Date.now() - fetchStart;
|
|
369
|
+
cacheSet(key, result);
|
|
370
|
+
if (logVerbose()) {
|
|
371
|
+
logEvent("cache.miss", {
|
|
372
|
+
path: redactPath(path),
|
|
373
|
+
...params ? { paramFields: Object.keys(params) } : {},
|
|
374
|
+
reason: lookup.reason,
|
|
375
|
+
latencyMs
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
213
380
|
async function capsulePost(path, body) {
|
|
214
381
|
if (isReadOnly()) throw new CapsuleReadOnlyError("POST");
|
|
215
382
|
const token = getToken();
|
|
@@ -396,7 +563,195 @@ var ICONS = [
|
|
|
396
563
|
}
|
|
397
564
|
];
|
|
398
565
|
|
|
566
|
+
// src/tasks/store.ts
|
|
567
|
+
import { InMemoryTaskStore } from "@modelcontextprotocol/sdk/experimental/tasks/stores/in-memory.js";
|
|
568
|
+
import {
|
|
569
|
+
ErrorCode,
|
|
570
|
+
McpError
|
|
571
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
572
|
+
|
|
573
|
+
// src/tasks/config.ts
|
|
574
|
+
var DEFAULT_TTL_MS2 = 5 * 60 * 1e3;
|
|
575
|
+
var DEFAULT_MAX_KEEP_ALIVE_MS = 15 * 60 * 1e3;
|
|
576
|
+
var MIN_TASK_TTL_MS = 1e3;
|
|
577
|
+
var DEFAULT_POLL_FREQUENCY_MS = 1500;
|
|
578
|
+
var MIN_POLL_FREQUENCY_MS = 500;
|
|
579
|
+
var DEFAULT_MAX_PER_CLIENT = 20;
|
|
580
|
+
var DEFAULT_MAX_TOTAL = 200;
|
|
581
|
+
function getTasksConfig() {
|
|
582
|
+
const enabled = readBool("MCP_TASKS_ENABLED");
|
|
583
|
+
const maxKeepAliveMs = Math.max(
|
|
584
|
+
readPositiveInt("MCP_TASKS_MAX_KEEP_ALIVE_MS", DEFAULT_MAX_KEEP_ALIVE_MS),
|
|
585
|
+
MIN_TASK_TTL_MS
|
|
586
|
+
);
|
|
587
|
+
const defaultTtlMs = Math.min(
|
|
588
|
+
Math.max(readPositiveInt("MCP_TASKS_DEFAULT_TTL_MS", DEFAULT_TTL_MS2), MIN_TASK_TTL_MS),
|
|
589
|
+
maxKeepAliveMs
|
|
590
|
+
);
|
|
591
|
+
const defaultPollFrequencyMs = Math.max(
|
|
592
|
+
readPositiveInt("MCP_TASKS_DEFAULT_POLL_FREQUENCY_MS", DEFAULT_POLL_FREQUENCY_MS),
|
|
593
|
+
MIN_POLL_FREQUENCY_MS
|
|
594
|
+
);
|
|
595
|
+
const maxPerClient = readPositiveInt("MCP_TASKS_MAX_PER_CLIENT", DEFAULT_MAX_PER_CLIENT);
|
|
596
|
+
const maxTotal = readPositiveInt("MCP_TASKS_MAX_TOTAL", DEFAULT_MAX_TOTAL);
|
|
597
|
+
return {
|
|
598
|
+
enabled,
|
|
599
|
+
defaultTtlMs,
|
|
600
|
+
maxKeepAliveMs,
|
|
601
|
+
defaultPollFrequencyMs,
|
|
602
|
+
maxPerClient,
|
|
603
|
+
maxTotal
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/tasks/store.ts
|
|
608
|
+
var _globalStore = null;
|
|
609
|
+
function getGlobalStore() {
|
|
610
|
+
if (_globalStore === null) {
|
|
611
|
+
_globalStore = new InMemoryTaskStore();
|
|
612
|
+
}
|
|
613
|
+
return _globalStore;
|
|
614
|
+
}
|
|
615
|
+
var owners = /* @__PURE__ */ new Map();
|
|
616
|
+
var abortControllers = /* @__PURE__ */ new Map();
|
|
617
|
+
function registerAbortController(taskId, controller) {
|
|
618
|
+
abortControllers.set(taskId, controller);
|
|
619
|
+
}
|
|
620
|
+
function countPerClient(clientId) {
|
|
621
|
+
let n = 0;
|
|
622
|
+
for (const owner of owners.values()) {
|
|
623
|
+
if (owner === clientId) n++;
|
|
624
|
+
}
|
|
625
|
+
return n;
|
|
626
|
+
}
|
|
627
|
+
function createScopedTaskStore(clientId) {
|
|
628
|
+
if (!clientId) {
|
|
629
|
+
throw new Error("createScopedTaskStore: clientId is required");
|
|
630
|
+
}
|
|
631
|
+
const global = getGlobalStore();
|
|
632
|
+
async function getOwned(taskId) {
|
|
633
|
+
if (owners.get(taskId) !== clientId) return null;
|
|
634
|
+
return global.getTask(taskId);
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
async createTask(taskParams, requestId, request, sessionId) {
|
|
638
|
+
const cfg = getTasksConfig();
|
|
639
|
+
const totalNow = owners.size;
|
|
640
|
+
if (totalNow >= cfg.maxTotal) {
|
|
641
|
+
logEvent("task.rejected", {
|
|
642
|
+
reason: "max_total",
|
|
643
|
+
clientId,
|
|
644
|
+
totalNow,
|
|
645
|
+
cap: cfg.maxTotal
|
|
646
|
+
});
|
|
647
|
+
throw new McpError(ErrorCode.InvalidParams, "Task quota exceeded for this server instance");
|
|
648
|
+
}
|
|
649
|
+
const perClientNow = countPerClient(clientId);
|
|
650
|
+
if (perClientNow >= cfg.maxPerClient) {
|
|
651
|
+
logEvent("task.rejected", {
|
|
652
|
+
reason: "max_per_client",
|
|
653
|
+
clientId,
|
|
654
|
+
perClientNow,
|
|
655
|
+
cap: cfg.maxPerClient
|
|
656
|
+
});
|
|
657
|
+
throw new McpError(ErrorCode.InvalidParams, "Task quota exceeded for this client");
|
|
658
|
+
}
|
|
659
|
+
const requestedTtl = taskParams.ttl;
|
|
660
|
+
const clampedTtl = requestedTtl === null ? cfg.maxKeepAliveMs : Math.max(
|
|
661
|
+
MIN_TASK_TTL_MS,
|
|
662
|
+
Math.min(requestedTtl ?? cfg.defaultTtlMs, cfg.maxKeepAliveMs)
|
|
663
|
+
);
|
|
664
|
+
const requestedPoll = taskParams.pollInterval ?? cfg.defaultPollFrequencyMs;
|
|
665
|
+
const clampedPoll = Math.max(cfg.defaultPollFrequencyMs, Math.floor(requestedPoll));
|
|
666
|
+
const task = await global.createTask(
|
|
667
|
+
{ ttl: clampedTtl, pollInterval: clampedPoll, context: taskParams.context },
|
|
668
|
+
requestId,
|
|
669
|
+
request,
|
|
670
|
+
sessionId
|
|
671
|
+
);
|
|
672
|
+
owners.set(task.taskId, clientId);
|
|
673
|
+
const timer = setTimeout(() => {
|
|
674
|
+
owners.delete(task.taskId);
|
|
675
|
+
abortControllers.delete(task.taskId);
|
|
676
|
+
logEvent("task.evicted", { taskId: task.taskId, clientId, reason: "ttl" });
|
|
677
|
+
}, clampedTtl);
|
|
678
|
+
timer.unref?.();
|
|
679
|
+
logEvent("task.created", {
|
|
680
|
+
taskId: task.taskId,
|
|
681
|
+
clientId,
|
|
682
|
+
ttl: clampedTtl,
|
|
683
|
+
pollInterval: clampedPoll,
|
|
684
|
+
method: typeof request.method === "string" ? request.method : "unknown"
|
|
685
|
+
});
|
|
686
|
+
return task;
|
|
687
|
+
},
|
|
688
|
+
async getTask(taskId) {
|
|
689
|
+
return getOwned(taskId);
|
|
690
|
+
},
|
|
691
|
+
async storeTaskResult(taskId, status, result, sessionId) {
|
|
692
|
+
if (owners.get(taskId) !== clientId) {
|
|
693
|
+
throw new McpError(ErrorCode.InvalidParams, "Task not found");
|
|
694
|
+
}
|
|
695
|
+
logEvent("task.transition", { taskId, clientId, status });
|
|
696
|
+
await global.storeTaskResult(taskId, status, result, sessionId);
|
|
697
|
+
},
|
|
698
|
+
async getTaskResult(taskId, sessionId) {
|
|
699
|
+
if (owners.get(taskId) !== clientId) {
|
|
700
|
+
throw new McpError(ErrorCode.InvalidParams, "Task not found");
|
|
701
|
+
}
|
|
702
|
+
return global.getTaskResult(taskId, sessionId);
|
|
703
|
+
},
|
|
704
|
+
async updateTaskStatus(taskId, status, statusMessage, sessionId) {
|
|
705
|
+
if (owners.get(taskId) !== clientId) {
|
|
706
|
+
throw new McpError(ErrorCode.InvalidParams, "Task not found");
|
|
707
|
+
}
|
|
708
|
+
logEvent("task.transition", { taskId, clientId, status, statusMessage });
|
|
709
|
+
await global.updateTaskStatus(taskId, status, statusMessage, sessionId);
|
|
710
|
+
if (status === "cancelled") {
|
|
711
|
+
const ctrl = abortControllers.get(taskId);
|
|
712
|
+
if (ctrl && !ctrl.signal.aborted) ctrl.abort();
|
|
713
|
+
}
|
|
714
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
715
|
+
abortControllers.delete(taskId);
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
async listTasks(cursor, sessionId) {
|
|
719
|
+
const page = await global.listTasks(cursor, sessionId);
|
|
720
|
+
const filtered = page.tasks.filter((t) => owners.get(t.taskId) === clientId);
|
|
721
|
+
return page.nextCursor ? { tasks: filtered, nextCursor: page.nextCursor } : { tasks: filtered };
|
|
722
|
+
}
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
|
|
399
726
|
// src/server/register-tool.ts
|
|
727
|
+
var READ_PREFIXES = ["search_", "filter_", "get_", "list_", "show_", "run_"];
|
|
728
|
+
var DESTRUCTIVE_NON_DELETE = /* @__PURE__ */ new Set(["remove_track", "remove_additional_party"]);
|
|
729
|
+
function isDestructive(name) {
|
|
730
|
+
return name.startsWith("delete_") || DESTRUCTIVE_NON_DELETE.has(name);
|
|
731
|
+
}
|
|
732
|
+
function inferAnnotations(name) {
|
|
733
|
+
if (READ_PREFIXES.some((p) => name.startsWith(p))) {
|
|
734
|
+
return { readOnlyHint: true };
|
|
735
|
+
}
|
|
736
|
+
if (isDestructive(name)) {
|
|
737
|
+
return { destructiveHint: true };
|
|
738
|
+
}
|
|
739
|
+
return void 0;
|
|
740
|
+
}
|
|
741
|
+
function argFieldNames(input) {
|
|
742
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) return [];
|
|
743
|
+
return Object.keys(input);
|
|
744
|
+
}
|
|
745
|
+
function emitToolCall(opts) {
|
|
746
|
+
logEvent("tool.call", {
|
|
747
|
+
tool: opts.tool,
|
|
748
|
+
...opts.clientId ? { clientId: opts.clientId } : {},
|
|
749
|
+
argFields: opts.argFields,
|
|
750
|
+
durationMs: Date.now() - opts.startedAt,
|
|
751
|
+
outcome: opts.outcome,
|
|
752
|
+
...opts.taskAugmented ? { taskAugmented: true } : {}
|
|
753
|
+
});
|
|
754
|
+
}
|
|
400
755
|
function wrapAsText(result) {
|
|
401
756
|
return {
|
|
402
757
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
@@ -404,10 +759,93 @@ function wrapAsText(result) {
|
|
|
404
759
|
}
|
|
405
760
|
function registerTool(server2, name, description, schema, handler) {
|
|
406
761
|
const registerWithSchema = server2.registerTool.bind(server2);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
762
|
+
const annotations = inferAnnotations(name);
|
|
763
|
+
registerWithSchema(
|
|
764
|
+
name,
|
|
765
|
+
{ description, inputSchema: schema, ...annotations ? { annotations } : {} },
|
|
766
|
+
async (input) => {
|
|
767
|
+
const startedAt = Date.now();
|
|
768
|
+
const argFields = argFieldNames(input);
|
|
769
|
+
const clientId = getRequestContext()?.clientId;
|
|
770
|
+
try {
|
|
771
|
+
const result = await handler(input);
|
|
772
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "success" });
|
|
773
|
+
return wrapAsText(result);
|
|
774
|
+
} catch (err) {
|
|
775
|
+
emitToolCall({ tool: name, clientId, argFields, startedAt, outcome: "error" });
|
|
776
|
+
throw err;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
function registerToolTask(server2, name, description, schema, handler) {
|
|
782
|
+
const registerWithSchema = server2.experimental.tasks.registerToolTask.bind(
|
|
783
|
+
server2.experimental.tasks
|
|
784
|
+
);
|
|
785
|
+
const annotations = inferAnnotations(name);
|
|
786
|
+
registerWithSchema(
|
|
787
|
+
name,
|
|
788
|
+
{
|
|
789
|
+
description,
|
|
790
|
+
inputSchema: schema,
|
|
791
|
+
execution: { taskSupport: "optional" },
|
|
792
|
+
...annotations ? { annotations } : {}
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
createTask: async (input, extra) => {
|
|
796
|
+
const task = await extra.taskStore.createTask({
|
|
797
|
+
ttl: extra.taskRequestedTtl
|
|
798
|
+
});
|
|
799
|
+
const abortController = new AbortController();
|
|
800
|
+
registerAbortController(task.taskId, abortController);
|
|
801
|
+
const requestClientId = getRequestContext()?.clientId;
|
|
802
|
+
const argFields = argFieldNames(input);
|
|
803
|
+
void (async () => {
|
|
804
|
+
if (abortController.signal.aborted) return;
|
|
805
|
+
try {
|
|
806
|
+
await extra.taskStore.updateTaskStatus(task.taskId, "working");
|
|
807
|
+
} catch {
|
|
808
|
+
}
|
|
809
|
+
const handlerStart = Date.now();
|
|
810
|
+
let payload;
|
|
811
|
+
let outcome = "success";
|
|
812
|
+
try {
|
|
813
|
+
const result = await handler(input, {
|
|
814
|
+
signal: abortController.signal
|
|
815
|
+
});
|
|
816
|
+
payload = wrapAsText(result);
|
|
817
|
+
} catch (err) {
|
|
818
|
+
if (abortController.signal.aborted) return;
|
|
819
|
+
outcome = "error";
|
|
820
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
821
|
+
payload = {
|
|
822
|
+
content: [{ type: "text", text: message }],
|
|
823
|
+
isError: true
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
emitToolCall({
|
|
827
|
+
tool: name,
|
|
828
|
+
clientId: requestClientId,
|
|
829
|
+
argFields,
|
|
830
|
+
startedAt: handlerStart,
|
|
831
|
+
outcome,
|
|
832
|
+
taskAugmented: true
|
|
833
|
+
});
|
|
834
|
+
if (abortController.signal.aborted) return;
|
|
835
|
+
try {
|
|
836
|
+
await extra.taskStore.storeTaskResult(task.taskId, "completed", payload);
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
})();
|
|
840
|
+
return { task };
|
|
841
|
+
},
|
|
842
|
+
getTask: async (_input, extra) => extra.taskStore.getTask(extra.taskId),
|
|
843
|
+
getTaskResult: async (_input, extra) => {
|
|
844
|
+
const r = await extra.taskStore.getTaskResult(extra.taskId);
|
|
845
|
+
return r;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
);
|
|
411
849
|
}
|
|
412
850
|
|
|
413
851
|
// src/tools/parties.ts
|
|
@@ -424,6 +862,98 @@ function confirmFlag() {
|
|
|
424
862
|
return z.literal(true, { error: () => CONFIRM_REQUIRED_MESSAGE });
|
|
425
863
|
}
|
|
426
864
|
|
|
865
|
+
// src/capsule/batch.ts
|
|
866
|
+
function chunk(arr, size) {
|
|
867
|
+
if (size <= 0) throw new Error("chunk size must be positive");
|
|
868
|
+
const out = [];
|
|
869
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
870
|
+
out.push(arr.slice(i, i + size));
|
|
871
|
+
}
|
|
872
|
+
return out;
|
|
873
|
+
}
|
|
874
|
+
var DEFAULT_CONCURRENCY = 5;
|
|
875
|
+
var MAX_CONCURRENCY = 50;
|
|
876
|
+
function getBatchConcurrency() {
|
|
877
|
+
return Math.min(
|
|
878
|
+
readPositiveInt("CAPSULE_MCP_BATCH_CONCURRENCY", DEFAULT_CONCURRENCY),
|
|
879
|
+
MAX_CONCURRENCY
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
async function batchExecute(tool, items, action, options = {}) {
|
|
883
|
+
const concurrency = getBatchConcurrency();
|
|
884
|
+
const results = new Array(items.length);
|
|
885
|
+
const startedAt = Date.now();
|
|
886
|
+
const signal = options.signal;
|
|
887
|
+
let cursor = 0;
|
|
888
|
+
async function worker() {
|
|
889
|
+
while (true) {
|
|
890
|
+
const i = cursor;
|
|
891
|
+
cursor += 1;
|
|
892
|
+
if (i >= items.length) return;
|
|
893
|
+
if (signal?.aborted) {
|
|
894
|
+
results[i] = {
|
|
895
|
+
ok: false,
|
|
896
|
+
error: { message: "cancelled by tasks/cancel" }
|
|
897
|
+
};
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
try {
|
|
901
|
+
const result = await action(items[i], i);
|
|
902
|
+
results[i] = { ok: true, result };
|
|
903
|
+
} catch (err) {
|
|
904
|
+
results[i] = { ok: false, error: extractError(err) };
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const workers = [];
|
|
909
|
+
for (let w = 0; w < Math.min(concurrency, items.length); w++) {
|
|
910
|
+
workers.push(worker());
|
|
911
|
+
}
|
|
912
|
+
await Promise.all(workers);
|
|
913
|
+
const succeeded = results.filter((r) => r.ok).length;
|
|
914
|
+
const failed = results.length - succeeded;
|
|
915
|
+
const summary = { total: results.length, succeeded, failed };
|
|
916
|
+
const failureReasons = logVerbose() ? topFailureReasons(results, 5) : [];
|
|
917
|
+
logEvent(
|
|
918
|
+
"batch.complete",
|
|
919
|
+
{
|
|
920
|
+
tool,
|
|
921
|
+
total: summary.total,
|
|
922
|
+
succeeded: summary.succeeded,
|
|
923
|
+
failed: summary.failed,
|
|
924
|
+
durationMs: Date.now() - startedAt,
|
|
925
|
+
concurrency,
|
|
926
|
+
...failureReasons.length > 0 ? { failureReasons } : {}
|
|
927
|
+
},
|
|
928
|
+
{ force: true }
|
|
929
|
+
);
|
|
930
|
+
return { results, summary };
|
|
931
|
+
}
|
|
932
|
+
function extractError(err) {
|
|
933
|
+
if (err instanceof Error) {
|
|
934
|
+
const maybeStatus = err.status;
|
|
935
|
+
return {
|
|
936
|
+
...typeof maybeStatus === "number" ? { status: maybeStatus } : {},
|
|
937
|
+
message: err.message
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
return { message: String(err) };
|
|
941
|
+
}
|
|
942
|
+
function topFailureReasons(results, n) {
|
|
943
|
+
const counts = /* @__PURE__ */ new Map();
|
|
944
|
+
for (const r of results) {
|
|
945
|
+
if (r.ok) continue;
|
|
946
|
+
const key = `${r.error.status ?? "?"}::${r.error.message}`;
|
|
947
|
+
const existing = counts.get(key);
|
|
948
|
+
if (existing) {
|
|
949
|
+
existing.count += 1;
|
|
950
|
+
} else {
|
|
951
|
+
counts.set(key, { ...r.error, count: 1 });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return Array.from(counts.values()).sort((a, b) => b.count - a.count).slice(0, n);
|
|
955
|
+
}
|
|
956
|
+
|
|
427
957
|
// src/capsule/idempotent.ts
|
|
428
958
|
var isCapsule404 = (err) => err instanceof CapsuleApiError && err.status === 404;
|
|
429
959
|
var isCapsuleTagNotFound = (err) => err instanceof CapsuleApiError && err.status === 422 && /tag not found/i.test(err.message);
|
|
@@ -562,14 +1092,26 @@ async function getParty(input) {
|
|
|
562
1092
|
return data;
|
|
563
1093
|
}
|
|
564
1094
|
var getPartiesSchema = z3.object({
|
|
565
|
-
ids: z3.array(z3.number().int().positive()).min(1).max(
|
|
1095
|
+
ids: z3.array(z3.number().int().positive()).min(1).max(50).describe(
|
|
1096
|
+
"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."
|
|
1097
|
+
),
|
|
566
1098
|
embed: z3.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
567
1099
|
});
|
|
568
1100
|
async function getParties(input) {
|
|
569
|
-
const {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1101
|
+
const { ids, embed } = input;
|
|
1102
|
+
if (ids.length <= 10) {
|
|
1103
|
+
const { data } = await capsuleGet(`/parties/${ids.join(",")}`, {
|
|
1104
|
+
embed
|
|
1105
|
+
});
|
|
1106
|
+
return data;
|
|
1107
|
+
}
|
|
1108
|
+
const chunks = chunk(ids, 10);
|
|
1109
|
+
const responses = await Promise.all(
|
|
1110
|
+
chunks.map(
|
|
1111
|
+
(chunkIds) => capsuleGet(`/parties/${chunkIds.join(",")}`, { embed })
|
|
1112
|
+
)
|
|
1113
|
+
);
|
|
1114
|
+
return { parties: responses.flatMap((r) => r.data.parties) };
|
|
573
1115
|
}
|
|
574
1116
|
var listPartyOpportunitiesSchema = z3.object({
|
|
575
1117
|
partyId: z3.number().int().positive(),
|
|
@@ -653,6 +1195,14 @@ async function updateParty(input) {
|
|
|
653
1195
|
if (mappedFields !== void 0) body["fields"] = mappedFields;
|
|
654
1196
|
return capsulePut(`/parties/${id}`, { party: body });
|
|
655
1197
|
}
|
|
1198
|
+
var batchUpdatePartySchema = z3.object({
|
|
1199
|
+
items: z3.array(updatePartySchema).min(1).max(50).describe(
|
|
1200
|
+
"Array of 1\u201350 update_party inputs. Each item is the same shape as a single update_party call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget (~4000 req/h)."
|
|
1201
|
+
)
|
|
1202
|
+
});
|
|
1203
|
+
async function batchUpdateParty(input, opts = {}) {
|
|
1204
|
+
return batchExecute("batch_update_party", input.items, (item) => updateParty(item), opts);
|
|
1205
|
+
}
|
|
656
1206
|
var deletePartySchema = z3.object({
|
|
657
1207
|
id: z3.number().int().positive(),
|
|
658
1208
|
confirm: confirmFlag().describe(
|
|
@@ -855,15 +1405,29 @@ async function getOpportunity(input) {
|
|
|
855
1405
|
return data;
|
|
856
1406
|
}
|
|
857
1407
|
var getOpportunitiesSchema = z4.object({
|
|
858
|
-
ids: z4.array(z4.number().int().positive()).min(1).max(
|
|
1408
|
+
ids: z4.array(z4.number().int().positive()).min(1).max(50).describe(
|
|
1409
|
+
"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."
|
|
1410
|
+
),
|
|
859
1411
|
embed: z4.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
860
1412
|
});
|
|
861
1413
|
async function getOpportunities(input) {
|
|
862
|
-
const {
|
|
863
|
-
|
|
864
|
-
{
|
|
1414
|
+
const { ids, embed } = input;
|
|
1415
|
+
if (ids.length <= 10) {
|
|
1416
|
+
const { data } = await capsuleGet(
|
|
1417
|
+
`/opportunities/${ids.join(",")}`,
|
|
1418
|
+
{ embed }
|
|
1419
|
+
);
|
|
1420
|
+
return data;
|
|
1421
|
+
}
|
|
1422
|
+
const chunks = chunk(ids, 10);
|
|
1423
|
+
const responses = await Promise.all(
|
|
1424
|
+
chunks.map(
|
|
1425
|
+
(chunkIds) => capsuleGet(`/opportunities/${chunkIds.join(",")}`, {
|
|
1426
|
+
embed
|
|
1427
|
+
})
|
|
1428
|
+
)
|
|
865
1429
|
);
|
|
866
|
-
return data;
|
|
1430
|
+
return { opportunities: responses.flatMap((r) => r.data.opportunities) };
|
|
867
1431
|
}
|
|
868
1432
|
var createOpportunitySchema = z4.object({
|
|
869
1433
|
name: z4.string().min(1),
|
|
@@ -924,6 +1488,19 @@ async function updateOpportunity(input) {
|
|
|
924
1488
|
opportunity: body
|
|
925
1489
|
});
|
|
926
1490
|
}
|
|
1491
|
+
var batchUpdateOpportunitySchema = z4.object({
|
|
1492
|
+
items: z4.array(updateOpportunitySchema).min(1).max(50).describe(
|
|
1493
|
+
"Array of 1\u201350 update_opportunity inputs. Each item is the same shape as a single update_opportunity call \u2014 id is required, every other field is optional. Capped at 50 so a single tool call can't burn an outsized share of Capsule's hourly per-token rate budget."
|
|
1494
|
+
)
|
|
1495
|
+
});
|
|
1496
|
+
async function batchUpdateOpportunity(input, opts = {}) {
|
|
1497
|
+
return batchExecute(
|
|
1498
|
+
"batch_update_opportunity",
|
|
1499
|
+
input.items,
|
|
1500
|
+
(item) => updateOpportunity(item),
|
|
1501
|
+
opts
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
927
1504
|
var deleteOpportunitySchema = z4.object({
|
|
928
1505
|
id: z4.number().int().positive(),
|
|
929
1506
|
confirm: confirmFlag().describe(
|
|
@@ -969,14 +1546,26 @@ async function getProject(input) {
|
|
|
969
1546
|
return data;
|
|
970
1547
|
}
|
|
971
1548
|
var getProjectsSchema = z5.object({
|
|
972
|
-
ids: z5.array(z5.number().int().positive()).min(1).max(
|
|
1549
|
+
ids: z5.array(z5.number().int().positive()).min(1).max(50).describe(
|
|
1550
|
+
"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."
|
|
1551
|
+
),
|
|
973
1552
|
embed: z5.string().optional().describe(EMBED_TAGS_FIELDS_DESCRIPTION)
|
|
974
1553
|
});
|
|
975
1554
|
async function getProjects(input) {
|
|
976
|
-
const {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1555
|
+
const { ids, embed } = input;
|
|
1556
|
+
if (ids.length <= 10) {
|
|
1557
|
+
const { data } = await capsuleGet(`/kases/${ids.join(",")}`, {
|
|
1558
|
+
embed
|
|
1559
|
+
});
|
|
1560
|
+
return data;
|
|
1561
|
+
}
|
|
1562
|
+
const chunks = chunk(ids, 10);
|
|
1563
|
+
const responses = await Promise.all(
|
|
1564
|
+
chunks.map(
|
|
1565
|
+
(chunkIds) => capsuleGet(`/kases/${chunkIds.join(",")}`, { embed })
|
|
1566
|
+
)
|
|
1567
|
+
);
|
|
1568
|
+
return { kases: responses.flatMap((r) => r.data.kases) };
|
|
980
1569
|
}
|
|
981
1570
|
var createProjectSchema = z5.object({
|
|
982
1571
|
name: z5.string().min(1),
|
|
@@ -1104,11 +1693,21 @@ async function getTask(input) {
|
|
|
1104
1693
|
return data;
|
|
1105
1694
|
}
|
|
1106
1695
|
var getTasksSchema = z6.object({
|
|
1107
|
-
ids: z6.array(z6.number().int().positive()).min(1).max(
|
|
1696
|
+
ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
|
|
1697
|
+
"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."
|
|
1698
|
+
)
|
|
1108
1699
|
});
|
|
1109
1700
|
async function getTasks(input) {
|
|
1110
|
-
const {
|
|
1111
|
-
|
|
1701
|
+
const { ids } = input;
|
|
1702
|
+
if (ids.length <= 10) {
|
|
1703
|
+
const { data } = await capsuleGet(`/tasks/${ids.join(",")}`);
|
|
1704
|
+
return data;
|
|
1705
|
+
}
|
|
1706
|
+
const chunks = chunk(ids, 10);
|
|
1707
|
+
const responses = await Promise.all(
|
|
1708
|
+
chunks.map((chunkIds) => capsuleGet(`/tasks/${chunkIds.join(",")}`))
|
|
1709
|
+
);
|
|
1710
|
+
return { tasks: responses.flatMap((r) => r.data.tasks) };
|
|
1112
1711
|
}
|
|
1113
1712
|
var createTaskSchema = z6.object({
|
|
1114
1713
|
description: z6.string().min(1),
|
|
@@ -1168,6 +1767,14 @@ async function completeTask(input) {
|
|
|
1168
1767
|
task: { status: "COMPLETED" }
|
|
1169
1768
|
});
|
|
1170
1769
|
}
|
|
1770
|
+
var batchCompleteTaskSchema = z6.object({
|
|
1771
|
+
ids: z6.array(z6.number().int().positive()).min(1).max(50).describe(
|
|
1772
|
+
"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."
|
|
1773
|
+
)
|
|
1774
|
+
});
|
|
1775
|
+
async function batchCompleteTask(input, opts = {}) {
|
|
1776
|
+
return batchExecute("batch_complete_task", input.ids, (id) => completeTask({ id }), opts);
|
|
1777
|
+
}
|
|
1171
1778
|
var deleteTaskSchema = z6.object({
|
|
1172
1779
|
id: z6.number().int().positive(),
|
|
1173
1780
|
confirm: confirmFlag().describe(
|
|
@@ -1314,7 +1921,7 @@ var paginationFields = {
|
|
|
1314
1921
|
};
|
|
1315
1922
|
var listPipelinesSchema = z8.object({ ...paginationFields });
|
|
1316
1923
|
async function listPipelines(input) {
|
|
1317
|
-
const { data, nextPage } = await
|
|
1924
|
+
const { data, nextPage } = await capsuleGetCached("/pipelines", {
|
|
1318
1925
|
page: input.page ?? 1,
|
|
1319
1926
|
perPage: input.perPage ?? 100
|
|
1320
1927
|
});
|
|
@@ -1325,7 +1932,7 @@ var listMilestonesSchema = z8.object({
|
|
|
1325
1932
|
...paginationFields
|
|
1326
1933
|
});
|
|
1327
1934
|
async function listMilestones(input) {
|
|
1328
|
-
const { data, nextPage } = await
|
|
1935
|
+
const { data, nextPage } = await capsuleGetCached(
|
|
1329
1936
|
`/pipelines/${input.pipelineId}/milestones`,
|
|
1330
1937
|
{ page: input.page ?? 1, perPage: input.perPage ?? 100 }
|
|
1331
1938
|
);
|
|
@@ -1340,7 +1947,7 @@ var paginationFields2 = {
|
|
|
1340
1947
|
};
|
|
1341
1948
|
var listBoardsSchema = z9.object({ ...paginationFields2 });
|
|
1342
1949
|
async function listBoards(input) {
|
|
1343
|
-
const { data, nextPage } = await
|
|
1950
|
+
const { data, nextPage } = await capsuleGetCached("/boards", {
|
|
1344
1951
|
page: input.page ?? 1,
|
|
1345
1952
|
perPage: input.perPage ?? 100
|
|
1346
1953
|
});
|
|
@@ -1354,7 +1961,7 @@ var listStagesSchema = z9.object({
|
|
|
1354
1961
|
});
|
|
1355
1962
|
async function listStages(input) {
|
|
1356
1963
|
const path = input.boardId !== void 0 ? `/boards/${input.boardId}/stages` : "/stages";
|
|
1357
|
-
const { data, nextPage } = await
|
|
1964
|
+
const { data, nextPage } = await capsuleGetCached(path, {
|
|
1358
1965
|
page: input.page ?? 1,
|
|
1359
1966
|
perPage: input.perPage ?? 100
|
|
1360
1967
|
});
|
|
@@ -1381,7 +1988,7 @@ var listTagsSchema = z10.object({
|
|
|
1381
1988
|
});
|
|
1382
1989
|
async function listTags(input) {
|
|
1383
1990
|
const path = TAG_LIST_PATH[input.entity];
|
|
1384
|
-
const { data, nextPage } = await
|
|
1991
|
+
const { data, nextPage } = await capsuleGetCached(path, {
|
|
1385
1992
|
page: input.page ?? 1,
|
|
1386
1993
|
perPage: input.perPage ?? 100
|
|
1387
1994
|
});
|
|
@@ -1397,9 +2004,11 @@ var addTagSchema = z10.object({
|
|
|
1397
2004
|
async function addTag(input) {
|
|
1398
2005
|
const { entity, entityId, tagName } = input;
|
|
1399
2006
|
const wrapper = ENTITY_TO_WRAPPER[entity];
|
|
1400
|
-
|
|
2007
|
+
const result = await capsulePut(`/${entity}/${entityId}`, {
|
|
1401
2008
|
[wrapper]: { tags: [{ name: tagName }] }
|
|
1402
2009
|
});
|
|
2010
|
+
invalidateByPrefix(TAG_LIST_PATH[entity], "add_tag");
|
|
2011
|
+
return result;
|
|
1403
2012
|
}
|
|
1404
2013
|
var removeTagByIdSchema = z10.object({
|
|
1405
2014
|
entity: TagEntity,
|
|
@@ -1411,17 +2020,17 @@ var removeTagByIdSchema = z10.object({
|
|
|
1411
2020
|
async function removeTagById(input) {
|
|
1412
2021
|
const { entity, entityId, tagId } = input;
|
|
1413
2022
|
const wrapper = ENTITY_TO_WRAPPER[entity];
|
|
1414
|
-
|
|
2023
|
+
const result = await idempotentWithResult(
|
|
1415
2024
|
() => capsulePut(`/${entity}/${entityId}`, {
|
|
1416
2025
|
[wrapper]: { tags: [{ id: tagId, _delete: true }] }
|
|
1417
2026
|
}),
|
|
1418
|
-
(
|
|
2027
|
+
(result2) => ({
|
|
1419
2028
|
removed: true,
|
|
1420
2029
|
alreadyRemoved: false,
|
|
1421
2030
|
entity,
|
|
1422
2031
|
entityId,
|
|
1423
2032
|
tagId,
|
|
1424
|
-
...
|
|
2033
|
+
...result2
|
|
1425
2034
|
}),
|
|
1426
2035
|
() => ({ removed: true, alreadyRemoved: true, entity, entityId, tagId }),
|
|
1427
2036
|
// Tag detach uses PUT with _delete: true and 422s with "tag not
|
|
@@ -1429,6 +2038,24 @@ async function removeTagById(input) {
|
|
|
1429
2038
|
// 404. Other 422s with different wording still surface.
|
|
1430
2039
|
isCapsuleTagNotFound
|
|
1431
2040
|
);
|
|
2041
|
+
invalidateByPrefix(TAG_LIST_PATH[entity], "remove_tag_by_id");
|
|
2042
|
+
return result;
|
|
2043
|
+
}
|
|
2044
|
+
var batchAddTagSchema = z10.object({
|
|
2045
|
+
items: z10.array(addTagSchema).min(1).max(50).describe(
|
|
2046
|
+
"Array of 1\u201350 add_tag inputs. Useful for mass-tagging \u2014 e.g. 'tag these 20 contacts as RSAC26'. Each item is the same shape as a single add_tag call. The list_tags cache is invalidated for each affected entity type. Capped at 50."
|
|
2047
|
+
)
|
|
2048
|
+
});
|
|
2049
|
+
async function batchAddTag(input, opts = {}) {
|
|
2050
|
+
return batchExecute("batch_add_tag", input.items, (item) => addTag(item), opts);
|
|
2051
|
+
}
|
|
2052
|
+
var batchRemoveTagByIdSchema = z10.object({
|
|
2053
|
+
items: z10.array(removeTagByIdSchema).min(1).max(50).describe(
|
|
2054
|
+
"Array of 1\u201350 remove_tag_by_id inputs. Each item is the same shape as a single remove_tag_by_id call. Detaches the tag from each specified entity; the tag definition itself persists in the tenant. Capped at 50."
|
|
2055
|
+
)
|
|
2056
|
+
});
|
|
2057
|
+
async function batchRemoveTagById(input, opts = {}) {
|
|
2058
|
+
return batchExecute("batch_remove_tag_by_id", input.items, (item) => removeTagById(item), opts);
|
|
1432
2059
|
}
|
|
1433
2060
|
|
|
1434
2061
|
// src/tools/users.ts
|
|
@@ -1438,7 +2065,7 @@ var listUsersSchema = z11.object({
|
|
|
1438
2065
|
perPage: z11.number().int().min(1).max(100).optional()
|
|
1439
2066
|
});
|
|
1440
2067
|
async function listUsers(input) {
|
|
1441
|
-
const { data, nextPage } = await
|
|
2068
|
+
const { data, nextPage } = await capsuleGetCached("/users", {
|
|
1442
2069
|
page: input.page ?? 1,
|
|
1443
2070
|
perPage: input.perPage ?? 100
|
|
1444
2071
|
});
|
|
@@ -1507,7 +2134,7 @@ var paginationFields3 = {
|
|
|
1507
2134
|
};
|
|
1508
2135
|
var listTeamsSchema = z13.object({ ...paginationFields3 });
|
|
1509
2136
|
async function listTeams(input) {
|
|
1510
|
-
const { data, nextPage } = await
|
|
2137
|
+
const { data, nextPage } = await capsuleGetCached("/teams", {
|
|
1511
2138
|
page: input.page ?? 1,
|
|
1512
2139
|
perPage: input.perPage ?? 100
|
|
1513
2140
|
});
|
|
@@ -1515,7 +2142,7 @@ async function listTeams(input) {
|
|
|
1515
2142
|
}
|
|
1516
2143
|
var listLostReasonsSchema = z13.object({ ...paginationFields3 });
|
|
1517
2144
|
async function listLostReasons(input) {
|
|
1518
|
-
const { data, nextPage } = await
|
|
2145
|
+
const { data, nextPage } = await capsuleGetCached("/lostreasons", {
|
|
1519
2146
|
page: input.page ?? 1,
|
|
1520
2147
|
perPage: input.perPage ?? 100
|
|
1521
2148
|
});
|
|
@@ -1523,20 +2150,23 @@ async function listLostReasons(input) {
|
|
|
1523
2150
|
}
|
|
1524
2151
|
var listActivityTypesSchema = z13.object({ ...paginationFields3 });
|
|
1525
2152
|
async function listActivityTypes(input) {
|
|
1526
|
-
const { data, nextPage } = await
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
2153
|
+
const { data, nextPage } = await capsuleGetCached(
|
|
2154
|
+
"/activitytypes",
|
|
2155
|
+
{
|
|
2156
|
+
page: input.page ?? 1,
|
|
2157
|
+
perPage: input.perPage ?? 100
|
|
2158
|
+
}
|
|
2159
|
+
);
|
|
1530
2160
|
return { ...data, nextPage };
|
|
1531
2161
|
}
|
|
1532
2162
|
var getSiteSchema = z13.object({});
|
|
1533
2163
|
async function getSite(_input) {
|
|
1534
|
-
const { data } = await
|
|
2164
|
+
const { data } = await capsuleGetCached("/site");
|
|
1535
2165
|
return data;
|
|
1536
2166
|
}
|
|
1537
2167
|
var listTrackDefinitionsSchema = z13.object({ ...paginationFields3 });
|
|
1538
2168
|
async function listTrackDefinitions(input) {
|
|
1539
|
-
const { data, nextPage } = await
|
|
2169
|
+
const { data, nextPage } = await capsuleGetCached(
|
|
1540
2170
|
"/trackdefinitions",
|
|
1541
2171
|
{ page: input.page ?? 1, perPage: input.perPage ?? 100 }
|
|
1542
2172
|
);
|
|
@@ -1544,7 +2174,7 @@ async function listTrackDefinitions(input) {
|
|
|
1544
2174
|
}
|
|
1545
2175
|
var listCategoriesSchema = z13.object({ ...paginationFields3 });
|
|
1546
2176
|
async function listCategories(input) {
|
|
1547
|
-
const { data, nextPage } = await
|
|
2177
|
+
const { data, nextPage } = await capsuleGetCached("/categories", {
|
|
1548
2178
|
page: input.page ?? 1,
|
|
1549
2179
|
perPage: input.perPage ?? 100
|
|
1550
2180
|
});
|
|
@@ -1552,7 +2182,7 @@ async function listCategories(input) {
|
|
|
1552
2182
|
}
|
|
1553
2183
|
var listGoalsSchema = z13.object({ ...paginationFields3 });
|
|
1554
2184
|
async function listGoals(input) {
|
|
1555
|
-
const { data, nextPage } = await
|
|
2185
|
+
const { data, nextPage } = await capsuleGetCached("/goals", {
|
|
1556
2186
|
page: input.page ?? 1,
|
|
1557
2187
|
perPage: input.perPage ?? 100
|
|
1558
2188
|
});
|
|
@@ -1711,7 +2341,7 @@ var listCustomFieldsSchema = z16.object({
|
|
|
1711
2341
|
entity: CustomFieldEntity
|
|
1712
2342
|
});
|
|
1713
2343
|
async function listCustomFields(input) {
|
|
1714
|
-
const { data } = await
|
|
2344
|
+
const { data } = await capsuleGetCached(
|
|
1715
2345
|
`/${input.entity}/fields/definitions`
|
|
1716
2346
|
);
|
|
1717
2347
|
return data;
|
|
@@ -1721,7 +2351,7 @@ var getCustomFieldSchema = z16.object({
|
|
|
1721
2351
|
fieldId: z16.number().int().positive().describe("Custom field definition id.")
|
|
1722
2352
|
});
|
|
1723
2353
|
async function getCustomField(input) {
|
|
1724
|
-
const { data } = await
|
|
2354
|
+
const { data } = await capsuleGetCached(
|
|
1725
2355
|
`/${input.entity}/fields/definitions/${input.fieldId}`
|
|
1726
2356
|
);
|
|
1727
2357
|
return data;
|
|
@@ -1892,7 +2522,7 @@ var listSavedFiltersSchema = z19.object({
|
|
|
1892
2522
|
entity: EntitySchema
|
|
1893
2523
|
});
|
|
1894
2524
|
async function listSavedFilters(input) {
|
|
1895
|
-
const { data } = await
|
|
2525
|
+
const { data } = await capsuleGetCached(`/${input.entity}/filters`);
|
|
1896
2526
|
return data;
|
|
1897
2527
|
}
|
|
1898
2528
|
var runSavedFilterSchema = z19.object({
|
|
@@ -1911,15 +2541,35 @@ async function runSavedFilter(input) {
|
|
|
1911
2541
|
}
|
|
1912
2542
|
|
|
1913
2543
|
// src/server.ts
|
|
1914
|
-
function createCapsuleMcpServer() {
|
|
2544
|
+
function createCapsuleMcpServer(opts) {
|
|
1915
2545
|
const readOnly = isReadOnly();
|
|
1916
|
-
const
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
2546
|
+
const tasksCfg = getTasksConfig();
|
|
2547
|
+
const tasksWired = tasksCfg.enabled && !!opts?.clientId;
|
|
2548
|
+
const server2 = new McpServer(
|
|
2549
|
+
{
|
|
2550
|
+
name: "capsulemcp",
|
|
2551
|
+
version: "1.6.0",
|
|
2552
|
+
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
2553
|
+
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
2554
|
+
icons: ICONS
|
|
2555
|
+
},
|
|
2556
|
+
tasksWired ? {
|
|
2557
|
+
// tasksWired guards clientId presence; narrow explicitly
|
|
2558
|
+
// for the type-checker rather than using `!`.
|
|
2559
|
+
taskStore: createScopedTaskStore(opts?.clientId ?? ""),
|
|
2560
|
+
capabilities: {
|
|
2561
|
+
tasks: {
|
|
2562
|
+
// The SDK's task capability schema uses {} for "present"
|
|
2563
|
+
// markers, not booleans — see ServerTasksCapabilitySchema
|
|
2564
|
+
// in @modelcontextprotocol/sdk types.ts.
|
|
2565
|
+
list: {},
|
|
2566
|
+
cancel: {},
|
|
2567
|
+
requests: { tools: { call: {} } }
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
} : void 0
|
|
2571
|
+
);
|
|
2572
|
+
const registerBatchTool = tasksWired ? registerToolTask : (s, name, description, schema, handler) => registerTool(s, name, description, schema, (input) => handler(input, {}));
|
|
1923
2573
|
registerTool(
|
|
1924
2574
|
server2,
|
|
1925
2575
|
"search_parties",
|
|
@@ -1937,14 +2587,14 @@ function createCapsuleMcpServer() {
|
|
|
1937
2587
|
registerTool(
|
|
1938
2588
|
server2,
|
|
1939
2589
|
"get_party",
|
|
1940
|
-
"Fetch a single party (person or organisation) by its numeric
|
|
2590
|
+
"Fetch a single party (person or organisation) by its numeric id. Returns the full record including type, name fields, emails, phones, addresses, websites, and any embedded tags or custom fields. Use embed='tags,fields' to include those in one round-trip. For batch fetches of up to 50 parties at once, use get_parties instead.",
|
|
1941
2591
|
getPartySchema,
|
|
1942
2592
|
getParty
|
|
1943
2593
|
);
|
|
1944
2594
|
registerTool(
|
|
1945
2595
|
server2,
|
|
1946
2596
|
"get_parties",
|
|
1947
|
-
"Batch-fetch up to
|
|
2597
|
+
"Batch-fetch up to 50 parties by ID. For 1\u201310 ids this is a single Capsule round trip (native multi-id endpoint); for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged. Use this whenever Claude has several party IDs to avoid N sequential round trips of get_party.",
|
|
1948
2598
|
getPartiesSchema,
|
|
1949
2599
|
getParties
|
|
1950
2600
|
);
|
|
@@ -2005,6 +2655,13 @@ function createCapsuleMcpServer() {
|
|
|
2005
2655
|
updatePartySchema,
|
|
2006
2656
|
updateParty
|
|
2007
2657
|
);
|
|
2658
|
+
registerBatchTool(
|
|
2659
|
+
server2,
|
|
2660
|
+
"batch_update_party",
|
|
2661
|
+
"Update 1\u201350 parties in parallel. Same input shape as update_party but wrapped in an `items` array. Use this \u2014 not N sequential update_party calls \u2014 for any homogeneous multi-record write (mass owner reassignment, bulk metadata corrections, etc.). Capsule has no batch-write API, so the connector fans out parallel HTTP requests with a default concurrency cap of 5 (configurable via CAPSULE_MCP_BATCH_CONCURRENCY). Returns { results: [{ok, ...} per item], summary: {total, succeeded, failed} }. Partial failures are possible \u2014 Capsule has no rollback, so successful items stay applied even if other items 4xx. Read the per-item result array to know which ones need follow-up.",
|
|
2662
|
+
batchUpdatePartySchema,
|
|
2663
|
+
batchUpdateParty
|
|
2664
|
+
);
|
|
2008
2665
|
registerTool(
|
|
2009
2666
|
server2,
|
|
2010
2667
|
"delete_party",
|
|
@@ -2086,14 +2743,14 @@ function createCapsuleMcpServer() {
|
|
|
2086
2743
|
registerTool(
|
|
2087
2744
|
server2,
|
|
2088
2745
|
"get_opportunity",
|
|
2089
|
-
"Fetch a single opportunity by its numeric id. Returns the full record including value, milestone, owner, party, and any embedded tags/custom fields. Use embed='tags,fields' to include those in one round-trip. For batch fetches of up to
|
|
2746
|
+
"Fetch a single opportunity by its numeric id. Returns the full record including value, milestone, owner, party, and any embedded tags/custom fields. Use embed='tags,fields' to include those in one round-trip. For batch fetches of up to 50 opportunities at once, use get_opportunities instead.",
|
|
2090
2747
|
getOpportunitySchema,
|
|
2091
2748
|
getOpportunity
|
|
2092
2749
|
);
|
|
2093
2750
|
registerTool(
|
|
2094
2751
|
server2,
|
|
2095
2752
|
"get_opportunities",
|
|
2096
|
-
"Batch-fetch up to
|
|
2753
|
+
"Batch-fetch up to 50 opportunities by id. For 1\u201310 ids this is a single Capsule round trip (native multi-id endpoint); for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged. Returns each opportunity's full record (value, milestone, owner, party). For a single id, use get_opportunity instead.",
|
|
2097
2754
|
getOpportunitiesSchema,
|
|
2098
2755
|
getOpportunities
|
|
2099
2756
|
);
|
|
@@ -2133,6 +2790,13 @@ function createCapsuleMcpServer() {
|
|
|
2133
2790
|
updateOpportunitySchema,
|
|
2134
2791
|
updateOpportunity
|
|
2135
2792
|
);
|
|
2793
|
+
registerBatchTool(
|
|
2794
|
+
server2,
|
|
2795
|
+
"batch_update_opportunity",
|
|
2796
|
+
"Update 1\u201350 opportunities in parallel. Same input shape as update_opportunity but wrapped in an `items` array. Use this \u2014 not N sequential update_opportunity calls \u2014 for mass stage transitions (e.g. move a milestone batch to Won), owner reassignments, or value adjustments. Connector fans out parallel HTTP requests, default cap 5 (CAPSULE_MCP_BATCH_CONCURRENCY). Returns { results: [{ok, ...} per item], summary: {total, succeeded, failed} }. Partial failures possible; Capsule has no rollback.",
|
|
2797
|
+
batchUpdateOpportunitySchema,
|
|
2798
|
+
batchUpdateOpportunity
|
|
2799
|
+
);
|
|
2136
2800
|
registerTool(
|
|
2137
2801
|
server2,
|
|
2138
2802
|
"delete_opportunity",
|
|
@@ -2158,14 +2822,14 @@ function createCapsuleMcpServer() {
|
|
|
2158
2822
|
registerTool(
|
|
2159
2823
|
server2,
|
|
2160
2824
|
"get_project",
|
|
2161
|
-
"Fetch a single project (case) by its numeric
|
|
2825
|
+
"Fetch a single project (Capsule's term: 'case') by its numeric id. Returns the full record including name, description, status (OPEN/CLOSED), owner, stage, board, opportunityId (if linked), and timestamps. Use embed='tags,fields' to include attached tags and custom field values in one round-trip. For batch fetches of up to 50 projects at once, use get_projects instead. For the project's timeline (notes, captured emails, completed-task records) use list_project_entries.",
|
|
2162
2826
|
getProjectSchema,
|
|
2163
2827
|
getProject
|
|
2164
2828
|
);
|
|
2165
2829
|
registerTool(
|
|
2166
2830
|
server2,
|
|
2167
2831
|
"get_projects",
|
|
2168
|
-
"Batch-fetch up to
|
|
2832
|
+
"Batch-fetch up to 50 projects (cases) by ID. For 1\u201310 ids this is a single Capsule round trip; for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged.",
|
|
2169
2833
|
getProjectsSchema,
|
|
2170
2834
|
getProjects
|
|
2171
2835
|
);
|
|
@@ -2244,14 +2908,14 @@ function createCapsuleMcpServer() {
|
|
|
2244
2908
|
registerTool(
|
|
2245
2909
|
server2,
|
|
2246
2910
|
"get_task",
|
|
2247
|
-
"Fetch a single task by its numeric id. Returns the task's description, due date, owner, completion state, and the entity it's attached to (party / opportunity / project, if any \u2014 standalone tasks not tied to a record are also valid). For batch fetches of up to
|
|
2911
|
+
"Fetch a single task by its numeric id. Returns the task's description, due date, owner, completion state, and the entity it's attached to (party / opportunity / project, if any \u2014 standalone tasks not tied to a record are also valid). For batch fetches of up to 50 tasks at once, use get_tasks instead.",
|
|
2248
2912
|
getTaskSchema,
|
|
2249
2913
|
getTask
|
|
2250
2914
|
);
|
|
2251
2915
|
registerTool(
|
|
2252
2916
|
server2,
|
|
2253
2917
|
"get_tasks",
|
|
2254
|
-
"Batch-fetch up to
|
|
2918
|
+
"Batch-fetch up to 50 tasks by ID. For 1\u201310 ids this is a single Capsule round trip; for 11\u201350 ids the connector transparently splits into 10-id chunks and fans out parallel Capsule requests, so the caller sees a single tool call with all results merged.",
|
|
2255
2919
|
getTasksSchema,
|
|
2256
2920
|
getTasks
|
|
2257
2921
|
);
|
|
@@ -2266,7 +2930,7 @@ function createCapsuleMcpServer() {
|
|
|
2266
2930
|
registerTool(
|
|
2267
2931
|
server2,
|
|
2268
2932
|
"update_task",
|
|
2269
|
-
"Update fields on an existing task
|
|
2933
|
+
"Update fields on an existing task: `description`, `dueOn`, `dueTime`, `detail`, `status` (OPEN or COMPLETED), and `ownerId`. Only the fields you provide are changed. To mark a task done, prefer the dedicated `complete_task` tool \u2014 it's idempotent (a no-op success on an already-completed task) and semantically clearer than `update_task status=COMPLETED`. Capsule rejects directly setting status=PENDING (which exists only internally for track-driven tasks); use OPEN or COMPLETED. Completed tasks remain fully editable \u2014 Capsule does not enforce closed-record immutability.",
|
|
2270
2934
|
updateTaskSchema,
|
|
2271
2935
|
updateTask
|
|
2272
2936
|
);
|
|
@@ -2277,6 +2941,13 @@ function createCapsuleMcpServer() {
|
|
|
2277
2941
|
completeTaskSchema,
|
|
2278
2942
|
completeTask
|
|
2279
2943
|
);
|
|
2944
|
+
registerBatchTool(
|
|
2945
|
+
server2,
|
|
2946
|
+
"batch_complete_task",
|
|
2947
|
+
"Mark 1\u201350 tasks COMPLETED in parallel. Pass `ids: [task_id, \u2026]`. Natural for end-of-week catchups, 'close all the follow-ups from this campaign', etc. Connector fans out parallel HTTP requests, default cap 5 (CAPSULE_MCP_BATCH_CONCURRENCY). Returns { results: [{ok, ...} per id], summary: {total, succeeded, failed} }. A task that's already completed or deleted shows up as a per-item failure with the Capsule status; the rest still complete.",
|
|
2948
|
+
batchCompleteTaskSchema,
|
|
2949
|
+
batchCompleteTask
|
|
2950
|
+
);
|
|
2280
2951
|
registerTool(
|
|
2281
2952
|
server2,
|
|
2282
2953
|
"delete_task",
|
|
@@ -2288,7 +2959,7 @@ function createCapsuleMcpServer() {
|
|
|
2288
2959
|
registerTool(
|
|
2289
2960
|
server2,
|
|
2290
2961
|
"list_party_entries",
|
|
2291
|
-
"List timeline entries (notes, captured emails, completed-task records) for a party. Use this to read the conversation history with a contact or organisation.",
|
|
2962
|
+
"List timeline entries (notes, captured emails, completed-task records) for a party. Returns entries newest-first. Each entry has a type ('note', 'email', 'task'), free-text content, and timestamps. Use this to read the conversation history with a contact or organisation \u2014 answers questions like 'what's the latest with X?' For opportunity or project timelines, use list_opportunity_entries or list_project_entries respectively.",
|
|
2292
2963
|
listPartyEntriesSchema,
|
|
2293
2964
|
listPartyEntries
|
|
2294
2965
|
);
|
|
@@ -2309,7 +2980,7 @@ function createCapsuleMcpServer() {
|
|
|
2309
2980
|
registerTool(
|
|
2310
2981
|
server2,
|
|
2311
2982
|
"get_entry",
|
|
2312
|
-
"Fetch a single timeline entry by its numeric
|
|
2983
|
+
"Fetch a single timeline entry by its numeric id. Returns the full payload \u2014 for a note: the body text; for a captured email: subject, body, from/to, and timestamps; for a completed-task record: the original task fields. Useful when you have an entry id from one of the `list_*_entries` calls and want the full content. To modify the body or activity-type of an existing entry use `update_entry`; to delete one use `delete_entry`.",
|
|
2313
2984
|
getEntrySchema,
|
|
2314
2985
|
getEntry
|
|
2315
2986
|
);
|
|
@@ -2324,6 +2995,10 @@ function createCapsuleMcpServer() {
|
|
|
2324
2995
|
"get_attachment",
|
|
2325
2996
|
"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.",
|
|
2326
2997
|
getAttachmentSchema.shape,
|
|
2998
|
+
// get_attachment is read-only — downloads a binary, never mutates.
|
|
2999
|
+
// Mirrors the auto-inferred `readOnlyHint: true` that
|
|
3000
|
+
// `registerTool` applies to every other `get_*` tool.
|
|
3001
|
+
{ readOnlyHint: true },
|
|
2327
3002
|
async (input) => {
|
|
2328
3003
|
const result = await getAttachment(input);
|
|
2329
3004
|
if (result.truncated) {
|
|
@@ -2451,7 +3126,7 @@ function createCapsuleMcpServer() {
|
|
|
2451
3126
|
registerTool(
|
|
2452
3127
|
server2,
|
|
2453
3128
|
"list_stages",
|
|
2454
|
-
"List project stages. Without arguments returns every stage across every board (each carries a
|
|
3129
|
+
"List project (case) stages. Without arguments returns every stage across every board (each entry carries a `.board` reference so you can tell them apart). Pass `boardId` to scope the result to one specific board's stages. Use this to discover the numeric `stage.id` that `create_project` / `update_project` consume \u2014 stage names alone won't do, Capsule resolves by id. For opportunity (deal) stages, use `list_pipelines` instead \u2014 opportunities don't have stages in the project sense.",
|
|
2455
3130
|
listStagesSchema,
|
|
2456
3131
|
listStages
|
|
2457
3132
|
);
|
|
@@ -2465,7 +3140,7 @@ function createCapsuleMcpServer() {
|
|
|
2465
3140
|
registerTool(
|
|
2466
3141
|
server2,
|
|
2467
3142
|
"list_lostreasons",
|
|
2468
|
-
"List all configured opportunity-loss reasons (e.g. 'Poor Qualification', 'Lost to competitor'). Useful for analysing closed-lost opportunities by reason.",
|
|
3143
|
+
"List all configured opportunity-loss reasons (e.g. 'Poor Qualification', 'Lost to competitor', 'Price too high'). Returns each reason's id and name; the set is account-configured rather than a fixed enum, so call this to discover valid ids before referencing a lostReason in update_opportunity when closing a deal as lost. Useful for analysing closed-lost opportunities by reason.",
|
|
2469
3144
|
listLostReasonsSchema,
|
|
2470
3145
|
listLostReasons
|
|
2471
3146
|
);
|
|
@@ -2479,7 +3154,7 @@ function createCapsuleMcpServer() {
|
|
|
2479
3154
|
registerTool(
|
|
2480
3155
|
server2,
|
|
2481
3156
|
"list_categories",
|
|
2482
|
-
"List configured entry/task categories (Call, Email, Meeting, Follow-up, etc.) with their colours. Used to label and filter timeline entries and tasks.",
|
|
3157
|
+
"List configured entry/task categories (Call, Email, Meeting, Follow-up, etc.) with their colours. Returns each category's id, name, and colour. The set is account-configured rather than a fixed enum \u2014 call this to discover valid category ids before referencing one in add_note or create_task. Used to label and filter timeline entries and tasks.",
|
|
2483
3158
|
listCategoriesSchema,
|
|
2484
3159
|
listCategories
|
|
2485
3160
|
);
|
|
@@ -2554,6 +3229,20 @@ function createCapsuleMcpServer() {
|
|
|
2554
3229
|
removeTagByIdSchema,
|
|
2555
3230
|
removeTagById
|
|
2556
3231
|
);
|
|
3232
|
+
registerBatchTool(
|
|
3233
|
+
server2,
|
|
3234
|
+
"batch_add_tag",
|
|
3235
|
+
"Attach tags to many entities in parallel \u2014 e.g. tag a list of 20 contacts as 'RSAC26' after a conference, or apply the 'Departed' tag to 10 people in a layoff batch. Pass `items: [{ entity, entityId, tagName }, ...]` (1\u201350 items). Each item is processed identically to a single add_tag call. Connector fans out parallel HTTP requests, default cap 5 (CAPSULE_MCP_BATCH_CONCURRENCY). Returns { results: [{ok, ...} per item], summary: {total, succeeded, failed} }. The list_tags cache is invalidated for each affected entity type.",
|
|
3236
|
+
batchAddTagSchema,
|
|
3237
|
+
batchAddTag
|
|
3238
|
+
);
|
|
3239
|
+
registerBatchTool(
|
|
3240
|
+
server2,
|
|
3241
|
+
"batch_remove_tag_by_id",
|
|
3242
|
+
"Detach tags from many entities in parallel \u2014 cleanup counterpart to batch_add_tag. Pass `items: [{ entity, entityId, tagId }, ...]` (1\u201350 items). Each item is processed identically to a single remove_tag_by_id call (already-detached tags are reported as idempotent successes, not failures). Connector fans out parallel HTTP requests, default cap 5. Returns { results: [{ok, ...} per item], summary: {total, succeeded, failed} }.",
|
|
3243
|
+
batchRemoveTagByIdSchema,
|
|
3244
|
+
batchRemoveTagById
|
|
3245
|
+
);
|
|
2557
3246
|
}
|
|
2558
3247
|
registerTool(
|
|
2559
3248
|
server2,
|