honeybook-mcp 0.6.0 → 0.7.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.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "HoneyBook client-portal tools for Claude Code",
10
- "version": "0.6.0"
10
+ "version": "0.7.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "HoneyBook",
16
16
  "source": "./",
17
17
  "description": "HoneyBook client-portal MCP for Claude — view wedding-vendor contracts and invoices via MCP",
18
- "version": "0.6.0",
18
+ "version": "0.7.0",
19
19
  "author": {
20
20
  "name": "Chris Chall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "honeybook",
3
3
  "displayName": "HoneyBook",
4
- "version": "0.6.0",
4
+ "version": "0.7.0",
5
5
  "description": "HoneyBook client-portal MCP for Claude — view wedding-vendor contracts and invoices via MCP",
6
6
  "author": {
7
7
  "name": "Chris Chall",
package/dist/bundle.js CHANGED
@@ -2990,7 +2990,7 @@ var require_compile = __commonJS({
2990
2990
  const schOrFunc = root.refs[ref];
2991
2991
  if (schOrFunc)
2992
2992
  return schOrFunc;
2993
- let _sch = resolve.call(this, root, ref);
2993
+ let _sch = resolve2.call(this, root, ref);
2994
2994
  if (_sch === void 0) {
2995
2995
  const schema = (_a3 = root.localRefs) === null || _a3 === void 0 ? void 0 : _a3[ref];
2996
2996
  const { schemaId } = this.opts;
@@ -3017,7 +3017,7 @@ var require_compile = __commonJS({
3017
3017
  function sameSchemaEnv(s1, s2) {
3018
3018
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
3019
3019
  }
3020
- function resolve(root, ref) {
3020
+ function resolve2(root, ref) {
3021
3021
  let sch;
3022
3022
  while (typeof (sch = this.refs[ref]) == "string")
3023
3023
  ref = sch;
@@ -3648,7 +3648,7 @@ var require_fast_uri = __commonJS({
3648
3648
  }
3649
3649
  return uri;
3650
3650
  }
3651
- function resolve(baseURI, relativeURI, options) {
3651
+ function resolve2(baseURI, relativeURI, options) {
3652
3652
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3653
3653
  const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3654
3654
  const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
@@ -3932,7 +3932,7 @@ var require_fast_uri = __commonJS({
3932
3932
  var fastUri = {
3933
3933
  SCHEMES,
3934
3934
  normalize,
3935
- resolve,
3935
+ resolve: resolve2,
3936
3936
  resolveComponent,
3937
3937
  equal,
3938
3938
  serialize,
@@ -10592,7 +10592,7 @@ var require_websocket_server = __commonJS({
10592
10592
  });
10593
10593
 
10594
10594
  // src/index.ts
10595
- import { dirname as dirname2, join as join4 } from "path";
10595
+ import { dirname as dirname2, join as join5 } from "path";
10596
10596
  import { fileURLToPath } from "url";
10597
10597
 
10598
10598
  // node_modules/zod/v3/helpers/util.js
@@ -32553,7 +32553,7 @@ var Protocol = class {
32553
32553
  return;
32554
32554
  }
32555
32555
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
32556
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
32556
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
32557
32557
  options?.signal?.throwIfAborted();
32558
32558
  }
32559
32559
  } catch (error51) {
@@ -32570,7 +32570,7 @@ var Protocol = class {
32570
32570
  */
32571
32571
  request(request, resultSchema, options) {
32572
32572
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
32573
- return new Promise((resolve, reject) => {
32573
+ return new Promise((resolve2, reject) => {
32574
32574
  const earlyReject = (error51) => {
32575
32575
  reject(error51);
32576
32576
  };
@@ -32648,7 +32648,7 @@ var Protocol = class {
32648
32648
  if (!parseResult.success) {
32649
32649
  reject(parseResult.error);
32650
32650
  } else {
32651
- resolve(parseResult.data);
32651
+ resolve2(parseResult.data);
32652
32652
  }
32653
32653
  } catch (error51) {
32654
32654
  reject(error51);
@@ -32909,12 +32909,12 @@ var Protocol = class {
32909
32909
  }
32910
32910
  } catch {
32911
32911
  }
32912
- return new Promise((resolve, reject) => {
32912
+ return new Promise((resolve2, reject) => {
32913
32913
  if (signal.aborted) {
32914
32914
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
32915
32915
  return;
32916
32916
  }
32917
- const timeoutId = setTimeout(resolve, interval);
32917
+ const timeoutId = setTimeout(resolve2, interval);
32918
32918
  signal.addEventListener("abort", () => {
32919
32919
  clearTimeout(timeoutId);
32920
32920
  reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -34005,7 +34005,7 @@ var McpServer = class {
34005
34005
  let task = createTaskResult.task;
34006
34006
  const pollInterval = task.pollInterval ?? 5e3;
34007
34007
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
34008
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
34008
+ await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
34009
34009
  const updatedTask = await extra.taskStore.getTask(taskId);
34010
34010
  if (!updatedTask) {
34011
34011
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -34669,12 +34669,12 @@ var StdioServerTransport = class {
34669
34669
  this.onclose?.();
34670
34670
  }
34671
34671
  send(message) {
34672
- return new Promise((resolve) => {
34672
+ return new Promise((resolve2) => {
34673
34673
  const json2 = serializeMessage(message);
34674
34674
  if (this._stdout.write(json2)) {
34675
- resolve();
34675
+ resolve2();
34676
34676
  } else {
34677
- this._stdout.once("drain", resolve);
34677
+ this._stdout.once("drain", resolve2);
34678
34678
  }
34679
34679
  });
34680
34680
  }
@@ -34891,11 +34891,11 @@ var pageSchema = {
34891
34891
 
34892
34892
  // src/sessions.ts
34893
34893
  import { homedir } from "node:os";
34894
- import { join } from "node:path";
34894
+ import { join as join2 } from "node:path";
34895
34895
 
34896
34896
  // node_modules/@chrischall/mcp-utils/dist/session/index.js
34897
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync } from "node:fs";
34898
- import { dirname } from "node:path";
34897
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync, unlinkSync } from "node:fs";
34898
+ import { dirname, join, resolve } from "node:path";
34899
34899
  function normalizeOrigin(input) {
34900
34900
  try {
34901
34901
  return new URL(input).origin.replace(/\/$/, "");
@@ -35031,9 +35031,9 @@ var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
35031
35031
 
35032
35032
  // src/sessions.ts
35033
35033
  var sessionsDir = readEnvVar("HONEYBOOK_SESSIONS_DIR", {
35034
- default: join(homedir(), ".honeybook-mcp")
35034
+ default: join2(homedir(), ".honeybook-mcp")
35035
35035
  });
35036
- var sessionsFilePath = join(sessionsDir, "sessions.json");
35036
+ var sessionsFilePath = join2(sessionsDir, "sessions.json");
35037
35037
  var sessionStore = new SessionStore({
35038
35038
  filePath: sessionsFilePath,
35039
35039
  keyOf: (session) => session.portalOrigin,
@@ -35116,14 +35116,13 @@ var HoneyBookClient = class {
35116
35116
  function clearClientCache() {
35117
35117
  clientCache.clear();
35118
35118
  }
35119
+ var NO_ACTIVE_SESSION_MESSAGE = "No active HoneyBook session. Use the `use_magic_link` tool with a magic-link URL from a vendor's email to activate one.";
35119
35120
  async function getActiveClient(origin) {
35120
35121
  const session = sessionStore.get(origin);
35121
35122
  if (!session) {
35122
35123
  const active = sessionStore.list();
35123
35124
  if (active.length === 0) {
35124
- throw new Error(
35125
- "No active HoneyBook session. Use the `use_magic_link` tool with a magic-link URL from a vendor's email to activate one."
35126
- );
35125
+ throw new Error(NO_ACTIVE_SESSION_MESSAGE);
35127
35126
  }
35128
35127
  throw new Error(
35129
35128
  `No active session for origin "${origin}". Active origins: ${active.map((s) => s.portalOrigin).join(", ")}`
@@ -36317,7 +36316,7 @@ var DEFAULT_BIND_TIMEOUT_MS = 5e3;
36317
36316
  async function electRole(opts) {
36318
36317
  const server = createServer();
36319
36318
  const bindTimeoutMs = opts.bindTimeoutMs ?? DEFAULT_BIND_TIMEOUT_MS;
36320
- return new Promise((resolve, reject) => {
36319
+ return new Promise((resolve2, reject) => {
36321
36320
  let timer;
36322
36321
  const clearBindTimer = () => {
36323
36322
  if (timer) {
@@ -36333,7 +36332,7 @@ async function electRole(opts) {
36333
36332
  server.close();
36334
36333
  } catch {
36335
36334
  }
36336
- resolve({ role: "peer" });
36335
+ resolve2({ role: "peer" });
36337
36336
  } else {
36338
36337
  reject(e);
36339
36338
  }
@@ -36341,7 +36340,7 @@ async function electRole(opts) {
36341
36340
  const onListening = () => {
36342
36341
  clearBindTimer();
36343
36342
  server.removeListener("error", onError);
36344
- resolve({ role: "host", server });
36343
+ resolve2({ role: "host", server });
36345
36344
  };
36346
36345
  server.once("error", onError);
36347
36346
  server.once("listening", onListening);
@@ -36503,16 +36502,16 @@ async function awaitSessionReady(ready, opts) {
36503
36502
 
36504
36503
  // node_modules/@fetchproxy/server/dist/extension-trust.js
36505
36504
  import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
36506
- import { join as join3 } from "node:path";
36505
+ import { join as join4 } from "node:path";
36507
36506
 
36508
36507
  // node_modules/@fetchproxy/server/dist/identity.js
36509
36508
  import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
36510
- import { join as join2 } from "node:path";
36509
+ import { join as join3 } from "node:path";
36511
36510
  import { homedir as homedir2 } from "node:os";
36512
36511
  var SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
36513
36512
  var SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
36514
36513
  function defaultIdentityDir() {
36515
- return join2(homedir2(), ".fetchproxy", "identity");
36514
+ return join3(homedir2(), ".fetchproxy", "identity");
36516
36515
  }
36517
36516
  function safeIdentityFileBase(serverName) {
36518
36517
  if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
@@ -36522,7 +36521,7 @@ function safeIdentityFileBase(serverName) {
36522
36521
  }
36523
36522
  async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
36524
36523
  const safeFile = safeIdentityFileBase(serverName);
36525
- const path = join2(dir, `${safeFile}.json`);
36524
+ const path = join3(dir, `${safeFile}.json`);
36526
36525
  await mkdir(dir, { recursive: true, mode: 448 });
36527
36526
  try {
36528
36527
  const raw = await readFile(path, "utf8");
@@ -36594,13 +36593,13 @@ function decideExtensionTrust(args) {
36594
36593
  };
36595
36594
  }
36596
36595
  function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
36597
- return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36596
+ return join4(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
36598
36597
  }
36599
36598
  function extensionTrustPathHint(serverName) {
36600
36599
  try {
36601
36600
  return extensionTrustPath(serverName);
36602
36601
  } catch {
36603
- return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
36602
+ return join4(defaultIdentityDir(), "<server-name>.extension-trust.json");
36604
36603
  }
36605
36604
  }
36606
36605
  function isPin(x) {
@@ -36687,8 +36686,8 @@ async function startHost(opts) {
36687
36686
  let rejectOwnSession;
36688
36687
  let ownSessionReady;
36689
36688
  function resetSessionPromise() {
36690
- ownSessionReady = new Promise((resolve, reject) => {
36691
- resolveOwnSession = resolve;
36689
+ ownSessionReady = new Promise((resolve2, reject) => {
36690
+ resolveOwnSession = resolve2;
36692
36691
  rejectOwnSession = reject;
36693
36692
  });
36694
36693
  ownSessionReady.catch(() => {
@@ -36909,7 +36908,7 @@ async function startHost(opts) {
36909
36908
  });
36910
36909
  });
36911
36910
  return {
36912
- close: () => new Promise((resolve) => {
36911
+ close: () => new Promise((resolve2) => {
36913
36912
  for (const client of wss.clients) {
36914
36913
  try {
36915
36914
  client.terminate();
@@ -36917,7 +36916,7 @@ async function startHost(opts) {
36917
36916
  }
36918
36917
  }
36919
36918
  wss.close(() => {
36920
- opts.httpServer.close(() => resolve());
36919
+ opts.httpServer.close(() => resolve2());
36921
36920
  });
36922
36921
  }),
36923
36922
  sendOwnInner: async (inner) => {
@@ -36947,8 +36946,8 @@ async function startHost(opts) {
36947
36946
  var enc3 = new TextEncoder();
36948
36947
  async function startPeer(opts) {
36949
36948
  const ws = new import_websocket.default(`ws://${opts.host}:${opts.port}`);
36950
- await new Promise((resolve, reject) => {
36951
- ws.once("open", () => resolve());
36949
+ await new Promise((resolve2, reject) => {
36950
+ ws.once("open", () => resolve2());
36952
36951
  ws.once("error", reject);
36953
36952
  });
36954
36953
  const hello = await buildServerHello({
@@ -36978,8 +36977,8 @@ async function startPeer(opts) {
36978
36977
  let pendingPairCode = null;
36979
36978
  let resolveFirstReady;
36980
36979
  let rejectFirstReady;
36981
- const sessionPromise = new Promise((resolve, reject) => {
36982
- resolveFirstReady = resolve;
36980
+ const sessionPromise = new Promise((resolve2, reject) => {
36981
+ resolveFirstReady = resolve2;
36983
36982
  rejectFirstReady = reject;
36984
36983
  });
36985
36984
  let extensionHello = null;
@@ -37879,8 +37878,8 @@ var FetchproxyServer = class {
37879
37878
  async _fetchOnceWithTimeout(init) {
37880
37879
  const id = this.nextRequestId++;
37881
37880
  const inner = { type: "request", id, op: "fetch", init };
37882
- const pending = new Promise((resolve) => {
37883
- this.pending.set(id, resolve);
37881
+ const pending = new Promise((resolve2) => {
37882
+ this.pending.set(id, resolve2);
37884
37883
  });
37885
37884
  await this.sendInnerFrame(inner);
37886
37885
  const timeoutMs = this.opts.fetchTimeoutMs;
@@ -37891,12 +37890,12 @@ var FetchproxyServer = class {
37891
37890
  try {
37892
37891
  return await Promise.race([
37893
37892
  pending,
37894
- new Promise((resolve) => {
37893
+ new Promise((resolve2) => {
37895
37894
  timer = setTimeout(() => {
37896
37895
  this.pending.delete(id);
37897
37896
  const elapsedMs = Date.now() - start;
37898
37897
  const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
37899
- resolve({
37898
+ resolve2({
37900
37899
  ok: false,
37901
37900
  error: error51,
37902
37901
  kind: "timeout",
@@ -38258,8 +38257,8 @@ var FetchproxyServer = class {
38258
38257
  const tabUrl = `https://${host}/`;
38259
38258
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
38260
38259
  }
38261
- const pending = new Promise((resolve) => {
38262
- this.pendingReadCookies.set(id, resolve);
38260
+ const pending = new Promise((resolve2) => {
38261
+ this.pendingReadCookies.set(id, resolve2);
38263
38262
  });
38264
38263
  await this.sendInnerFrame(inner);
38265
38264
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -38317,8 +38316,8 @@ var FetchproxyServer = class {
38317
38316
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
38318
38317
  }
38319
38318
  };
38320
- const pending = new Promise((resolve, reject) => {
38321
- this.pendingWriteCookies.set(id, { resolve, reject });
38319
+ const pending = new Promise((resolve2, reject) => {
38320
+ this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
38322
38321
  });
38323
38322
  await this.sendInnerFrame(inner);
38324
38323
  return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
@@ -38380,8 +38379,8 @@ var FetchproxyServer = class {
38380
38379
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
38381
38380
  }
38382
38381
  };
38383
- const pending = new Promise((resolve, reject) => {
38384
- this.pendingStorage.set(id, { resolve, reject });
38382
+ const pending = new Promise((resolve2, reject) => {
38383
+ this.pendingStorage.set(id, { resolve: resolve2, reject });
38385
38384
  });
38386
38385
  await this.sendInnerFrame(inner);
38387
38386
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -38485,8 +38484,8 @@ var FetchproxyServer = class {
38485
38484
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38486
38485
  }
38487
38486
  };
38488
- const pending = new Promise((resolve, reject) => {
38489
- this.pendingCapture.set(id, { resolve, reject });
38487
+ const pending = new Promise((resolve2, reject) => {
38488
+ this.pendingCapture.set(id, { resolve: resolve2, reject });
38490
38489
  });
38491
38490
  await this.sendInnerFrame(inner);
38492
38491
  return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38570,8 +38569,8 @@ var FetchproxyServer = class {
38570
38569
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38571
38570
  }
38572
38571
  };
38573
- const pending = new Promise((resolve, reject) => {
38574
- this.pendingRedirect.set(id, { resolve, reject });
38572
+ const pending = new Promise((resolve2, reject) => {
38573
+ this.pendingRedirect.set(id, { resolve: resolve2, reject });
38575
38574
  });
38576
38575
  await this.sendInnerFrame(inner);
38577
38576
  return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38652,8 +38651,8 @@ var FetchproxyServer = class {
38652
38651
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38653
38652
  }
38654
38653
  };
38655
- const pending = new Promise((resolve, reject) => {
38656
- this.pendingDownload.set(id, { resolve, reject });
38654
+ const pending = new Promise((resolve2, reject) => {
38655
+ this.pendingDownload.set(id, { resolve: resolve2, reject });
38657
38656
  });
38658
38657
  await this.sendInnerFrame(inner);
38659
38658
  return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
@@ -38700,8 +38699,8 @@ var FetchproxyServer = class {
38700
38699
  keys: [...opts.keys]
38701
38700
  }
38702
38701
  };
38703
- const pending = new Promise((resolve, reject) => {
38704
- this.pendingIdb.set(id, { resolve, reject });
38702
+ const pending = new Promise((resolve2, reject) => {
38703
+ this.pendingIdb.set(id, { resolve: resolve2, reject });
38705
38704
  });
38706
38705
  await this.sendInnerFrame(inner);
38707
38706
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -38740,8 +38739,8 @@ var FetchproxyServer = class {
38740
38739
  op: "read_dom",
38741
38740
  init: { origin, names: [...opts.names] }
38742
38741
  };
38743
- const pending = new Promise((resolve, reject) => {
38744
- this.pendingStorage.set(id, { resolve, reject });
38742
+ const pending = new Promise((resolve2, reject) => {
38743
+ this.pendingStorage.set(id, { resolve: resolve2, reject });
38745
38744
  });
38746
38745
  await this.sendInnerFrame(inner);
38747
38746
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
@@ -38786,8 +38785,8 @@ var FetchproxyServer = class {
38786
38785
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
38787
38786
  }
38788
38787
  };
38789
- const pending = new Promise((resolve, reject) => {
38790
- this.pendingGraphql.set(id, { resolve, reject });
38788
+ const pending = new Promise((resolve2, reject) => {
38789
+ this.pendingGraphql.set(id, { resolve: resolve2, reject });
38791
38790
  });
38792
38791
  await this.sendInnerFrame(inner);
38793
38792
  return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
@@ -39226,6 +39225,118 @@ var BootstrapDisabledError = class extends Error {
39226
39225
  }
39227
39226
  };
39228
39227
 
39228
+ // node_modules/@chrischall/mcp-utils/dist/healthcheck/index.js
39229
+ function statusOf(err) {
39230
+ if (typeof err !== "object" || err === null)
39231
+ return void 0;
39232
+ const s = err.status ?? err.statusCode;
39233
+ return typeof s === "number" ? s : void 0;
39234
+ }
39235
+ function credentialHint(arm, prefix, hostLabel, source) {
39236
+ switch (arm) {
39237
+ case "ok":
39238
+ return `Credential from '${source}' works: ${hostLabel} accepted an authenticated request. If a real tool still fails, the problem is that tool, not auth.`;
39239
+ case "no_credential":
39240
+ return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
39241
+ case "credential_rejected":
39242
+ return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
39243
+ case "timeout":
39244
+ return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
39245
+ case "http":
39246
+ return `${hostLabel} answered with an error status that is not an auth rejection. That is USUALLY a ${hostLabel}-side problem rather than an auth one \u2014 but a 404 here more often means the probe path is wrong than that ${hostLabel} is broken, so check error.message and probe.url before concluding anything about the credential.`;
39247
+ case "transport":
39248
+ return `Could not reach ${hostLabel} at all. Check network egress; the credential itself was never judged.`;
39249
+ default:
39250
+ return `Unexpected failure \u2014 see error.message.`;
39251
+ }
39252
+ }
39253
+ function registerCredentialHealthcheckTool(args) {
39254
+ const { server, prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
39255
+ const probeUrl = probePath ? `https://${hostLabel}${probePath}` : void 0;
39256
+ server.registerTool(`${prefix}_healthcheck`, {
39257
+ title: "Verify credentials and upstream reachability",
39258
+ description: `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only; never returns the credential itself.`,
39259
+ annotations: {
39260
+ title: "Verify credentials and upstream reachability",
39261
+ readOnlyHint: true,
39262
+ idempotentHint: true,
39263
+ openWorldHint: true
39264
+ },
39265
+ inputSchema: {}
39266
+ }, async () => {
39267
+ let probeStarted = 0;
39268
+ let state;
39269
+ try {
39270
+ state = await resolveCredential();
39271
+ } catch (e) {
39272
+ const result2 = {
39273
+ ok: false,
39274
+ credential: { source: null, resolved: false },
39275
+ // No `url`: nothing was probed, and naming one implies it was tried.
39276
+ probe: { elapsed_ms: 0 },
39277
+ error: { kind: "no_credential", message: truncateErrorMessage(messageOf(e)) },
39278
+ hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
39279
+ };
39280
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
39281
+ }
39282
+ const credential = {
39283
+ source: state.source,
39284
+ resolved: state.source !== null,
39285
+ ...state.detail !== void 0 ? { detail: state.detail } : {}
39286
+ };
39287
+ if (!credential.resolved) {
39288
+ const result2 = {
39289
+ ok: false,
39290
+ credential,
39291
+ probe: { elapsed_ms: 0 },
39292
+ error: { kind: "no_credential", message: "no credential source resolved" },
39293
+ hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
39294
+ };
39295
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
39296
+ }
39297
+ let arm = "ok";
39298
+ let error51;
39299
+ let status;
39300
+ let customHint;
39301
+ probeStarted = Date.now();
39302
+ try {
39303
+ await probeFn();
39304
+ } catch (e) {
39305
+ status = statusOf(e);
39306
+ const aborted2 = e instanceof Error && e.name === "AbortError";
39307
+ arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
39308
+ let kind = arm;
39309
+ let detail;
39310
+ const custom2 = classifyThrown?.(e);
39311
+ if (custom2) {
39312
+ kind = custom2.kind;
39313
+ customHint = custom2.hint;
39314
+ detail = custom2.detail;
39315
+ }
39316
+ error51 = {
39317
+ kind,
39318
+ // Redacted AND bounded before it reaches the result: an upstream
39319
+ // failure routinely quotes what it was sent, and a healthcheck is
39320
+ // the tool people paste into a chat when something is broken.
39321
+ message: truncateErrorMessage(messageOf(e)),
39322
+ ...detail !== void 0 ? { detail } : {}
39323
+ };
39324
+ }
39325
+ const result = {
39326
+ ok: error51 === void 0,
39327
+ credential,
39328
+ probe: {
39329
+ ...probeUrl ? { url: probeUrl } : {},
39330
+ elapsed_ms: Date.now() - probeStarted,
39331
+ ...status !== void 0 ? { status } : {}
39332
+ },
39333
+ ...error51 ? { error: error51 } : {},
39334
+ hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
39335
+ };
39336
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
39337
+ });
39338
+ }
39339
+
39229
39340
  // node_modules/@chrischall/mcp-utils/dist/fetchproxy/index.js
39230
39341
  function bridgeErrorInfo(err) {
39231
39342
  const kind = classifyBridgeError(err);
@@ -39262,7 +39373,7 @@ function bridgeErrorInfo(err) {
39262
39373
  // package.json
39263
39374
  var package_default = {
39264
39375
  name: "honeybook-mcp",
39265
- version: "0.6.0",
39376
+ version: "0.7.0",
39266
39377
  mcpName: "io.github.chrischall/honeybook-mcp",
39267
39378
  description: "HoneyBook client-portal MCP server for Claude",
39268
39379
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -39304,7 +39415,7 @@ var package_default = {
39304
39415
  typecheck: "tsc -p tsconfig.json --noEmit"
39305
39416
  },
39306
39417
  dependencies: {
39307
- "@chrischall/mcp-utils": "^0.15.0",
39418
+ "@chrischall/mcp-utils": "^0.19.2",
39308
39419
  "@fetchproxy/bootstrap": "^2.2.0",
39309
39420
  "@fetchproxy/server": "^2.2.0",
39310
39421
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -39501,6 +39612,40 @@ function registerSessionTools(server) {
39501
39612
  );
39502
39613
  }
39503
39614
 
39615
+ // src/tools/healthcheck.ts
39616
+ function registerHealthcheckTools(server) {
39617
+ registerCredentialHealthcheckTool({
39618
+ server,
39619
+ prefix: "honeybook",
39620
+ hostLabel: "api.honeybook.com",
39621
+ probePath: "/api/v2/users/{id}/payment_methods",
39622
+ resolveCredential: async () => {
39623
+ const active = sessionStore.list();
39624
+ if (active.length === 0) {
39625
+ throw new Error(NO_ACTIVE_SESSION_MESSAGE);
39626
+ }
39627
+ return {
39628
+ source: "magic-link session",
39629
+ detail: { origins: active.map((s) => s.portalOrigin) }
39630
+ };
39631
+ },
39632
+ probeFn: async () => {
39633
+ const client = await getActiveClient();
39634
+ return client.request("GET", `/api/v2/users/${client.scope.userId}/payment_methods`);
39635
+ },
39636
+ classifyThrown: (err) => {
39637
+ const msg = err instanceof Error ? err.message : String(err);
39638
+ if (/auth expired|use_magic_link|HBUnauthorizedError/.test(msg)) {
39639
+ return {
39640
+ kind: "credential_rejected",
39641
+ hint: "HoneyBook rejected the session \u2014 it has expired. Re-run `use_magic_link` with a fresh link from a vendor email. (HoneyBook signals this with 404 + HBUnauthorizedError rather than 401; the client normalises both.)"
39642
+ };
39643
+ }
39644
+ return void 0;
39645
+ }
39646
+ });
39647
+ }
39648
+
39504
39649
  // src/types.ts
39505
39650
  var FILE_TYPES = ["agreement", "invoice", "brochure", "proposal"];
39506
39651
 
@@ -39901,10 +40046,10 @@ function registerInvoiceTools(server) {
39901
40046
 
39902
40047
  // src/index.ts
39903
40048
  var __dirname = dirname2(fileURLToPath(import.meta.url));
39904
- await loadDotenvSafely({ path: join4(__dirname, "..", ".env"), override: false });
40049
+ await loadDotenvSafely({ path: join5(__dirname, "..", ".env"), override: false });
39905
40050
  await runMcp({
39906
40051
  name: "honeybook-mcp",
39907
- version: "0.6.0",
40052
+ version: "0.7.0",
39908
40053
  // x-release-please-version
39909
40054
  tools: [
39910
40055
  registerSessionTools,
@@ -39912,6 +40057,7 @@ await runMcp({
39912
40057
  registerWorkspaceTools,
39913
40058
  registerPaymentMethodTools,
39914
40059
  registerContractTools,
39915
- registerInvoiceTools
40060
+ registerInvoiceTools,
40061
+ registerHealthcheckTools
39916
40062
  ]
39917
40063
  });
package/dist/client.js CHANGED
@@ -96,12 +96,18 @@ export function resetClientsForTest() {
96
96
  clientCache.clear();
97
97
  moduleState.apiVersionPromise = null;
98
98
  }
99
+ /**
100
+ * The one "no session yet" message, shared with `honeybook_healthcheck` so the
101
+ * two cannot drift into telling the user different things about the same
102
+ * state.
103
+ */
104
+ export const NO_ACTIVE_SESSION_MESSAGE = 'No active HoneyBook session. Use the `use_magic_link` tool with a magic-link URL from a vendor\'s email to activate one.';
99
105
  export async function getActiveClient(origin) {
100
106
  const session = sessionStore.get(origin);
101
107
  if (!session) {
102
108
  const active = sessionStore.list();
103
109
  if (active.length === 0) {
104
- throw new Error('No active HoneyBook session. Use the `use_magic_link` tool with a magic-link URL from a vendor\'s email to activate one.');
110
+ throw new Error(NO_ACTIVE_SESSION_MESSAGE);
105
111
  }
106
112
  throw new Error(`No active session for origin "${origin}". Active origins: ${active.map((s) => s.portalOrigin).join(', ')}`);
107
113
  }
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import { runMcp, loadDotenvSafely } from '@chrischall/mcp-utils';
4
4
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
5
  await loadDotenvSafely({ path: join(__dirname, '..', '.env'), override: false });
6
6
  import { registerSessionTools } from './tools/sessions.js';
7
+ import { registerHealthcheckTools } from './tools/healthcheck.js';
7
8
  import { registerWorkspaceFileTools } from './tools/workspace_files.js';
8
9
  import { registerWorkspaceTools } from './tools/workspaces.js';
9
10
  import { registerPaymentMethodTools } from './tools/payment_methods.js';
@@ -11,7 +12,7 @@ import { registerContractTools } from './tools/contracts.js';
11
12
  import { registerInvoiceTools } from './tools/invoices.js';
12
13
  await runMcp({
13
14
  name: 'honeybook-mcp',
14
- version: '0.6.0', // x-release-please-version
15
+ version: '0.7.0', // x-release-please-version
15
16
  tools: [
16
17
  registerSessionTools,
17
18
  registerWorkspaceFileTools,
@@ -19,5 +20,6 @@ await runMcp({
19
20
  registerPaymentMethodTools,
20
21
  registerContractTools,
21
22
  registerInvoiceTools,
23
+ registerHealthcheckTools,
22
24
  ],
23
25
  });
@@ -0,0 +1,61 @@
1
+ import { registerCredentialHealthcheckTool } from '@chrischall/mcp-utils/healthcheck';
2
+ import { getActiveClient, NO_ACTIVE_SESSION_MESSAGE } from '../client.js';
3
+ import { sessionStore } from '../sessions.js';
4
+ /**
5
+ * Register `honeybook_healthcheck` — checks for an active portal session, then
6
+ * makes one authenticated call.
7
+ *
8
+ * HoneyBook's credential is a session captured from a magic link, so its
9
+ * failures are: no session was ever activated, the session has lapsed and
10
+ * HoneyBook rejects it, or HoneyBook is down. Those need different responses
11
+ * and all three previously surfaced as the same opaque tool error.
12
+ *
13
+ * HoneyBook answers a revoked token with **404 + HBUnauthorizedError**, not
14
+ * 401, so `classifyThrown` re-kinds that as a rejection. Without it a lapsed
15
+ * session reads as "that resource does not exist", which sends people looking
16
+ * for a missing workspace instead of re-activating their link.
17
+ */
18
+ export function registerHealthcheckTools(server) {
19
+ registerCredentialHealthcheckTool({
20
+ server,
21
+ prefix: 'honeybook',
22
+ hostLabel: 'api.honeybook.com',
23
+ probePath: '/api/v2/users/{id}/payment_methods',
24
+ resolveCredential: async () => {
25
+ const active = sessionStore.list();
26
+ if (active.length === 0) {
27
+ // The SAME constant getActiveClient throws, so the two cannot drift.
28
+ throw new Error(NO_ACTIVE_SESSION_MESSAGE);
29
+ }
30
+ return {
31
+ source: 'magic-link session',
32
+ detail: { origins: active.map((s) => s.portalOrigin) },
33
+ };
34
+ },
35
+ probeFn: async () => {
36
+ const client = await getActiveClient();
37
+ return client.request('GET', `/api/v2/users/${client.scope.userId}/payment_methods`);
38
+ },
39
+ classifyThrown: (err) => {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ // Match the message the CLIENT throws, not HoneyBook's wire error.
42
+ //
43
+ // `HoneyBookClient.request()` already converts both 401 and
44
+ // 404+`HBUnauthorizedError` into one "auth expired … use_magic_link"
45
+ // error (`authExpiredError`), so nothing carrying the literal
46
+ // `HBUnauthorizedError` ever reaches here. An earlier version matched
47
+ // that literal and therefore never fired in production — the mock in its
48
+ // test bypassed the very transformation that made it dead.
49
+ //
50
+ // `HBUnauthorizedError` is kept as a second alternative only for a raw
51
+ // error that has not been through `request()`.
52
+ if (/auth expired|use_magic_link|HBUnauthorizedError/.test(msg)) {
53
+ return {
54
+ kind: 'credential_rejected',
55
+ hint: 'HoneyBook rejected the session — it has expired. Re-run `use_magic_link` with a fresh link from a vendor email. (HoneyBook signals this with 404 + HBUnauthorizedError rather than 401; the client normalises both.)',
56
+ };
57
+ }
58
+ return undefined;
59
+ },
60
+ });
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeybook-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "mcpName": "io.github.chrischall/honeybook-mcp",
5
5
  "description": "HoneyBook client-portal MCP server for Claude",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -42,7 +42,7 @@
42
42
  "typecheck": "tsc -p tsconfig.json --noEmit"
43
43
  },
44
44
  "dependencies": {
45
- "@chrischall/mcp-utils": "^0.15.0",
45
+ "@chrischall/mcp-utils": "^0.19.2",
46
46
  "@fetchproxy/bootstrap": "^2.2.0",
47
47
  "@fetchproxy/server": "^2.2.0",
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/honeybook-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.6.0",
9
+ "version": "0.7.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "honeybook-mcp",
14
- "version": "0.6.0",
14
+ "version": "0.7.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },