honeybook-mcp 0.3.2 → 0.3.4

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.3.2"
10
+ "version": "0.3.4"
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.3.2",
18
+ "version": "0.3.4",
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.3.2",
4
+ "version": "0.3.4",
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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Chris Hall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # HoneyBook MCP
2
2
 
3
+ [![CI](https://github.com/chrischall/honeybook-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/chrischall/honeybook-mcp/actions/workflows/ci.yml)
4
+ [![npm](https://img.shields.io/npm/v/honeybook-mcp)](https://www.npmjs.com/package/honeybook-mcp)
5
+ [![license](https://img.shields.io/npm/l/honeybook-mcp)](LICENSE)
6
+
3
7
  A [Model Context Protocol](https://modelcontextprotocol.io) server that connects Claude to the [HoneyBook](https://www.honeybook.com) client portal, giving you natural-language access to contracts and invoices sent by your wedding vendors.
4
8
 
5
9
  > [!WARNING]
package/dist/auth.js CHANGED
@@ -57,15 +57,12 @@
57
57
  // verify the captured session round-trips through sessionStore.get.
58
58
  import { bootstrap } from '@fetchproxy/bootstrap';
59
59
  import { bridgeErrorInfo } from '@chrischall/mcp-utils/fetchproxy';
60
- import { readEnvVar } from '@chrischall/mcp-utils';
60
+ import { parseBoolEnv } from '@chrischall/mcp-utils';
61
61
  import pkg from '../package.json' with { type: 'json' };
62
62
  import { sessionStore, normalizeOrigin } from './sessions.js';
63
63
  /** True if the user has explicitly disabled the fetchproxy capture path. */
64
64
  function fetchproxyDisabled() {
65
- const raw = readEnvVar('HONEYBOOK_DISABLE_FETCHPROXY');
66
- if (raw === undefined)
67
- return false;
68
- return ['1', 'true', 'yes', 'on'].includes(raw.toLowerCase());
65
+ return parseBoolEnv('HONEYBOOK_DISABLE_FETCHPROXY');
69
66
  }
70
67
  /**
71
68
  * Capture a HoneyBook portal session by reading the user's signed-in
package/dist/bundle.js CHANGED
@@ -34684,22 +34684,30 @@ async function runMcp(opts) {
34684
34684
  return server;
34685
34685
  }
34686
34686
 
34687
- // node_modules/@chrischall/mcp-utils/dist/response/index.js
34688
- function textResult(data) {
34689
- return {
34690
- content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
34691
- };
34692
- }
34693
- function rawTextResult(text) {
34694
- return { content: [{ type: "text", text }] };
34695
- }
34696
-
34697
34687
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
34698
34688
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
34699
34689
  var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
34700
34690
  var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
34691
+ var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
34692
+ var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
34693
+ var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
34694
+ var API_KEY_RE = new RegExp([
34695
+ "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
34696
+ // OpenAI / Anthropic (incl. sk-ant-…)
34697
+ "gh[pousr]_[A-Za-z0-9]{36,}\\b",
34698
+ // GitHub ghp_/gho_/ghu_/ghs_/ghr_
34699
+ "xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
34700
+ // Slack
34701
+ "AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
34702
+ // Google API key (39 chars total)
34703
+ "AKIA[0-9A-Z]{16}\\b",
34704
+ // AWS access key id (20 chars total)
34705
+ "whsec_[A-Za-z0-9]{16,}\\b"
34706
+ // webhook signing secret (Stripe-style)
34707
+ ].map((p) => `\\b${p}`).join("|"), "g");
34708
+ var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
34701
34709
  function redactSecrets(text) {
34702
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
34710
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(JWT_RE, "[REDACTED]");
34703
34711
  }
34704
34712
  function truncateErrorMessage(text, max = DEFAULT_ERROR_MESSAGE_MAX) {
34705
34713
  const str = text === null || text === void 0 ? "" : String(text);
@@ -34714,6 +34722,16 @@ function messageOf(err) {
34714
34722
  return String(err);
34715
34723
  }
34716
34724
 
34725
+ // node_modules/@chrischall/mcp-utils/dist/response/index.js
34726
+ function textResult(data) {
34727
+ return {
34728
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
34729
+ };
34730
+ }
34731
+ function rawTextResult(text) {
34732
+ return { content: [{ type: "text", text }] };
34733
+ }
34734
+
34717
34735
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
34718
34736
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
34719
34737
  function readEnvVar(key, opts = {}) {
@@ -34727,6 +34745,20 @@ function readEnvVar(key, opts = {}) {
34727
34745
  }
34728
34746
  return opts.default;
34729
34747
  }
34748
+ var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
34749
+ var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
34750
+ function parseBoolEnv(key, opts = {}) {
34751
+ const fallback = opts.default ?? false;
34752
+ const raw = readEnvVar(key, { env: opts.env });
34753
+ if (raw === void 0)
34754
+ return fallback;
34755
+ const token = raw.toLowerCase();
34756
+ if (TRUE_TOKENS.has(token))
34757
+ return true;
34758
+ if (FALSE_TOKENS.has(token))
34759
+ return false;
34760
+ return fallback;
34761
+ }
34730
34762
  async function loadDotenvSafely(opts = {}) {
34731
34763
  try {
34732
34764
  const mod = await import(
@@ -34770,107 +34802,155 @@ var pageSchema = {
34770
34802
  };
34771
34803
 
34772
34804
  // src/sessions.ts
34773
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from "node:fs";
34774
34805
  import { homedir } from "node:os";
34775
- import { join, dirname } from "node:path";
34806
+ import { join } from "node:path";
34807
+
34808
+ // node_modules/@chrischall/mcp-utils/dist/session/index.js
34809
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, renameSync } from "node:fs";
34810
+ import { dirname } from "node:path";
34776
34811
  function normalizeOrigin(input) {
34777
34812
  try {
34778
- const url2 = new URL(input);
34779
- return url2.origin.replace(/\/$/, "");
34813
+ return new URL(input).origin.replace(/\/$/, "");
34780
34814
  } catch {
34781
34815
  return input.replace(/\/$/, "");
34782
34816
  }
34783
34817
  }
34784
- function serializeSessions(sessions) {
34785
- return JSON.stringify(Array.from(sessions.values()), null, 2);
34786
- }
34787
- function deserializeSessions(body) {
34788
- try {
34789
- const arr = JSON.parse(body);
34790
- if (!Array.isArray(arr)) return /* @__PURE__ */ new Map();
34791
- const map2 = /* @__PURE__ */ new Map();
34792
- for (const s of arr) {
34793
- if (s && typeof s.portalOrigin === "string") {
34794
- map2.set(s.portalOrigin, s);
34795
- }
34796
- }
34797
- return map2;
34798
- } catch {
34799
- return /* @__PURE__ */ new Map();
34800
- }
34801
- }
34802
34818
  var SessionStore = class {
34803
- sessions;
34804
- mostRecentOrigin;
34805
- diskPath;
34806
- constructor(diskPath) {
34807
- this.diskPath = diskPath ?? join(homedir(), ".honeybook-mcp", "sessions.json");
34808
- this.sessions = /* @__PURE__ */ new Map();
34809
- this.mostRecentOrigin = null;
34819
+ sessions = /* @__PURE__ */ new Map();
34820
+ mostRecentKey = null;
34821
+ filePath;
34822
+ keyOf;
34823
+ normalizeKey;
34824
+ constructor(opts) {
34825
+ this.filePath = opts.filePath;
34826
+ this.keyOf = opts.keyOf;
34827
+ this.normalizeKey = opts.normalizeKey ?? normalizeOrigin;
34810
34828
  this.loadFromDisk();
34811
34829
  }
34812
34830
  loadFromDisk() {
34813
- if (!existsSync(this.diskPath)) return;
34831
+ if (!existsSync(this.filePath))
34832
+ return;
34814
34833
  try {
34815
- const body = readFileSync(this.diskPath, "utf8");
34816
- this.sessions = deserializeSessions(body);
34817
- const last = Array.from(this.sessions.keys()).pop();
34818
- this.mostRecentOrigin = last ?? null;
34819
- } catch {
34834
+ this.sessions = this.deserialize(readFileSync(this.filePath, "utf8"));
34835
+ const keys = Array.from(this.sessions.keys());
34836
+ this.mostRecentKey = keys[keys.length - 1] ?? null;
34837
+ } catch (err) {
34838
+ const backupPath = this.preserveCorruptFile();
34839
+ const detail = err instanceof Error ? err.message : String(err);
34840
+ console.error(`[mcp-utils] SessionStore: failed to parse ${this.filePath} (${detail}); ` + (backupPath !== null ? `preserved the corrupt file at ${backupPath}; ` : "could not preserve the corrupt file; ") + "starting with an empty store.");
34820
34841
  this.sessions = /* @__PURE__ */ new Map();
34821
- this.mostRecentOrigin = null;
34842
+ this.mostRecentKey = null;
34843
+ }
34844
+ }
34845
+ /**
34846
+ * Move a corrupt store file to `<filePath>.corrupt` (or `.corrupt-<n>` if a
34847
+ * previous backup exists) so a subsequent save cannot overwrite the only
34848
+ * copy of the prior credentials. Best-effort: returns the backup path, or
34849
+ * `null` if the rename failed.
34850
+ */
34851
+ preserveCorruptFile() {
34852
+ try {
34853
+ let candidate = `${this.filePath}.corrupt`;
34854
+ for (let n = 1; existsSync(candidate) && n <= 100; n++) {
34855
+ candidate = `${this.filePath}.corrupt-${n}`;
34856
+ }
34857
+ if (existsSync(candidate))
34858
+ return null;
34859
+ renameSync(this.filePath, candidate);
34860
+ return candidate;
34861
+ } catch {
34862
+ return null;
34863
+ }
34864
+ }
34865
+ /** Serialize the store to its on-disk JSON form (array, insertion order). */
34866
+ serialize() {
34867
+ return JSON.stringify(Array.from(this.sessions.values()), null, 2);
34868
+ }
34869
+ /** Parse on-disk JSON back into a keyed `Map`; empty map on invalid input. */
34870
+ deserialize(body) {
34871
+ const map2 = /* @__PURE__ */ new Map();
34872
+ const arr = JSON.parse(body);
34873
+ if (!Array.isArray(arr))
34874
+ return map2;
34875
+ for (const raw of arr) {
34876
+ if (raw && typeof raw === "object") {
34877
+ const s = raw;
34878
+ const key = this.normalizeKey(this.keyOf(s));
34879
+ map2.set(key, s);
34880
+ }
34822
34881
  }
34882
+ return map2;
34823
34883
  }
34824
34884
  saveToDisk() {
34825
- mkdirSync(dirname(this.diskPath), { recursive: true });
34826
- writeFileSync(this.diskPath, serializeSessions(this.sessions), { mode: 384 });
34885
+ const dir = dirname(this.filePath);
34886
+ mkdirSync(dir, { recursive: true, mode: 448 });
34887
+ if (existsSync(this.filePath)) {
34888
+ try {
34889
+ chmodSync(this.filePath, 384);
34890
+ } catch {
34891
+ }
34892
+ }
34893
+ writeFileSync(this.filePath, this.serialize(), { mode: 384 });
34827
34894
  try {
34828
- chmodSync(dirname(this.diskPath), 448);
34895
+ chmodSync(this.filePath, 384);
34896
+ chmodSync(dir, 448);
34829
34897
  } catch {
34830
34898
  }
34831
34899
  }
34832
- /**
34833
- * Insert (or replace) a fully-constructed CapturedSession. Used by
34834
- * `src/auth.ts` after `@fetchproxy/bootstrap` has supplied the
34835
- * jStorage + fingerprint values from the user's browser tab.
34836
- *
34837
- * Marks the session as most-recent so the next tool call that doesn't
34838
- * pass `origin` picks it up automatically.
34839
- */
34900
+ /** Insert or replace a record, normalizing its key and marking it active. */
34840
34901
  add(session) {
34841
- const normalized = normalizeOrigin(session.portalOrigin);
34842
- const stored = { ...session, portalOrigin: normalized };
34843
- this.sessions.set(normalized, stored);
34844
- this.mostRecentOrigin = normalized;
34902
+ const key = this.normalizeKey(this.keyOf(session));
34903
+ this.sessions.set(key, session);
34904
+ this.mostRecentKey = key;
34845
34905
  this.saveToDisk();
34846
34906
  }
34847
- get(origin) {
34848
- if (origin) return this.sessions.get(normalizeOrigin(origin)) ?? null;
34849
- if (this.mostRecentOrigin) return this.sessions.get(this.mostRecentOrigin) ?? null;
34907
+ /** Look up by key; with no key, returns the active (most-recent) session. */
34908
+ get(key) {
34909
+ if (key !== void 0)
34910
+ return this.sessions.get(this.normalizeKey(key)) ?? null;
34911
+ if (this.mostRecentKey !== null)
34912
+ return this.sessions.get(this.mostRecentKey) ?? null;
34850
34913
  return null;
34851
34914
  }
34915
+ /** The most-recently-added session, or `null`. */
34916
+ getActiveSession() {
34917
+ return this.get();
34918
+ }
34919
+ /** All sessions in insertion order. */
34852
34920
  list() {
34853
34921
  return Array.from(this.sessions.values());
34854
34922
  }
34855
- deactivate(origin) {
34856
- const normalized = normalizeOrigin(origin);
34923
+ /** Remove a session; fixes up the active pointer. Returns whether it existed. */
34924
+ remove(key) {
34925
+ const normalized = this.normalizeKey(key);
34857
34926
  const had = this.sessions.delete(normalized);
34858
34927
  if (had) {
34859
- if (this.mostRecentOrigin === normalized) {
34860
- const remaining = Array.from(this.sessions.keys());
34861
- this.mostRecentOrigin = remaining[remaining.length - 1] ?? null;
34928
+ if (this.mostRecentKey === normalized) {
34929
+ const keys = Array.from(this.sessions.keys());
34930
+ this.mostRecentKey = keys[keys.length - 1] ?? null;
34862
34931
  }
34863
34932
  this.saveToDisk();
34864
34933
  }
34865
34934
  return had;
34866
34935
  }
34867
- /** Test-only — clears in-memory state without touching disk. */
34936
+ /** Clear in-memory state without touching disk. Test helper. */
34868
34937
  resetForTest() {
34869
34938
  this.sessions.clear();
34870
- this.mostRecentOrigin = null;
34939
+ this.mostRecentKey = null;
34871
34940
  }
34872
34941
  };
34873
- var sessionStore = new SessionStore();
34942
+ var TOKEN_REFRESH_SKEW_MS = 5 * 60 * 1e3;
34943
+
34944
+ // src/sessions.ts
34945
+ var sessionsDir = readEnvVar("HONEYBOOK_SESSIONS_DIR", {
34946
+ default: join(homedir(), ".honeybook-mcp")
34947
+ });
34948
+ var sessionsFilePath = join(sessionsDir, "sessions.json");
34949
+ var sessionStore = new SessionStore({
34950
+ filePath: sessionsFilePath,
34951
+ keyOf: (session) => session.portalOrigin,
34952
+ normalizeKey: normalizeOrigin
34953
+ });
34874
34954
 
34875
34955
  // src/client.ts
34876
34956
  var API_BASE = "https://api.honeybook.com";
@@ -34958,7 +35038,12 @@ async function getActiveClient(origin) {
34958
35038
  }
34959
35039
  const cached2 = clientCache.get(session.portalOrigin);
34960
35040
  if (cached2) return cached2;
34961
- if (!moduleState.apiVersionPromise) moduleState.apiVersionPromise = fetchApiVersion();
35041
+ if (!moduleState.apiVersionPromise) {
35042
+ moduleState.apiVersionPromise = fetchApiVersion().catch((err) => {
35043
+ moduleState.apiVersionPromise = null;
35044
+ throw err;
35045
+ });
35046
+ }
34962
35047
  const apiVersion = await moduleState.apiVersionPromise;
34963
35048
  const client = new HoneyBookClient(session, apiVersion);
34964
35049
  clientCache.set(session.portalOrigin, client);
@@ -34974,7 +35059,9 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34974
35059
  "read_local_storage",
34975
35060
  "read_session_storage",
34976
35061
  "capture_request_header",
34977
- "read_indexed_db"
35062
+ "capture_redirect",
35063
+ "read_indexed_db",
35064
+ "download"
34978
35065
  ]);
34979
35066
 
34980
35067
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
@@ -35534,6 +35621,31 @@ function validateInnerRequest(raw) {
35534
35621
  }
35535
35622
  return raw;
35536
35623
  }
35624
+ if (raw.op === "capture_redirect") {
35625
+ assertObject(raw.init, "inner.init");
35626
+ if (raw.init.host === void 0) {
35627
+ throw new ProtocolError("inner.init.host: missing");
35628
+ }
35629
+ assertString(raw.init.host, "inner.init.host");
35630
+ if (!HOSTNAME_RE.test(raw.init.host)) {
35631
+ throw new ProtocolError(`inner.init.host: invalid hostname ${JSON.stringify(raw.init.host)}`);
35632
+ }
35633
+ if (raw.init.path !== void 0) {
35634
+ assertString(raw.init.path, "inner.init.path");
35635
+ if (!CAPTURE_PATH_RE.test(raw.init.path)) {
35636
+ throw new ProtocolError(`inner.init.path: must start with '/' ${JSON.stringify(raw.init.path)}`);
35637
+ }
35638
+ }
35639
+ if (raw.init.timeoutMs !== void 0) {
35640
+ assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
35641
+ }
35642
+ for (const k of Object.keys(raw.init)) {
35643
+ if (k !== "host" && k !== "path" && k !== "timeoutMs") {
35644
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_redirect`);
35645
+ }
35646
+ }
35647
+ return raw;
35648
+ }
35537
35649
  if (raw.op === "read_indexed_db") {
35538
35650
  assertObject(raw.init, "inner.init");
35539
35651
  if (raw.init.origin === void 0)
@@ -35559,7 +35671,32 @@ function validateInnerRequest(raw) {
35559
35671
  }
35560
35672
  return raw;
35561
35673
  }
35562
- throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "read_indexed_db"; got ${JSON.stringify(raw.op)}`);
35674
+ if (raw.op === "download") {
35675
+ assertObject(raw.init, "inner.init");
35676
+ if (raw.init.url === void 0) {
35677
+ throw new ProtocolError("inner.init.url: missing");
35678
+ }
35679
+ assertHttpUrl(raw.init.url, "inner.init.url");
35680
+ if (raw.init.filename !== void 0) {
35681
+ assertString(raw.init.filename, "inner.init.filename");
35682
+ if (/^([/\\]|[A-Za-z]:)/.test(raw.init.filename)) {
35683
+ throw new ProtocolError(`inner.init.filename: must be relative ${JSON.stringify(raw.init.filename)}`);
35684
+ }
35685
+ if (raw.init.filename.split(/[/\\]/).includes("..")) {
35686
+ throw new ProtocolError(`inner.init.filename: must not contain '..' ${JSON.stringify(raw.init.filename)}`);
35687
+ }
35688
+ }
35689
+ if (raw.init.timeoutMs !== void 0) {
35690
+ assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
35691
+ }
35692
+ for (const k of Object.keys(raw.init)) {
35693
+ if (k !== "url" && k !== "filename" && k !== "timeoutMs") {
35694
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on download`);
35695
+ }
35696
+ }
35697
+ return raw;
35698
+ }
35699
+ throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
35563
35700
  }
35564
35701
  function assertNonEmptyKeyArray(value, label) {
35565
35702
  if (!Array.isArray(value)) {
@@ -35630,6 +35767,13 @@ function validateInnerResponse(raw) {
35630
35767
  assertString(raw.value, "inner.value");
35631
35768
  return raw;
35632
35769
  }
35770
+ if (op === "capture_redirect") {
35771
+ if (raw.value === void 0) {
35772
+ throw new ProtocolError("inner.value: missing on capture_redirect response");
35773
+ }
35774
+ assertString(raw.value, "inner.value");
35775
+ return raw;
35776
+ }
35633
35777
  if (op === "read_indexed_db") {
35634
35778
  if (raw.values === void 0) {
35635
35779
  throw new ProtocolError("inner.values: missing on read_indexed_db response");
@@ -35637,6 +35781,25 @@ function validateInnerResponse(raw) {
35637
35781
  assertObject(raw.values, "inner.values");
35638
35782
  return raw;
35639
35783
  }
35784
+ if (op === "download") {
35785
+ assertObject(raw.value, "inner.value");
35786
+ assertString(raw.value.path, "inner.value.path");
35787
+ if (typeof raw.value.bytes !== "number" || !Number.isInteger(raw.value.bytes) || raw.value.bytes < 0) {
35788
+ throw new ProtocolError("inner.value.bytes: expected non-negative integer");
35789
+ }
35790
+ if (raw.value.mime !== void 0) {
35791
+ assertString(raw.value.mime, "inner.value.mime");
35792
+ }
35793
+ if (raw.value.finalUrl !== void 0) {
35794
+ assertString(raw.value.finalUrl, "inner.value.finalUrl");
35795
+ }
35796
+ for (const k of Object.keys(raw.value)) {
35797
+ if (k !== "path" && k !== "bytes" && k !== "mime" && k !== "finalUrl") {
35798
+ throw new ProtocolError(`inner.value: unexpected field ${JSON.stringify(k)} on download response`);
35799
+ }
35800
+ }
35801
+ return raw;
35802
+ }
35640
35803
  throw new ProtocolError(`inner.op: unknown success-response op ${JSON.stringify(raw.op)}`);
35641
35804
  }
35642
35805
  if (raw.ok === false) {
@@ -36507,8 +36670,12 @@ var FetchproxyServer = class {
36507
36670
  pendingStorage = /* @__PURE__ */ new Map();
36508
36671
  // 0.3.0+: capture-header awaiters resolve a single string.
36509
36672
  pendingCapture = /* @__PURE__ */ new Map();
36673
+ // capture_redirect awaiters resolve the captured redirect URL string.
36674
+ pendingRedirect = /* @__PURE__ */ new Map();
36510
36675
  // 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
36511
36676
  pendingIdb = /* @__PURE__ */ new Map();
36677
+ // download awaiters resolve the saved-file metadata (path + size + mime).
36678
+ pendingDownload = /* @__PURE__ */ new Map();
36512
36679
  mcpId = null;
36513
36680
  identity = null;
36514
36681
  // 0.5.3+: in-flight role-election / handle-start promise. Set the
@@ -37440,6 +37607,181 @@ var FetchproxyServer = class {
37440
37607
  }
37441
37608
  return pending;
37442
37609
  }
37610
+ /**
37611
+ * Snapshot the redirect target URL of the next request the browser
37612
+ * makes to `(host, path?)`. Single-shot: the extension registers a
37613
+ * one-time `chrome.webRequest.onBeforeRedirect` listener filtered on
37614
+ * `https://${host}${path ?? '/*'}`, captures `details.redirectUrl` on
37615
+ * the first match, removes itself, and resolves with the URL. Times out
37616
+ * after `timeoutMs` (default 30s on the extension).
37617
+ *
37618
+ * Use case: a Cloudflare-walled endpoint that 302-redirects cross-origin
37619
+ * to a presigned URL — a page-level fetch sees only an opaque redirect,
37620
+ * but `onBeforeRedirect` exposes the target. Capture is limited to the
37621
+ * MCP's own declared `domains`; no per-entry declared scope is required.
37622
+ */
37623
+ async captureRedirect(opts) {
37624
+ if (!this.opts.capabilities.includes("capture_redirect")) {
37625
+ throw new Error('FetchproxyServer.captureRedirect(): MCP did not declare "capture_redirect" in capabilities');
37626
+ }
37627
+ await this.ensureConnected();
37628
+ this.throwIfPendingPair();
37629
+ try {
37630
+ const result = await this._captureRedirectOnce(opts);
37631
+ this.recordSuccess();
37632
+ return result;
37633
+ } catch (err) {
37634
+ const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
37635
+ if (!swDown) {
37636
+ this.recordFailure(`capture_redirect: ${err.message ?? String(err)}`);
37637
+ throw err;
37638
+ }
37639
+ this.lastEvictionDetectedAt = Date.now();
37640
+ const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
37641
+ if (reviveMs > 0) {
37642
+ this.lazyReviveAttempts += 1;
37643
+ await new Promise((r) => setTimeout(r, reviveMs));
37644
+ try {
37645
+ const result = await this._captureRedirectOnce(opts);
37646
+ this.lazyReviveSuccesses += 1;
37647
+ this.recordSuccess();
37648
+ return result;
37649
+ } catch (retryErr) {
37650
+ const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
37651
+ if (!stillDown) {
37652
+ this.recordFailure(`capture_redirect: ${retryErr.message ?? String(retryErr)}`);
37653
+ throw retryErr;
37654
+ }
37655
+ this.recordFailure(`capture_redirect bridge-down: ${retryErr.message}`);
37656
+ throw new FetchproxyBridgeDownError({
37657
+ originalError: retryErr.message,
37658
+ retryAttempted: true,
37659
+ op: "capture_redirect",
37660
+ url: `https://${opts.host}${opts.path ?? "/*"}`,
37661
+ role: this.role,
37662
+ port: this.opts.port
37663
+ });
37664
+ }
37665
+ }
37666
+ this.recordFailure(`capture_redirect bridge-down: ${err.message}`);
37667
+ throw new FetchproxyBridgeDownError({
37668
+ originalError: err.message,
37669
+ retryAttempted: false,
37670
+ op: "capture_redirect",
37671
+ url: `https://${opts.host}${opts.path ?? "/*"}`,
37672
+ role: this.role,
37673
+ port: this.opts.port
37674
+ });
37675
+ }
37676
+ }
37677
+ async _captureRedirectOnce(opts) {
37678
+ const id = this.nextRequestId++;
37679
+ const inner = {
37680
+ type: "request",
37681
+ id,
37682
+ op: "capture_redirect",
37683
+ init: {
37684
+ host: opts.host,
37685
+ ...opts.path !== void 0 ? { path: opts.path } : {},
37686
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
37687
+ }
37688
+ };
37689
+ const pending = new Promise((resolve, reject) => {
37690
+ this.pendingRedirect.set(id, { resolve, reject });
37691
+ });
37692
+ if (this.hostHandle) {
37693
+ await this.hostHandle.sendOwnInner(inner);
37694
+ } else if (this.peerHandle) {
37695
+ await this.peerHandle.sendInner(inner);
37696
+ }
37697
+ return pending;
37698
+ }
37699
+ /**
37700
+ * Download `url` through the BROWSER's own network stack via
37701
+ * `chrome.downloads` (real cookies + TLS/JA3 fingerprint). Unlike a
37702
+ * page-level `fetch()` (cors mode), this clears a Cloudflare bot-challenge
37703
+ * on the endpoint and follows the cross-origin redirect to the final file.
37704
+ * Resolves the saved local file path + size; the bridge is loopback-only /
37705
+ * single-host, so the MCP reads the bytes from the same disk. Requires
37706
+ * `'download'` in capabilities and the `url` host to be a declared `domain`.
37707
+ */
37708
+ async download(opts) {
37709
+ if (!this.opts.capabilities.includes("download")) {
37710
+ throw new Error('FetchproxyServer.download(): MCP did not declare "download" in capabilities');
37711
+ }
37712
+ assertUrlInDomains("download url", opts.url, this.opts.domains);
37713
+ await this.ensureConnected();
37714
+ this.throwIfPendingPair();
37715
+ try {
37716
+ const result = await this._downloadOnce(opts);
37717
+ this.recordSuccess();
37718
+ return result;
37719
+ } catch (err) {
37720
+ const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
37721
+ if (!swDown) {
37722
+ this.recordFailure(`download: ${err.message ?? String(err)}`);
37723
+ throw err;
37724
+ }
37725
+ this.lastEvictionDetectedAt = Date.now();
37726
+ const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
37727
+ if (reviveMs > 0) {
37728
+ this.lazyReviveAttempts += 1;
37729
+ await new Promise((r) => setTimeout(r, reviveMs));
37730
+ try {
37731
+ const result = await this._downloadOnce(opts);
37732
+ this.lazyReviveSuccesses += 1;
37733
+ this.recordSuccess();
37734
+ return result;
37735
+ } catch (retryErr) {
37736
+ const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
37737
+ if (!stillDown) {
37738
+ this.recordFailure(`download: ${retryErr.message ?? String(retryErr)}`);
37739
+ throw retryErr;
37740
+ }
37741
+ this.recordFailure(`download bridge-down: ${retryErr.message}`);
37742
+ throw new FetchproxyBridgeDownError({
37743
+ originalError: retryErr.message,
37744
+ retryAttempted: true,
37745
+ op: "download",
37746
+ url: opts.url,
37747
+ role: this.role,
37748
+ port: this.opts.port
37749
+ });
37750
+ }
37751
+ }
37752
+ this.recordFailure(`download bridge-down: ${err.message}`);
37753
+ throw new FetchproxyBridgeDownError({
37754
+ originalError: err.message,
37755
+ retryAttempted: false,
37756
+ op: "download",
37757
+ url: opts.url,
37758
+ role: this.role,
37759
+ port: this.opts.port
37760
+ });
37761
+ }
37762
+ }
37763
+ async _downloadOnce(opts) {
37764
+ const id = this.nextRequestId++;
37765
+ const inner = {
37766
+ type: "request",
37767
+ id,
37768
+ op: "download",
37769
+ init: {
37770
+ url: opts.url,
37771
+ ...opts.filename !== void 0 ? { filename: opts.filename } : {},
37772
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
37773
+ }
37774
+ };
37775
+ const pending = new Promise((resolve, reject) => {
37776
+ this.pendingDownload.set(id, { resolve, reject });
37777
+ });
37778
+ if (this.hostHandle) {
37779
+ await this.hostHandle.sendOwnInner(inner);
37780
+ } else if (this.peerHandle) {
37781
+ await this.peerHandle.sendInner(inner);
37782
+ }
37783
+ return pending;
37784
+ }
37443
37785
  /**
37444
37786
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
37445
37787
  * tab. Requires `'read_indexed_db'` in capabilities AND the
@@ -37587,6 +37929,20 @@ var FetchproxyServer = class {
37587
37929
  }
37588
37930
  return;
37589
37931
  }
37932
+ const redirectCb = this.pendingRedirect.get(inner.id);
37933
+ if (redirectCb) {
37934
+ this.pendingRedirect.delete(inner.id);
37935
+ if (inner.ok) {
37936
+ if (inner.op === "capture_redirect" && typeof inner.value === "string") {
37937
+ redirectCb.resolve(inner.value);
37938
+ } else {
37939
+ redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
37940
+ }
37941
+ } else {
37942
+ redirectCb.reject(new FetchproxyProtocolError(inner.error));
37943
+ }
37944
+ return;
37945
+ }
37590
37946
  const idbCb = this.pendingIdb.get(inner.id);
37591
37947
  if (idbCb) {
37592
37948
  this.pendingIdb.delete(inner.id);
@@ -37601,6 +37957,20 @@ var FetchproxyServer = class {
37601
37957
  }
37602
37958
  return;
37603
37959
  }
37960
+ const downloadCb = this.pendingDownload.get(inner.id);
37961
+ if (downloadCb) {
37962
+ this.pendingDownload.delete(inner.id);
37963
+ if (inner.ok) {
37964
+ if (inner.op === "download" && inner.value && typeof inner.value === "object") {
37965
+ downloadCb.resolve({ ...inner.value });
37966
+ } else {
37967
+ downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
37968
+ }
37969
+ } else {
37970
+ downloadCb.reject(new FetchproxyProtocolError(inner.error));
37971
+ }
37972
+ return;
37973
+ }
37604
37974
  const cookiesCb = this.pendingReadCookies.get(inner.id);
37605
37975
  if (cookiesCb) {
37606
37976
  this.pendingReadCookies.delete(inner.id);
@@ -37643,9 +38013,15 @@ var FetchproxyServer = class {
37643
38013
  for (const { reject } of this.pendingCapture.values())
37644
38014
  reject(err);
37645
38015
  this.pendingCapture.clear();
38016
+ for (const { reject } of this.pendingRedirect.values())
38017
+ reject(err);
38018
+ this.pendingRedirect.clear();
37646
38019
  for (const { reject } of this.pendingIdb.values())
37647
38020
  reject(err);
37648
38021
  this.pendingIdb.clear();
38022
+ for (const { reject } of this.pendingDownload.values())
38023
+ reject(err);
38024
+ this.pendingDownload.clear();
37649
38025
  }
37650
38026
  /**
37651
38027
  * 0.5.2+: read the current pair-pending pair code from whichever handle
@@ -37887,7 +38263,7 @@ function bridgeErrorInfo(err) {
37887
38263
  // package.json
37888
38264
  var package_default = {
37889
38265
  name: "honeybook-mcp",
37890
- version: "0.3.2",
38266
+ version: "0.3.4",
37891
38267
  mcpName: "io.github.chrischall/honeybook-mcp",
37892
38268
  description: "HoneyBook client-portal MCP server for Claude",
37893
38269
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -37927,7 +38303,7 @@ var package_default = {
37927
38303
  "test:watch": "vitest"
37928
38304
  },
37929
38305
  dependencies: {
37930
- "@chrischall/mcp-utils": "^0.5.0",
38306
+ "@chrischall/mcp-utils": "^0.7.0",
37931
38307
  "@fetchproxy/bootstrap": "^1.0.0",
37932
38308
  "@fetchproxy/server": "^1.0.0",
37933
38309
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -37945,9 +38321,7 @@ var package_default = {
37945
38321
 
37946
38322
  // src/auth.ts
37947
38323
  function fetchproxyDisabled() {
37948
- const raw = readEnvVar("HONEYBOOK_DISABLE_FETCHPROXY");
37949
- if (raw === void 0) return false;
37950
- return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
38324
+ return parseBoolEnv("HONEYBOOK_DISABLE_FETCHPROXY");
37951
38325
  }
37952
38326
  async function captureSessionViaFetchproxy(opts) {
37953
38327
  if (fetchproxyDisabled()) {
@@ -38521,7 +38895,7 @@ var __dirname = dirname2(fileURLToPath(import.meta.url));
38521
38895
  await loadDotenvSafely({ path: join3(__dirname, "..", ".env"), override: false });
38522
38896
  await runMcp({
38523
38897
  name: "honeybook-mcp",
38524
- version: "0.3.2",
38898
+ version: "0.3.4",
38525
38899
  // x-release-please-version
38526
38900
  tools: [
38527
38901
  registerSessionTools,
package/dist/client.js CHANGED
@@ -90,8 +90,15 @@ export async function getActiveClient(origin) {
90
90
  const cached = clientCache.get(session.portalOrigin);
91
91
  if (cached)
92
92
  return cached;
93
- if (!moduleState.apiVersionPromise)
94
- moduleState.apiVersionPromise = fetchApiVersion();
93
+ if (!moduleState.apiVersionPromise) {
94
+ // Memoize the one-time fetch, but only on success — if it rejects, clear
95
+ // the cache so the next call retries instead of re-awaiting the same
96
+ // rejection until process restart.
97
+ moduleState.apiVersionPromise = fetchApiVersion().catch((err) => {
98
+ moduleState.apiVersionPromise = null;
99
+ throw err;
100
+ });
101
+ }
95
102
  const apiVersion = await moduleState.apiVersionPromise;
96
103
  const client = new HoneyBookClient(session, apiVersion);
97
104
  clientCache.set(session.portalOrigin, client);
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { registerContractTools } from './tools/contracts.js';
11
11
  import { registerInvoiceTools } from './tools/invoices.js';
12
12
  await runMcp({
13
13
  name: 'honeybook-mcp',
14
- version: '0.3.2', // x-release-please-version
14
+ version: '0.3.4', // x-release-please-version
15
15
  tools: [
16
16
  registerSessionTools,
17
17
  registerWorkspaceFileTools,
package/dist/sessions.js CHANGED
@@ -1,127 +1,28 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
1
+ // Disk-persisted session cache — thin adapter over the shared SessionStore in
2
+ // @chrischall/mcp-utils/session (this repo was the donor that store was
3
+ // extracted from). The shared store keeps the same semantics (keyed by
4
+ // normalized portalOrigin, most-recently-added is active, JSON array on disk
5
+ // in insertion order) and adds two hardening upgrades the local copy lacked:
6
+ // - file perms are re-asserted 0600 (dir 0700) on EVERY save, so a loose
7
+ // pre-existing sessions.json gets tightened before secrets are written;
8
+ // - a corrupt sessions.json is preserved as `<path>.corrupt` instead of
9
+ // being silently overwritten on the next save.
2
10
  import { homedir } from 'node:os';
3
- import { join, dirname } from 'node:path';
4
- // ---------------------------------------------------------------------------
5
- // Pure helpers — testable without a browser
6
- // ---------------------------------------------------------------------------
7
- /**
8
- * Given a full URL or just an origin, return the origin without trailing slash.
9
- * e.g. normalizeOrigin('https://x.hbportal.co/app/workspace_file/123') → 'https://x.hbportal.co'
10
- * normalizeOrigin('https://x.hbportal.co/') → 'https://x.hbportal.co'
11
- */
12
- export function normalizeOrigin(input) {
13
- try {
14
- const url = new URL(input);
15
- return url.origin.replace(/\/$/, '');
16
- }
17
- catch {
18
- return input.replace(/\/$/, '');
19
- }
20
- }
21
- /**
22
- * JSON stringify — sessions as an array in insertion order.
23
- */
24
- export function serializeSessions(sessions) {
25
- return JSON.stringify(Array.from(sessions.values()), null, 2);
26
- }
27
- /**
28
- * Parse JSON array back into a Map keyed by portalOrigin; return empty map on invalid input.
29
- */
30
- export function deserializeSessions(body) {
31
- try {
32
- const arr = JSON.parse(body);
33
- if (!Array.isArray(arr))
34
- return new Map();
35
- const map = new Map();
36
- for (const s of arr) {
37
- if (s && typeof s.portalOrigin === 'string') {
38
- map.set(s.portalOrigin, s);
39
- }
40
- }
41
- return map;
42
- }
43
- catch {
44
- return new Map();
45
- }
46
- }
47
- // ---------------------------------------------------------------------------
48
- // SessionStore — manages in-memory + disk-persisted sessions
49
- // ---------------------------------------------------------------------------
50
- class SessionStore {
51
- sessions;
52
- mostRecentOrigin;
53
- diskPath;
54
- constructor(diskPath) {
55
- this.diskPath = diskPath ?? join(homedir(), '.honeybook-mcp', 'sessions.json');
56
- this.sessions = new Map();
57
- this.mostRecentOrigin = null;
58
- this.loadFromDisk();
59
- }
60
- loadFromDisk() {
61
- if (!existsSync(this.diskPath))
62
- return;
63
- try {
64
- const body = readFileSync(this.diskPath, 'utf8');
65
- this.sessions = deserializeSessions(body);
66
- const last = Array.from(this.sessions.keys()).pop();
67
- this.mostRecentOrigin = last ?? null;
68
- }
69
- catch {
70
- this.sessions = new Map();
71
- this.mostRecentOrigin = null;
72
- }
73
- }
74
- saveToDisk() {
75
- mkdirSync(dirname(this.diskPath), { recursive: true });
76
- writeFileSync(this.diskPath, serializeSessions(this.sessions), { mode: 0o600 });
77
- try {
78
- chmodSync(dirname(this.diskPath), 0o700);
79
- }
80
- catch {
81
- // best-effort
82
- }
83
- }
84
- /**
85
- * Insert (or replace) a fully-constructed CapturedSession. Used by
86
- * `src/auth.ts` after `@fetchproxy/bootstrap` has supplied the
87
- * jStorage + fingerprint values from the user's browser tab.
88
- *
89
- * Marks the session as most-recent so the next tool call that doesn't
90
- * pass `origin` picks it up automatically.
91
- */
92
- add(session) {
93
- const normalized = normalizeOrigin(session.portalOrigin);
94
- const stored = { ...session, portalOrigin: normalized };
95
- this.sessions.set(normalized, stored);
96
- this.mostRecentOrigin = normalized;
97
- this.saveToDisk();
98
- }
99
- get(origin) {
100
- if (origin)
101
- return this.sessions.get(normalizeOrigin(origin)) ?? null;
102
- if (this.mostRecentOrigin)
103
- return this.sessions.get(this.mostRecentOrigin) ?? null;
104
- return null;
105
- }
106
- list() {
107
- return Array.from(this.sessions.values());
108
- }
109
- deactivate(origin) {
110
- const normalized = normalizeOrigin(origin);
111
- const had = this.sessions.delete(normalized);
112
- if (had) {
113
- if (this.mostRecentOrigin === normalized) {
114
- const remaining = Array.from(this.sessions.keys());
115
- this.mostRecentOrigin = remaining[remaining.length - 1] ?? null;
116
- }
117
- this.saveToDisk();
118
- }
119
- return had;
120
- }
121
- /** Test-only — clears in-memory state without touching disk. */
122
- resetForTest() {
123
- this.sessions.clear();
124
- this.mostRecentOrigin = null;
125
- }
126
- }
127
- export const sessionStore = new SessionStore();
11
+ import { join } from 'node:path';
12
+ import { SessionStore, normalizeOrigin } from '@chrischall/mcp-utils/session';
13
+ import { readEnvVar } from '@chrischall/mcp-utils';
14
+ export { normalizeOrigin };
15
+ // Base directory holding `sessions.json`. Defaults to the real per-user store
16
+ // (`~/.honeybook-mcp`). Overridable via `HONEYBOOK_SESSIONS_DIR` so the test
17
+ // suite can redirect persistence into an isolated temp dir and never touch the
18
+ // developer's real captured-session credentials. Production never sets the env
19
+ // var, so the default is unchanged.
20
+ export const sessionsDir = readEnvVar('HONEYBOOK_SESSIONS_DIR', {
21
+ default: join(homedir(), '.honeybook-mcp'),
22
+ });
23
+ export const sessionsFilePath = join(sessionsDir, 'sessions.json');
24
+ export const sessionStore = new SessionStore({
25
+ filePath: sessionsFilePath,
26
+ keyOf: (session) => session.portalOrigin,
27
+ normalizeKey: normalizeOrigin,
28
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honeybook-mcp",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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>",
@@ -40,7 +40,7 @@
40
40
  "test:watch": "vitest"
41
41
  },
42
42
  "dependencies": {
43
- "@chrischall/mcp-utils": "^0.5.0",
43
+ "@chrischall/mcp-utils": "^0.7.0",
44
44
  "@fetchproxy/bootstrap": "^1.0.0",
45
45
  "@fetchproxy/server": "^1.0.0",
46
46
  "@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.3.2",
9
+ "version": "0.3.4",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "honeybook-mcp",
14
- "version": "0.3.2",
14
+ "version": "0.3.4",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },