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 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.7.0"` in `args`. If you're tracking a fork or an unreleased branch, use the GitHub-ref form instead: `"github:soil-dev/capsulemcp#v1.7.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.
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 CapsuleApiError(
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
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
283
- throw new CapsuleApiError(
284
- 504,
285
- `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.`
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
- return await body();
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 emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
325
- let path = "";
374
+ function redactedPath(url) {
326
375
  try {
327
- path = redactPath(new URL(url).pathname);
376
+ return redactPath(new URL(url).pathname);
328
377
  } catch {
329
- path = "?";
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
- const timer = setTimeout(() => {
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
- return { [responseKey]: responses.flatMap((r) => r.data[responseKey] ?? []) };
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.7.0",
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 CapsuleApiError(
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
- if (err instanceof Error && (err.name === "AbortError" || /aborted/i.test(err.message))) {
265
- throw new CapsuleApiError(
266
- 504,
267
- `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.`
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
- return await body();
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 emitCapsuleRequest(method, url, res, durationMs, retriedAfter429) {
307
- let path = "";
356
+ function redactedPath(url) {
308
357
  try {
309
- path = redactPath(new URL(url).pathname);
358
+ return redactPath(new URL(url).pathname);
310
359
  } catch {
311
- path = "?";
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
- const timer = setTimeout(() => {
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
- return { [responseKey]: responses.flatMap((r) => r.data[responseKey] ?? []) };
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.7.0",
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 server = createCapsuleMcpServer();
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.7.0",
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
  }