capsulemcp 1.7.0 → 1.8.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 +1 -1
- package/dist/http.js +120 -23
- package/dist/index.js +122 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ For most individual users the install is a single JSON snippet pasted into Claud
|
|
|
48
48
|
|
|
49
49
|
3. Restart Claude Desktop. The Capsule tools appear in the tool picker.
|
|
50
50
|
|
|
51
|
-
That's it. The first launch fetches the package from npm (a few seconds); subsequent launches are instant from the npx cache. To pin a specific version, use `"capsulemcp@1.
|
|
51
|
+
That's it. The first launch fetches the package from npm (a few seconds); subsequent launches are instant from the npx cache. To pin a specific version, use `"capsulemcp@1.8.0"` in `args`. If you're tracking a fork or an unreleased branch, use the GitHub-ref form instead: `"github:soil-dev/capsulemcp#v1.8.0"` — same arguments, just installs from a git clone rather than the npm registry. See [INSTALL.md](INSTALL.md) for the Claude Code path, manual install, and troubleshooting.
|
|
52
52
|
|
|
53
53
|
## Tools
|
|
54
54
|
|
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) {
|
|
@@ -264,30 +289,39 @@ async function mapAbort(p) {
|
|
|
264
289
|
return await p;
|
|
265
290
|
} catch (err) {
|
|
266
291
|
if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
|
|
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
|
-
);
|
|
292
|
+
throw new CapsuleTimeoutError();
|
|
271
293
|
}
|
|
272
294
|
throw err;
|
|
273
295
|
}
|
|
274
296
|
}
|
|
275
297
|
async function fetchWithTimeout(url, options) {
|
|
276
298
|
const { options: opts, cleanup } = withTimeout(options);
|
|
299
|
+
const startedAt = Date.now();
|
|
277
300
|
try {
|
|
278
301
|
const res = await fetch(url, opts);
|
|
279
302
|
return { res, cleanup };
|
|
280
303
|
} catch (err) {
|
|
281
304
|
cleanup();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
)
|
|
305
|
+
const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
|
|
306
|
+
emitCapsuleFailure(
|
|
307
|
+
options?.method ?? "GET",
|
|
308
|
+
url,
|
|
309
|
+
Date.now() - startedAt,
|
|
310
|
+
isAbort ? "timeout" : "network",
|
|
311
|
+
isAbort ? void 0 : err
|
|
312
|
+
);
|
|
313
|
+
if (isAbort) {
|
|
314
|
+
throw new CapsuleTimeoutError();
|
|
287
315
|
}
|
|
288
316
|
throw err;
|
|
289
317
|
}
|
|
290
318
|
}
|
|
319
|
+
async function drainBody(res) {
|
|
320
|
+
try {
|
|
321
|
+
await res.body?.cancel();
|
|
322
|
+
} catch {
|
|
323
|
+
}
|
|
324
|
+
}
|
|
291
325
|
async function doFetch(url, options) {
|
|
292
326
|
const startedAt = Date.now();
|
|
293
327
|
const method = options?.method ?? "GET";
|
|
@@ -295,10 +329,13 @@ async function doFetch(url, options) {
|
|
|
295
329
|
if (first.res.status === 429) {
|
|
296
330
|
const delay = parseRateLimitDelay(first.res);
|
|
297
331
|
first.cleanup();
|
|
332
|
+
await drainBody(first.res);
|
|
298
333
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
299
334
|
const retried = await fetchWithTimeout(url, options);
|
|
300
335
|
if (retried.res.status === 429) {
|
|
301
336
|
retried.cleanup();
|
|
337
|
+
await drainBody(retried.res);
|
|
338
|
+
emitCapsuleRateLimited(method, url, Date.now() - startedAt);
|
|
302
339
|
throw new CapsuleApiError(
|
|
303
340
|
429,
|
|
304
341
|
"Rate limit exceeded after one retry. Please slow down your requests."
|
|
@@ -310,8 +347,7 @@ async function doFetch(url, options) {
|
|
|
310
347
|
}
|
|
311
348
|
async function consumeBody(start, body) {
|
|
312
349
|
try {
|
|
313
|
-
|
|
314
|
-
} finally {
|
|
350
|
+
const result = await body();
|
|
315
351
|
emitCapsuleRequest(
|
|
316
352
|
start.method,
|
|
317
353
|
start.url,
|
|
@@ -319,15 +355,31 @@ async function consumeBody(start, body) {
|
|
|
319
355
|
Date.now() - start.startedAt,
|
|
320
356
|
start.retriedAfter429
|
|
321
357
|
);
|
|
358
|
+
return result;
|
|
359
|
+
} catch (err) {
|
|
360
|
+
if (err instanceof CapsuleTimeoutError) {
|
|
361
|
+
emitCapsuleFailure(start.method, start.url, Date.now() - start.startedAt, "timeout");
|
|
362
|
+
} else {
|
|
363
|
+
emitCapsuleRequest(
|
|
364
|
+
start.method,
|
|
365
|
+
start.url,
|
|
366
|
+
start.res,
|
|
367
|
+
Date.now() - start.startedAt,
|
|
368
|
+
start.retriedAfter429
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
throw err;
|
|
322
372
|
}
|
|
323
373
|
}
|
|
324
|
-
function
|
|
325
|
-
let path = "";
|
|
374
|
+
function redactedPath(url) {
|
|
326
375
|
try {
|
|
327
|
-
|
|
376
|
+
return redactPath(new URL(url).pathname);
|
|
328
377
|
} catch {
|
|
329
|
-
|
|
378
|
+
return "?";
|
|
330
379
|
}
|
|
380
|
+
}
|
|
381
|
+
function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
382
|
+
const path = redactedPath(url);
|
|
331
383
|
const lenHeader = res.headers.get("content-length");
|
|
332
384
|
const responseBytes = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
|
|
333
385
|
logEvent("capsule.request", {
|
|
@@ -339,6 +391,37 @@ function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
|
339
391
|
...retriedAfter429 ? { retriedAfter429: true } : {}
|
|
340
392
|
});
|
|
341
393
|
}
|
|
394
|
+
function emitCapsuleFailure(method, url, elapsedMs, reason, err) {
|
|
395
|
+
const path = redactedPath(url);
|
|
396
|
+
if (reason === "timeout") {
|
|
397
|
+
logEvent(
|
|
398
|
+
"capsule.timeout",
|
|
399
|
+
{ method, path, elapsedMs, timeoutMs: REQUEST_TIMEOUT_MS },
|
|
400
|
+
{ force: true }
|
|
401
|
+
);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const code = extractErrorCode(err);
|
|
405
|
+
logEvent(
|
|
406
|
+
"capsule.error",
|
|
407
|
+
{ method, path, elapsedMs, ...code ? { code } : {} },
|
|
408
|
+
{ force: true }
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
function emitCapsuleRateLimited(method, url, elapsedMs) {
|
|
412
|
+
logEvent(
|
|
413
|
+
"capsule.ratelimit",
|
|
414
|
+
{ method, path: redactedPath(url), elapsedMs, status: 429 },
|
|
415
|
+
{ force: true }
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
function extractErrorCode(err) {
|
|
419
|
+
const e = err;
|
|
420
|
+
const code = e?.cause?.code ?? e?.code;
|
|
421
|
+
if (typeof code === "string") return code;
|
|
422
|
+
if (typeof e?.name === "string" && e.name !== "Error") return e.name;
|
|
423
|
+
return void 0;
|
|
424
|
+
}
|
|
342
425
|
async function throwForStatus(res) {
|
|
343
426
|
if (res.status === 401) {
|
|
344
427
|
const detail = await parseErrorBody(res);
|
|
@@ -1142,6 +1225,22 @@ var abortControllers = /* @__PURE__ */ new Map();
|
|
|
1142
1225
|
function registerAbortController(taskId, controller) {
|
|
1143
1226
|
abortControllers.set(taskId, controller);
|
|
1144
1227
|
}
|
|
1228
|
+
var evictionTimers = /* @__PURE__ */ new Map();
|
|
1229
|
+
var taskTtls = /* @__PURE__ */ new Map();
|
|
1230
|
+
function scheduleEviction(taskId, clientId, ttlMs) {
|
|
1231
|
+
const existing = evictionTimers.get(taskId);
|
|
1232
|
+
if (existing) clearTimeout(existing);
|
|
1233
|
+
taskTtls.set(taskId, ttlMs);
|
|
1234
|
+
const timer = setTimeout(() => {
|
|
1235
|
+
owners.delete(taskId);
|
|
1236
|
+
abortControllers.delete(taskId);
|
|
1237
|
+
evictionTimers.delete(taskId);
|
|
1238
|
+
taskTtls.delete(taskId);
|
|
1239
|
+
logEvent("task.evicted", { taskId, clientId, reason: "ttl" });
|
|
1240
|
+
}, ttlMs);
|
|
1241
|
+
timer.unref?.();
|
|
1242
|
+
evictionTimers.set(taskId, timer);
|
|
1243
|
+
}
|
|
1145
1244
|
function countPerClient(clientId) {
|
|
1146
1245
|
let n = 0;
|
|
1147
1246
|
for (const owner of owners.values()) {
|
|
@@ -1195,12 +1294,7 @@ function createScopedTaskStore(clientId) {
|
|
|
1195
1294
|
sessionId
|
|
1196
1295
|
);
|
|
1197
1296
|
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?.();
|
|
1297
|
+
scheduleEviction(task.taskId, clientId, clampedTtl);
|
|
1204
1298
|
logEvent("task.created", {
|
|
1205
1299
|
taskId: task.taskId,
|
|
1206
1300
|
clientId,
|
|
@@ -1219,6 +1313,7 @@ function createScopedTaskStore(clientId) {
|
|
|
1219
1313
|
}
|
|
1220
1314
|
logEvent("task.transition", { taskId, clientId, status });
|
|
1221
1315
|
await global.storeTaskResult(taskId, status, result, sessionId);
|
|
1316
|
+
scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
|
|
1222
1317
|
},
|
|
1223
1318
|
async getTaskResult(taskId, sessionId) {
|
|
1224
1319
|
if (owners.get(taskId) !== clientId) {
|
|
@@ -1238,6 +1333,7 @@ function createScopedTaskStore(clientId) {
|
|
|
1238
1333
|
}
|
|
1239
1334
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
1240
1335
|
abortControllers.delete(taskId);
|
|
1336
|
+
scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
|
|
1241
1337
|
}
|
|
1242
1338
|
},
|
|
1243
1339
|
async listTasks(cursor, sessionId) {
|
|
@@ -1581,7 +1677,8 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
|
|
|
1581
1677
|
(chunkIds) => capsuleGet(`${base}/${chunkIds.join(",")}`, params)
|
|
1582
1678
|
)
|
|
1583
1679
|
);
|
|
1584
|
-
|
|
1680
|
+
const merged = responses.flatMap((r) => r.data[responseKey] ?? []);
|
|
1681
|
+
return { ...responses[0]?.data ?? {}, [responseKey]: merged };
|
|
1585
1682
|
}
|
|
1586
1683
|
|
|
1587
1684
|
// src/tools/custom-field-helpers.ts
|
|
@@ -3223,7 +3320,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
3223
3320
|
const server = new McpServer(
|
|
3224
3321
|
{
|
|
3225
3322
|
name: "capsulemcp",
|
|
3226
|
-
version: "1.
|
|
3323
|
+
version: "1.8.0",
|
|
3227
3324
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
3228
3325
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
3229
3326
|
icons: ICONS
|
package/dist/index.js
CHANGED
|
@@ -31,6 +31,22 @@ var chainHandlers = {
|
|
|
31
31
|
"capsule.request": (ctx) => {
|
|
32
32
|
ctx.capsuleCalls += 1;
|
|
33
33
|
},
|
|
34
|
+
// A timed-out or connection-failed call is still an attempt that
|
|
35
|
+
// never reaches the `capsule.request` emit (it throws at the fetch
|
|
36
|
+
// stage). Count it here so `tool.chain.capsuleCalls` stays honest and
|
|
37
|
+
// a chain whose duration ballooned is explained by a visible failure.
|
|
38
|
+
"capsule.timeout": (ctx) => {
|
|
39
|
+
ctx.capsuleCalls += 1;
|
|
40
|
+
},
|
|
41
|
+
"capsule.error": (ctx) => {
|
|
42
|
+
ctx.capsuleCalls += 1;
|
|
43
|
+
},
|
|
44
|
+
// A request that exhausted its 429 retry is a real (doubly-attempted)
|
|
45
|
+
// outbound call that throws before `capsule.request` fires — count it
|
|
46
|
+
// so a chain whose latency ballooned on rate-limit backoff is explained.
|
|
47
|
+
"capsule.ratelimit": (ctx) => {
|
|
48
|
+
ctx.capsuleCalls += 1;
|
|
49
|
+
},
|
|
34
50
|
// Cache-hit events feed the aggregate so the chain stat is right
|
|
35
51
|
// even on tools whose Capsule calls all hit the cache.
|
|
36
52
|
"cache.hit": (ctx) => {
|
|
@@ -166,6 +182,15 @@ var CapsuleApiError = class extends Error {
|
|
|
166
182
|
}
|
|
167
183
|
status;
|
|
168
184
|
};
|
|
185
|
+
var CapsuleTimeoutError = class extends CapsuleApiError {
|
|
186
|
+
constructor() {
|
|
187
|
+
super(
|
|
188
|
+
504,
|
|
189
|
+
`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.`
|
|
190
|
+
);
|
|
191
|
+
this.name = "CapsuleTimeoutError";
|
|
192
|
+
}
|
|
193
|
+
};
|
|
169
194
|
function getToken() {
|
|
170
195
|
const token = process.env["CAPSULE_API_TOKEN"];
|
|
171
196
|
if (!token) {
|
|
@@ -246,30 +271,39 @@ async function mapAbort(p) {
|
|
|
246
271
|
return await p;
|
|
247
272
|
} catch (err) {
|
|
248
273
|
if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
|
|
249
|
-
throw new
|
|
250
|
-
504,
|
|
251
|
-
`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.`
|
|
252
|
-
);
|
|
274
|
+
throw new CapsuleTimeoutError();
|
|
253
275
|
}
|
|
254
276
|
throw err;
|
|
255
277
|
}
|
|
256
278
|
}
|
|
257
279
|
async function fetchWithTimeout(url, options) {
|
|
258
280
|
const { options: opts, cleanup } = withTimeout(options);
|
|
281
|
+
const startedAt = Date.now();
|
|
259
282
|
try {
|
|
260
283
|
const res = await fetch(url, opts);
|
|
261
284
|
return { res, cleanup };
|
|
262
285
|
} catch (err) {
|
|
263
286
|
cleanup();
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
)
|
|
287
|
+
const isAbort = err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message));
|
|
288
|
+
emitCapsuleFailure(
|
|
289
|
+
options?.method ?? "GET",
|
|
290
|
+
url,
|
|
291
|
+
Date.now() - startedAt,
|
|
292
|
+
isAbort ? "timeout" : "network",
|
|
293
|
+
isAbort ? void 0 : err
|
|
294
|
+
);
|
|
295
|
+
if (isAbort) {
|
|
296
|
+
throw new CapsuleTimeoutError();
|
|
269
297
|
}
|
|
270
298
|
throw err;
|
|
271
299
|
}
|
|
272
300
|
}
|
|
301
|
+
async function drainBody(res) {
|
|
302
|
+
try {
|
|
303
|
+
await res.body?.cancel();
|
|
304
|
+
} catch {
|
|
305
|
+
}
|
|
306
|
+
}
|
|
273
307
|
async function doFetch(url, options) {
|
|
274
308
|
const startedAt = Date.now();
|
|
275
309
|
const method = options?.method ?? "GET";
|
|
@@ -277,10 +311,13 @@ async function doFetch(url, options) {
|
|
|
277
311
|
if (first.res.status === 429) {
|
|
278
312
|
const delay = parseRateLimitDelay(first.res);
|
|
279
313
|
first.cleanup();
|
|
314
|
+
await drainBody(first.res);
|
|
280
315
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
281
316
|
const retried = await fetchWithTimeout(url, options);
|
|
282
317
|
if (retried.res.status === 429) {
|
|
283
318
|
retried.cleanup();
|
|
319
|
+
await drainBody(retried.res);
|
|
320
|
+
emitCapsuleRateLimited(method, url, Date.now() - startedAt);
|
|
284
321
|
throw new CapsuleApiError(
|
|
285
322
|
429,
|
|
286
323
|
"Rate limit exceeded after one retry. Please slow down your requests."
|
|
@@ -292,8 +329,7 @@ async function doFetch(url, options) {
|
|
|
292
329
|
}
|
|
293
330
|
async function consumeBody(start, body) {
|
|
294
331
|
try {
|
|
295
|
-
|
|
296
|
-
} finally {
|
|
332
|
+
const result = await body();
|
|
297
333
|
emitCapsuleRequest(
|
|
298
334
|
start.method,
|
|
299
335
|
start.url,
|
|
@@ -301,15 +337,31 @@ async function consumeBody(start, body) {
|
|
|
301
337
|
Date.now() - start.startedAt,
|
|
302
338
|
start.retriedAfter429
|
|
303
339
|
);
|
|
340
|
+
return result;
|
|
341
|
+
} catch (err) {
|
|
342
|
+
if (err instanceof CapsuleTimeoutError) {
|
|
343
|
+
emitCapsuleFailure(start.method, start.url, Date.now() - start.startedAt, "timeout");
|
|
344
|
+
} else {
|
|
345
|
+
emitCapsuleRequest(
|
|
346
|
+
start.method,
|
|
347
|
+
start.url,
|
|
348
|
+
start.res,
|
|
349
|
+
Date.now() - start.startedAt,
|
|
350
|
+
start.retriedAfter429
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
throw err;
|
|
304
354
|
}
|
|
305
355
|
}
|
|
306
|
-
function
|
|
307
|
-
let path = "";
|
|
356
|
+
function redactedPath(url) {
|
|
308
357
|
try {
|
|
309
|
-
|
|
358
|
+
return redactPath(new URL(url).pathname);
|
|
310
359
|
} catch {
|
|
311
|
-
|
|
360
|
+
return "?";
|
|
312
361
|
}
|
|
362
|
+
}
|
|
363
|
+
function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
364
|
+
const path = redactedPath(url);
|
|
313
365
|
const lenHeader = res.headers.get("content-length");
|
|
314
366
|
const responseBytes = lenHeader ? Number.parseInt(lenHeader, 10) : 0;
|
|
315
367
|
logEvent("capsule.request", {
|
|
@@ -321,6 +373,37 @@ function emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
|
|
|
321
373
|
...retriedAfter429 ? { retriedAfter429: true } : {}
|
|
322
374
|
});
|
|
323
375
|
}
|
|
376
|
+
function emitCapsuleFailure(method, url, elapsedMs, reason, err) {
|
|
377
|
+
const path = redactedPath(url);
|
|
378
|
+
if (reason === "timeout") {
|
|
379
|
+
logEvent(
|
|
380
|
+
"capsule.timeout",
|
|
381
|
+
{ method, path, elapsedMs, timeoutMs: REQUEST_TIMEOUT_MS },
|
|
382
|
+
{ force: true }
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const code = extractErrorCode(err);
|
|
387
|
+
logEvent(
|
|
388
|
+
"capsule.error",
|
|
389
|
+
{ method, path, elapsedMs, ...code ? { code } : {} },
|
|
390
|
+
{ force: true }
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
function emitCapsuleRateLimited(method, url, elapsedMs) {
|
|
394
|
+
logEvent(
|
|
395
|
+
"capsule.ratelimit",
|
|
396
|
+
{ method, path: redactedPath(url), elapsedMs, status: 429 },
|
|
397
|
+
{ force: true }
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
function extractErrorCode(err) {
|
|
401
|
+
const e = err;
|
|
402
|
+
const code = e?.cause?.code ?? e?.code;
|
|
403
|
+
if (typeof code === "string") return code;
|
|
404
|
+
if (typeof e?.name === "string" && e.name !== "Error") return e.name;
|
|
405
|
+
return void 0;
|
|
406
|
+
}
|
|
324
407
|
async function throwForStatus(res) {
|
|
325
408
|
if (res.status === 401) {
|
|
326
409
|
const detail = await parseErrorBody(res);
|
|
@@ -639,6 +722,22 @@ var abortControllers = /* @__PURE__ */ new Map();
|
|
|
639
722
|
function registerAbortController(taskId, controller) {
|
|
640
723
|
abortControllers.set(taskId, controller);
|
|
641
724
|
}
|
|
725
|
+
var evictionTimers = /* @__PURE__ */ new Map();
|
|
726
|
+
var taskTtls = /* @__PURE__ */ new Map();
|
|
727
|
+
function scheduleEviction(taskId, clientId, ttlMs) {
|
|
728
|
+
const existing = evictionTimers.get(taskId);
|
|
729
|
+
if (existing) clearTimeout(existing);
|
|
730
|
+
taskTtls.set(taskId, ttlMs);
|
|
731
|
+
const timer = setTimeout(() => {
|
|
732
|
+
owners.delete(taskId);
|
|
733
|
+
abortControllers.delete(taskId);
|
|
734
|
+
evictionTimers.delete(taskId);
|
|
735
|
+
taskTtls.delete(taskId);
|
|
736
|
+
logEvent("task.evicted", { taskId, clientId, reason: "ttl" });
|
|
737
|
+
}, ttlMs);
|
|
738
|
+
timer.unref?.();
|
|
739
|
+
evictionTimers.set(taskId, timer);
|
|
740
|
+
}
|
|
642
741
|
function countPerClient(clientId) {
|
|
643
742
|
let n = 0;
|
|
644
743
|
for (const owner of owners.values()) {
|
|
@@ -692,12 +791,7 @@ function createScopedTaskStore(clientId) {
|
|
|
692
791
|
sessionId
|
|
693
792
|
);
|
|
694
793
|
owners.set(task.taskId, clientId);
|
|
695
|
-
|
|
696
|
-
owners.delete(task.taskId);
|
|
697
|
-
abortControllers.delete(task.taskId);
|
|
698
|
-
logEvent("task.evicted", { taskId: task.taskId, clientId, reason: "ttl" });
|
|
699
|
-
}, clampedTtl);
|
|
700
|
-
timer.unref?.();
|
|
794
|
+
scheduleEviction(task.taskId, clientId, clampedTtl);
|
|
701
795
|
logEvent("task.created", {
|
|
702
796
|
taskId: task.taskId,
|
|
703
797
|
clientId,
|
|
@@ -716,6 +810,7 @@ function createScopedTaskStore(clientId) {
|
|
|
716
810
|
}
|
|
717
811
|
logEvent("task.transition", { taskId, clientId, status });
|
|
718
812
|
await global.storeTaskResult(taskId, status, result, sessionId);
|
|
813
|
+
scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
|
|
719
814
|
},
|
|
720
815
|
async getTaskResult(taskId, sessionId) {
|
|
721
816
|
if (owners.get(taskId) !== clientId) {
|
|
@@ -735,6 +830,7 @@ function createScopedTaskStore(clientId) {
|
|
|
735
830
|
}
|
|
736
831
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
737
832
|
abortControllers.delete(taskId);
|
|
833
|
+
scheduleEviction(taskId, clientId, taskTtls.get(taskId) ?? getTasksConfig().defaultTtlMs);
|
|
738
834
|
}
|
|
739
835
|
},
|
|
740
836
|
async listTasks(cursor, sessionId) {
|
|
@@ -1078,7 +1174,8 @@ async function chunkedMultiGet(base, responseKey, ids, params) {
|
|
|
1078
1174
|
(chunkIds) => capsuleGet(`${base}/${chunkIds.join(",")}`, params)
|
|
1079
1175
|
)
|
|
1080
1176
|
);
|
|
1081
|
-
|
|
1177
|
+
const merged = responses.flatMap((r) => r.data[responseKey] ?? []);
|
|
1178
|
+
return { ...responses[0]?.data ?? {}, [responseKey]: merged };
|
|
1082
1179
|
}
|
|
1083
1180
|
|
|
1084
1181
|
// src/tools/custom-field-helpers.ts
|
|
@@ -2720,7 +2817,7 @@ function createCapsuleMcpServer(opts) {
|
|
|
2720
2817
|
const server2 = new McpServer(
|
|
2721
2818
|
{
|
|
2722
2819
|
name: "capsulemcp",
|
|
2723
|
-
version: "1.
|
|
2820
|
+
version: "1.8.0",
|
|
2724
2821
|
description: "Read and (optionally) modify Capsule CRM data \u2014 parties, opportunities, projects, tasks, timeline entries, pipelines, tags.",
|
|
2725
2822
|
websiteUrl: "https://github.com/soil-dev/capsulemcp",
|
|
2726
2823
|
icons: ICONS
|
|
@@ -3457,7 +3554,8 @@ if (!process.env["CAPSULE_API_TOKEN"]) {
|
|
|
3457
3554
|
);
|
|
3458
3555
|
process.exit(1);
|
|
3459
3556
|
}
|
|
3460
|
-
var
|
|
3557
|
+
var STDIO_CLIENT_ID = "stdio-local";
|
|
3558
|
+
var server = createCapsuleMcpServer({ clientId: STDIO_CLIENT_ID });
|
|
3461
3559
|
var transport = new StdioServerTransport();
|
|
3462
3560
|
if (isReadOnly()) {
|
|
3463
3561
|
console.error("[capsulemcp] read-only mode: write/delete tools are not registered");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capsulemcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Model Context Protocol server for Capsule CRM. Lets Claude (Desktop, Code, or web Projects via Custom Connector) read and write your CRM in plain English. Covers contacts, opportunities, projects, tasks, timeline activity, structured filters, saved filters with sort, workflow tracks, file attachments, audit, and batch fetches.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -62,6 +62,6 @@
|
|
|
62
62
|
"vitest": "^4.1.7"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|
|
65
|
-
"node": ">=22"
|
|
65
|
+
"node": ">=22.19.0"
|
|
66
66
|
}
|
|
67
67
|
}
|