@chrischall/eventbrite-mcp 0.1.0 → 0.1.2

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": "MCP server for Eventbrite — your tickets and orders, organizer data, and public event discovery",
10
- "version": "0.1.0"
10
+ "version": "0.1.2"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Eventbrite",
16
16
  "source": "./",
17
17
  "description": "MCP server for Eventbrite — tickets, orders, organizer data, and public event search. Account tools use a personal API token; discovery search routes through the user's signed-in eventbrite.com tab via the fetchproxy bridge, reusing their authenticated session.",
18
- "version": "0.1.0",
18
+ "version": "0.1.2",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "eventbrite-mcp",
3
3
  "displayName": "Eventbrite",
4
- "version": "0.1.0",
4
+ "version": "0.1.2",
5
5
  "description": "MCP server for Eventbrite — your tickets and orders, organizer data, and public event discovery",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/README.md CHANGED
@@ -79,20 +79,12 @@ Search flow: `eb_resolve_place {location: "Charlotte, NC"}` →
79
79
  `eb_resolve_place` also accepts a raw slug (`nc--charlotte`). A bare city with
80
80
  no state or country is rejected rather than guessed.
81
81
 
82
- ## Hosted connector
83
-
84
- `src/worker.ts` deploys the token-API tools as a Cloudflare Worker remote
85
- connector for claude.ai (OAuth login collects your Eventbrite token). The
86
- discovery tools are **excluded** there — the browser bridge doesn't exist in a
87
- Worker. See `docs/DEPLOY-CONNECTOR.md`.
88
-
89
82
  ## Development
90
83
 
91
84
  ```sh
92
85
  npm install
93
86
  npm test # node suite
94
87
  npm run build # tsc + esbuild bundle
95
- npm run worker:test
96
88
  ```
97
89
 
98
90
  API shape notes (captured + verified): `docs/EVENTBRITE-API.md`. A
package/dist/bundle.js CHANGED
@@ -3659,7 +3659,12 @@ var require_fast_uri = __commonJS({
3659
3659
  }
3660
3660
  function resolve(baseURI, relativeURI, options) {
3661
3661
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3662
- const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
3662
+ const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3663
+ const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
3664
+ if (baseMalformed || relativeMalformed) {
3665
+ throw new Error(baseParsed.error || relativeParsed.error || "URI is malformed.");
3666
+ }
3667
+ const resolved = resolveComponent(baseParsed, relativeParsed, schemelessOptions, true);
3663
3668
  schemelessOptions.skipEscape = true;
3664
3669
  return serialize(resolved, schemelessOptions);
3665
3670
  }
@@ -3785,6 +3790,7 @@ var require_fast_uri = __commonJS({
3785
3790
  }
3786
3791
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3787
3792
  var AUTHORITY_PREFIX = /^(?:[^#/:?]+:)?\/\/([^/?#]*)/;
3793
+ var AUTHORITY_INTRODUCER_REGION = /^(?:[^#/:?]+:)?([/\\\t\n\r]*)/;
3788
3794
  function getParseError(parsed, matches) {
3789
3795
  if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3790
3796
  return 'URI path must start with "/" when authority is present.';
@@ -3819,6 +3825,20 @@ var require_fast_uri = __commonJS({
3819
3825
  parsed.error = "URI authority must not contain a literal backslash.";
3820
3826
  malformedAuthorityOrPort = true;
3821
3827
  }
3828
+ const introducerMatch = uri.match(AUTHORITY_INTRODUCER_REGION);
3829
+ if (introducerMatch !== null) {
3830
+ const region = introducerMatch[1];
3831
+ const normalizedRegion = region.replace(/[\t\n\r]/g, "");
3832
+ if (normalizedRegion.length >= 2) {
3833
+ if (normalizedRegion.slice(0, 2) !== "//") {
3834
+ parsed.error = parsed.error || "URI authority must not contain a literal backslash.";
3835
+ malformedAuthorityOrPort = true;
3836
+ } else if (region.length !== normalizedRegion.length) {
3837
+ parsed.error = parsed.error || "URI authority introducer must not contain whitespace.";
3838
+ malformedAuthorityOrPort = true;
3839
+ }
3840
+ }
3841
+ }
3822
3842
  const matches = uri.match(URI_PARSE);
3823
3843
  if (matches) {
3824
3844
  parsed.scheme = matches[1];
@@ -6985,10 +7005,17 @@ var init_errors = __esm({
6985
7005
  });
6986
7006
 
6987
7007
  // node_modules/@fetchproxy/protocol/dist/frames.js
7008
+ function readySignaturePayload(mcpHelloNonce, extHelloNonce, extensionSessionPub) {
7009
+ const out = new Uint8Array(mcpHelloNonce.length + extHelloNonce.length + extensionSessionPub.length);
7010
+ out.set(mcpHelloNonce, 0);
7011
+ out.set(extHelloNonce, mcpHelloNonce.length);
7012
+ out.set(extensionSessionPub, mcpHelloNonce.length + extHelloNonce.length);
7013
+ return out;
7014
+ }
6988
7015
  var PROTOCOL_VERSION, HKDF_SESSION_INFO, KNOWN_CAPABILITIES;
6989
7016
  var init_frames = __esm({
6990
7017
  "node_modules/@fetchproxy/protocol/dist/frames.js"() {
6991
- PROTOCOL_VERSION = 2;
7018
+ PROTOCOL_VERSION = 3;
6992
7019
  HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
6993
7020
  KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
6994
7021
  "fetch",
@@ -7000,7 +7027,8 @@ var init_frames = __esm({
7000
7027
  "read_indexed_db",
7001
7028
  "read_dom",
7002
7029
  "download",
7003
- "graphql"
7030
+ "graphql",
7031
+ "write_cookies"
7004
7032
  ]);
7005
7033
  }
7006
7034
  });
@@ -7110,6 +7138,15 @@ function assertHttpUrl(x, label) {
7110
7138
  throw new ProtocolError(`${label}: must be http(s), got ${u.protocol}`);
7111
7139
  }
7112
7140
  }
7141
+ function assertCookiePath(x, label) {
7142
+ assertString(x, label);
7143
+ if (!x.startsWith("/") || x.startsWith("//")) {
7144
+ throw new ProtocolError(`${label}: must be an absolute path like "/campus"`);
7145
+ }
7146
+ if (x.includes("?") || x.includes("#") || x.includes("\\")) {
7147
+ throw new ProtocolError(`${label}: must not contain a query, fragment, or backslash`);
7148
+ }
7149
+ }
7113
7150
  function assertHttpsOriginOnly(x, label) {
7114
7151
  assertString(x, label);
7115
7152
  let u;
@@ -7549,13 +7586,43 @@ function validateInnerRequest(raw) {
7549
7586
  }
7550
7587
  assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
7551
7588
  assertNonEmptyKeyArray(raw.init.keys, "inner.init.keys");
7589
+ if (raw.init.path !== void 0)
7590
+ assertCookiePath(raw.init.path, "inner.init.path");
7552
7591
  for (const k of Object.keys(raw.init)) {
7553
- if (k !== "origin" && k !== "keys") {
7592
+ if (k !== "origin" && k !== "keys" && k !== "path") {
7554
7593
  throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on read_cookies`);
7555
7594
  }
7556
7595
  }
7557
7596
  return raw;
7558
7597
  }
7598
+ if (raw.op === "write_cookies") {
7599
+ assertObject(raw.init, "inner.init");
7600
+ assertHttpsOriginOnly(raw.init.origin, "inner.init.origin");
7601
+ if (!Array.isArray(raw.init.cookies) || raw.init.cookies.length === 0) {
7602
+ throw new ProtocolError("inner.init.cookies: must be a non-empty array");
7603
+ }
7604
+ for (const [i, entry] of raw.init.cookies.entries()) {
7605
+ assertObject(entry, `inner.init.cookies[${i}]`);
7606
+ assertString(entry.name, `inner.init.cookies[${i}].name`);
7607
+ if (!SCOPE_KEY_RE.test(entry.name)) {
7608
+ throw new ProtocolError(`inner.init.cookies[${i}].name: invalid key ${JSON.stringify(entry.name)}`);
7609
+ }
7610
+ assertString(entry.value, `inner.init.cookies[${i}].value`);
7611
+ for (const k of Object.keys(entry)) {
7612
+ if (k !== "name" && k !== "value") {
7613
+ throw new ProtocolError(`inner.init.cookies[${i}]: unexpected field ${JSON.stringify(k)}`);
7614
+ }
7615
+ }
7616
+ }
7617
+ if (raw.init.path !== void 0)
7618
+ assertCookiePath(raw.init.path, "inner.init.path");
7619
+ for (const k of Object.keys(raw.init)) {
7620
+ if (k !== "origin" && k !== "cookies" && k !== "path") {
7621
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on write_cookies`);
7622
+ }
7623
+ }
7624
+ return raw;
7625
+ }
7559
7626
  if (raw.op === "read_local_storage" || raw.op === "read_session_storage") {
7560
7627
  assertObject(raw.init, "inner.init");
7561
7628
  if (raw.init.origin === void 0) {
@@ -7742,7 +7809,7 @@ function validateInnerRequest(raw) {
7742
7809
  }
7743
7810
  return raw;
7744
7811
  }
7745
- 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", "read_dom", "download", "graphql_query"; got ${JSON.stringify(raw.op)}`);
7812
+ 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", "read_dom", "download", "graphql_query", "write_cookies"; got ${JSON.stringify(raw.op)}`);
7746
7813
  }
7747
7814
  function assertNonEmptyKeyArray(value, label) {
7748
7815
  if (!Array.isArray(value)) {
@@ -7799,6 +7866,18 @@ function validateInnerResponse(raw) {
7799
7866
  }
7800
7867
  return raw;
7801
7868
  }
7869
+ if (op === "write_cookies") {
7870
+ if (raw.written === void 0) {
7871
+ throw new ProtocolError("inner.written: missing on write_cookies response");
7872
+ }
7873
+ if (!Array.isArray(raw.written)) {
7874
+ throw new ProtocolError("inner.written: must be an array");
7875
+ }
7876
+ for (const [i, name] of raw.written.entries()) {
7877
+ assertString(name, `inner.written[${i}]`);
7878
+ }
7879
+ return raw;
7880
+ }
7802
7881
  if (op === "read_local_storage" || op === "read_session_storage") {
7803
7882
  if (raw.values === void 0) {
7804
7883
  throw new ProtocolError(`inner.values: missing on ${String(op)} response`);
@@ -12037,6 +12116,158 @@ var init_session_ready = __esm({
12037
12116
  }
12038
12117
  });
12039
12118
 
12119
+ // node_modules/@fetchproxy/server/dist/identity.js
12120
+ import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
12121
+ import { join as join2 } from "node:path";
12122
+ import { homedir } from "node:os";
12123
+ function defaultIdentityDir() {
12124
+ return join2(homedir(), ".fetchproxy", "identity");
12125
+ }
12126
+ function safeIdentityFileBase(serverName) {
12127
+ if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
12128
+ throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
12129
+ }
12130
+ return serverName.replace(/\//g, "_");
12131
+ }
12132
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
12133
+ const safeFile = safeIdentityFileBase(serverName);
12134
+ const path = join2(dir, `${safeFile}.json`);
12135
+ await mkdir(dir, { recursive: true, mode: 448 });
12136
+ try {
12137
+ const raw = await readFile(path, "utf8");
12138
+ const j2 = JSON.parse(raw);
12139
+ return {
12140
+ x25519Priv: fromB64(j2.x25519Priv),
12141
+ x25519Pub: fromB64(j2.x25519Pub),
12142
+ ed25519Priv: fromB64(j2.ed25519Priv),
12143
+ ed25519Pub: fromB64(j2.ed25519Pub),
12144
+ createdAt: j2.createdAt
12145
+ };
12146
+ } catch (e) {
12147
+ if (e.code !== "ENOENT")
12148
+ throw e;
12149
+ }
12150
+ const x = await generateX25519();
12151
+ const ed = await generateEd25519();
12152
+ const id = {
12153
+ x25519Priv: x.privateKey,
12154
+ x25519Pub: x.publicKey,
12155
+ ed25519Priv: ed.privateKey,
12156
+ ed25519Pub: ed.publicKey,
12157
+ createdAt: Date.now()
12158
+ };
12159
+ const j = {
12160
+ x25519Priv: toB64(id.x25519Priv),
12161
+ x25519Pub: toB64(id.x25519Pub),
12162
+ ed25519Priv: toB64(id.ed25519Priv),
12163
+ ed25519Pub: toB64(id.ed25519Pub),
12164
+ createdAt: id.createdAt
12165
+ };
12166
+ await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
12167
+ await chmod(path, 384);
12168
+ return id;
12169
+ }
12170
+ var SAFE_PLAIN, SAFE_SCOPED;
12171
+ var init_identity = __esm({
12172
+ "node_modules/@fetchproxy/server/dist/identity.js"() {
12173
+ init_dist();
12174
+ SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
12175
+ SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
12176
+ }
12177
+ });
12178
+
12179
+ // node_modules/@fetchproxy/server/dist/extension-trust.js
12180
+ import { readFile as readFile2, writeFile as writeFile2, rename, unlink, mkdir as mkdir2, chmod as chmod2 } from "node:fs/promises";
12181
+ import { join as join3 } from "node:path";
12182
+ function fileExtensionTrust(args) {
12183
+ return {
12184
+ allowNew: args.allowNew,
12185
+ location: extensionTrustPath(args.serverName, args.dir ?? defaultIdentityDir()),
12186
+ read: () => readExtensionPin(args.serverName, args.dir ?? defaultIdentityDir()),
12187
+ write: (pin) => writeExtensionPin(args.serverName, pin, args.dir ?? defaultIdentityDir())
12188
+ };
12189
+ }
12190
+ function allowNewExtensionIdentity(explicit, env = process.env) {
12191
+ if (explicit !== void 0)
12192
+ return explicit;
12193
+ return env[TRUST_NEW_EXTENSION_ENV] === "1";
12194
+ }
12195
+ function decideExtensionTrust(args) {
12196
+ const { pin, hello, allowNew, serverName } = args;
12197
+ if (!pin)
12198
+ return { decision: "first-use" };
12199
+ if (pin.identityX25519Pub === hello.identityX25519Pub && pin.identityEd25519Pub === hello.identityEd25519Pub) {
12200
+ return { decision: "pinned" };
12201
+ }
12202
+ const trustPath = args.location ?? extensionTrustPathHint(serverName);
12203
+ if (allowNew) {
12204
+ return {
12205
+ decision: "replace",
12206
+ message: `[fetchproxy] ${serverName}: accepting a NEW extension identity because ${TRUST_NEW_EXTENSION_ENV}=1 \u2014 re-pinning. Unset it once the browser you expect is connected.`
12207
+ };
12208
+ }
12209
+ return {
12210
+ decision: "refused",
12211
+ message: `[fetchproxy] ${serverName}: refusing an extension whose identity is not the one this MCP paired with. If you re-installed the extension or moved to another browser, re-pair deliberately: run this MCP once with ${TRUST_NEW_EXTENSION_ENV}=1, or delete ${trustPath}. If you did neither, something else is answering as your browser.`
12212
+ };
12213
+ }
12214
+ function extensionTrustPath(serverName, dir = defaultIdentityDir()) {
12215
+ return join3(dir, `${safeIdentityFileBase(serverName)}.extension-trust.json`);
12216
+ }
12217
+ function extensionTrustPathHint(serverName) {
12218
+ try {
12219
+ return extensionTrustPath(serverName);
12220
+ } catch {
12221
+ return join3(defaultIdentityDir(), "<server-name>.extension-trust.json");
12222
+ }
12223
+ }
12224
+ function isPin(x) {
12225
+ if (!x || typeof x !== "object")
12226
+ return false;
12227
+ const r = x;
12228
+ return typeof r.identityX25519Pub === "string" && typeof r.identityEd25519Pub === "string" && typeof r.pinnedAt === "number";
12229
+ }
12230
+ async function readExtensionPin(serverName, dir = defaultIdentityDir()) {
12231
+ const path = extensionTrustPath(serverName, dir);
12232
+ let raw;
12233
+ try {
12234
+ raw = await readFile2(path, "utf8");
12235
+ } catch (e) {
12236
+ if (e.code === "ENOENT")
12237
+ return null;
12238
+ throw e;
12239
+ }
12240
+ let parsed;
12241
+ try {
12242
+ parsed = JSON.parse(raw);
12243
+ } catch {
12244
+ throw new Error(`unreadable extension pin at ${path} (not JSON) \u2014 delete it to re-pair`);
12245
+ }
12246
+ if (!isPin(parsed)) {
12247
+ throw new Error(`unreadable extension pin at ${path} (wrong shape) \u2014 delete it to re-pair`);
12248
+ }
12249
+ return {
12250
+ identityX25519Pub: parsed.identityX25519Pub,
12251
+ identityEd25519Pub: parsed.identityEd25519Pub,
12252
+ pinnedAt: parsed.pinnedAt
12253
+ };
12254
+ }
12255
+ async function writeExtensionPin(serverName, pin, dir = defaultIdentityDir()) {
12256
+ const path = extensionTrustPath(serverName, dir);
12257
+ await mkdir2(dir, { recursive: true, mode: 448 });
12258
+ const tmp = `${path}.tmp`;
12259
+ await writeFile2(tmp, JSON.stringify(pin, null, 2), { mode: 384 });
12260
+ await chmod2(tmp, 384);
12261
+ await rename(tmp, path);
12262
+ }
12263
+ var TRUST_NEW_EXTENSION_ENV;
12264
+ var init_extension_trust = __esm({
12265
+ "node_modules/@fetchproxy/server/dist/extension-trust.js"() {
12266
+ init_identity();
12267
+ TRUST_NEW_EXTENSION_ENV = "FETCHPROXY_TRUST_NEW_EXTENSION";
12268
+ }
12269
+ });
12270
+
12040
12271
  // node_modules/@fetchproxy/server/dist/host.js
12041
12272
  async function startHost(opts) {
12042
12273
  const wss = new import_websocket_server.default({
@@ -12088,9 +12319,12 @@ async function startHost(opts) {
12088
12319
  }
12089
12320
  resetSessionPromise();
12090
12321
  let extensionHello = null;
12322
+ let extensionClaim = null;
12091
12323
  wss.on("connection", (ws) => {
12092
12324
  let identified = null;
12093
12325
  let peerMcpId = null;
12326
+ let closed = false;
12327
+ let pinOnReady = false;
12094
12328
  ws.on("message", async (data) => {
12095
12329
  try {
12096
12330
  let frame;
@@ -12102,10 +12336,43 @@ async function startHost(opts) {
12102
12336
  return;
12103
12337
  }
12104
12338
  if (frame.type === "hello" && frame.role === "extension") {
12105
- if (extensionWs) {
12339
+ if (extensionWs || extensionClaim) {
12106
12340
  ws.close(1008, "extension already connected");
12107
12341
  return;
12108
12342
  }
12343
+ extensionClaim = ws;
12344
+ let pin;
12345
+ try {
12346
+ pin = await opts.extensionTrust.read();
12347
+ } catch (e) {
12348
+ console.error(`[fetchproxy] ${String(e)}`);
12349
+ if (extensionClaim === ws)
12350
+ extensionClaim = null;
12351
+ ws.close(1008, "extension pin unreadable");
12352
+ return;
12353
+ }
12354
+ const outcome = decideExtensionTrust({
12355
+ pin,
12356
+ hello: frame,
12357
+ allowNew: opts.extensionTrust.allowNew,
12358
+ serverName: opts.ownServerName,
12359
+ location: opts.extensionTrust.location
12360
+ });
12361
+ if (outcome.decision === "refused") {
12362
+ console.warn(outcome.message);
12363
+ if (extensionClaim === ws)
12364
+ extensionClaim = null;
12365
+ ws.close(1008, "extension identity is not the pinned one");
12366
+ return;
12367
+ }
12368
+ if (outcome.decision === "replace")
12369
+ console.warn(outcome.message);
12370
+ if (closed || ws.readyState !== import_websocket.default.OPEN) {
12371
+ if (extensionClaim === ws)
12372
+ extensionClaim = null;
12373
+ return;
12374
+ }
12375
+ pinOnReady = outcome.decision !== "pinned";
12109
12376
  identified = "extension";
12110
12377
  extensionWs = ws;
12111
12378
  extensionHello = frame;
@@ -12117,6 +12384,8 @@ async function startHost(opts) {
12117
12384
  console.error("[fetchproxy] onPairCode threw:", e);
12118
12385
  }
12119
12386
  }
12387
+ for (const slot of peers.values())
12388
+ slot.ws.send(JSON.stringify(frame));
12120
12389
  ws.send(JSON.stringify(ownHello));
12121
12390
  for (const slot of peers.values()) {
12122
12391
  ws.send(JSON.stringify(slot.helloFrame));
@@ -12152,6 +12421,8 @@ async function startHost(opts) {
12152
12421
  peers.set(frame.mcpId, { ws, helloFrame: frame });
12153
12422
  if (extensionWs)
12154
12423
  extensionWs.send(JSON.stringify(frame));
12424
+ if (extensionHello)
12425
+ ws.send(JSON.stringify(extensionHello));
12155
12426
  return;
12156
12427
  }
12157
12428
  if (frame.type === "ready") {
@@ -12163,7 +12434,7 @@ async function startHost(opts) {
12163
12434
  }
12164
12435
  const extEdPub = fromB64(extensionHello.identityEd25519Pub);
12165
12436
  const extNonce = fromB64(extensionHello.sessionNonce);
12166
- const msg = concatBytes(ownSessionNonce, extNonce);
12437
+ const msg = readySignaturePayload(ownSessionNonce, extNonce, fromB64(frame.extensionSessionPub));
12167
12438
  const sig = fromB64(frame.sessionSig);
12168
12439
  let sigOk = false;
12169
12440
  try {
@@ -12176,6 +12447,18 @@ async function startHost(opts) {
12176
12447
  ws.close(1008, "extension session signature invalid");
12177
12448
  return;
12178
12449
  }
12450
+ if (pinOnReady) {
12451
+ pinOnReady = false;
12452
+ try {
12453
+ await opts.extensionTrust.write({
12454
+ identityX25519Pub: extensionHello.identityX25519Pub,
12455
+ identityEd25519Pub: extensionHello.identityEd25519Pub,
12456
+ pinnedAt: Date.now()
12457
+ });
12458
+ } catch (e) {
12459
+ console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
12460
+ }
12461
+ }
12179
12462
  const extPub = fromB64(frame.extensionSessionPub);
12180
12463
  const shared = await ecdhX25519(opts.ownIdentity.x25519Priv, extPub);
12181
12464
  const key = await hkdfSha256(shared, ownSessionNonce, enc2.encode(HKDF_SESSION_INFO), 32);
@@ -12229,6 +12512,9 @@ async function startHost(opts) {
12229
12512
  }
12230
12513
  });
12231
12514
  ws.on("close", () => {
12515
+ closed = true;
12516
+ if (extensionClaim === ws)
12517
+ extensionClaim = null;
12232
12518
  if (identified === "extension" && extensionWs === ws) {
12233
12519
  extensionWs = null;
12234
12520
  extensionHello = null;
@@ -12287,6 +12573,7 @@ var init_host = __esm({
12287
12573
  init_build_server_hello();
12288
12574
  init_session();
12289
12575
  init_session_ready();
12576
+ init_extension_trust();
12290
12577
  PUBLIC_ORIGIN_RE = /^https?:\/\/(?!(127\.0\.0\.1|localhost)(:|$))/i;
12291
12578
  enc2 = new TextEncoder();
12292
12579
  }
@@ -12330,11 +12617,84 @@ async function startPeer(opts) {
12330
12617
  resolveFirstReady = resolve;
12331
12618
  rejectFirstReady = reject;
12332
12619
  });
12620
+ let extensionHello = null;
12621
+ let warnedUnverifiable = false;
12622
+ let cachedPin = void 0;
12623
+ const authenticateExtension = async (sessionSig, extensionSessionPub) => {
12624
+ if (!extensionHello) {
12625
+ if (opts.requireExtensionIdentity) {
12626
+ console.error(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity, so this session cannot be verified \u2014 refusing. Upgrade the MCP holding the bridge port to 1.12.0 or later.`);
12627
+ return false;
12628
+ }
12629
+ if (!warnedUnverifiable) {
12630
+ warnedUnverifiable = true;
12631
+ console.warn(`[fetchproxy] ${opts.serverName}: the concentrator does not forward the extension's identity (pre-1.12.0), so this peer cannot verify which browser it is talking to. Upgrade the MCP holding the bridge port to close this.`);
12632
+ }
12633
+ return true;
12634
+ }
12635
+ const payload = readySignaturePayload(sessionNonce, fromB64(extensionHello.sessionNonce), fromB64(extensionSessionPub));
12636
+ let sigOk = false;
12637
+ try {
12638
+ sigOk = await ed25519Verify(fromB64(extensionHello.identityEd25519Pub), payload, fromB64(sessionSig));
12639
+ } catch {
12640
+ sigOk = false;
12641
+ }
12642
+ if (!sigOk) {
12643
+ console.warn(`[fetchproxy] ${opts.serverName}: extension session signature invalid \u2014 refusing (the concentrator may be answering in the browser's place)`);
12644
+ return false;
12645
+ }
12646
+ if (cachedPin === void 0) {
12647
+ try {
12648
+ cachedPin = await opts.extensionTrust.read();
12649
+ } catch (e) {
12650
+ console.error(`[fetchproxy] ${String(e)}`);
12651
+ return false;
12652
+ }
12653
+ }
12654
+ const pin = cachedPin;
12655
+ const outcome = decideExtensionTrust({
12656
+ pin,
12657
+ hello: extensionHello,
12658
+ allowNew: opts.extensionTrust.allowNew,
12659
+ serverName: opts.serverName,
12660
+ location: opts.extensionTrust.location
12661
+ });
12662
+ if (outcome.decision === "refused") {
12663
+ console.warn(outcome.message);
12664
+ return false;
12665
+ }
12666
+ if (outcome.decision === "replace")
12667
+ console.warn(outcome.message);
12668
+ if (outcome.decision !== "pinned") {
12669
+ try {
12670
+ const written = {
12671
+ identityX25519Pub: extensionHello.identityX25519Pub,
12672
+ identityEd25519Pub: extensionHello.identityEd25519Pub,
12673
+ pinnedAt: Date.now()
12674
+ };
12675
+ await opts.extensionTrust.write(written);
12676
+ cachedPin = written;
12677
+ } catch (e) {
12678
+ console.error(`[fetchproxy] could not persist the extension pin: ${String(e)}`);
12679
+ }
12680
+ }
12681
+ return true;
12682
+ };
12333
12683
  const onMessage = async (data) => {
12334
12684
  try {
12335
12685
  const raw = JSON.parse(data.toString());
12336
12686
  const frame = validateFrame(raw);
12687
+ if (frame.type === "hello" && frame.role === "extension") {
12688
+ extensionHello = frame;
12689
+ return;
12690
+ }
12337
12691
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
12692
+ const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
12693
+ if (!authorised) {
12694
+ ws.close(1008, "extension identity refused");
12695
+ rejectFirstReady(new Error("peer: extension identity refused"));
12696
+ return;
12697
+ }
12338
12698
  const extPub = fromB64(frame.extensionSessionPub);
12339
12699
  const shared = await ecdhX25519(opts.identity.x25519Priv, extPub);
12340
12700
  const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
@@ -12422,67 +12782,11 @@ var init_peer = __esm({
12422
12782
  init_build_server_hello();
12423
12783
  init_session();
12424
12784
  init_session_ready();
12785
+ init_extension_trust();
12425
12786
  enc3 = new TextEncoder();
12426
12787
  }
12427
12788
  });
12428
12789
 
12429
- // node_modules/@fetchproxy/server/dist/identity.js
12430
- import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
12431
- import { join as join2 } from "node:path";
12432
- import { homedir } from "node:os";
12433
- function defaultIdentityDir() {
12434
- return join2(homedir(), ".fetchproxy", "identity");
12435
- }
12436
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
12437
- if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
12438
- throw new Error(`unsafe serverName for identity file: ${JSON.stringify(serverName)}`);
12439
- }
12440
- const safeFile = serverName.replace(/\//g, "_");
12441
- const path = join2(dir, `${safeFile}.json`);
12442
- await mkdir(dir, { recursive: true, mode: 448 });
12443
- try {
12444
- const raw = await readFile(path, "utf8");
12445
- const j2 = JSON.parse(raw);
12446
- return {
12447
- x25519Priv: fromB64(j2.x25519Priv),
12448
- x25519Pub: fromB64(j2.x25519Pub),
12449
- ed25519Priv: fromB64(j2.ed25519Priv),
12450
- ed25519Pub: fromB64(j2.ed25519Pub),
12451
- createdAt: j2.createdAt
12452
- };
12453
- } catch (e) {
12454
- if (e.code !== "ENOENT")
12455
- throw e;
12456
- }
12457
- const x = await generateX25519();
12458
- const ed = await generateEd25519();
12459
- const id = {
12460
- x25519Priv: x.privateKey,
12461
- x25519Pub: x.publicKey,
12462
- ed25519Priv: ed.privateKey,
12463
- ed25519Pub: ed.publicKey,
12464
- createdAt: Date.now()
12465
- };
12466
- const j = {
12467
- x25519Priv: toB64(id.x25519Priv),
12468
- x25519Pub: toB64(id.x25519Pub),
12469
- ed25519Priv: toB64(id.ed25519Priv),
12470
- ed25519Pub: toB64(id.ed25519Pub),
12471
- createdAt: id.createdAt
12472
- };
12473
- await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
12474
- await chmod(path, 384);
12475
- return id;
12476
- }
12477
- var SAFE_PLAIN, SAFE_SCOPED;
12478
- var init_identity = __esm({
12479
- "node_modules/@fetchproxy/server/dist/identity.js"() {
12480
- init_dist();
12481
- SAFE_PLAIN = /^[A-Za-z0-9._-]+$/;
12482
- SAFE_SCOPED = /^@[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
12483
- }
12484
- });
12485
-
12486
12790
  // node_modules/@fetchproxy/server/dist/error-kind.js
12487
12791
  function classifyFetchError(error51) {
12488
12792
  if (/Could not establish connection/i.test(error51) || /Receiving end does not exist/i.test(error51)) {
@@ -12532,6 +12836,24 @@ var init_classify_bridge_error = __esm({
12532
12836
  });
12533
12837
 
12534
12838
  // node_modules/@fetchproxy/server/dist/ws-server.js
12839
+ function protocolErrorFrom(error51) {
12840
+ if (SCOPE_REJECTION.test(error51))
12841
+ return new FetchproxyScopeError(error51);
12842
+ if (NO_TAB_REJECTION.test(error51))
12843
+ return new FetchproxyNoTabError(error51);
12844
+ return new FetchproxyProtocolError(error51);
12845
+ }
12846
+ function normalizeCookiePath(path) {
12847
+ if (path === void 0 || path === "")
12848
+ return void 0;
12849
+ const trimmed = path.endsWith("/") && path !== "/" ? path.slice(0, -1) : path;
12850
+ try {
12851
+ assertCookiePath(trimmed, "path");
12852
+ } catch (e) {
12853
+ throw new Error(`FetchproxyServer: ${e instanceof Error ? e.message : String(e)} (got ${JSON.stringify(path)})`);
12854
+ }
12855
+ return trimmed;
12856
+ }
12535
12857
  function assertSubdomainLabel(label) {
12536
12858
  if (!SUBDOMAIN_LABEL_RE.test(label)) {
12537
12859
  throw new Error(`FetchproxyServer: subdomain must be a DNS label like "www" or "api" (or dot-separated like "auth.api"), got ${JSON.stringify(label)}`);
@@ -12556,7 +12878,7 @@ function assertUrlInDomains(field, url2, domains) {
12556
12878
  const declared = domains.map((d) => JSON.stringify(d)).join(", ");
12557
12879
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
12558
12880
  }
12559
- var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
12881
+ var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, SCOPE_REJECTION, NO_TAB_REJECTION, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
12560
12882
  var init_ws_server = __esm({
12561
12883
  "node_modules/@fetchproxy/server/dist/ws-server.js"() {
12562
12884
  init_dist();
@@ -12564,6 +12886,7 @@ var init_ws_server = __esm({
12564
12886
  init_host();
12565
12887
  init_peer();
12566
12888
  init_identity();
12889
+ init_extension_trust();
12567
12890
  init_error_kind();
12568
12891
  init_classify_bridge_error();
12569
12892
  FetchproxyProtocolError = class extends Error {
@@ -12607,6 +12930,32 @@ var init_ws_server = __esm({
12607
12930
  this.hint = hint;
12608
12931
  }
12609
12932
  };
12933
+ FetchproxyHintedError = class extends FetchproxyProtocolError {
12934
+ /** The extension's raw rejection, unmodified. */
12935
+ originalError;
12936
+ /** What the user should actually do, in prose. */
12937
+ hint;
12938
+ constructor(originalError, hint) {
12939
+ super(`${originalError} \u2014 ${hint}`);
12940
+ this.name = "FetchproxyHintedError";
12941
+ this.originalError = originalError;
12942
+ this.hint = hint;
12943
+ }
12944
+ };
12945
+ FetchproxyScopeError = class extends FetchproxyHintedError {
12946
+ constructor(originalError) {
12947
+ super(originalError, "the declared scope changed since you paired, so the extension is refusing the request. Revoke this MCP in the Transporter extension popup, then re-run \u2014 you will be asked to approve the new scope. This is not a version problem and does not need an update.");
12948
+ this.name = "FetchproxyScopeError";
12949
+ }
12950
+ };
12951
+ FetchproxyNoTabError = class extends FetchproxyHintedError {
12952
+ constructor(originalError) {
12953
+ super(originalError, "open a tab on that host and sign in, then re-run. This is not a version problem and does not need an update.");
12954
+ this.name = "FetchproxyNoTabError";
12955
+ }
12956
+ };
12957
+ SCOPE_REJECTION = /not in declared/;
12958
+ NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
12610
12959
  FetchproxyTimeoutError = class extends FetchproxyProtocolError {
12611
12960
  url;
12612
12961
  timeoutMs;
@@ -12676,6 +13025,9 @@ var init_ws_server = __esm({
12676
13025
  // them off from `pending` (fetch) and `pendingReadCookies` (legacy
12677
13026
  // string-shape) so the response routing in `onInner` stays linear.
12678
13027
  pendingStorage = /* @__PURE__ */ new Map();
13028
+ // 1.12.0+: write-cookies awaiters resolve the list of names actually
13029
+ // written, so a caller can confirm rather than assume.
13030
+ pendingWriteCookies = /* @__PURE__ */ new Map();
12679
13031
  // 0.3.0+: capture-header awaiters resolve a single string.
12680
13032
  pendingCapture = /* @__PURE__ */ new Map();
12681
13033
  // capture_redirect awaiters resolve the captured redirect URL string.
@@ -12802,6 +13154,8 @@ var init_ws_server = __esm({
12802
13154
  keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
12803
13155
  keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
12804
13156
  identityDir: opts.identityDir,
13157
+ allowNewExtensionIdentity: opts.allowNewExtensionIdentity,
13158
+ requireExtensionIdentity: opts.requireExtensionIdentity,
12805
13159
  onPairCode: opts.onPairCode
12806
13160
  };
12807
13161
  }
@@ -12901,7 +13255,8 @@ var init_ws_server = __esm({
12901
13255
  ownSessionStoragePointers: this.opts.sessionStoragePointers,
12902
13256
  ownDomSelectors: this.opts.domSelectors,
12903
13257
  ownGraphqlOps: this.opts.graphqlOps,
12904
- onPairCode: this.opts.onPairCode
13258
+ onPairCode: this.opts.onPairCode,
13259
+ extensionTrust: this.extensionTrust()
12905
13260
  });
12906
13261
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
12907
13262
  this.hostHandle.onExtensionDisconnect(() => {
@@ -12930,7 +13285,9 @@ var init_ws_server = __esm({
12930
13285
  localStoragePointers: this.opts.localStoragePointers,
12931
13286
  sessionStoragePointers: this.opts.sessionStoragePointers,
12932
13287
  domSelectors: this.opts.domSelectors,
12933
- graphqlOps: this.opts.graphqlOps
13288
+ graphqlOps: this.opts.graphqlOps,
13289
+ extensionTrust: this.extensionTrust(),
13290
+ requireExtensionIdentity: this.opts.requireExtensionIdentity
12934
13291
  });
12935
13292
  this.peerHandle.onInner((inner) => this.onInner(inner));
12936
13293
  this.peerHandle.onRenegotiate(() => {
@@ -13069,6 +13426,23 @@ var init_ws_server = __esm({
13069
13426
  markActive() {
13070
13427
  this.noteActivityForKeepalive();
13071
13428
  }
13429
+ /**
13430
+ * #208: this MCP's pin on the extension's identity, stored beside its own
13431
+ * identity key and so following `identityDir` wherever the caller put it.
13432
+ *
13433
+ * `allowNewExtensionIdentity` falls back to an environment variable when the
13434
+ * caller expressed no opinion, because the thirteen MCPs that construct this
13435
+ * class are separate packages: an operator whose extension re-install has
13436
+ * just locked all of them out needs one lever that does not require patching
13437
+ * every one of them.
13438
+ */
13439
+ extensionTrust() {
13440
+ return fileExtensionTrust({
13441
+ serverName: this.opts.serverName,
13442
+ dir: this.opts.identityDir,
13443
+ allowNew: allowNewExtensionIdentity(this.opts.allowNewExtensionIdentity)
13444
+ });
13445
+ }
13072
13446
  noteActivityForKeepalive() {
13073
13447
  const intervalMs = this.opts.keepAliveIntervalMs;
13074
13448
  if (intervalMs <= 0)
@@ -13132,6 +13506,7 @@ var init_ws_server = __esm({
13132
13506
  this.pending.delete(id);
13133
13507
  this.pendingReadCookies.delete(id);
13134
13508
  this.pendingStorage.delete(id);
13509
+ this.pendingWriteCookies.delete(id);
13135
13510
  this.pendingCapture.delete(id);
13136
13511
  this.pendingRedirect.delete(id);
13137
13512
  this.pendingDownload.delete(id);
@@ -13253,7 +13628,7 @@ var init_ws_server = __esm({
13253
13628
  port: this.opts.port
13254
13629
  });
13255
13630
  }
13256
- return new FetchproxyProtocolError(result.error);
13631
+ return protocolErrorFrom(result.error);
13257
13632
  }
13258
13633
  /**
13259
13634
  * Convenience wrapper around `fetch()`. Builds the URL from a path
@@ -13289,10 +13664,20 @@ var init_ws_server = __esm({
13289
13664
  }
13290
13665
  const url2 = isAbsolute ? path : `https://${host}${path}`;
13291
13666
  assertUrlInDomains("request url", url2, this.opts.domains);
13667
+ let tabUrl = `https://${host}/`;
13668
+ if (opts.viaTab !== void 0) {
13669
+ try {
13670
+ new URL(opts.viaTab);
13671
+ } catch {
13672
+ throw new Error(`FetchproxyServer.request: viaTab is not a valid URL: ${JSON.stringify(opts.viaTab)}`);
13673
+ }
13674
+ assertUrlInDomains("viaTab", opts.viaTab, this.opts.domains);
13675
+ tabUrl = opts.viaTab;
13676
+ }
13292
13677
  const init = {
13293
13678
  url: url2,
13294
13679
  method,
13295
- tabUrl: `https://${host}/`,
13680
+ tabUrl,
13296
13681
  headers: opts.headers,
13297
13682
  body: opts.body
13298
13683
  };
@@ -13504,9 +13889,14 @@ var init_ws_server = __esm({
13504
13889
  let inner;
13505
13890
  if (opts.keys !== void 0) {
13506
13891
  this.assertScopeSubset(opts.keys, this.opts.cookieKeys, "cookieKeys");
13892
+ const cookiePath = normalizeCookiePath(opts.path);
13507
13893
  const initV3 = {
13894
+ // Origin stays BARE. The path travels as its own validated field —
13895
+ // `assertHttpsOriginOnly` deliberately refuses a path here so one
13896
+ // cannot be used to re-point the read past the domain gate.
13508
13897
  origin: `https://${host}`,
13509
- keys: [...opts.keys]
13898
+ keys: [...opts.keys],
13899
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
13510
13900
  };
13511
13901
  inner = { type: "request", id, op: "read_cookies", init: initV3 };
13512
13902
  } else {
@@ -13519,10 +13909,65 @@ var init_ws_server = __esm({
13519
13909
  await this.sendInnerFrame(inner);
13520
13910
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
13521
13911
  if (!result.ok) {
13522
- throw new FetchproxyProtocolError(result.error);
13912
+ throw protocolErrorFrom(result.error);
13523
13913
  }
13524
13914
  return result.cookies;
13525
13915
  }
13916
+ /**
13917
+ * 1.12.0+: overwrite the value of cookies this MCP already declares.
13918
+ *
13919
+ * The bridge's only write verb, and it exists for one failure class. Sites
13920
+ * that ROTATE a credential cookie hand back a new value on every refresh; if
13921
+ * the MCP refreshes and keeps the result to itself, the copy in the browser's
13922
+ * cookie jar is dead, and the user gets signed out of a tab they never
13923
+ * touched — usually reported to them as "inactivity". Writing the rotated
13924
+ * value back is the only thing that repairs it.
13925
+ *
13926
+ * Requires `'write_cookies'` in capabilities, which the user approves at pair
13927
+ * time as its own line. Every name must ALSO be in declared `cookieKeys`: a
13928
+ * write can never reach a cookie the MCP was not already trusted to read, so
13929
+ * granting it cannot widen which cookies are in play — only what may be done
13930
+ * to the ones already listed.
13931
+ *
13932
+ * The extension refuses the whole request unless every named cookie already
13933
+ * exists; this refreshes a value in place and deliberately cannot author new
13934
+ * cookies. Returns the names actually written.
13935
+ */
13936
+ async writeCookies(opts) {
13937
+ if (!this.opts.capabilities.includes("write_cookies")) {
13938
+ throw new Error('FetchproxyServer.writeCookies(): MCP did not declare "write_cookies" in capabilities \u2014 add it to FetchproxyServerOpts.capabilities to enable this verb');
13939
+ }
13940
+ const names = Object.keys(opts.cookies);
13941
+ if (names.length === 0) {
13942
+ throw new Error("FetchproxyServer.writeCookies(): no cookies given");
13943
+ }
13944
+ await this.ensureConnected();
13945
+ this.throwIfPendingPair();
13946
+ if (opts.subdomain !== void 0)
13947
+ assertSubdomainLabel(opts.subdomain);
13948
+ const baseDomain = this.resolveBaseDomain(opts.domain);
13949
+ const host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
13950
+ this.assertScopeSubset(names, this.opts.cookieKeys, "cookieKeys");
13951
+ const cookiePath = normalizeCookiePath(opts.path);
13952
+ const id = this.nextRequestId++;
13953
+ const inner = {
13954
+ type: "request",
13955
+ id,
13956
+ op: "write_cookies",
13957
+ init: {
13958
+ // Bare origin, same invariant as the read path: a path must never be
13959
+ // able to move the request past the domain gate.
13960
+ origin: `https://${host}`,
13961
+ cookies: Object.entries(opts.cookies).map(([name, value]) => ({ name, value })),
13962
+ ...cookiePath !== void 0 ? { path: cookiePath } : {}
13963
+ }
13964
+ };
13965
+ const pending = new Promise((resolve, reject) => {
13966
+ this.pendingWriteCookies.set(id, { resolve, reject });
13967
+ });
13968
+ await this.sendInnerFrame(inner);
13969
+ return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
13970
+ }
13526
13971
  /**
13527
13972
  * 0.3.0+: read declared localStorage keys from the user's signed-in
13528
13973
  * tab. Requires `'read_local_storage'` in capabilities AND each key
@@ -14069,7 +14514,7 @@ var init_ws_server = __esm({
14069
14514
  storageCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on storage awaiter`));
14070
14515
  }
14071
14516
  } else {
14072
- storageCb.reject(new FetchproxyProtocolError(inner.error));
14517
+ storageCb.reject(protocolErrorFrom(inner.error));
14073
14518
  }
14074
14519
  return;
14075
14520
  }
@@ -14083,7 +14528,7 @@ var init_ws_server = __esm({
14083
14528
  captureCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture awaiter`));
14084
14529
  }
14085
14530
  } else {
14086
- captureCb.reject(new FetchproxyProtocolError(inner.error));
14531
+ captureCb.reject(protocolErrorFrom(inner.error));
14087
14532
  }
14088
14533
  return;
14089
14534
  }
@@ -14097,7 +14542,7 @@ var init_ws_server = __esm({
14097
14542
  redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
14098
14543
  }
14099
14544
  } else {
14100
- redirectCb.reject(new FetchproxyProtocolError(inner.error));
14545
+ redirectCb.reject(protocolErrorFrom(inner.error));
14101
14546
  }
14102
14547
  return;
14103
14548
  }
@@ -14111,7 +14556,7 @@ var init_ws_server = __esm({
14111
14556
  idbCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on read_indexed_db awaiter`));
14112
14557
  }
14113
14558
  } else {
14114
- idbCb.reject(new FetchproxyProtocolError(inner.error));
14559
+ idbCb.reject(protocolErrorFrom(inner.error));
14115
14560
  }
14116
14561
  return;
14117
14562
  }
@@ -14125,7 +14570,7 @@ var init_ws_server = __esm({
14125
14570
  downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
14126
14571
  }
14127
14572
  } else {
14128
- downloadCb.reject(new FetchproxyProtocolError(inner.error));
14573
+ downloadCb.reject(protocolErrorFrom(inner.error));
14129
14574
  }
14130
14575
  return;
14131
14576
  }
@@ -14139,7 +14584,17 @@ var init_ws_server = __esm({
14139
14584
  graphqlCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on graphql_query awaiter`));
14140
14585
  }
14141
14586
  } else {
14142
- graphqlCb.reject(new FetchproxyProtocolError(inner.error));
14587
+ graphqlCb.reject(protocolErrorFrom(inner.error));
14588
+ }
14589
+ return;
14590
+ }
14591
+ const writeCookiesCb = this.pendingWriteCookies.get(inner.id);
14592
+ if (writeCookiesCb) {
14593
+ this.pendingWriteCookies.delete(inner.id);
14594
+ if (inner.ok && inner.op === "write_cookies") {
14595
+ writeCookiesCb.resolve([...inner.written]);
14596
+ } else {
14597
+ writeCookiesCb.reject(protocolErrorFrom(inner.ok ? "write_cookies response had the wrong op" : inner.error));
14143
14598
  }
14144
14599
  return;
14145
14600
  }
@@ -14182,6 +14637,9 @@ var init_ws_server = __esm({
14182
14637
  for (const { reject } of this.pendingStorage.values())
14183
14638
  reject(err);
14184
14639
  this.pendingStorage.clear();
14640
+ for (const { reject } of this.pendingWriteCookies.values())
14641
+ reject(err);
14642
+ this.pendingWriteCookies.clear();
14185
14643
  for (const { reject } of this.pendingCapture.values())
14186
14644
  reject(err);
14187
14645
  this.pendingCapture.clear();
@@ -14560,6 +15018,8 @@ var init_dist2 = __esm({
14560
15018
  init_deadline();
14561
15019
  init_parse_html();
14562
15020
  init_batch();
15021
+ init_extension_trust();
15022
+ init_identity();
14563
15023
  }
14564
15024
  });
14565
15025
 
@@ -39265,7 +39725,7 @@ var pageSchema = {
39265
39725
  init_errors();
39266
39726
 
39267
39727
  // src/version.ts
39268
- var VERSION = "0.1.0";
39728
+ var VERSION = "0.1.2";
39269
39729
 
39270
39730
  // src/client.ts
39271
39731
  import { dirname, join } from "path";
@@ -39286,10 +39746,10 @@ var EventbriteClient = class {
39286
39746
  * host's install-time smoke test) when EVENTBRITE_TOKEN isn't set yet.
39287
39747
  * Tool calls re-raise the error at request time.
39288
39748
  *
39289
- * Optional constructor seam: the hosted Cloudflare connector builds one
39749
+ * Optional constructor seam: a hosted per-user deployment builds one
39290
39750
  * client per request with that user's `token` injected. The stdio path
39291
39751
  * passes no options, so the token resolves from the environment.
39292
- * The constructor is PURE (no I/O, no randomness) — it is run in Worker
39752
+ * The constructor is PURE (no I/O, no randomness) — it may run in module
39293
39753
  * global scope via the module singleton below.
39294
39754
  */
39295
39755
  constructor(opts) {
@@ -39510,7 +39970,7 @@ var DiscoveryClient = class {
39510
39970
  * 1. `api` — `POST eventbriteapi.com/v3/destination/search/` with a bearer
39511
39971
  * token. Verified live 2026-07-30: works with a private OR public token,
39512
39972
  * no WAF, no CSRF, no cookies. This is the default because it needs no
39513
- * browser and therefore works inside a Worker.
39973
+ * browser and therefore works without the bridge.
39514
39974
  * 2. `transport` — the fetchproxy bridge through a signed-in tab. Retained as
39515
39975
  * a fallback for when no token is configured, or the API route refuses.
39516
39976
  *
package/dist/client.js CHANGED
@@ -4,9 +4,9 @@ import { loadDotenvSafely, readEnvVar, createApiClient } from '@chrischall/mcp-u
4
4
  // Load .env for local dev; silently skip if dotenv is unavailable (e.g. mcpb
5
5
  // bundle). `loadDotenvSafely` swallows a missing dotenv module and never lets
6
6
  // .env override a host-provided value.
7
- // The try/catch guards the Cloudflare Worker runtime, where `import.meta.url`
7
+ // The try/catch guards a non-Node runtime, where `import.meta.url`
8
8
  // is undefined and `fileURLToPath(undefined)` would throw at module init
9
- // (Worker startup validation) — there is no filesystem / .env to load there.
9
+ // startup validation in such a runtime and there is no .env to load there.
10
10
  try {
11
11
  const dir = dirname(fileURLToPath(import.meta.url));
12
12
  await loadDotenvSafely({ path: join(dir, '..', '.env'), override: false });
@@ -32,10 +32,10 @@ export class EventbriteClient {
32
32
  * host's install-time smoke test) when EVENTBRITE_TOKEN isn't set yet.
33
33
  * Tool calls re-raise the error at request time.
34
34
  *
35
- * Optional constructor seam: the hosted Cloudflare connector builds one
35
+ * Optional constructor seam: a hosted per-user deployment builds one
36
36
  * client per request with that user's `token` injected. The stdio path
37
37
  * passes no options, so the token resolves from the environment.
38
- * The constructor is PURE (no I/O, no randomness) — it is run in Worker
38
+ * The constructor is PURE (no I/O, no randomness) — it may run in module
39
39
  * global scope via the module singleton below.
40
40
  */
41
41
  constructor(opts) {
package/dist/discovery.js CHANGED
@@ -19,7 +19,7 @@ function isUsableBrowsePage(result) {
19
19
  return false;
20
20
  return typeof result.body === 'string' && PLACE_ID_RE.test(result.body);
21
21
  }
22
- /** `fetch` with an AbortController deadline; called directly for workerd. */
22
+ /** `fetch` with an AbortController deadline. */
23
23
  async function fetchWithTimeout(url, ms) {
24
24
  const controller = new AbortController();
25
25
  const timer = setTimeout(() => controller.abort(), ms);
@@ -219,7 +219,7 @@ export class DiscoveryClient {
219
219
  * 1. `api` — `POST eventbriteapi.com/v3/destination/search/` with a bearer
220
220
  * token. Verified live 2026-07-30: works with a private OR public token,
221
221
  * no WAF, no CSRF, no cookies. This is the default because it needs no
222
- * browser and therefore works inside a Worker.
222
+ * browser and therefore works without the bridge.
223
223
  * 2. `transport` — the fetchproxy bridge through a signed-in tab. Retained as
224
224
  * a fallback for when no token is configured, or the API route refuses.
225
225
  *
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { registerDiscoveryTools } from './tools/discovery.js';
15
15
  // documented host serves the consumer search at
16
16
  // POST /destination/search/ with a plain bearer token — no WAF, no CSRF,
17
17
  // no browser. That is now the primary route, so discovery works in a
18
- // Worker too. The fetchproxy bridge (port 37149, bound lazily) is kept as
18
+ // a bridge-less deployment too. The bridge (port 37149, bound lazily) is kept as
19
19
  // a FALLBACK for when no token is configured or the API route refuses.
20
20
  const transport = new FetchproxyTransport({
21
21
  version: VERSION,
@@ -3,7 +3,7 @@ import { textResult } from '@chrischall/mcp-utils';
3
3
  import { enc, qs, schemaContinuation } from './params.js';
4
4
  /**
5
5
  * Account-side tools on the documented API (`eventbriteapi.com/v3`, bearer
6
- * token). Transport-neutral: the hosted connector registers these with a
6
+ * token). Transport-neutral: a hosted deployment registers these with a
7
7
  * per-user client.
8
8
  */
9
9
  export function registerAccountTools(server, deps) {
@@ -5,7 +5,7 @@ import { toCompactEvent } from '../discovery.js';
5
5
  * Public event discovery. Verified live 2026-07-30: the documented host serves
6
6
  * the consumer search at POST /destination/search/ with a plain bearer token —
7
7
  * no WAF, no CSRF, no cookies — so these tools no longer require a browser and
8
- * ARE registered by the hosted connector. The fetchproxy bridge remains a
8
+ * ARE registered without a bridge. The fetchproxy bridge remains a
9
9
  * fallback on the stdio path.
10
10
  */
11
11
  export async function registerDiscoveryTools(server, deps) {
@@ -99,13 +99,13 @@ export async function registerDiscoveryTools(server, deps) {
99
99
  const data = await discovery.eventsByIds(event_ids, expand ? expand.split(',').map((s) => s.trim()) : undefined);
100
100
  return textResult(data);
101
101
  });
102
- // eb_healthcheck diagnoses the BRIDGE. With no bridge (the Worker connector)
102
+ // eb_healthcheck diagnoses the BRIDGE. With no bridge
103
103
  // there is nothing for it to report on, so it is not registered at all —
104
104
  // better than a tool that always answers "no transport".
105
105
  if (!transport)
106
106
  return;
107
107
  // Imported lazily, AFTER the guard: a static import would drag the fetchproxy
108
- // helper into the Worker bundle where it can never run.
108
+ // helper into a bundle where it can never run.
109
109
  const { registerBridgeHealthcheckTool } = await import('@chrischall/mcp-utils/fetchproxy');
110
110
  // The categories endpoint answers 200 JSON on the www host regardless of
111
111
  // login state, so it isolates bridge problems from Eventbrite-side problems.
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.0'; // x-release-please-version
1
+ export const VERSION = '0.1.2'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/eventbrite-mcp",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "mcpName": "io.github.chrischall/eventbrite-mcp",
5
5
  "description": "Eventbrite MCP server for Claude — developed and maintained by AI (Claude Code)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -40,33 +40,23 @@
40
40
  "dev": "node dist/index.js",
41
41
  "test": "vitest run",
42
42
  "test:watch": "vitest",
43
- "test:coverage": "vitest run --coverage",
44
- "worker:dev": "wrangler dev",
45
- "worker:deploy": "wrangler deploy",
46
- "worker:test": "vitest run --config vitest.workers.config.ts"
43
+ "test:coverage": "vitest run --coverage"
47
44
  },
48
45
  "dependencies": {
49
46
  "@chrischall/mcp-utils": "^0.14.0",
50
- "@fetchproxy/server": "^1.7.0",
47
+ "@fetchproxy/server": "^2.0.0",
51
48
  "@modelcontextprotocol/sdk": "^1.29.0",
52
49
  "dotenv": "^17.4.0",
53
50
  "zod": "^4.4.2"
54
51
  },
55
52
  "devDependencies": {
56
- "@chrischall/mcp-connector": "^1.1.1",
57
- "@cloudflare/vitest-pool-workers": "^0.19.0",
58
- "@cloudflare/workers-oauth-provider": "^0.8.1",
59
- "@cloudflare/workers-types": "^5.20260708.1",
60
53
  "@types/node": "^26.0.0",
61
54
  "@vitest/coverage-v8": "^4.1.2",
62
- "agents": "^0.19.0",
63
55
  "esbuild": "^0.28.0",
64
56
  "typescript": "^7.0.2",
65
- "vitest": "^4.1.2",
66
- "wrangler": "^4.110.0"
57
+ "vitest": "^4.1.2"
67
58
  },
68
59
  "allowScripts": {
69
- "esbuild@0.28.1": true,
70
- "workerd@1.20260722.1": true
60
+ "esbuild@0.28.1": true
71
61
  }
72
62
  }
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/eventbrite-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.1.0",
9
+ "version": "0.1.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/eventbrite-mcp",
14
- "version": "0.1.0",
14
+ "version": "0.1.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -1,18 +0,0 @@
1
- import { EventbriteClient } from './client.js';
2
- /**
3
- * `ConnectorAuth` for the Eventbrite remote connector: the login page collects
4
- * the user's personal Eventbrite token (eventbrite.com/platform/api-keys),
5
- * verifies it against the current-user endpoint (a bad token throws, which
6
- * the connector surfaces back on the login page), and stores `{ token }`.
7
- */
8
- export const eventbriteAuth = {
9
- service: 'Eventbrite',
10
- accent: '#F05537',
11
- privacyNote: 'Your Eventbrite private token is stored encrypted and used only to call the Eventbrite API on your behalf.',
12
- fields: [{ name: 'token', label: 'Eventbrite private token', type: 'password' }],
13
- async login(fields) {
14
- const client = new EventbriteClient({ token: fields.token });
15
- await client.request('GET', '/users/me/');
16
- return { token: fields.token };
17
- },
18
- };