honeybook-mcp 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/auth.js +2 -5
- package/dist/bundle.js +456 -82
- package/dist/client.js +9 -2
- package/dist/index.js +1 -1
- package/dist/sessions.js +27 -126
- package/package.json +2 -2
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "HoneyBook client-portal tools for Claude Code",
|
|
10
|
-
"version": "0.3.
|
|
10
|
+
"version": "0.3.3"
|
|
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.
|
|
18
|
+
"version": "0.3.3",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Chall"
|
|
21
21
|
},
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
34805
|
-
|
|
34806
|
-
|
|
34807
|
-
|
|
34808
|
-
|
|
34809
|
-
this.
|
|
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.
|
|
34831
|
+
if (!existsSync(this.filePath))
|
|
34832
|
+
return;
|
|
34814
34833
|
try {
|
|
34815
|
-
|
|
34816
|
-
|
|
34817
|
-
|
|
34818
|
-
|
|
34819
|
-
|
|
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.
|
|
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
|
-
|
|
34826
|
-
|
|
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(
|
|
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
|
|
34842
|
-
|
|
34843
|
-
this.
|
|
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
|
-
|
|
34848
|
-
|
|
34849
|
-
if (
|
|
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
|
-
|
|
34856
|
-
|
|
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.
|
|
34860
|
-
const
|
|
34861
|
-
this.
|
|
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
|
-
/**
|
|
34936
|
+
/** Clear in-memory state without touching disk. Test helper. */
|
|
34868
34937
|
resetForTest() {
|
|
34869
34938
|
this.sessions.clear();
|
|
34870
|
-
this.
|
|
34939
|
+
this.mostRecentKey = null;
|
|
34871
34940
|
}
|
|
34872
34941
|
};
|
|
34873
|
-
var
|
|
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)
|
|
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
|
-
"
|
|
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
|
-
|
|
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.
|
|
38266
|
+
version: "0.3.3",
|
|
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.
|
|
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
|
-
|
|
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.
|
|
38898
|
+
version: "0.3.3",
|
|
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
|
-
|
|
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.
|
|
14
|
+
version: '0.3.3', // x-release-please-version
|
|
15
15
|
tools: [
|
|
16
16
|
registerSessionTools,
|
|
17
17
|
registerWorkspaceFileTools,
|
package/dist/sessions.js
CHANGED
|
@@ -1,127 +1,28 @@
|
|
|
1
|
-
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.3",
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.3.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "honeybook-mcp",
|
|
14
|
-
"version": "0.3.
|
|
14
|
+
"version": "0.3.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|