honeybook-mcp 0.5.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.5.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.5.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.5.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;
@@ -3313,8 +3313,8 @@ var require_utils = __commonJS({
3313
3313
  var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3314
3314
  var HOST_DELIM_RE = /[@/?#:]/g;
3315
3315
  var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3316
- function reescapeHostDelimiters(host, isIP) {
3317
- const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3316
+ function reescapeHostDelimiters(host, isIP2) {
3317
+ const re = isIP2 ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3318
3318
  re.lastIndex = 0;
3319
3319
  return host.replace(re, (ch) => HOST_DELIMS[ch]);
3320
3320
  }
@@ -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);
@@ -3803,7 +3803,7 @@ var require_fast_uri = __commonJS({
3803
3803
  fragment: void 0
3804
3804
  };
3805
3805
  let malformedAuthorityOrPort = false;
3806
- let isIP = false;
3806
+ let isIP2 = false;
3807
3807
  if (options.reference === "suffix") {
3808
3808
  if (options.scheme) {
3809
3809
  uri = options.scheme + ":" + uri;
@@ -3852,9 +3852,9 @@ var require_fast_uri = __commonJS({
3852
3852
  if (ipv4result === false) {
3853
3853
  const ipv6result = normalizeIPv6(parsed.host);
3854
3854
  parsed.host = ipv6result.host.toLowerCase();
3855
- isIP = ipv6result.isIPV6;
3855
+ isIP2 = ipv6result.isIPV6;
3856
3856
  } else {
3857
- isIP = true;
3857
+ isIP2 = true;
3858
3858
  }
3859
3859
  }
3860
3860
  if (parsed.scheme === void 0 && parsed.userinfo === void 0 && parsed.host === void 0 && parsed.port === void 0 && parsed.query === void 0 && !parsed.path) {
@@ -3871,7 +3871,7 @@ var require_fast_uri = __commonJS({
3871
3871
  }
3872
3872
  const schemeHandler = getSchemeHandler(options.scheme || parsed.scheme);
3873
3873
  if (!options.unicodeSupport && (!schemeHandler || !schemeHandler.unicodeSupport)) {
3874
- if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP === false && nonSimpleDomain(parsed.host)) {
3874
+ if (parsed.host && (options.domainHost || schemeHandler && schemeHandler.domainHost) && isIP2 === false && nonSimpleDomain(parsed.host)) {
3875
3875
  try {
3876
3876
  parsed.host = new URL("http://" + parsed.host).hostname;
3877
3877
  } catch (e) {
@@ -3885,7 +3885,7 @@ var require_fast_uri = __commonJS({
3885
3885
  parsed.scheme = unescape(parsed.scheme);
3886
3886
  }
3887
3887
  if (parsed.host !== void 0) {
3888
- parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
3888
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP2);
3889
3889
  }
3890
3890
  }
3891
3891
  if (parsed.path) {
@@ -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,
@@ -7198,7 +7198,7 @@ var require_permessage_deflate = __commonJS({
7198
7198
  acceptAsServer(offers) {
7199
7199
  const opts = this._options;
7200
7200
  const accepted = offers.find((params) => {
7201
- if (opts.serverNoContextTakeover === false && params.server_no_context_takeover || params.server_max_window_bits && (opts.serverMaxWindowBits === false || typeof opts.serverMaxWindowBits === "number" && opts.serverMaxWindowBits > params.server_max_window_bits) || typeof opts.clientMaxWindowBits === "number" && !params.client_max_window_bits) {
7201
+ if (opts.serverNoContextTakeover === false && params.server_no_context_takeover || params.server_max_window_bits && (opts.serverMaxWindowBits === false || typeof opts.serverMaxWindowBits === "number" && opts.serverMaxWindowBits > params.server_max_window_bits) || typeof opts.clientMaxWindowBits === "number" && (typeof params.client_max_window_bits === "number" ? opts.clientMaxWindowBits > params.client_max_window_bits : !params.client_max_window_bits)) {
7202
7202
  return false;
7203
7203
  }
7204
7204
  return true;
@@ -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;
@@ -37180,6 +37179,7 @@ function classifyBridgeError(err) {
37180
37179
  }
37181
37180
 
37182
37181
  // node_modules/@fetchproxy/server/dist/ws-server.js
37182
+ import { isIP } from "node:net";
37183
37183
  function envWsPort() {
37184
37184
  const raw = process.env.FETCHPROXY_WS_PORT;
37185
37185
  if (raw === void 0 || raw.trim() === "")
@@ -37191,6 +37191,15 @@ function envWsPort() {
37191
37191
  return void 0;
37192
37192
  return port;
37193
37193
  }
37194
+ function envWsHost() {
37195
+ const raw = process.env.FETCHPROXY_WS_HOST;
37196
+ if (raw === void 0)
37197
+ return void 0;
37198
+ const host = raw.trim();
37199
+ if (host === "" || isIP(host) === 0)
37200
+ return void 0;
37201
+ return host;
37202
+ }
37194
37203
  var FetchproxyProtocolError = class extends Error {
37195
37204
  constructor(message) {
37196
37205
  super(message);
@@ -37439,7 +37448,7 @@ var FetchproxyServer = class {
37439
37448
  }
37440
37449
  this.opts = {
37441
37450
  port: opts.port ?? envWsPort() ?? 37149,
37442
- host: opts.host ?? "127.0.0.1",
37451
+ host: opts.host ?? envWsHost() ?? "127.0.0.1",
37443
37452
  serverName: opts.serverName,
37444
37453
  version: opts.version,
37445
37454
  domains: [...opts.domains],
@@ -37725,6 +37734,7 @@ var FetchproxyServer = class {
37725
37734
  return {
37726
37735
  role: this.role,
37727
37736
  port: this.opts.port,
37737
+ host: this.opts.host,
37728
37738
  serverVersion: this.opts.version,
37729
37739
  fetchTimeoutMs: this.opts.fetchTimeoutMs ?? 0,
37730
37740
  bridgeReviveDelayMs: this.opts.bridgeReviveDelayMs ?? 0,
@@ -37868,8 +37878,8 @@ var FetchproxyServer = class {
37868
37878
  async _fetchOnceWithTimeout(init) {
37869
37879
  const id = this.nextRequestId++;
37870
37880
  const inner = { type: "request", id, op: "fetch", init };
37871
- const pending = new Promise((resolve) => {
37872
- this.pending.set(id, resolve);
37881
+ const pending = new Promise((resolve2) => {
37882
+ this.pending.set(id, resolve2);
37873
37883
  });
37874
37884
  await this.sendInnerFrame(inner);
37875
37885
  const timeoutMs = this.opts.fetchTimeoutMs;
@@ -37880,12 +37890,12 @@ var FetchproxyServer = class {
37880
37890
  try {
37881
37891
  return await Promise.race([
37882
37892
  pending,
37883
- new Promise((resolve) => {
37893
+ new Promise((resolve2) => {
37884
37894
  timer = setTimeout(() => {
37885
37895
  this.pending.delete(id);
37886
37896
  const elapsedMs = Date.now() - start;
37887
37897
  const error51 = `fetchproxy: ${init.url} did not respond within ${timeoutMs}ms`;
37888
- resolve({
37898
+ resolve2({
37889
37899
  ok: false,
37890
37900
  error: error51,
37891
37901
  kind: "timeout",
@@ -38247,8 +38257,8 @@ var FetchproxyServer = class {
38247
38257
  const tabUrl = `https://${host}/`;
38248
38258
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
38249
38259
  }
38250
- const pending = new Promise((resolve) => {
38251
- this.pendingReadCookies.set(id, resolve);
38260
+ const pending = new Promise((resolve2) => {
38261
+ this.pendingReadCookies.set(id, resolve2);
38252
38262
  });
38253
38263
  await this.sendInnerFrame(inner);
38254
38264
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
@@ -38306,8 +38316,8 @@ var FetchproxyServer = class {
38306
38316
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
38307
38317
  }
38308
38318
  };
38309
- const pending = new Promise((resolve, reject) => {
38310
- this.pendingWriteCookies.set(id, { resolve, reject });
38319
+ const pending = new Promise((resolve2, reject) => {
38320
+ this.pendingWriteCookies.set(id, { resolve: resolve2, reject });
38311
38321
  });
38312
38322
  await this.sendInnerFrame(inner);
38313
38323
  return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
@@ -38369,8 +38379,8 @@ var FetchproxyServer = class {
38369
38379
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
38370
38380
  }
38371
38381
  };
38372
- const pending = new Promise((resolve, reject) => {
38373
- this.pendingStorage.set(id, { resolve, reject });
38382
+ const pending = new Promise((resolve2, reject) => {
38383
+ this.pendingStorage.set(id, { resolve: resolve2, reject });
38374
38384
  });
38375
38385
  await this.sendInnerFrame(inner);
38376
38386
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
@@ -38474,8 +38484,8 @@ var FetchproxyServer = class {
38474
38484
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38475
38485
  }
38476
38486
  };
38477
- const pending = new Promise((resolve, reject) => {
38478
- this.pendingCapture.set(id, { resolve, reject });
38487
+ const pending = new Promise((resolve2, reject) => {
38488
+ this.pendingCapture.set(id, { resolve: resolve2, reject });
38479
38489
  });
38480
38490
  await this.sendInnerFrame(inner);
38481
38491
  return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38559,8 +38569,8 @@ var FetchproxyServer = class {
38559
38569
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38560
38570
  }
38561
38571
  };
38562
- const pending = new Promise((resolve, reject) => {
38563
- this.pendingRedirect.set(id, { resolve, reject });
38572
+ const pending = new Promise((resolve2, reject) => {
38573
+ this.pendingRedirect.set(id, { resolve: resolve2, reject });
38564
38574
  });
38565
38575
  await this.sendInnerFrame(inner);
38566
38576
  return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
@@ -38641,8 +38651,8 @@ var FetchproxyServer = class {
38641
38651
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
38642
38652
  }
38643
38653
  };
38644
- const pending = new Promise((resolve, reject) => {
38645
- this.pendingDownload.set(id, { resolve, reject });
38654
+ const pending = new Promise((resolve2, reject) => {
38655
+ this.pendingDownload.set(id, { resolve: resolve2, reject });
38646
38656
  });
38647
38657
  await this.sendInnerFrame(inner);
38648
38658
  return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
@@ -38689,8 +38699,8 @@ var FetchproxyServer = class {
38689
38699
  keys: [...opts.keys]
38690
38700
  }
38691
38701
  };
38692
- const pending = new Promise((resolve, reject) => {
38693
- this.pendingIdb.set(id, { resolve, reject });
38702
+ const pending = new Promise((resolve2, reject) => {
38703
+ this.pendingIdb.set(id, { resolve: resolve2, reject });
38694
38704
  });
38695
38705
  await this.sendInnerFrame(inner);
38696
38706
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
@@ -38729,8 +38739,8 @@ var FetchproxyServer = class {
38729
38739
  op: "read_dom",
38730
38740
  init: { origin, names: [...opts.names] }
38731
38741
  };
38732
- const pending = new Promise((resolve, reject) => {
38733
- this.pendingStorage.set(id, { resolve, reject });
38742
+ const pending = new Promise((resolve2, reject) => {
38743
+ this.pendingStorage.set(id, { resolve: resolve2, reject });
38734
38744
  });
38735
38745
  await this.sendInnerFrame(inner);
38736
38746
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
@@ -38775,8 +38785,8 @@ var FetchproxyServer = class {
38775
38785
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
38776
38786
  }
38777
38787
  };
38778
- const pending = new Promise((resolve, reject) => {
38779
- this.pendingGraphql.set(id, { resolve, reject });
38788
+ const pending = new Promise((resolve2, reject) => {
38789
+ this.pendingGraphql.set(id, { resolve: resolve2, reject });
38780
38790
  });
38781
38791
  await this.sendInnerFrame(inner);
38782
38792
  return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
@@ -39215,6 +39225,118 @@ var BootstrapDisabledError = class extends Error {
39215
39225
  }
39216
39226
  };
39217
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
+
39218
39340
  // node_modules/@chrischall/mcp-utils/dist/fetchproxy/index.js
39219
39341
  function bridgeErrorInfo(err) {
39220
39342
  const kind = classifyBridgeError(err);
@@ -39251,7 +39373,7 @@ function bridgeErrorInfo(err) {
39251
39373
  // package.json
39252
39374
  var package_default = {
39253
39375
  name: "honeybook-mcp",
39254
- version: "0.5.0",
39376
+ version: "0.7.0",
39255
39377
  mcpName: "io.github.chrischall/honeybook-mcp",
39256
39378
  description: "HoneyBook client-portal MCP server for Claude",
39257
39379
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -39293,9 +39415,9 @@ var package_default = {
39293
39415
  typecheck: "tsc -p tsconfig.json --noEmit"
39294
39416
  },
39295
39417
  dependencies: {
39296
- "@chrischall/mcp-utils": "^0.15.0",
39297
- "@fetchproxy/bootstrap": "^2.0.0",
39298
- "@fetchproxy/server": "^2.0.0",
39418
+ "@chrischall/mcp-utils": "^0.19.2",
39419
+ "@fetchproxy/bootstrap": "^2.2.0",
39420
+ "@fetchproxy/server": "^2.2.0",
39299
39421
  "@modelcontextprotocol/sdk": "^1.29.0",
39300
39422
  dotenv: "^17.4.2",
39301
39423
  zod: "^4.4.3"
@@ -39490,6 +39612,40 @@ function registerSessionTools(server) {
39490
39612
  );
39491
39613
  }
39492
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
+
39493
39649
  // src/types.ts
39494
39650
  var FILE_TYPES = ["agreement", "invoice", "brochure", "proposal"];
39495
39651
 
@@ -39890,10 +40046,10 @@ function registerInvoiceTools(server) {
39890
40046
 
39891
40047
  // src/index.ts
39892
40048
  var __dirname = dirname2(fileURLToPath(import.meta.url));
39893
- await loadDotenvSafely({ path: join4(__dirname, "..", ".env"), override: false });
40049
+ await loadDotenvSafely({ path: join5(__dirname, "..", ".env"), override: false });
39894
40050
  await runMcp({
39895
40051
  name: "honeybook-mcp",
39896
- version: "0.5.0",
40052
+ version: "0.7.0",
39897
40053
  // x-release-please-version
39898
40054
  tools: [
39899
40055
  registerSessionTools,
@@ -39901,6 +40057,7 @@ await runMcp({
39901
40057
  registerWorkspaceTools,
39902
40058
  registerPaymentMethodTools,
39903
40059
  registerContractTools,
39904
- registerInvoiceTools
40060
+ registerInvoiceTools,
40061
+ registerHealthcheckTools
39905
40062
  ]
39906
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.5.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.5.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,9 +42,9 @@
42
42
  "typecheck": "tsc -p tsconfig.json --noEmit"
43
43
  },
44
44
  "dependencies": {
45
- "@chrischall/mcp-utils": "^0.15.0",
46
- "@fetchproxy/bootstrap": "^2.0.0",
47
- "@fetchproxy/server": "^2.0.0",
45
+ "@chrischall/mcp-utils": "^0.19.2",
46
+ "@fetchproxy/bootstrap": "^2.2.0",
47
+ "@fetchproxy/server": "^2.2.0",
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
49
  "dotenv": "^17.4.2",
50
50
  "zod": "^4.4.3"
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.5.0",
9
+ "version": "0.7.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "honeybook-mcp",
14
- "version": "0.5.0",
14
+ "version": "0.7.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },