@chrischall/eventbrite-mcp 0.3.0 → 0.3.1

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.3.0"
10
+ "version": "0.3.1"
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.3.0",
18
+ "version": "0.3.1",
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.3.0",
4
+ "version": "0.3.1",
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/dist/bundle.js CHANGED
@@ -7302,6 +7302,7 @@ var init_frames = __esm({
7302
7302
  HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
7303
7303
  KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
7304
7304
  "fetch",
7305
+ "fetch_in_page",
7305
7306
  "read_cookies",
7306
7307
  "read_local_storage",
7307
7308
  "read_session_storage",
@@ -7404,6 +7405,11 @@ function assertBase64(x, label) {
7404
7405
  if (!BASE64_RE.test(x))
7405
7406
  throw new ProtocolError(`${label}: invalid base64`);
7406
7407
  }
7408
+ function assertBoolean(x, label) {
7409
+ if (typeof x !== "boolean") {
7410
+ throw new ProtocolError(`${label}: must be boolean`);
7411
+ }
7412
+ }
7407
7413
  function assertPositiveInt(x, label) {
7408
7414
  if (typeof x !== "number" || !Number.isInteger(x) || x <= 0) {
7409
7415
  throw new ProtocolError(`${label}: expected positive integer`);
@@ -7697,6 +7703,10 @@ function validateFrame(raw) {
7697
7703
  return validateEncrypted(raw);
7698
7704
  if (t === "pair-pending")
7699
7705
  return validatePairPending(raw);
7706
+ if (t === "hello-rejected")
7707
+ return validateHelloRejected(raw);
7708
+ if (t === "extension-disconnected")
7709
+ return { type: "extension-disconnected" };
7700
7710
  throw new ProtocolError(`unknown frame type: ${String(t)}`);
7701
7711
  }
7702
7712
  function validateHello(raw) {
@@ -7764,6 +7774,16 @@ function validateHello(raw) {
7764
7774
  if (raw.domSelectors !== void 0) {
7765
7775
  assertDomSelectorsArray(raw.domSelectors, "hello.domSelectors");
7766
7776
  }
7777
+ if (raw.accepts !== void 0) {
7778
+ if (!Array.isArray(raw.accepts)) {
7779
+ throw new ProtocolError("hello.accepts: expected array");
7780
+ }
7781
+ for (const a of raw.accepts) {
7782
+ if (typeof a !== "string") {
7783
+ throw new ProtocolError(`hello.accepts: entry must be string, got ${typeof a}`);
7784
+ }
7785
+ }
7786
+ }
7767
7787
  if (raw.graphqlOps !== void 0) {
7768
7788
  assertGraphqlOpsArray(raw.graphqlOps, "hello.graphqlOps");
7769
7789
  }
@@ -7814,6 +7834,16 @@ function validatePairPending(raw) {
7814
7834
  }
7815
7835
  return { type: "pair-pending", mcpId: raw.mcpId, pairCode: raw.pairCode };
7816
7836
  }
7837
+ function validateHelloRejected(raw) {
7838
+ assertString(raw.mcpId, "hello-rejected.mcpId");
7839
+ if (!isValidMcpId(raw.mcpId))
7840
+ throw new ProtocolError("hello-rejected.mcpId: invalid format");
7841
+ assertString(raw.reason, "hello-rejected.reason");
7842
+ if (raw.reason.length > 200) {
7843
+ throw new ProtocolError("hello-rejected.reason: must be at most 200 characters");
7844
+ }
7845
+ return { type: "hello-rejected", mcpId: raw.mcpId, reason: raw.reason };
7846
+ }
7817
7847
  function validateInnerFrame(raw) {
7818
7848
  assertObject(raw, "inner");
7819
7849
  const t = raw.type;
@@ -7845,6 +7875,14 @@ function validateInnerRequest(raw) {
7845
7875
  if (raw.init.body !== void 0) {
7846
7876
  assertString(raw.init.body, "inner.init.body");
7847
7877
  }
7878
+ if (raw.init.inPage !== void 0) {
7879
+ assertBoolean(raw.init.inPage, "inner.init.inPage");
7880
+ }
7881
+ if (raw.init.credentials !== void 0) {
7882
+ if (raw.init.credentials !== "include" && raw.init.credentials !== "omit") {
7883
+ throw new ProtocolError("inner.init.credentials: must be 'include' or 'omit'");
7884
+ }
7885
+ }
7848
7886
  return raw;
7849
7887
  }
7850
7888
  if (raw.op === "read_cookies") {
@@ -12274,6 +12312,8 @@ async function buildServerHello(opts) {
12274
12312
  sessionNonce: toB64(sessionNonce),
12275
12313
  sessionSig: toB64(sig)
12276
12314
  };
12315
+ if (opts.accepts && opts.accepts.length > 0)
12316
+ hello.accepts = [...opts.accepts];
12277
12317
  if (opts.cookieKeys && opts.cookieKeys.length > 0) {
12278
12318
  hello.cookieKeys = [...opts.cookieKeys];
12279
12319
  }
@@ -12375,7 +12415,7 @@ async function awaitSessionReady(ready, opts) {
12375
12415
  clearTimeout(timer);
12376
12416
  }
12377
12417
  }
12378
- var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError;
12418
+ var SESSION_READY_TIMEOUT_MS, FetchproxySessionNotReadyError, FetchproxyHelloRejectedError;
12379
12419
  var init_session_ready = __esm({
12380
12420
  "node_modules/@fetchproxy/server/dist/session-ready.js"() {
12381
12421
  SESSION_READY_TIMEOUT_MS = 3e4;
@@ -12396,15 +12436,35 @@ var init_session_ready = __esm({
12396
12436
  Object.setPrototypeOf(this, new.target.prototype);
12397
12437
  }
12398
12438
  };
12439
+ FetchproxyHelloRejectedError = class extends Error {
12440
+ mcpId;
12441
+ reason;
12442
+ constructor(info) {
12443
+ super(`fetchproxy: the extension refused the connection for "${info.mcpId}": ${info.reason}. This is the extension's own reason \u2014 it is not a timeout, and retrying unchanged will be refused the same way.`);
12444
+ this.name = "FetchproxyHelloRejectedError";
12445
+ this.mcpId = info.mcpId;
12446
+ this.reason = info.reason;
12447
+ Object.setPrototypeOf(this, new.target.prototype);
12448
+ }
12449
+ };
12399
12450
  }
12400
12451
  });
12401
12452
 
12402
12453
  // node_modules/@fetchproxy/server/dist/identity.js
12403
12454
  import { readFile, writeFile, mkdir, chmod } from "node:fs/promises";
12404
- import { join as join2 } from "node:path";
12455
+ import { isAbsolute, join as join2 } from "node:path";
12405
12456
  import { homedir } from "node:os";
12457
+ function envIdentityDir() {
12458
+ const raw = process.env.FETCHPROXY_IDENTITY_DIR;
12459
+ if (raw === void 0)
12460
+ return void 0;
12461
+ const dir = raw.trim();
12462
+ if (dir === "" || !isAbsolute(dir))
12463
+ return void 0;
12464
+ return dir;
12465
+ }
12406
12466
  function defaultIdentityDir() {
12407
- return join2(homedir(), ".fetchproxy", "identity");
12467
+ return envIdentityDir() ?? join2(homedir(), ".fetchproxy", "identity");
12408
12468
  }
12409
12469
  function safeIdentityFileBase(serverName) {
12410
12470
  if (!serverName || serverName === ".." || serverName.includes("..") || !SAFE_PLAIN.test(serverName) && !SAFE_SCOPED.test(serverName)) {
@@ -12412,42 +12472,57 @@ function safeIdentityFileBase(serverName) {
12412
12472
  }
12413
12473
  return serverName.replace(/\//g, "_");
12414
12474
  }
12415
- async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
12416
- const safeFile = safeIdentityFileBase(serverName);
12417
- const path = join2(dir, `${safeFile}.json`);
12418
- await mkdir(dir, { recursive: true, mode: 448 });
12419
- try {
12420
- const raw = await readFile(path, "utf8");
12421
- const j2 = JSON.parse(raw);
12422
- return {
12423
- x25519Priv: fromB64(j2.x25519Priv),
12424
- x25519Pub: fromB64(j2.x25519Pub),
12425
- ed25519Priv: fromB64(j2.ed25519Priv),
12426
- ed25519Pub: fromB64(j2.ed25519Pub),
12427
- createdAt: j2.createdAt
12428
- };
12429
- } catch (e) {
12430
- if (e.code !== "ENOENT")
12431
- throw e;
12432
- }
12475
+ function identityFilePath(dir, serverName) {
12476
+ return join2(dir, `${safeIdentityFileBase(serverName)}.json`);
12477
+ }
12478
+ async function generateIdentity() {
12433
12479
  const x = await generateX25519();
12434
12480
  const ed = await generateEd25519();
12435
- const id = {
12481
+ return {
12436
12482
  x25519Priv: x.privateKey,
12437
12483
  x25519Pub: x.publicKey,
12438
12484
  ed25519Priv: ed.privateKey,
12439
12485
  ed25519Pub: ed.publicKey,
12440
12486
  createdAt: Date.now()
12441
12487
  };
12442
- const j = {
12488
+ }
12489
+ function serializeIdentity(id) {
12490
+ return JSON.stringify({
12443
12491
  x25519Priv: toB64(id.x25519Priv),
12444
12492
  x25519Pub: toB64(id.x25519Pub),
12445
12493
  ed25519Priv: toB64(id.ed25519Priv),
12446
12494
  ed25519Pub: toB64(id.ed25519Pub),
12447
12495
  createdAt: id.createdAt
12496
+ }, null, 2);
12497
+ }
12498
+ function parseIdentity(text) {
12499
+ const j = JSON.parse(text);
12500
+ return {
12501
+ x25519Priv: fromB64(j.x25519Priv),
12502
+ x25519Pub: fromB64(j.x25519Pub),
12503
+ ed25519Priv: fromB64(j.ed25519Priv),
12504
+ ed25519Pub: fromB64(j.ed25519Pub),
12505
+ createdAt: j.createdAt
12448
12506
  };
12449
- await writeFile(path, JSON.stringify(j, null, 2), { mode: 384 });
12507
+ }
12508
+ async function writeIdentityFile(dir, serverName, id) {
12509
+ const path = identityFilePath(dir, serverName);
12510
+ await mkdir(dir, { recursive: true, mode: 448 });
12511
+ await writeFile(path, serializeIdentity(id), { mode: 384 });
12450
12512
  await chmod(path, 384);
12513
+ return path;
12514
+ }
12515
+ async function loadOrCreateIdentity(serverName, dir = defaultIdentityDir()) {
12516
+ const path = identityFilePath(dir, serverName);
12517
+ await mkdir(dir, { recursive: true, mode: 448 });
12518
+ try {
12519
+ return parseIdentity(await readFile(path, "utf8"));
12520
+ } catch (e) {
12521
+ if (e.code !== "ENOENT")
12522
+ throw e;
12523
+ }
12524
+ const id = await generateIdentity();
12525
+ await writeIdentityFile(dir, serverName, id);
12451
12526
  return id;
12452
12527
  }
12453
12528
  var SAFE_PLAIN, SAFE_SCOPED;
@@ -12565,6 +12640,9 @@ async function startHost(opts) {
12565
12640
  }
12566
12641
  });
12567
12642
  const ownHello = await buildServerHello({
12643
+ // 2.6.0: tell the extension it can say WHY it refused a hello, instead of
12644
+ // leaving us to time out and guess.
12645
+ accepts: ["hello-rejected"],
12568
12646
  identity: opts.ownIdentity,
12569
12647
  mcpId: opts.ownMcpId,
12570
12648
  serverName: opts.ownServerName,
@@ -12776,6 +12854,16 @@ async function startHost(opts) {
12776
12854
  extensionWs.send(JSON.stringify(frame));
12777
12855
  }
12778
12856
  }
12857
+ if (frame.type === "hello-rejected" && identified === "extension") {
12858
+ if (frame.mcpId === opts.ownMcpId) {
12859
+ rejectOwnSession(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
12860
+ } else {
12861
+ const slot = peers.get(frame.mcpId);
12862
+ if (slot?.helloFrame.accepts?.includes("hello-rejected")) {
12863
+ slot.ws.send(JSON.stringify(frame));
12864
+ }
12865
+ }
12866
+ }
12779
12867
  if (frame.type === "pair-pending" && identified === "extension") {
12780
12868
  if (frame.mcpId === opts.ownMcpId) {
12781
12869
  ownPendingPairCode = frame.pairCode;
@@ -12805,8 +12893,18 @@ async function startHost(opts) {
12805
12893
  rejectOwnSession(new Error("extension disconnected before ready"));
12806
12894
  }
12807
12895
  ownSession = null;
12896
+ ownPendingPairCode = null;
12808
12897
  resetSessionPromise();
12809
12898
  disconnectListeners.forEach((cb) => cb());
12899
+ const notice = JSON.stringify({ type: "extension-disconnected" });
12900
+ for (const slot of peers.values()) {
12901
+ if (slot.helloFrame.accepts?.includes("extension-disconnected")) {
12902
+ try {
12903
+ slot.ws.send(notice);
12904
+ } catch {
12905
+ }
12906
+ }
12907
+ }
12810
12908
  }
12811
12909
  if (identified === "peer" && peerMcpId) {
12812
12910
  if (peers.get(peerMcpId)?.ws === ws)
@@ -12845,7 +12943,9 @@ async function startHost(opts) {
12845
12943
  onPendingPair: (cb) => {
12846
12944
  pendingPairListeners.push(cb);
12847
12945
  },
12848
- pendingPairCode: () => ownPendingPairCode
12946
+ pendingPairCode: () => ownPendingPairCode,
12947
+ extensionConnected: () => extensionWs !== null,
12948
+ sessionLinked: () => ownSession !== null
12849
12949
  };
12850
12950
  }
12851
12951
  var PUBLIC_ORIGIN_RE, enc2;
@@ -12884,7 +12984,11 @@ async function startPeer(opts) {
12884
12984
  domSelectors: opts.domSelectors,
12885
12985
  localStoragePointers: opts.localStoragePointers,
12886
12986
  sessionStoragePointers: opts.sessionStoragePointers,
12887
- graphqlOps: opts.graphqlOps
12987
+ graphqlOps: opts.graphqlOps,
12988
+ // 2.5.0: let a host ≥2.5.0 tell us when the extension leaves, so
12989
+ // `extensionConnected()` / `sessionLinked()` can go back to false.
12990
+ // 2.6.0: `hello-rejected` for the same reason, from the extension.
12991
+ accepts: ["extension-disconnected", "hello-rejected"]
12888
12992
  });
12889
12993
  const sessionNonce = fromB64(hello.sessionNonce);
12890
12994
  ws.send(JSON.stringify(hello));
@@ -12901,6 +13005,7 @@ async function startPeer(opts) {
12901
13005
  rejectFirstReady = reject;
12902
13006
  });
12903
13007
  let extensionHello = null;
13008
+ let extensionGone = false;
12904
13009
  let warnedUnverifiable = false;
12905
13010
  let cachedPin = void 0;
12906
13011
  const authenticateExtension = async (sessionSig, extensionSessionPub) => {
@@ -12971,6 +13076,12 @@ async function startPeer(opts) {
12971
13076
  extensionHello = frame;
12972
13077
  return;
12973
13078
  }
13079
+ if (frame.type === "extension-disconnected") {
13080
+ extensionHello = null;
13081
+ extensionGone = true;
13082
+ pendingPairCode = null;
13083
+ return;
13084
+ }
12974
13085
  if (frame.type === "ready" && frame.mcpId === opts.mcpId) {
12975
13086
  const authorised = await authenticateExtension(frame.sessionSig, frame.extensionSessionPub);
12976
13087
  if (!authorised) {
@@ -12983,6 +13094,7 @@ async function startPeer(opts) {
12983
13094
  const sessionKey = await hkdfSha256(shared, sessionNonce, enc3.encode(HKDF_SESSION_INFO), 32);
12984
13095
  const isRenegotiation = session !== null;
12985
13096
  session = new SessionState(sessionKey);
13097
+ extensionGone = false;
12986
13098
  pendingPairCode = null;
12987
13099
  if (isRenegotiation) {
12988
13100
  renegotiateListeners.forEach((cb) => cb());
@@ -12991,6 +13103,9 @@ async function startPeer(opts) {
12991
13103
  }
12992
13104
  return;
12993
13105
  }
13106
+ if (frame.type === "hello-rejected" && frame.mcpId === opts.mcpId) {
13107
+ rejectFirstReady(new FetchproxyHelloRejectedError({ mcpId: frame.mcpId, reason: frame.reason }));
13108
+ }
12994
13109
  if (frame.type === "pair-pending" && frame.mcpId === opts.mcpId) {
12995
13110
  pendingPairCode = frame.pairCode;
12996
13111
  pendingPairListeners.forEach((cb) => cb(frame.pairCode));
@@ -13050,6 +13165,8 @@ async function startPeer(opts) {
13050
13165
  pendingPairListeners.push(cb);
13051
13166
  },
13052
13167
  pendingPairCode: () => pendingPairCode,
13168
+ extensionConnected: () => extensionHello !== null,
13169
+ sessionLinked: () => session !== null && !extensionGone,
13053
13170
  onClose: (cb) => {
13054
13171
  closeListeners.push(cb);
13055
13172
  },
@@ -13078,7 +13195,7 @@ function classifyFetchError(error61) {
13078
13195
  if (/^tab fetch failed:/.test(error61)) {
13079
13196
  return "tab_fetch_failed";
13080
13197
  }
13081
- if (/^fetch threw:/.test(error61)) {
13198
+ if (/^(in-page )?fetch threw:/.test(error61)) {
13082
13199
  return "tab_fetch_failed";
13083
13200
  }
13084
13201
  if (/^no tab matching /.test(error61)) {
@@ -13102,6 +13219,10 @@ var init_error_kind = __esm({
13102
13219
 
13103
13220
  // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
13104
13221
  function classifyBridgeError(err) {
13222
+ if (err instanceof FetchproxyHelloRejectedError)
13223
+ return "hello_rejected";
13224
+ if (err instanceof FetchproxySessionNotReadyError)
13225
+ return "session_not_ready";
13105
13226
  if (err instanceof FetchproxyTimeoutError)
13106
13227
  return "timeout";
13107
13228
  if (err instanceof FetchproxyBridgeDownError)
@@ -13115,6 +13236,7 @@ function classifyBridgeError(err) {
13115
13236
  var init_classify_bridge_error = __esm({
13116
13237
  "node_modules/@fetchproxy/server/dist/classify-bridge-error.js"() {
13117
13238
  init_ws_server();
13239
+ init_session_ready();
13118
13240
  }
13119
13241
  });
13120
13242
 
@@ -13143,6 +13265,8 @@ function envWsHost() {
13143
13265
  function protocolErrorFrom(error61) {
13144
13266
  if (SCOPE_REJECTION.test(error61))
13145
13267
  return new FetchproxyScopeError(error61);
13268
+ if (TAB_OPENING_REJECTION.test(error61))
13269
+ return new FetchproxyTabOpeningError(error61);
13146
13270
  if (NO_TAB_REJECTION.test(error61))
13147
13271
  return new FetchproxyNoTabError(error61);
13148
13272
  return new FetchproxyProtocolError(error61);
@@ -13182,7 +13306,7 @@ function assertUrlInDomains(field, url2, domains) {
13182
13306
  const declared = domains.map((d) => JSON.stringify(d)).join(", ");
13183
13307
  throw new Error(`FetchproxyServer: ${field} host "${host}" is outside declared domains [${declared}] \u2014 must be one of them or a subdomain`);
13184
13308
  }
13185
- var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, SCOPE_REJECTION, NO_TAB_REJECTION, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
13309
+ var FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyHintedError, FetchproxyScopeError, FetchproxyNoTabError, FetchproxyTabOpeningError, SCOPE_REJECTION, NO_TAB_REJECTION, TAB_OPENING_REJECTION, FetchproxyTimeoutError, SUBDOMAIN_LABEL_RE, DEFAULT_JSON_OK_STATUSES, FetchproxyServer;
13186
13310
  var init_ws_server = __esm({
13187
13311
  "node_modules/@fetchproxy/server/dist/ws-server.js"() {
13188
13312
  init_dist();
@@ -13258,8 +13382,15 @@ var init_ws_server = __esm({
13258
13382
  this.name = "FetchproxyNoTabError";
13259
13383
  }
13260
13384
  };
13385
+ FetchproxyTabOpeningError = class extends FetchproxyHintedError {
13386
+ constructor(originalError) {
13387
+ super(originalError, "the extension is opening that tab now \u2014 retry in a few seconds. You do not need to open one yourself, and this is not a version problem.");
13388
+ this.name = "FetchproxyTabOpeningError";
13389
+ }
13390
+ };
13261
13391
  SCOPE_REJECTION = /not in declared/;
13262
- NO_TAB_REJECTION = /no tab matching (?!.*content script loaded)/;
13392
+ NO_TAB_REJECTION = /no tab matching (?!.*(content script loaded|one is still opening))/;
13393
+ TAB_OPENING_REJECTION = /no tab matching .*one is still opening/;
13263
13394
  FetchproxyTimeoutError = class extends FetchproxyProtocolError {
13264
13395
  url;
13265
13396
  timeoutMs;
@@ -13277,8 +13408,17 @@ var init_ws_server = __esm({
13277
13408
  * the first attempt.
13278
13409
  */
13279
13410
  retryAttempted;
13411
+ /**
13412
+ * 2.4.2+ (#277): what the CALL asked for, when that is more than the
13413
+ * deadline that actually fired. A per-call `timeoutMs` is forwarded to the
13414
+ * extension but does not raise this server's `fetchTimeoutMs`, so the
13415
+ * shorter deadline wins — and used to report only its own number, a value
13416
+ * the caller never supplied, from an option the message never named.
13417
+ */
13418
+ requestedTimeoutMs;
13280
13419
  constructor(args) {
13281
- super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
13420
+ const capped = args.requestedTimeoutMs !== void 0 && args.requestedTimeoutMs > args.timeoutMs;
13421
+ super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms` + (capped ? ` \u2014 that is this server's fetchTimeoutMs, not the ${args.requestedTimeoutMs}ms this call asked for. A per-call timeoutMs cannot exceed it; raise fetchTimeoutMs on the transport to wait longer.` : ""));
13282
13422
  this.name = "FetchproxyTimeoutError";
13283
13423
  this.url = args.url;
13284
13424
  this.timeoutMs = args.timeoutMs;
@@ -13286,6 +13426,7 @@ var init_ws_server = __esm({
13286
13426
  this.port = args.port ?? 0;
13287
13427
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
13288
13428
  this.retryAttempted = args.retryAttempted ?? false;
13429
+ this.requestedTimeoutMs = args.requestedTimeoutMs ?? null;
13289
13430
  }
13290
13431
  };
13291
13432
  SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -13694,6 +13835,7 @@ var init_ws_server = __esm({
13694
13835
  lastFailureReason: this.lastFailureReason,
13695
13836
  consecutiveFailures: this.consecutiveFailures,
13696
13837
  lastExtensionMessageAt: this.lastExtensionMessageAt,
13838
+ session: this.sessionSnapshot(),
13697
13839
  keepAlive: {
13698
13840
  enabled: intervalMs > 0,
13699
13841
  intervalMs,
@@ -13829,9 +13971,9 @@ var init_ws_server = __esm({
13829
13971
  async _fetchOnceWithTimeout(init) {
13830
13972
  const id = this.nextRequestId++;
13831
13973
  const inner = { type: "request", id, op: "fetch", init };
13832
- const pending = new Promise((resolve) => {
13974
+ const pending = this.guardPending(new Promise((resolve) => {
13833
13975
  this.pending.set(id, resolve);
13834
- });
13976
+ }));
13835
13977
  await this.sendInnerFrame(inner);
13836
13978
  const timeoutMs = this.opts.fetchTimeoutMs;
13837
13979
  if (timeoutMs === void 0 || timeoutMs <= 0)
@@ -13878,7 +14020,35 @@ var init_ws_server = __esm({
13878
14020
  * already surface for fetch timeouts). `0`/unset opts out (unbounded),
13879
14021
  * matching the fetch path.
13880
14022
  */
13881
- async _withVerbTimeout(pending, pendingMap, id, url2) {
14023
+ /**
14024
+ * Register a verb's pending promise and keep an early rejection from being
14025
+ * fatal.
14026
+ *
14027
+ * Every verb stores its resolver, then AWAITS `sendInnerFrame` before the
14028
+ * caller attaches a handler — and that send does real work (sealing, a
14029
+ * socket write), so it yields across event-loop turns. Anything that drains
14030
+ * the pending maps inside that window rejects a promise nothing is
14031
+ * listening to yet: a refused pairing (`rejectAllPending(pairingErrorMessage
14032
+ * (code))`), an extension disconnect, a `close()`. Under Node's default
14033
+ * `--unhandled-rejections=throw` that ends the PROCESS, so an MCP that
14034
+ * should have reported "approve pair code NNN-NN" dies instead, and each
14035
+ * respawn mints a fresh code nobody can approve (#329).
14036
+ *
14037
+ * The no-op catch is a listener, not a swallow. `.catch()` derives a NEW
14038
+ * promise and leaves this one's rejection intact, so `_withVerbTimeout` and
14039
+ * the caller still see it — the only thing that changes is that the
14040
+ * rejection is never unhandled.
14041
+ *
14042
+ * This is the same hazard `_withVerbTimeout`'s timer already guards against
14043
+ * from the other side, where it drops the resolver "so a late bridge
14044
+ * response doesn't become an unhandled promise that crashes the host".
14045
+ */
14046
+ guardPending(pending) {
14047
+ pending.catch(() => {
14048
+ });
14049
+ return pending;
14050
+ }
14051
+ async _withVerbTimeout(pending, pendingMap, id, url2, requestedTimeoutMs) {
13882
14052
  const timeoutMs = this.opts.fetchTimeoutMs;
13883
14053
  if (timeoutMs === void 0 || timeoutMs <= 0)
13884
14054
  return pending;
@@ -13896,7 +14066,8 @@ var init_ws_server = __esm({
13896
14066
  role: this.role,
13897
14067
  port: this.opts.port,
13898
14068
  elapsedMs: Date.now() - start,
13899
- retryAttempted: false
14069
+ retryAttempted: false,
14070
+ ...requestedTimeoutMs !== void 0 ? { requestedTimeoutMs } : {}
13900
14071
  }));
13901
14072
  }, timeoutMs);
13902
14073
  })
@@ -13956,9 +14127,9 @@ var init_ws_server = __esm({
13956
14127
  if (opts.subdomain !== void 0)
13957
14128
  assertSubdomainLabel(opts.subdomain);
13958
14129
  const baseDomain = this.resolveBaseDomain(opts.domain);
13959
- const isAbsolute = path.startsWith("http://") || path.startsWith("https://");
14130
+ const isAbsolute2 = path.startsWith("http://") || path.startsWith("https://");
13960
14131
  let host;
13961
- if (isAbsolute) {
14132
+ if (isAbsolute2) {
13962
14133
  try {
13963
14134
  host = new URL(path).host;
13964
14135
  } catch {
@@ -13967,7 +14138,7 @@ var init_ws_server = __esm({
13967
14138
  } else {
13968
14139
  host = opts.subdomain ? `${opts.subdomain}.${baseDomain}` : baseDomain;
13969
14140
  }
13970
- const url2 = isAbsolute ? path : `https://${host}${path}`;
14141
+ const url2 = isAbsolute2 ? path : `https://${host}${path}`;
13971
14142
  assertUrlInDomains("request url", url2, this.opts.domains);
13972
14143
  let tabUrl = `https://${host}/`;
13973
14144
  if (opts.viaTab !== void 0) {
@@ -13984,7 +14155,15 @@ var init_ws_server = __esm({
13984
14155
  method,
13985
14156
  tabUrl,
13986
14157
  headers: opts.headers,
13987
- body: opts.body
14158
+ body: opts.body,
14159
+ // Spread rather than `inPage: opts.inPage`: the wire's validator treats
14160
+ // the key's PRESENCE as the request, and a literal `undefined` would
14161
+ // both serialize away inconsistently and read as "asked for" here.
14162
+ ...opts.inPage === true ? { inPage: true } : {},
14163
+ // Spread for the reason `inPage` is: only a deliberate 'omit' goes on
14164
+ // the wire, so an untouched request serialises byte-identically to
14165
+ // every version before this option existed.
14166
+ ...opts.credentials === "omit" ? { credentials: "omit" } : {}
13988
14167
  };
13989
14168
  const result = await this.fetch(init);
13990
14169
  if (!result.ok) {
@@ -14158,7 +14337,11 @@ var init_ws_server = __esm({
14158
14337
  last_success_at: health.lastSuccessAt,
14159
14338
  last_failure_at: health.lastFailureAt,
14160
14339
  last_failure_reason: health.lastFailureReason,
14161
- consecutive_failures: health.consecutiveFailures
14340
+ consecutive_failures: health.consecutiveFailures,
14341
+ session_state: health.session.state,
14342
+ pending_pair_code: health.session.pairCode,
14343
+ extension_connected: health.session.extensionConnected,
14344
+ last_extension_message_at: health.lastExtensionMessageAt
14162
14345
  },
14163
14346
  ...error61 ? { error: error61 } : {}
14164
14347
  };
@@ -14208,9 +14391,9 @@ var init_ws_server = __esm({
14208
14391
  const tabUrl = `https://${host}/`;
14209
14392
  inner = { type: "request", id, op: "read_cookies", init: { tabUrl } };
14210
14393
  }
14211
- const pending = new Promise((resolve) => {
14394
+ const pending = this.guardPending(new Promise((resolve) => {
14212
14395
  this.pendingReadCookies.set(id, resolve);
14213
- });
14396
+ }));
14214
14397
  await this.sendInnerFrame(inner);
14215
14398
  const result = await this._withVerbTimeout(pending, this.pendingReadCookies, id, `https://${host}`);
14216
14399
  if (!result.ok) {
@@ -14267,9 +14450,9 @@ var init_ws_server = __esm({
14267
14450
  ...cookiePath !== void 0 ? { path: cookiePath } : {}
14268
14451
  }
14269
14452
  };
14270
- const pending = new Promise((resolve, reject) => {
14453
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14271
14454
  this.pendingWriteCookies.set(id, { resolve, reject });
14272
- });
14455
+ }));
14273
14456
  await this.sendInnerFrame(inner);
14274
14457
  return this._withVerbTimeout(pending, this.pendingWriteCookies, id, `https://${host}`);
14275
14458
  }
@@ -14330,9 +14513,9 @@ var init_ws_server = __esm({
14330
14513
  ...opts.pointers ? { pointers: { ...opts.pointers } } : {}
14331
14514
  }
14332
14515
  };
14333
- const pending = new Promise((resolve, reject) => {
14516
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14334
14517
  this.pendingStorage.set(id, { resolve, reject });
14335
- });
14518
+ }));
14336
14519
  await this.sendInnerFrame(inner);
14337
14520
  return this._withVerbTimeout(pending, this.pendingStorage, id, `https://${host}`);
14338
14521
  }
@@ -14435,11 +14618,19 @@ var init_ws_server = __esm({
14435
14618
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
14436
14619
  }
14437
14620
  };
14438
- const pending = new Promise((resolve, reject) => {
14621
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14439
14622
  this.pendingCapture.set(id, { resolve, reject });
14440
- });
14623
+ }));
14441
14624
  await this.sendInnerFrame(inner);
14442
- return this._withVerbTimeout(pending, this.pendingCapture, id, `https://${opts.host}${opts.path ?? "/*"}`);
14625
+ return this._withVerbTimeout(
14626
+ pending,
14627
+ this.pendingCapture,
14628
+ id,
14629
+ `https://${opts.host}${opts.path ?? "/*"}`,
14630
+ // #277: not to extend the deadline — so a timeout can say the deadline,
14631
+ // and not the value asked for here, is what fired.
14632
+ opts.timeoutMs
14633
+ );
14443
14634
  }
14444
14635
  /**
14445
14636
  * Snapshot the redirect target URL of the next request the browser
@@ -14520,11 +14711,19 @@ var init_ws_server = __esm({
14520
14711
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
14521
14712
  }
14522
14713
  };
14523
- const pending = new Promise((resolve, reject) => {
14714
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14524
14715
  this.pendingRedirect.set(id, { resolve, reject });
14525
- });
14716
+ }));
14526
14717
  await this.sendInnerFrame(inner);
14527
- return this._withVerbTimeout(pending, this.pendingRedirect, id, `https://${opts.host}${opts.path ?? "/*"}`);
14718
+ return this._withVerbTimeout(
14719
+ pending,
14720
+ this.pendingRedirect,
14721
+ id,
14722
+ `https://${opts.host}${opts.path ?? "/*"}`,
14723
+ // #277: not to extend the deadline — so a timeout can say the deadline,
14724
+ // and not the value asked for here, is what fired.
14725
+ opts.timeoutMs
14726
+ );
14528
14727
  }
14529
14728
  /**
14530
14729
  * Download `url` through the BROWSER's own network stack via
@@ -14602,11 +14801,21 @@ var init_ws_server = __esm({
14602
14801
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
14603
14802
  }
14604
14803
  };
14605
- const pending = new Promise((resolve, reject) => {
14804
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14606
14805
  this.pendingDownload.set(id, { resolve, reject });
14607
- });
14806
+ }));
14608
14807
  await this.sendInnerFrame(inner);
14609
- return this._withVerbTimeout(pending, this.pendingDownload, id, opts.url);
14808
+ return this._withVerbTimeout(
14809
+ pending,
14810
+ this.pendingDownload,
14811
+ id,
14812
+ opts.url,
14813
+ // #277, same as the two capture verbs: `download` takes a per-call
14814
+ // timeoutMs too, so it hits the identical cap and needs the identical
14815
+ // explanation. Missing it was the point of #279 — "both verbs" was
14816
+ // counted off the two call sites in view rather than off the type.
14817
+ opts.timeoutMs
14818
+ );
14610
14819
  }
14611
14820
  /**
14612
14821
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
@@ -14650,9 +14859,9 @@ var init_ws_server = __esm({
14650
14859
  keys: [...opts.keys]
14651
14860
  }
14652
14861
  };
14653
- const pending = new Promise((resolve, reject) => {
14862
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14654
14863
  this.pendingIdb.set(id, { resolve, reject });
14655
- });
14864
+ }));
14656
14865
  await this.sendInnerFrame(inner);
14657
14866
  return this._withVerbTimeout(pending, this.pendingIdb, id, origin);
14658
14867
  }
@@ -14690,9 +14899,9 @@ var init_ws_server = __esm({
14690
14899
  op: "read_dom",
14691
14900
  init: { origin, names: [...opts.names] }
14692
14901
  };
14693
- const pending = new Promise((resolve, reject) => {
14902
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14694
14903
  this.pendingStorage.set(id, { resolve, reject });
14695
- });
14904
+ }));
14696
14905
  await this.sendInnerFrame(inner);
14697
14906
  return this._withVerbTimeout(pending, this.pendingStorage, id, origin);
14698
14907
  }
@@ -14736,9 +14945,9 @@ var init_ws_server = __esm({
14736
14945
  ...opts.tabUrl !== void 0 ? { tabUrl: opts.tabUrl } : {}
14737
14946
  }
14738
14947
  };
14739
- const pending = new Promise((resolve, reject) => {
14948
+ const pending = this.guardPending(new Promise((resolve, reject) => {
14740
14949
  this.pendingGraphql.set(id, { resolve, reject });
14741
- });
14950
+ }));
14742
14951
  await this.sendInnerFrame(inner);
14743
14952
  return this._withVerbTimeout(pending, this.pendingGraphql, id, opts.name);
14744
14953
  }
@@ -14975,6 +15184,25 @@ var init_ws_server = __esm({
14975
15184
  return this.peerHandle.pendingPairCode();
14976
15185
  return null;
14977
15186
  }
15187
+ /**
15188
+ * 2.5.0: the extension link as {@link BridgeSessionState}, read off
15189
+ * whichever handle is live. A pending pair code outranks "extension not
15190
+ * seen" because the code could only have come from the extension — a
15191
+ * peer behind an older host never sees the extension hello but does see
15192
+ * pair-pending frames. Where the extension is KNOWN to be gone (a host's
15193
+ * socket closed, a peer told by a 2.5.0+ host) the handle has already
15194
+ * dropped the code, so the precedence never lies there (#283).
15195
+ */
15196
+ sessionSnapshot() {
15197
+ const handle = this.hostHandle ?? this.peerHandle;
15198
+ const pairCode = this.currentPendingPairCode();
15199
+ if (!handle || this.role === null) {
15200
+ return { state: "not_listening", pairCode, extensionConnected: false };
15201
+ }
15202
+ const extensionConnected = handle.extensionConnected();
15203
+ const state = handle.sessionLinked() ? "linked" : pairCode !== null ? "pair_pending" : !extensionConnected ? "extension_disconnected" : "no_session";
15204
+ return { state, pairCode, extensionConnected };
15205
+ }
14978
15206
  /**
14979
15207
  * 0.5.2+: throw `FetchproxyProtocolError` with the actionable pair-code
14980
15208
  * message if the bridge is waiting on user approval. Used by the verb
@@ -15346,6 +15574,10 @@ function credentialHint(arm, prefix, hostLabel, source) {
15346
15574
  return `No credential resolved. Nothing was available to authenticate with \u2014 sign in and reconnect the connector so ${prefix} receives a token, or set the documented environment variable.`;
15347
15575
  case "credential_rejected":
15348
15576
  return `${hostLabel} rejected the credential from '${source}'. It is present but no longer valid \u2014 most often expired or revoked upstream. Re-authenticate and reconnect; retrying will not fix it.`;
15577
+ case "session_expired":
15578
+ return `The credential from '${source}' is configured, but no session is live \u2014 ${hostLabel} served a sign-in page rather than the data. Sign in again; a cookie-session portal expires these on its own, so this recurs between uses.`;
15579
+ case "verification_pending":
15580
+ return `${hostLabel} is holding the sign-in on a second factor rather than refusing it. Supply the verification code the ACCOUNT HOLDER received \u2014 the credential itself is not the problem, so changing it will not help.`;
15349
15581
  case "timeout":
15350
15582
  return `The credential from '${source}' resolved, but ${hostLabel} did not answer in time. Usually transient \u2014 retry. If it persists, ${hostLabel} is slow or unreachable from here.`;
15351
15583
  case "http":
@@ -15356,12 +15588,102 @@ function credentialHint(arm, prefix, hostLabel, source) {
15356
15588
  return `Unexpected failure \u2014 see error.message.`;
15357
15589
  }
15358
15590
  }
15591
+ function credentialHealthcheckDescription(hostLabel) {
15592
+ return `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Read-only; never returns the credential itself.`;
15593
+ }
15594
+ async function runCredentialHealthcheck(args) {
15595
+ const { prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
15596
+ const probeUrl = probePath ? `https://${hostLabel}${probePath.startsWith("/") ? "" : "/"}${probePath}` : void 0;
15597
+ let probeStarted = 0;
15598
+ let state;
15599
+ try {
15600
+ state = await resolveCredential();
15601
+ } catch (e) {
15602
+ const classified = classifyThrown?.(e);
15603
+ const result2 = {
15604
+ ok: false,
15605
+ // Still false, and still no source: a classification explains WHY
15606
+ // nothing resolved, it does not invent a credential that did.
15607
+ credential: { source: null, resolved: false },
15608
+ // No `url`: nothing was probed, and naming one implies it was tried.
15609
+ probe: { elapsed_ms: 0 },
15610
+ error: {
15611
+ kind: classified?.kind ?? "no_credential",
15612
+ message: truncateErrorMessage(messageOf(e)),
15613
+ ...classified?.detail !== void 0 ? { detail: classified.detail } : {}
15614
+ },
15615
+ // The hint must follow the KIND beside it. Falling back to
15616
+ // `no_credential`'s copy under a classified kind would state a cause
15617
+ // the kind contradicts — the same disagreement this path exists to
15618
+ // remove. So: an inline hint wins; else the classified arm's own
15619
+ // copy (consumer override first); else, for a kind this module has
15620
+ // no copy for, the neutral `unknown` text rather than one that
15621
+ // asserts a cause; else the unclassified `no_credential` default.
15622
+ hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
15623
+ };
15624
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
15625
+ }
15626
+ const credential = {
15627
+ source: state.source,
15628
+ resolved: state.source !== null,
15629
+ ...state.detail !== void 0 ? { detail: state.detail } : {}
15630
+ };
15631
+ if (!credential.resolved) {
15632
+ const result2 = {
15633
+ ok: false,
15634
+ credential,
15635
+ probe: { elapsed_ms: 0 },
15636
+ error: { kind: "no_credential", message: "no credential source resolved" },
15637
+ hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
15638
+ };
15639
+ return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
15640
+ }
15641
+ let arm = "ok";
15642
+ let error61;
15643
+ let status;
15644
+ let customHint;
15645
+ probeStarted = Date.now();
15646
+ try {
15647
+ await probeFn();
15648
+ } catch (e) {
15649
+ status = statusOf(e);
15650
+ const aborted2 = e instanceof Error && e.name === "AbortError";
15651
+ arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
15652
+ let kind = arm;
15653
+ let detail;
15654
+ const custom2 = classifyThrown?.(e);
15655
+ if (custom2) {
15656
+ kind = custom2.kind;
15657
+ customHint = custom2.hint;
15658
+ detail = custom2.detail;
15659
+ arm = isArm(kind) ? kind : "unknown";
15660
+ }
15661
+ error61 = {
15662
+ kind,
15663
+ // Redacted AND bounded before it reaches the result: an upstream
15664
+ // failure routinely quotes what it was sent, and a healthcheck is
15665
+ // the tool people paste into a chat when something is broken.
15666
+ message: truncateErrorMessage(messageOf(e)),
15667
+ ...detail !== void 0 ? { detail } : {}
15668
+ };
15669
+ }
15670
+ const result = {
15671
+ ok: error61 === void 0,
15672
+ credential,
15673
+ probe: {
15674
+ ...probeUrl ? { url: probeUrl } : {},
15675
+ elapsed_ms: Date.now() - probeStarted,
15676
+ ...status !== void 0 ? { status } : {}
15677
+ },
15678
+ ...error61 ? { error: error61 } : {},
15679
+ hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
15680
+ };
15681
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
15682
+ }
15359
15683
  function registerCredentialHealthcheckTool(args) {
15360
- const { server, prefix, hostLabel, probePath, resolveCredential, probeFn, classifyThrown, hints } = args;
15361
- const probeUrl = probePath ? `https://${hostLabel}${probePath}` : void 0;
15362
- server.registerTool(`${prefix}_healthcheck`, {
15684
+ args.server.registerTool(`${args.prefix}_healthcheck`, {
15363
15685
  title: "Verify credentials and upstream reachability",
15364
- description: `Resolves the credential the way real tools do, then makes one authenticated request to ${hostLabel}. Reports which source supplied the credential, whether ${hostLabel} accepted it, the round-trip time, and a plain-English hint distinguishing 'no credential' from 'credential rejected' from 'a ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only; never returns the credential itself.`,
15686
+ description: `${credentialHealthcheckDescription(args.hostLabel)} Call this when a real tool fails and you want to know which hop broke.`,
15365
15687
  annotations: {
15366
15688
  title: "Verify credentials and upstream reachability",
15367
15689
  readOnlyHint: true,
@@ -15369,92 +15691,7 @@ function registerCredentialHealthcheckTool(args) {
15369
15691
  openWorldHint: true
15370
15692
  },
15371
15693
  inputSchema: {}
15372
- }, async () => {
15373
- let probeStarted = 0;
15374
- let state;
15375
- try {
15376
- state = await resolveCredential();
15377
- } catch (e) {
15378
- const classified = classifyThrown?.(e);
15379
- const result2 = {
15380
- ok: false,
15381
- // Still false, and still no source: a classification explains WHY
15382
- // nothing resolved, it does not invent a credential that did.
15383
- credential: { source: null, resolved: false },
15384
- // No `url`: nothing was probed, and naming one implies it was tried.
15385
- probe: { elapsed_ms: 0 },
15386
- error: {
15387
- kind: classified?.kind ?? "no_credential",
15388
- message: truncateErrorMessage(messageOf(e)),
15389
- ...classified?.detail !== void 0 ? { detail: classified.detail } : {}
15390
- },
15391
- // The hint must follow the KIND beside it. Falling back to
15392
- // `no_credential`'s copy under a classified kind would state a cause
15393
- // the kind contradicts — the same disagreement this path exists to
15394
- // remove. So: an inline hint wins; else the classified arm's own
15395
- // copy (consumer override first); else, for a kind this module has
15396
- // no copy for, the neutral `unknown` text rather than one that
15397
- // asserts a cause; else the unclassified `no_credential` default.
15398
- hint: classified?.hint ?? (isArm(classified?.kind) ? hints?.[classified.kind] ?? credentialHint(classified.kind, prefix, hostLabel, null) : classified !== void 0 ? credentialHint("unknown", prefix, hostLabel, null) : hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null))
15399
- };
15400
- return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
15401
- }
15402
- const credential = {
15403
- source: state.source,
15404
- resolved: state.source !== null,
15405
- ...state.detail !== void 0 ? { detail: state.detail } : {}
15406
- };
15407
- if (!credential.resolved) {
15408
- const result2 = {
15409
- ok: false,
15410
- credential,
15411
- probe: { elapsed_ms: 0 },
15412
- error: { kind: "no_credential", message: "no credential source resolved" },
15413
- hint: hints?.no_credential ?? credentialHint("no_credential", prefix, hostLabel, null)
15414
- };
15415
- return { content: [{ type: "text", text: JSON.stringify(result2, null, 2) }] };
15416
- }
15417
- let arm = "ok";
15418
- let error61;
15419
- let status;
15420
- let customHint;
15421
- probeStarted = Date.now();
15422
- try {
15423
- await probeFn();
15424
- } catch (e) {
15425
- status = statusOf(e);
15426
- const aborted2 = e instanceof Error && e.name === "AbortError";
15427
- arm = status === 401 || status === 403 ? "credential_rejected" : status !== void 0 ? "http" : aborted2 || /timeout|timed out|ETIMEDOUT/i.test(messageOf(e)) ? "timeout" : /fetch failed|ENOTFOUND|ECONNREFUSED|ECONNRESET|network/i.test(messageOf(e)) ? "transport" : "unknown";
15428
- let kind = arm;
15429
- let detail;
15430
- const custom2 = classifyThrown?.(e);
15431
- if (custom2) {
15432
- kind = custom2.kind;
15433
- customHint = custom2.hint;
15434
- detail = custom2.detail;
15435
- }
15436
- error61 = {
15437
- kind,
15438
- // Redacted AND bounded before it reaches the result: an upstream
15439
- // failure routinely quotes what it was sent, and a healthcheck is
15440
- // the tool people paste into a chat when something is broken.
15441
- message: truncateErrorMessage(messageOf(e)),
15442
- ...detail !== void 0 ? { detail } : {}
15443
- };
15444
- }
15445
- const result = {
15446
- ok: error61 === void 0,
15447
- credential,
15448
- probe: {
15449
- ...probeUrl ? { url: probeUrl } : {},
15450
- elapsed_ms: Date.now() - probeStarted,
15451
- ...status !== void 0 ? { status } : {}
15452
- },
15453
- ...error61 ? { error: error61 } : {},
15454
- hint: customHint ?? hints?.[arm] ?? credentialHint(arm, prefix, hostLabel, state.source)
15455
- };
15456
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
15457
- });
15694
+ }, async () => runCredentialHealthcheck(args));
15458
15695
  }
15459
15696
  var CREDENTIAL_ARMS;
15460
15697
  var init_healthcheck = __esm({
@@ -15464,6 +15701,8 @@ var init_healthcheck = __esm({
15464
15701
  "ok",
15465
15702
  "no_credential",
15466
15703
  "credential_rejected",
15704
+ "session_expired",
15705
+ "verification_pending",
15467
15706
  "timeout",
15468
15707
  "http",
15469
15708
  "transport",
@@ -15485,6 +15724,7 @@ __export(fetchproxy_exports, {
15485
15724
  TokenBucket: () => TokenBucket,
15486
15725
  backoffDelayMs: () => backoffDelayMs,
15487
15726
  bridgeErrorInfo: () => bridgeErrorInfo,
15727
+ bridgeHealthcheckDescription: () => bridgeHealthcheckDescription,
15488
15728
  chunk: () => chunk,
15489
15729
  classifyBotWall: () => classifyBotWall,
15490
15730
  classifyBridgeError: () => classifyBridgeError,
@@ -15497,9 +15737,11 @@ __export(fetchproxy_exports, {
15497
15737
  extractImgTags: () => extractImgTags,
15498
15738
  lastPathSegment: () => lastPathSegment,
15499
15739
  mapWithConcurrency: () => mapWithConcurrency,
15740
+ registerAdaptiveHealthcheckTool: () => registerAdaptiveHealthcheckTool,
15500
15741
  registerBridgeHealthcheckTool: () => registerBridgeHealthcheckTool,
15501
15742
  registerCredentialHealthcheckTool: () => registerCredentialHealthcheckTool,
15502
15743
  retryOnceOnTimeout: () => retryOnceOnTimeout,
15744
+ runBridgeHealthcheck: () => runBridgeHealthcheck,
15503
15745
  sleep: () => sleep,
15504
15746
  withDeadline: () => withDeadline
15505
15747
  });
@@ -15535,9 +15777,9 @@ function createFetchproxyTransport(opts) {
15535
15777
  async start() {
15536
15778
  await server.listen();
15537
15779
  if (logListening) {
15538
- console.error(`[${serverOpts.serverName}:bridge] listening on 127.0.0.1:${server.bridgeHealth().port} (role=${server.role ?? "unknown"}, version=${serverOpts.version})`);
15780
+ console.error(`[${serverOpts.serverName}:bridge] ready \u2014 127.0.0.1:${server.bridgeHealth().port} binds on the first request (version=${serverOpts.version})`);
15539
15781
  } else if (debug) {
15540
- console.error(`[${serverOpts.serverName}:bridge] listening (role=${server.role ?? "unknown"}, version=${serverOpts.version})`);
15782
+ console.error(`[${serverOpts.serverName}:bridge] ready \u2014 binds on the first request (version=${serverOpts.version})`);
15541
15783
  }
15542
15784
  },
15543
15785
  async close() {
@@ -15716,13 +15958,122 @@ function projectBridgeStatus(health) {
15716
15958
  } : {}
15717
15959
  };
15718
15960
  }
15719
- function registerBridgeHealthcheckTool(args) {
15720
- const { server, prefix, probePath, hostLabel, probeFn, classifyThrown, hints, path } = args;
15721
- const probeUrl = `https://${hostLabel}${probePath}`;
15961
+ function probeDisplayUrl(hostLabel, probePath) {
15962
+ return `https://${hostLabel}${probePath.startsWith("/") ? "" : "/"}${probePath}`;
15963
+ }
15964
+ function bridgeHealthcheckDescription(hostLabel, probePath) {
15965
+ return `Round-trips a small public ${hostLabel} URL (${probePath}) through the fetchproxy bridge and returns diagnostics: the bridge's role (host/peer/null), port, version, the extension link (linked / pair pending / not attached / never answered), the elapsed round-trip time, and a plain-English hint distinguishing 'bridge never came up' from 'extension not connected' from 'real ${hostLabel}-side problem'. Read-only, no auth required.`;
15966
+ }
15967
+ async function runBridgeHealthcheck(args) {
15968
+ const { prefix, probePath, hostLabel, probeFn, classifyThrown, hints, path } = args;
15969
+ const probeUrl = probeDisplayUrl(hostLabel, probePath);
15722
15970
  const resolveTransport = () => typeof args.transport === "function" ? args.transport() : args.transport;
15723
- server.registerTool(`${prefix}_healthcheck`, {
15971
+ let probeBody = "";
15972
+ let thrown;
15973
+ const wrappedProbe = async (p) => {
15974
+ try {
15975
+ probeBody = await probeFn(p);
15976
+ return probeBody;
15977
+ } catch (e) {
15978
+ thrown = e;
15979
+ throw e;
15980
+ }
15981
+ };
15982
+ let ok;
15983
+ let elapsedMs;
15984
+ let bridge;
15985
+ let rawError;
15986
+ let pathNow;
15987
+ if (path) {
15988
+ const start = Date.now();
15989
+ try {
15990
+ await wrappedProbe(probePath);
15991
+ ok = true;
15992
+ } catch (e) {
15993
+ ok = false;
15994
+ rawError = { kind: classifyBridgeError(e), message: truncateErrorMessage(messageOf(e)) };
15995
+ }
15996
+ elapsedMs = Date.now() - start;
15997
+ pathNow = path();
15998
+ const transport2 = resolveTransport();
15999
+ bridge = transport2 ? projectBridgeStatus(transport2.status()) : void 0;
16000
+ } else {
16001
+ const transport2 = resolveTransport();
16002
+ if (!transport2) {
16003
+ throw new Error("bridge healthcheck: transport() returned nothing and no `path` was supplied \u2014 a bridge-only healthcheck needs its bridge.");
16004
+ }
16005
+ const probeResult = await transport2.runProbe(wrappedProbe, probePath);
16006
+ ok = probeResult.ok;
16007
+ elapsedMs = probeResult.elapsed_ms;
16008
+ bridge = {
16009
+ ...probeResult.bridge,
16010
+ last_extension_message_at: transport2.status().lastExtensionMessageAt
16011
+ };
16012
+ rawError = probeResult.error;
16013
+ }
16014
+ const probe = ok ? { url: probeUrl, elapsed_ms: elapsedMs, status: 200, body_length: probeBody.length } : { url: probeUrl, elapsed_ms: elapsedMs };
16015
+ let error61;
16016
+ let bridgeHint;
16017
+ let customHint;
16018
+ let customDetail;
16019
+ if (rawError) {
16020
+ let kind = rawError.kind === "other" ? "unknown" : rawError.kind;
16021
+ if (thrown instanceof FetchproxySessionNotReadyError) {
16022
+ kind = "session_not_ready";
16023
+ bridgeHint = thrown.hint;
16024
+ } else if (thrown instanceof FetchproxyBridgeDownError) {
16025
+ bridgeHint = thrown.hint;
16026
+ }
16027
+ if (thrown !== void 0 && classifyThrown) {
16028
+ const custom2 = classifyThrown(thrown);
16029
+ if (custom2) {
16030
+ kind = custom2.kind;
16031
+ customHint = custom2.hint;
16032
+ customDetail = custom2.detail;
16033
+ }
16034
+ }
16035
+ error61 = {
16036
+ kind,
16037
+ message: rawError.message,
16038
+ ...bridgeHint !== void 0 ? { bridge_hint: bridgeHint } : {},
16039
+ ...customDetail !== void 0 ? { detail: customDetail } : {}
16040
+ };
16041
+ }
16042
+ const direct = pathNow ? pathNow.transport === "direct" : bridge === void 0;
16043
+ const arm = ok ? "ok" : error61?.kind === "session_not_ready" ? "session_not_ready" : error61?.kind === "bridge_down" ? "bridge_down" : direct ? "direct" : bridge === void 0 || bridge.role === null ? "no_role" : error61?.kind === "timeout" ? "timeout" : error61?.kind === "protocol" || error61?.kind === "http" ? "protocol" : "unknown";
16044
+ const defaultHint = healthcheckHint({
16045
+ ok,
16046
+ role: bridge?.role ?? null,
16047
+ // The real configured port from bridgeHealth(), never a literal 37149.
16048
+ port: bridge?.port ?? null,
16049
+ hostLabel,
16050
+ prefix,
16051
+ probePath,
16052
+ errorKind: error61?.kind,
16053
+ bridgeHint: error61?.kind === "bridge_down" ? bridgeHint : void 0,
16054
+ session: {
16055
+ ...bridge?.session_state !== void 0 ? { state: bridge.session_state } : {},
16056
+ pairCode: bridge?.pending_pair_code ?? (thrown instanceof FetchproxySessionNotReadyError ? thrown.pairCode : null)
16057
+ },
16058
+ direct
16059
+ });
16060
+ const result = {
16061
+ ok,
16062
+ ...bridge ? { bridge } : {},
16063
+ ...pathNow ? { transport: pathNow } : {},
16064
+ probe,
16065
+ ...error61 ? { error: error61 } : {},
16066
+ // Precedence: classifyThrown's hint > per-arm override > default ladder.
16067
+ hint: customHint ?? hints?.[arm] ?? defaultHint
16068
+ };
16069
+ return {
16070
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
16071
+ };
16072
+ }
16073
+ function registerBridgeHealthcheckTool(args) {
16074
+ args.server.registerTool(`${args.prefix}_healthcheck`, {
15724
16075
  title: "Verify the fetchproxy bridge end-to-end",
15725
- description: `Round-trips a small public ${hostLabel} URL (${probePath}) through the fetchproxy bridge and returns diagnostics: the bridge's role (host/peer/null), port, version, the extension link (linked / pair pending / not attached / never answered), the elapsed round-trip time, and a plain-English hint distinguishing 'bridge never came up' from 'extension not connected' from 'real ${hostLabel}-side problem'. Call this when a real tool fails and you want to know which hop broke. Read-only, no auth required.`,
16076
+ description: `${bridgeHealthcheckDescription(args.hostLabel, args.probePath)} Call this when a real tool fails and you want to know which hop broke.`,
15726
16077
  annotations: {
15727
16078
  title: "Verify the fetchproxy bridge end-to-end",
15728
16079
  readOnlyHint: true,
@@ -15730,109 +16081,21 @@ function registerBridgeHealthcheckTool(args) {
15730
16081
  openWorldHint: true
15731
16082
  },
15732
16083
  inputSchema: {}
15733
- }, async () => {
15734
- let probeBody = "";
15735
- let thrown;
15736
- const wrappedProbe = async (p) => {
15737
- try {
15738
- probeBody = await probeFn(p);
15739
- return probeBody;
15740
- } catch (e) {
15741
- thrown = e;
15742
- throw e;
15743
- }
15744
- };
15745
- let ok;
15746
- let elapsedMs;
15747
- let bridge;
15748
- let rawError;
15749
- let pathNow;
15750
- if (path) {
15751
- const start = Date.now();
15752
- try {
15753
- await wrappedProbe(probePath);
15754
- ok = true;
15755
- } catch (e) {
15756
- ok = false;
15757
- rawError = { kind: classifyBridgeError(e), message: truncateErrorMessage(messageOf(e)) };
15758
- }
15759
- elapsedMs = Date.now() - start;
15760
- pathNow = path();
15761
- const transport2 = resolveTransport();
15762
- bridge = transport2 ? projectBridgeStatus(transport2.status()) : void 0;
15763
- } else {
15764
- const transport2 = resolveTransport();
15765
- if (!transport2) {
15766
- throw new Error("registerBridgeHealthcheckTool: transport() returned nothing and no `path` was supplied \u2014 a bridge-only healthcheck needs its bridge.");
15767
- }
15768
- const probeResult = await transport2.runProbe(wrappedProbe, probePath);
15769
- ok = probeResult.ok;
15770
- elapsedMs = probeResult.elapsed_ms;
15771
- bridge = {
15772
- ...probeResult.bridge,
15773
- last_extension_message_at: transport2.status().lastExtensionMessageAt
15774
- };
15775
- rawError = probeResult.error;
15776
- }
15777
- const probe = ok ? { url: probeUrl, elapsed_ms: elapsedMs, status: 200, body_length: probeBody.length } : { url: probeUrl, elapsed_ms: elapsedMs };
15778
- let error61;
15779
- let bridgeHint;
15780
- let customHint;
15781
- let customDetail;
15782
- if (rawError) {
15783
- let kind = rawError.kind === "other" ? "unknown" : rawError.kind;
15784
- if (thrown instanceof FetchproxySessionNotReadyError) {
15785
- kind = "session_not_ready";
15786
- bridgeHint = thrown.hint;
15787
- } else if (thrown instanceof FetchproxyBridgeDownError) {
15788
- bridgeHint = thrown.hint;
15789
- }
15790
- if (thrown !== void 0 && classifyThrown) {
15791
- const custom2 = classifyThrown(thrown);
15792
- if (custom2) {
15793
- kind = custom2.kind;
15794
- customHint = custom2.hint;
15795
- customDetail = custom2.detail;
15796
- }
15797
- }
15798
- error61 = {
15799
- kind,
15800
- message: rawError.message,
15801
- ...bridgeHint !== void 0 ? { bridge_hint: bridgeHint } : {},
15802
- ...customDetail !== void 0 ? { detail: customDetail } : {}
15803
- };
15804
- }
15805
- const direct = pathNow ? pathNow.transport === "direct" : bridge === void 0;
15806
- const arm = ok ? "ok" : error61?.kind === "session_not_ready" ? "session_not_ready" : error61?.kind === "bridge_down" ? "bridge_down" : direct ? "direct" : bridge === void 0 || bridge.role === null ? "no_role" : error61?.kind === "timeout" ? "timeout" : error61?.kind === "protocol" || error61?.kind === "http" ? "protocol" : "unknown";
15807
- const defaultHint = healthcheckHint({
15808
- ok,
15809
- role: bridge?.role ?? null,
15810
- // The real configured port from bridgeHealth(), never a literal 37149.
15811
- port: bridge?.port ?? null,
15812
- hostLabel,
15813
- prefix,
15814
- probePath,
15815
- errorKind: error61?.kind,
15816
- bridgeHint: error61?.kind === "bridge_down" ? bridgeHint : void 0,
15817
- session: {
15818
- ...bridge?.session_state !== void 0 ? { state: bridge.session_state } : {},
15819
- pairCode: bridge?.pending_pair_code ?? (thrown instanceof FetchproxySessionNotReadyError ? thrown.pairCode : null)
15820
- },
15821
- direct
15822
- });
15823
- const result = {
15824
- ok,
15825
- ...bridge ? { bridge } : {},
15826
- ...pathNow ? { transport: pathNow } : {},
15827
- probe,
15828
- ...error61 ? { error: error61 } : {},
15829
- // Precedence: classifyThrown's hint > per-arm override > default ladder.
15830
- hint: customHint ?? hints?.[arm] ?? defaultHint
15831
- };
15832
- return {
15833
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
15834
- };
15835
- });
16084
+ }, async () => runBridgeHealthcheck(args));
16085
+ }
16086
+ function registerAdaptiveHealthcheckTool(args) {
16087
+ const { server, prefix, hostLabel, usingBridge, bridge, credential } = args;
16088
+ server.registerTool(`${prefix}_healthcheck`, {
16089
+ title: "Verify this server can reach its upstream",
16090
+ description: `Reports which hop is broken when a real tool fails, for whichever path this server is actually using. Relaying through your signed-in browser tab: ${bridgeHealthcheckDescription(hostLabel, bridge.probePath)} Signing in server-side with configured credentials: ${credentialHealthcheckDescription(hostLabel)}`,
16091
+ annotations: {
16092
+ title: "Verify this server can reach its upstream",
16093
+ readOnlyHint: true,
16094
+ idempotentHint: true,
16095
+ openWorldHint: true
16096
+ },
16097
+ inputSchema: {}
16098
+ }, async () => usingBridge() ? runBridgeHealthcheck({ server, prefix, hostLabel, ...bridge }) : runCredentialHealthcheck({ server, prefix, hostLabel, ...credential }));
15836
16099
  }
15837
16100
  var PLACEHOLDER_RE2;
15838
16101
  var init_fetchproxy = __esm({
@@ -15841,6 +16104,7 @@ var init_fetchproxy = __esm({
15841
16104
  init_errors();
15842
16105
  init_dist2();
15843
16106
  init_healthcheck();
16107
+ init_healthcheck();
15844
16108
  PLACEHOLDER_RE2 = /^\$\{[^}]*\}$/;
15845
16109
  }
15846
16110
  });
@@ -44754,7 +45018,7 @@ var pageSchema = {
44754
45018
  init_errors();
44755
45019
 
44756
45020
  // src/version.ts
44757
- var VERSION = "0.3.0";
45021
+ var VERSION = "0.3.1";
44758
45022
 
44759
45023
  // src/client.ts
44760
45024
  import { dirname, join } from "path";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.3.0'; // x-release-please-version
1
+ export const VERSION = '0.3.1'; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrischall/eventbrite-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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>",
@@ -45,18 +45,18 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^0.23.2",
49
- "@fetchproxy/server": "^2.2.0",
50
- "@modelcontextprotocol/sdk": "^1.29.0",
48
+ "@chrischall/mcp-utils": "^0.26.1",
49
+ "@fetchproxy/server": "^2.10.0",
50
+ "@modelcontextprotocol/sdk": "^1.30.0",
51
51
  "dotenv": "^17.4.0",
52
- "zod": "^4.4.2"
52
+ "zod": "^4.5.4"
53
53
  },
54
54
  "devDependencies": {
55
55
  "@types/node": "^26.0.0",
56
- "@vitest/coverage-v8": "^4.1.2",
56
+ "@vitest/coverage-v8": "^5.0.0",
57
57
  "esbuild": "^0.28.0",
58
58
  "typescript": "^7.0.2",
59
- "vitest": "^4.1.2"
59
+ "vitest": "^5.0.0"
60
60
  },
61
61
  "allowScripts": {
62
62
  "esbuild@0.28.1": true
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.3.0",
9
+ "version": "0.3.1",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@chrischall/eventbrite-mcp",
14
- "version": "0.3.0",
14
+ "version": "0.3.1",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },