jefrichat-mcp 0.48.9 → 0.48.11

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.
Files changed (3) hide show
  1. package/dist/http.js +592 -89
  2. package/dist/index.js +163 -61
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -98,7 +98,7 @@ var require_depd = __commonJS({
98
98
  var site = callSiteLocation(stack[1]);
99
99
  var file = site[0];
100
100
  function deprecate(message) {
101
- log2.call(deprecate, message);
101
+ log3.call(deprecate, message);
102
102
  }
103
103
  deprecate._file = file;
104
104
  deprecate._ignored = isignored(namespace);
@@ -127,7 +127,7 @@ var require_depd = __commonJS({
127
127
  var str = process.env.TRACE_DEPRECATION || "";
128
128
  return containsNamespace(str, namespace);
129
129
  }
130
- function log2(message, site) {
130
+ function log3(message, site) {
131
131
  var haslisteners = eehaslisteners(process, "deprecation");
132
132
  if (!haslisteners && this._ignored) {
133
133
  return;
@@ -267,7 +267,7 @@ var require_depd = __commonJS({
267
267
  "message",
268
268
  "site",
269
269
  '"use strict"\nreturn function (' + args + ") {log.call(deprecate, message, site)\nreturn fn.apply(this, arguments)\n}"
270
- )(fn, log2, this, message, site);
270
+ )(fn, log3, this, message, site);
271
271
  return deprecatedfn;
272
272
  }
273
273
  function wrapproperty(obj, prop, message) {
@@ -292,13 +292,13 @@ var require_depd = __commonJS({
292
292
  var set = descriptor.set;
293
293
  if (typeof get === "function") {
294
294
  descriptor.get = function getter() {
295
- log2.call(deprecate, message, site);
295
+ log3.call(deprecate, message, site);
296
296
  return get.apply(this, arguments);
297
297
  };
298
298
  }
299
299
  if (typeof set === "function") {
300
300
  descriptor.set = function setter() {
301
- log2.call(deprecate, message, site);
301
+ log3.call(deprecate, message, site);
302
302
  return set.apply(this, arguments);
303
303
  };
304
304
  }
@@ -1135,7 +1135,7 @@ var require_debug = __commonJS({
1135
1135
  var require_browser = __commonJS({
1136
1136
  "../../node_modules/.pnpm/debug@2.6.9/node_modules/debug/src/browser.js"(exports, module) {
1137
1137
  exports = module.exports = require_debug();
1138
- exports.log = log2;
1138
+ exports.log = log3;
1139
1139
  exports.formatArgs = formatArgs;
1140
1140
  exports.save = save;
1141
1141
  exports.load = load;
@@ -1183,7 +1183,7 @@ var require_browser = __commonJS({
1183
1183
  });
1184
1184
  args.splice(lastC, 0, c);
1185
1185
  }
1186
- function log2() {
1186
+ function log3() {
1187
1187
  return "object" === typeof console && console.log && Function.prototype.apply.call(console.log, console, arguments);
1188
1188
  }
1189
1189
  function save(namespaces) {
@@ -1224,7 +1224,7 @@ var require_node = __commonJS({
1224
1224
  var util2 = __require("util");
1225
1225
  exports = module.exports = require_debug();
1226
1226
  exports.init = init;
1227
- exports.log = log2;
1227
+ exports.log = log3;
1228
1228
  exports.formatArgs = formatArgs;
1229
1229
  exports.save = save;
1230
1230
  exports.load = load;
@@ -1275,7 +1275,7 @@ var require_node = __commonJS({
1275
1275
  args[0] = (/* @__PURE__ */ new Date()).toUTCString() + " " + name + " " + args[0];
1276
1276
  }
1277
1277
  }
1278
- function log2() {
1278
+ function log3() {
1279
1279
  return stream.write(util2.format.apply(util2, arguments) + "\n");
1280
1280
  }
1281
1281
  function save(namespaces) {
@@ -27279,11 +27279,11 @@ var require_core = __commonJS({
27279
27279
  Ajv2.ValidationError = validation_error_1.default;
27280
27280
  Ajv2.MissingRefError = ref_error_1.default;
27281
27281
  exports.default = Ajv2;
27282
- function checkOptions(checkOpts, options, msg, log2 = "error") {
27282
+ function checkOptions(checkOpts, options, msg, log3 = "error") {
27283
27283
  for (const key in checkOpts) {
27284
27284
  const opt = key;
27285
27285
  if (opt in options)
27286
- this.logger[log2](`${msg}: option ${key}. ${checkOpts[opt]}`);
27286
+ this.logger[log3](`${msg}: option ${key}. ${checkOpts[opt]}`);
27287
27287
  }
27288
27288
  }
27289
27289
  function getSchEnv(keyRef) {
@@ -32012,7 +32012,7 @@ var require_websocket = __commonJS({
32012
32012
  var http2 = __require("http");
32013
32013
  var net = __require("net");
32014
32014
  var tls = __require("tls");
32015
- var { randomBytes, createHash: createHash2 } = __require("crypto");
32015
+ var { randomBytes, createHash: createHash3 } = __require("crypto");
32016
32016
  var { Duplex, Readable: Readable2 } = __require("stream");
32017
32017
  var { URL: URL2 } = __require("url");
32018
32018
  var PerMessageDeflate2 = require_permessage_deflate();
@@ -32680,7 +32680,7 @@ var require_websocket = __commonJS({
32680
32680
  abortHandshake(websocket, socket, "Invalid Upgrade header");
32681
32681
  return;
32682
32682
  }
32683
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
32683
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
32684
32684
  if (res.headers["sec-websocket-accept"] !== digest) {
32685
32685
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
32686
32686
  return;
@@ -33049,7 +33049,7 @@ var require_websocket_server = __commonJS({
33049
33049
  var EventEmitter = __require("events");
33050
33050
  var http2 = __require("http");
33051
33051
  var { Duplex } = __require("stream");
33052
- var { createHash: createHash2 } = __require("crypto");
33052
+ var { createHash: createHash3 } = __require("crypto");
33053
33053
  var extension3 = require_extension();
33054
33054
  var PerMessageDeflate2 = require_permessage_deflate();
33055
33055
  var subprotocol2 = require_subprotocol();
@@ -33356,7 +33356,7 @@ var require_websocket_server = __commonJS({
33356
33356
  );
33357
33357
  }
33358
33358
  if (this._state > RUNNING) return abortHandshake(socket, 503);
33359
- const digest = createHash2("sha1").update(key + GUID).digest("base64");
33359
+ const digest = createHash3("sha1").update(key + GUID).digest("base64");
33360
33360
  const headers = [
33361
33361
  "HTTP/1.1 101 Switching Protocols",
33362
33362
  "Upgrade: websocket",
@@ -33549,7 +33549,7 @@ for (const [k, v] of snapshot) {
33549
33549
 
33550
33550
  // src/http.ts
33551
33551
  import http from "node:http";
33552
- import { randomUUID } from "node:crypto";
33552
+ import { randomUUID as randomUUID3 } from "node:crypto";
33553
33553
 
33554
33554
  // src/session-auth.ts
33555
33555
  import { createHash, timingSafeEqual } from "node:crypto";
@@ -49051,7 +49051,7 @@ var wrapper_default = import_websocket.default;
49051
49051
  import fs from "node:fs";
49052
49052
  import os from "node:os";
49053
49053
  import path2 from "node:path";
49054
- import { webcrypto } from "node:crypto";
49054
+ import { webcrypto, randomUUID as randomUUID2 } from "node:crypto";
49055
49055
 
49056
49056
  // ../../node_modules/.pnpm/bail@2.0.2/node_modules/bail/index.js
49057
49057
  function bail(error2) {
@@ -60649,6 +60649,238 @@ function groupConversationId(groupId) {
60649
60649
  return "group:" + groupId;
60650
60650
  }
60651
60651
 
60652
+ // ../sdk/src/http-client.ts
60653
+ import { randomUUID } from "node:crypto";
60654
+ function idem(key) {
60655
+ return { "idempotency-key": key || `sdk-${randomUUID()}` };
60656
+ }
60657
+ var JefriHttpError = class extends Error {
60658
+ constructor(message, status, body) {
60659
+ super(message);
60660
+ this.status = status;
60661
+ this.body = body;
60662
+ this.name = "JefriHttpError";
60663
+ }
60664
+ status;
60665
+ body;
60666
+ /** The hub's machine-readable state field, when it sent one. */
60667
+ get state() {
60668
+ return this.body?.state;
60669
+ }
60670
+ };
60671
+ var UnsupportedOverHttp = class extends Error {
60672
+ constructor(what, why) {
60673
+ super(`${what} is not available without a live connection \u2014 ${why}`);
60674
+ this.name = "UnsupportedOverHttp";
60675
+ }
60676
+ };
60677
+ var DEFAULT_TIMEOUT_MS = 3e4;
60678
+ var JefriHttpClient = class {
60679
+ base;
60680
+ /** Discriminant for callers that accept either client: `stateless` is true
60681
+ * here and absent on the socket client, so shared code can branch without
60682
+ * instanceof (which breaks across duplicated package instances). */
60683
+ stateless = true;
60684
+ /** PUBLIC, like JefriClient.token: the connector's HTTP upload/download
60685
+ * helpers authenticate raw fetches with it. */
60686
+ token;
60687
+ f;
60688
+ timeoutMs;
60689
+ /** Populated by whoami(); mirrors JefriClient.identity for callers that read it. */
60690
+ identity = null;
60691
+ constructor(opts) {
60692
+ this.base = opts.server.replace(/\/+$/, "");
60693
+ this.token = opts.token;
60694
+ this.f = opts.fetchImpl ?? fetch;
60695
+ const t = Number(opts.timeoutMs);
60696
+ this.timeoutMs = Number.isFinite(t) && t > 0 ? t : DEFAULT_TIMEOUT_MS;
60697
+ }
60698
+ async call(method, path3, body, query, extraHeaders) {
60699
+ const url = new URL(this.base + path3);
60700
+ for (const [k, v] of Object.entries(query ?? {})) if (v !== void 0 && v !== "") url.searchParams.set(k, v);
60701
+ const ac = new AbortController();
60702
+ const timer = setTimeout(() => ac.abort(), this.timeoutMs);
60703
+ let res;
60704
+ try {
60705
+ res = await this.f(url.toString(), {
60706
+ method,
60707
+ signal: ac.signal,
60708
+ headers: {
60709
+ authorization: `Bearer ${this.token}`,
60710
+ ...body !== void 0 ? { "content-type": "application/json" } : {},
60711
+ ...extraHeaders ?? {}
60712
+ },
60713
+ ...body !== void 0 ? { body: JSON.stringify(body) } : {}
60714
+ });
60715
+ } catch (e) {
60716
+ clearTimeout(timer);
60717
+ if (e?.name === "AbortError") throw new Error(`${method} ${path3} timed out after ${this.timeoutMs}ms`);
60718
+ throw e;
60719
+ }
60720
+ let text5;
60721
+ try {
60722
+ text5 = await res.text();
60723
+ } catch (e) {
60724
+ if (e?.name === "AbortError") throw new Error(`${method} ${path3} timed out after ${this.timeoutMs}ms reading the body`);
60725
+ throw e;
60726
+ } finally {
60727
+ clearTimeout(timer);
60728
+ }
60729
+ let parsed = null;
60730
+ try {
60731
+ parsed = text5 ? JSON.parse(text5) : null;
60732
+ } catch {
60733
+ }
60734
+ if (!res.ok) {
60735
+ throw new JefriHttpError(parsed?.error ?? `${method} ${path3} failed with ${res.status}`, res.status, parsed);
60736
+ }
60737
+ return parsed;
60738
+ }
60739
+ // --- identity ------------------------------------------------------------
60740
+ /** /api/me answers `{ identity, online }`, NOT a bare Identity — unwrapping is
60741
+ * the caller's job and getting it wrong is silent: every field reads as
60742
+ * undefined and the object still looks like an Identity. Caught by running
60743
+ * this against the real hub; a stubbed test would have returned whatever
60744
+ * shape the stub was written to expect. */
60745
+ async whoami() {
60746
+ const body = await this.call("GET", "/api/me");
60747
+ const me = body?.identity ?? body;
60748
+ if (!me?.username) throw new Error("unexpected /api/me response shape");
60749
+ this.identity = me;
60750
+ return me;
60751
+ }
60752
+ // --- reads ---------------------------------------------------------------
60753
+ identities() {
60754
+ return this.call("GET", "/api/identities");
60755
+ }
60756
+ groups() {
60757
+ return this.call("GET", "/api/groups");
60758
+ }
60759
+ inbox(since) {
60760
+ return this.call("GET", "/api/inbox", void 0, { since });
60761
+ }
60762
+ search(query) {
60763
+ return this.call("GET", "/api/search", void 0, { q: query });
60764
+ }
60765
+ history(conversationId, before) {
60766
+ return this.call("GET", "/api/history", void 0, { conversationId, before });
60767
+ }
60768
+ // --- writes --------------------------------------------------------------
60769
+ /** Sends carry an idempotency key.
60770
+ *
60771
+ * The hub persists the key in the SAME transaction as the message, so a retry
60772
+ * is exactly-once rather than at-least-once. This matters far more for a
60773
+ * stateless client than a socket one: a timed-out or connection-reset POST
60774
+ * gives no evidence of whether the send landed, and the natural response — retry
60775
+ * — is precisely what produces duplicates during a deployment.
60776
+ *
60777
+ * Callers may pass their own key when a retry must reuse it; otherwise one is
60778
+ * minted per call. */
60779
+ message(to, content3, idempotencyKey) {
60780
+ return this.call("POST", "/api/messages", { to, content: content3 }, void 0, idem(idempotencyKey));
60781
+ }
60782
+ groupMessage(groupId, content3, idempotencyKey) {
60783
+ return this.call("POST", "/api/messages", { groupId, content: content3 }, void 0, idem(idempotencyKey));
60784
+ }
60785
+ /** DECLARE a status — a user-level choice that hits every connection the
60786
+ * identity holds. The returned status is the SETTLED one, which may differ
60787
+ * from the request if another lease outvoted it. */
60788
+ /** `declId` (optional) makes the declaration retry-safe: a re-POST of the
60789
+ * same id replays — answering the CURRENT settled state — instead of
60790
+ * reapplying, so a stale retry can never overwrite a status another device
60791
+ * chose in between. Reusing an id with a DIFFERENT status is a 409. */
60792
+ presence(status, declId) {
60793
+ return this.call("POST", "/api/presence", declId ? { status, declId } : { status });
60794
+ }
60795
+ /** KEEPALIVE — "still here", declaring nothing. This is the call for a timer.
60796
+ * Heartbeating through presence("online") instead is a declaration on a
60797
+ * timer, and it silently undoes an appear-offline chosen elsewhere.
60798
+ *
60799
+ * REJECTS (409, "signed off") when the identity has signed off: the beat was
60800
+ * refused and no lease exists, so the timer should STOP and the client must
60801
+ * declare a presence to resume. The refusal used to arrive as ok:true with a
60802
+ * full TTL — a lease lifetime that did not exist. */
60803
+ async heartbeat() {
60804
+ try {
60805
+ return await this.call("POST", "/api/presence", { keepalive: true });
60806
+ } catch (e) {
60807
+ if (e instanceof JefriHttpError && e.status === 409 && e.state === "signedOff")
60808
+ return { ok: false, state: "signedOff", status: String(e.body?.status ?? "offline"), expiresInMs: 0 };
60809
+ throw e;
60810
+ }
60811
+ }
60812
+ addFriend(to) {
60813
+ return this.call("POST", "/api/friends/request", { to });
60814
+ }
60815
+ /** Upload and deliver a file. The hub's raw-body route takes the bytes
60816
+ * directly, so there is no base64 round-trip. */
60817
+ async sendFile(target, fileName, mime, bytes, idempotencyKey) {
60818
+ const url = new URL(this.base + "/api/files");
60819
+ if (target.to) url.searchParams.set("to", target.to);
60820
+ if (target.groupId) url.searchParams.set("groupId", target.groupId);
60821
+ url.searchParams.set("fileName", fileName);
60822
+ if (target.caption) url.searchParams.set("caption", target.caption);
60823
+ const uploadTimeout = Math.max(this.timeoutMs, 3e4 + bytes.length / 20);
60824
+ const ac = new AbortController();
60825
+ const timer = setTimeout(() => ac.abort(), uploadTimeout);
60826
+ let res;
60827
+ try {
60828
+ res = await this.f(url.toString(), {
60829
+ method: "POST",
60830
+ signal: ac.signal,
60831
+ headers: {
60832
+ authorization: `Bearer ${this.token}`,
60833
+ "content-type": mime || "application/octet-stream",
60834
+ ...idem(idempotencyKey)
60835
+ },
60836
+ body: bytes
60837
+ });
60838
+ } catch (e) {
60839
+ clearTimeout(timer);
60840
+ if (e?.name === "AbortError") throw new Error(`file upload timed out after ${Math.round(uploadTimeout)}ms`);
60841
+ throw e;
60842
+ }
60843
+ let text5;
60844
+ try {
60845
+ text5 = await res.text();
60846
+ } catch (e) {
60847
+ if (e?.name === "AbortError") throw new Error(`file upload timed out after ${Math.round(uploadTimeout)}ms reading the response`);
60848
+ throw e;
60849
+ } finally {
60850
+ clearTimeout(timer);
60851
+ }
60852
+ if (!res.ok) {
60853
+ let msg = `upload failed with ${res.status}`;
60854
+ try {
60855
+ msg = JSON.parse(text5)?.error ?? msg;
60856
+ } catch {
60857
+ }
60858
+ throw new Error(msg);
60859
+ }
60860
+ return text5 ? JSON.parse(text5) : null;
60861
+ }
60862
+ // --- deliberately unsupported -------------------------------------------
60863
+ /** Realtime delivery needs a socket. A stateless caller polls /api/inbox. */
60864
+ on() {
60865
+ throw new UnsupportedOverHttp("Realtime events", "this client holds no socket; poll the inbox instead");
60866
+ }
60867
+ off() {
60868
+ throw new UnsupportedOverHttp("Realtime events", "this client holds no socket; poll the inbox instead");
60869
+ }
60870
+ /** E2E sends are excluded until key custody is durable — see the file header.
60871
+ * Throwing names the reason so the next person does not "fix" it by wiring the
60872
+ * call through and shipping unreadable ciphertext. */
60873
+ privateMessage() {
60874
+ throw new UnsupportedOverHttp("End-to-end encrypted send", "private keys are not durably held; use the stateful connector");
60875
+ }
60876
+ privateGroupMessage() {
60877
+ throw new UnsupportedOverHttp("End-to-end encrypted send", "private keys are not durably held; use the stateful connector");
60878
+ }
60879
+ /** Nothing to close — kept so callers can treat both clients alike. */
60880
+ close() {
60881
+ }
60882
+ };
60883
+
60652
60884
  // ../sdk/src/index.ts
60653
60885
  var subtle = webcrypto.subtle;
60654
60886
  var enc = new TextEncoder();
@@ -60659,6 +60891,9 @@ var b64 = (bytes) => Buffer.from(bytes instanceof Uint8Array ? bytes : new Uint8
60659
60891
  var fromB64 = (s) => new Uint8Array(Buffer.from(s, "base64"));
60660
60892
  var safeName = (s) => s.replace(/[^a-zA-Z0-9_.-]/g, "_");
60661
60893
  var canonicalJwk = (jwk) => JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
60894
+ function newDeclId() {
60895
+ return "d-" + randomUUID2();
60896
+ }
60662
60897
  var JefriAuthError = class extends Error {
60663
60898
  code = "ACP_AUTH_FAILED";
60664
60899
  constructor(message = "invalid or expired Jefri Chat token") {
@@ -60693,6 +60928,9 @@ var JefriClient = class _JefriClient {
60693
60928
  * life of the process, stomping whatever a newer device had chosen since.
60694
60929
  * The same stale-overwrite, one level down. */
60695
60930
  pendingPresence = null;
60931
+ /** The id minted for the pending declaration. Its ECHO on a presence_update
60932
+ * is the only genuine acknowledgment — see notePresenceAck. */
60933
+ pendingDeclId = null;
60696
60934
  /** The self statusRev BASELINE captured when the pending declaration was made.
60697
60935
  * An ack must be CAUSALLY newer than this — the audit reproduced a delayed
60698
60936
  * rev-9 event spending intent declared at a rev-10 baseline, after which the
@@ -60718,11 +60956,12 @@ var JefriClient = class _JefriClient {
60718
60956
  * deleted, which is how the previous round's test enshrined the bug. */
60719
60957
  notePresenceAck(ev) {
60720
60958
  if (ev?.event !== "presence_update" || ev.username !== this.identity?.username) return;
60721
- const frontier = this.selfStatusRev;
60722
60959
  if (typeof ev.rev === "number" && ev.rev > this.selfStatusRev) this.selfStatusRev = ev.rev;
60723
- if (this.pendingPresence === null || ev.status !== this.pendingPresence) return;
60724
- if (typeof ev.rev !== "number" || ev.rev <= this.pendingBaselineRev || ev.rev < frontier) return;
60725
- this.pendingPresence = null;
60960
+ if (this.pendingPresence === null) return;
60961
+ if (this.pendingDeclId !== null && ev.declId === this.pendingDeclId) {
60962
+ this.pendingPresence = null;
60963
+ this.pendingDeclId = null;
60964
+ }
60726
60965
  }
60727
60966
  clientInfo;
60728
60967
  // handshake, re-sent on reconnect
@@ -60819,7 +61058,7 @@ var JefriClient = class _JefriClient {
60819
61058
  this.startPing();
60820
61059
  try {
60821
61060
  if (this.pendingPresence !== null)
60822
- this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence }));
61061
+ this.ws.send(JSON.stringify({ type: "presence", status: this.pendingPresence, declId: this.pendingDeclId ?? void 0 }));
60823
61062
  if (this.clientInfo)
60824
61063
  this.ws.send(JSON.stringify({ type: "client_info", ...this.clientInfo }));
60825
61064
  } catch {
@@ -60947,7 +61186,8 @@ var JefriClient = class _JefriClient {
60947
61186
  presence(status) {
60948
61187
  this.pendingPresence = status;
60949
61188
  this.pendingBaselineRev = this.selfStatusRev;
60950
- this.send({ type: "presence", status });
61189
+ this.pendingDeclId = newDeclId();
61190
+ this.send({ type: "presence", status, declId: this.pendingDeclId });
60951
61191
  }
60952
61192
  message(to, content3) {
60953
61193
  this.send({ type: "message", to, content: content3 });
@@ -61277,6 +61517,96 @@ var JefriClient = class _JefriClient {
61277
61517
  };
61278
61518
  var serveDir = path2.join(os.homedir(), ".acp", "serve");
61279
61519
 
61520
+ // src/stateless-presence.ts
61521
+ var log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
61522
+ async function statelessPresenceBringUp(acp) {
61523
+ try {
61524
+ const hb = await acp.heartbeat();
61525
+ if (hb.state === "signedOff") await acp.presence("online");
61526
+ return true;
61527
+ } catch (e) {
61528
+ log("stateless presence bring-up failed (the sweep retries it):", e?.message ?? e);
61529
+ return false;
61530
+ }
61531
+ }
61532
+ async function statelessSweepBeat(acp, pool, onAuthRevoked) {
61533
+ if (pool.closed || pool.hbSignedOff) return;
61534
+ try {
61535
+ const hb = await acp.heartbeat();
61536
+ if (hb.state === "signedOff") pool.hbSignedOff = true;
61537
+ } catch (e) {
61538
+ if (e instanceof JefriHttpError && (e.status === 401 || e.status === 403)) {
61539
+ onAuthRevoked();
61540
+ return;
61541
+ }
61542
+ }
61543
+ }
61544
+
61545
+ // src/stateless-mcp.ts
61546
+ var rpcError = (res, status, code3, message) => res.status(status).json({ jsonrpc: "2.0", error: { code: code3, message }, id: null });
61547
+ function statelessMcpHandler(deps) {
61548
+ return async (req, res) => {
61549
+ if (deps.isDraining()) {
61550
+ res.setHeader("retry-after", "1");
61551
+ return rpcError(res, 503, -32e3, "server is shutting down \u2014 retry; another instance will answer");
61552
+ }
61553
+ const token = deps.bearer(req);
61554
+ if (!token)
61555
+ return rpcError(res, 401, -32001, "Missing Jefri Chat token \u2014 every request must include Authorization: Bearer <token> (or ?token=).");
61556
+ if (!deps.tryAcquireInflight(token))
61557
+ return rpcError(res, 429, -32003, "too many concurrent requests \u2014 slow down.");
61558
+ try {
61559
+ let acp;
61560
+ try {
61561
+ ({ acp } = await deps.acquire(token));
61562
+ } catch (e) {
61563
+ if (deps.classifyError(e) === "auth")
61564
+ return rpcError(
61565
+ res,
61566
+ 401,
61567
+ -32001,
61568
+ "Invalid or expired Jefri Chat token. Rotate it in the Jefri Chat web app (the agent's owner \u2192 \u{1F511} connect) and reconnect with the fresh command."
61569
+ );
61570
+ return rpcError(res, 502, -32002, `Hub unreachable: ${e?.message ?? e}`);
61571
+ }
61572
+ const mcp = new McpServer({ name: "jefrichat", version: "0.1.0" });
61573
+ const deferred = [];
61574
+ deps.registerTools(mcp, acp, { deferUntilDelivered: (fn) => deferred.push(fn) });
61575
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
61576
+ res.on("finish", () => {
61577
+ for (const fn of deferred) {
61578
+ try {
61579
+ void Promise.resolve(fn()).catch((e) => deps.log?.("deferred post-delivery work failed:", e?.message ?? e));
61580
+ } catch (e) {
61581
+ deps.log?.("deferred post-delivery work failed:", e?.message ?? e);
61582
+ }
61583
+ }
61584
+ deferred.length = 0;
61585
+ });
61586
+ res.on("close", () => {
61587
+ try {
61588
+ transport.close();
61589
+ } catch {
61590
+ }
61591
+ try {
61592
+ void mcp.close();
61593
+ } catch {
61594
+ }
61595
+ });
61596
+ await mcp.connect(transport);
61597
+ await transport.handleRequest(req, res, req.body);
61598
+ } catch (e) {
61599
+ deps.log?.("stateless request failed:", e?.message ?? e);
61600
+ if (!res.headersSent) rpcError(res, 500, -32603, "internal error handling the request");
61601
+ } finally {
61602
+ deps.releaseInflight(token);
61603
+ }
61604
+ };
61605
+ }
61606
+ function statelessMethodNotAllowed(_req, res) {
61607
+ res.status(405).json({ jsonrpc: "2.0", error: { code: -32e3, message: "stateless server: POST only (no SSE stream, no session to terminate)" }, id: null });
61608
+ }
61609
+
61280
61610
  // src/conn-pool.ts
61281
61611
  function attachRef(c, id) {
61282
61612
  if (c.closed) return false;
@@ -61315,7 +61645,8 @@ function createKeyedPool() {
61315
61645
  return v;
61316
61646
  },
61317
61647
  has: (key) => ready.has(key),
61318
- size: () => ready.size
61648
+ size: () => ready.size,
61649
+ keys: () => [...ready.keys()]
61319
61650
  };
61320
61651
  }
61321
61652
 
@@ -61343,6 +61674,7 @@ import cp3 from "node:child_process";
61343
61674
  import os4 from "node:os";
61344
61675
  import nnet from "node:net";
61345
61676
  import ndns from "node:dns/promises";
61677
+ import { createHash as createHash2 } from "node:crypto";
61346
61678
  import nhttp from "node:http";
61347
61679
  import nhttps from "node:https";
61348
61680
 
@@ -61750,6 +62082,35 @@ var debateJobs = /* @__PURE__ */ new Map();
61750
62082
  var DEBATE_SEQ_TTL_MS = 10 * 60 * 1e3;
61751
62083
 
61752
62084
  // src/tools.ts
62085
+ var isStateless = (c) => c.stateless === true;
62086
+ async function confirmWrite(c, fire) {
62087
+ if (isStateless(c)) {
62088
+ try {
62089
+ await fire();
62090
+ return {};
62091
+ } catch (e2) {
62092
+ return { error: e2?.message ?? String(e2) };
62093
+ }
62094
+ }
62095
+ const err = waitFor(c, "error", () => true, 800);
62096
+ fire();
62097
+ const e = await err;
62098
+ return e ? { error: e.message } : {};
62099
+ }
62100
+ function retryKey(extra, parts) {
62101
+ const rid = extra?.requestId;
62102
+ if (rid === void 0 || rid === null) return void 0;
62103
+ const h = extra?.requestInfo?.headers;
62104
+ const ua = String((typeof h?.get === "function" ? h.get("user-agent") : h?.["user-agent"]) ?? "");
62105
+ const bucket = Math.floor(Date.now() / 6e5);
62106
+ return "mcp-" + createHash2("sha256").update(JSON.stringify([bucket, ua, rid, ...parts])).digest("hex").slice(0, 40);
62107
+ }
62108
+ async function fetchHistory(c, convId) {
62109
+ if (isStateless(c)) return c.history(convId);
62110
+ const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
62111
+ c.history(convId);
62112
+ return p;
62113
+ }
61753
62114
  var MIME = {
61754
62115
  ".jpg": "image/jpeg",
61755
62116
  ".jpeg": "image/jpeg",
@@ -62085,7 +62446,7 @@ function saveWatermark() {
62085
62446
  }
62086
62447
  function registerAcpTools(server2, ctx) {
62087
62448
  const ENABLE_E2E = process.env.JEFRI_ENABLE_E2E === "true";
62088
- const e2eServer = ENABLE_E2E ? server2 : { registerTool: () => void 0 };
62449
+ const e2eServer = ENABLE_E2E && !ctx.stateless ? server2 : { registerTool: () => void 0 };
62089
62450
  const withClient = async (fn) => {
62090
62451
  try {
62091
62452
  const c = await ctx.ensureClient();
@@ -62202,11 +62563,12 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62202
62563
  text: external_exports.string().describe("the message body")
62203
62564
  }
62204
62565
  },
62205
- async ({ to, text: text5 }) => withClient(async (c) => {
62206
- const err = waitFor(c, "error", () => true, 800);
62207
- c.message(to, text5);
62208
- const e = await err;
62209
- if (e) return fail(`Message rejected: ${e.message}`);
62566
+ async ({ to, text: text5 }, extra) => withClient(async (c) => {
62567
+ const r = await confirmWrite(
62568
+ c,
62569
+ () => isStateless(c) ? c.message(to, text5, retryKey(extra, ["message", to, text5])) : c.message(to, text5)
62570
+ );
62571
+ if (r.error) return fail(`Message rejected: ${r.error}`);
62210
62572
  return ok3(`Sent to @${to}: "${text5}"`);
62211
62573
  })
62212
62574
  );
@@ -62221,8 +62583,9 @@ Use jefri_agent_apps with agent=<name> to see its live app + active state.`);
62221
62583
  }
62222
62584
  },
62223
62585
  async ({ to, text: text5 }) => withClient(async (c) => {
62224
- const err = waitFor(c, "error", () => true, 800);
62225
- await c.privateMessage(to, text5);
62586
+ const sc = c;
62587
+ const err = waitFor(sc, "error", () => true, 800);
62588
+ await sc.privateMessage(to, text5);
62226
62589
  const e = await err;
62227
62590
  if (e) return fail(`Private message rejected: ${e.message}`);
62228
62591
  return ok3(`Sent private E2E message to @${to}.`);
@@ -62279,8 +62642,9 @@ ${fp}`);
62279
62642
  } else {
62280
62643
  return fail("provide a local file `path` (or `dataUrl` + `fileName`)");
62281
62644
  }
62282
- await c.privateFile(to, name, mime, url);
62283
- if (caption) await c.privateMessage(to, caption);
62645
+ const sc = c;
62646
+ await sc.privateFile(to, name, mime, url);
62647
+ if (caption) await sc.privateMessage(to, caption);
62284
62648
  return ok3(`Sent private E2E file to @${to}.`);
62285
62649
  })
62286
62650
  );
@@ -62295,8 +62659,9 @@ ${fp}`);
62295
62659
  }
62296
62660
  },
62297
62661
  async ({ groupId, text: text5 }) => withClient(async (c) => {
62298
- const err = waitFor(c, "error", () => true, 800);
62299
- await c.privateGroupMessage(groupId, text5);
62662
+ const sc = c;
62663
+ const err = waitFor(sc, "error", () => true, 800);
62664
+ await sc.privateGroupMessage(groupId, text5);
62300
62665
  const e = await err;
62301
62666
  if (e) return fail(`Private group message rejected: ${e.message}`);
62302
62667
  return ok3(`Sent private E2E group message.`);
@@ -62359,7 +62724,7 @@ ${fp}`);
62359
62724
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
62360
62725
  }
62361
62726
  },
62362
- async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
62727
+ async ({ to, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
62363
62728
  let name, mime, url;
62364
62729
  if (path3) {
62365
62730
  if (!ctx.local) {
@@ -62411,9 +62776,23 @@ ${psCmd}
62411
62776
  return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
62412
62777
  }
62413
62778
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
62779
+ if (rawBytes.length > MAX_UPLOAD_BYTES)
62780
+ return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62781
+ if (isStateless(c)) {
62782
+ try {
62783
+ await c.sendFile(
62784
+ { to, caption },
62785
+ name,
62786
+ mime,
62787
+ rawBytes,
62788
+ retryKey(extra, ["file", to, name, createHash2("sha256").update(rawBytes).digest("hex")])
62789
+ );
62790
+ } catch (e2) {
62791
+ return fail(`Rejected: ${e2?.message ?? e2}`);
62792
+ }
62793
+ return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
62794
+ }
62414
62795
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
62415
- if (rawBytes.length > MAX_UPLOAD_BYTES)
62416
- return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62417
62796
  try {
62418
62797
  await uploadFileHttp(c, ctx.serverUrl, { to }, name, mime, rawBytes, caption);
62419
62798
  } catch (e2) {
@@ -62441,13 +62820,27 @@ ${psCmd}
62441
62820
  caption: external_exports.string().optional()
62442
62821
  }
62443
62822
  },
62444
- async ({ to, fileName, content: content3, caption }) => withClient(async (c) => {
62823
+ async ({ to, fileName, content: content3, caption }, extra) => withClient(async (c) => {
62445
62824
  const known = mimeOf(fileName);
62446
62825
  const mime = known === "application/octet-stream" ? "text/plain" : known;
62447
62826
  const contentBytes = Buffer.from(content3, "utf8");
62827
+ if (contentBytes.length > MAX_UPLOAD_BYTES)
62828
+ return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62829
+ if (isStateless(c)) {
62830
+ try {
62831
+ await c.sendFile(
62832
+ { to, caption },
62833
+ fileName,
62834
+ mime,
62835
+ contentBytes,
62836
+ retryKey(extra, ["file", to, fileName, createHash2("sha256").update(contentBytes).digest("hex")])
62837
+ );
62838
+ } catch (e2) {
62839
+ return fail(`Rejected: ${e2?.message ?? e2}`);
62840
+ }
62841
+ return ok3(`Sent \u{1F4C4} ${fileName} (${MB(contentBytes.length)} MB) to @${to}${caption ? ` \u2014 "${caption}"` : ""}`);
62842
+ }
62448
62843
  if (contentBytes.length > WS_SAFE_FILE_BYTES) {
62449
- if (contentBytes.length > MAX_UPLOAD_BYTES)
62450
- return fail(`That content is ${MB(contentBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62451
62844
  try {
62452
62845
  await uploadFileHttp(c, ctx.serverUrl, { to }, fileName, mime, contentBytes, caption);
62453
62846
  } catch (e2) {
@@ -62578,9 +62971,7 @@ ${psCmd}
62578
62971
  if (!from && !groupId) return fail("Give `from` (a username, for a DM) or `groupId` (for a group file).");
62579
62972
  const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, from);
62580
62973
  const where = groupId ? "this group" : `@${from}`;
62581
- const p = waitFor(c, "history", (e) => e.conversationId === convId, 4e3);
62582
- c.history(convId);
62583
- const res = await p;
62974
+ const res = await fetchHistory(c, convId);
62584
62975
  const files = (res?.messages ?? []).filter((m) => m.kind === "file");
62585
62976
  if (!files.length) return fail(`No files in ${where} yet.`);
62586
62977
  const match = fileName ? [...files].reverse().find((m) => m.fileName === fileName || m.fileName?.includes(fileName)) : files[files.length - 1];
@@ -62617,10 +63008,8 @@ It downloads the file to your machine (current folder unless you set a path). Fi
62617
63008
  inputSchema: { to: external_exports.string().describe("username to connect with") }
62618
63009
  },
62619
63010
  async ({ to }) => withClient(async (c) => {
62620
- const err = waitFor(c, "error", () => true, 800);
62621
- c.addFriend(to);
62622
- const e = await err;
62623
- if (e) return fail(`Request rejected: ${e.message}`);
63011
+ const r = await confirmWrite(c, () => c.addFriend(to));
63012
+ if (r.error) return fail(`Request rejected: ${r.error}`);
62624
63013
  return ok3(`\u{1F91D} Connection request sent to @${to}. They (or their owner) must approve before you can message.`);
62625
63014
  })
62626
63015
  );
@@ -62842,7 +63231,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62842
63231
  },
62843
63232
  async () => withClient(async (c) => {
62844
63233
  const me = c.identity.username;
62845
- const since = inboxWatermark.get(me);
63234
+ const since = ctx.stateless ? "stored" : inboxWatermark.get(me);
62846
63235
  const url = `${ctx.serverUrl.replace(/\/$/, "")}/api/inbox` + (since ? `?since=${encodeURIComponent(since)}` : "");
62847
63236
  let msgs = [];
62848
63237
  try {
@@ -62856,8 +63245,24 @@ Or just upload ${fname} from the agent's page in the web app.`
62856
63245
  }
62857
63246
  if (!msgs.length) return ok3("\u{1F4ED} No new messages.");
62858
63247
  msgs.sort((a, b) => a.createdAt < b.createdAt ? -1 : 1);
62859
- inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
62860
- saveWatermark();
63248
+ if (ctx.stateless) {
63249
+ const last = msgs[msgs.length - 1];
63250
+ const ack = async () => {
63251
+ try {
63252
+ await fetch(`${ctx.serverUrl.replace(/\/$/, "")}/api/inbox/seen`, {
63253
+ method: "POST",
63254
+ headers: { authorization: `Bearer ${c.token}`, "content-type": "application/json" },
63255
+ body: JSON.stringify({ until: last.createdAt, untilId: last.id })
63256
+ });
63257
+ } catch {
63258
+ }
63259
+ };
63260
+ if (ctx.deferUntilDelivered) ctx.deferUntilDelivered(ack);
63261
+ else await ack();
63262
+ } else {
63263
+ inboxWatermark.set(me, msgs[msgs.length - 1].createdAt);
63264
+ saveWatermark();
63265
+ }
62861
63266
  const groupNames = /* @__PURE__ */ new Map();
62862
63267
  if (msgs.some((m) => m.groupId)) {
62863
63268
  try {
@@ -62915,11 +63320,12 @@ Or just upload ${fname} from the agent's page in the web app.`
62915
63320
  text: external_exports.string().describe("the message to post to the group")
62916
63321
  }
62917
63322
  },
62918
- async ({ groupId, text: text5 }) => withClient(async (c) => {
62919
- const err = waitFor(c, "error", () => true, 800);
62920
- c.groupMessage(groupId, text5);
62921
- const e = await err;
62922
- if (e) return fail(`Couldn't post to the group: ${e.message}`);
63323
+ async ({ groupId, text: text5 }, extra) => withClient(async (c) => {
63324
+ const r = await confirmWrite(
63325
+ c,
63326
+ () => isStateless(c) ? c.groupMessage(groupId, text5, retryKey(extra, ["group_message", groupId, text5])) : c.groupMessage(groupId, text5)
63327
+ );
63328
+ if (r.error) return fail(`Couldn't post to the group: ${r.error}`);
62923
63329
  return ok3(`Posted to the group.`);
62924
63330
  })
62925
63331
  );
@@ -62937,7 +63343,7 @@ Or just upload ${fname} from the agent's page in the web app.`
62937
63343
  fileName: external_exports.string().optional().describe("file name (used with dataUrl, or to name a fileUrl download)")
62938
63344
  }
62939
63345
  },
62940
- async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }) => withClient(async (c) => {
63346
+ async ({ groupId, path: path3, caption, dataUrl, fileUrl, fileName }, extra) => withClient(async (c) => {
62941
63347
  let name, mime, url;
62942
63348
  if (path3) {
62943
63349
  if (!ctx.local) {
@@ -62979,9 +63385,23 @@ Or just upload ${fname} from the agent's page in the web app.`
62979
63385
  return fail("provide a local file `path`, a `dataUrl` + `fileName`, or a public `fileUrl`");
62980
63386
  }
62981
63387
  const rawBytes = Buffer.from(url.split(",")[1] ?? "", "base64");
63388
+ if (rawBytes.length > MAX_UPLOAD_BYTES)
63389
+ return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
63390
+ if (isStateless(c)) {
63391
+ try {
63392
+ await c.sendFile(
63393
+ { groupId, caption },
63394
+ name,
63395
+ mime,
63396
+ rawBytes,
63397
+ retryKey(extra, ["file", groupId, name, createHash2("sha256").update(rawBytes).digest("hex")])
63398
+ );
63399
+ } catch (e2) {
63400
+ return fail(`Rejected: ${e2?.message ?? e2}`);
63401
+ }
63402
+ return ok3(`Sent \u{1F4CE} ${name} (${MB(rawBytes.length)} MB) to the group.`);
63403
+ }
62982
63404
  if (rawBytes.length > WS_SAFE_FILE_BYTES) {
62983
- if (rawBytes.length > MAX_UPLOAD_BYTES)
62984
- return fail(`${name} is ${MB(rawBytes.length)} MB, over the ${Math.round(MAX_UPLOAD_BYTES / 1024 / 1024)} MB limit.`);
62985
63405
  try {
62986
63406
  await uploadFileHttp(c, ctx.serverUrl, { groupId }, name, mime, rawBytes, caption);
62987
63407
  } catch (e2) {
@@ -63012,9 +63432,7 @@ Or just upload ${fname} from the agent's page in the web app.`
63012
63432
  if (!other && !groupId) return fail("Give `with` (a username, for a DM) or `groupId` (for a group).");
63013
63433
  const convId = groupId ? groupConversationId(groupId) : dmConversationId(c.identity.username, other);
63014
63434
  const label = groupId ? "this group" : `@${other}`;
63015
- const p = waitFor(c, "history", (e) => e.conversationId === convId);
63016
- c.history(convId);
63017
- const res = await p;
63435
+ const res = await fetchHistory(c, convId);
63018
63436
  const msgs = (res?.messages ?? []).slice(-(limit ?? 20));
63019
63437
  if (!msgs.length) return ok3(`No messages yet in ${label}.`);
63020
63438
  const lines = await Promise.all(msgs.map(async (m) => {
@@ -63045,10 +63463,14 @@ Or just upload ${fname} from the agent's page in the web app.`
63045
63463
  inputSchema: { query: external_exports.string() }
63046
63464
  },
63047
63465
  async ({ query }) => withClient(async (c) => {
63048
- const res = waitFor(c, "search_results", () => true);
63049
- c.search(query);
63050
- const e = await res;
63051
- const results = e?.results ?? [];
63466
+ let results;
63467
+ if (isStateless(c)) {
63468
+ results = (await c.search(query))?.results ?? [];
63469
+ } else {
63470
+ const res = waitFor(c, "search_results", () => true);
63471
+ c.search(query);
63472
+ results = (await res)?.results ?? [];
63473
+ }
63052
63474
  if (!results.length) return ok3(`No agents matched "${query}".`);
63053
63475
  return ok3(
63054
63476
  results.map((r) => `\u2022 @${r.username} \u2014 ${r.displayName} (${r.status})${r.owner ? ` \xB7 by @${r.owner}` : ""} \xB7 ${r.tags.join(", ")}`).join("\n")
@@ -63064,8 +63486,11 @@ Or just upload ${fname} from the agent's page in the web app.`
63064
63486
  status: external_exports.enum(["online", "away", "busy", "coding", "reviewing", "planning", "offline"])
63065
63487
  }
63066
63488
  },
63067
- async ({ status }) => withClient(async (c) => {
63068
- c.presence(status);
63489
+ async ({ status }, extra) => withClient(async (c) => {
63490
+ const declId = isStateless(c) ? retryKey(extra, ["presence", status])?.replace(/^mcp-/, "d-") : void 0;
63491
+ const r = await confirmWrite(c, () => isStateless(c) ? c.presence(status, declId) : c.presence(status));
63492
+ if (r.error) return fail(`Couldn't set status: ${r.error}`);
63493
+ ctx.onPresenceDeclared?.(status);
63069
63494
  return ok3(`Status set to ${status}.`);
63070
63495
  })
63071
63496
  );
@@ -63257,11 +63682,13 @@ function sweepIdleSessions(map3) {
63257
63682
  // src/http.ts
63258
63683
  var HUB = process.env.JEFRI_SERVER ?? "http://localhost:4000";
63259
63684
  var PORT = Number(process.env.MCP_HTTP_PORT ?? 4001);
63260
- var log = (...a) => console.error("[jefrichat-mcp-http]", ...a);
63685
+ var STATELESS = process.env.JEFRI_STATELESS === "true";
63686
+ var log2 = (...a) => console.error("[jefrichat-mcp-http]", ...a);
63261
63687
  var sessions = /* @__PURE__ */ new Map();
63262
63688
  var HOST_FRESH_MS = Number(process.env.MCP_HOST_FRESH_MS ?? 10 * 60 * 1e3);
63263
63689
  var hubPool = createKeyedPool();
63264
63690
  var SESSION_TTL_MS = Number(process.env.MCP_SESSION_TTL_MS ?? 4 * 60 * 60 * 1e3);
63691
+ var STATELESS_POOL_IDLE_MS = Number(process.env.MCP_STATELESS_POOL_IDLE_MS ?? 10 * 60 * 1e3);
63265
63692
  var MAX_SESSIONS = Number(process.env.MCP_MAX_SESSIONS ?? 1500);
63266
63693
  var MAX_SESSIONS_PER_IDENTITY = Number(process.env.MCP_MAX_SESSIONS_PER_IDENTITY ?? 20);
63267
63694
  var INIT_WINDOW_MS = 6e4;
@@ -63292,7 +63719,7 @@ function closeSession(sid, reason) {
63292
63719
  if (detachRef(pooled, sid)) teardownPool(s.poolKey, pooled, "last session gone");
63293
63720
  else if (s.host) announcePoolHosts(s.poolKey, pooled);
63294
63721
  }
63295
- log(`session ${sid.slice(0, 8)}\u2026 closed (${reason}) (${sessions.size} active)`);
63722
+ log2(`session ${sid.slice(0, 8)}\u2026 closed (${reason}) (${sessions.size} active)`);
63296
63723
  }
63297
63724
  function teardownPool(poolKey, pooled, reason) {
63298
63725
  pooled.closed = true;
@@ -63301,9 +63728,10 @@ function teardownPool(poolKey, pooled, reason) {
63301
63728
  pooled.acp.close();
63302
63729
  } catch {
63303
63730
  }
63304
- log(`hub connection ${poolKey.slice(0, 8)}\u2026 closed (${reason})`);
63731
+ log2(`hub connection ${poolKey.slice(0, 8)}\u2026 closed (${reason})`);
63305
63732
  }
63306
63733
  function announcePoolHosts(poolKey, pooled) {
63734
+ if (STATELESS) return;
63307
63735
  const joined = aggregateFreshHosts(sessions.values(), poolKey, Date.now(), HOST_FRESH_MS);
63308
63736
  if (joined === pooled.lastHosts) return;
63309
63737
  pooled.lastHosts = joined;
@@ -63318,6 +63746,12 @@ function closePool(poolKey, reason) {
63318
63746
  }
63319
63747
  async function acquirePooledConn(token, poolKey) {
63320
63748
  return hubPool.acquire(poolKey, async () => {
63749
+ if (STATELESS) {
63750
+ const acp2 = new JefriHttpClient({ server: HUB, token });
63751
+ await acp2.whoami();
63752
+ const broughtUp = await statelessPresenceBringUp(acp2);
63753
+ return { acp: acp2, inbox: [], refs: /* @__PURE__ */ new Set(), closed: false, lastSeen: Date.now(), needsBringUp: !broughtUp };
63754
+ }
63321
63755
  const acp = await JefriClient.connect({ server: HUB, token, status: "online" });
63322
63756
  const inbox = [];
63323
63757
  if (acp.identity) attachInbox(acp, inbox, acp.identity.username);
@@ -63339,12 +63773,30 @@ setInterval(() => {
63339
63773
  for (const [sid, s] of sessions) {
63340
63774
  if (now - s.lastActive > SESSION_TTL_MS) closeSession(sid, "idle");
63341
63775
  }
63342
- const seenPools = /* @__PURE__ */ new Set();
63343
- for (const s of sessions.values()) {
63344
- if (seenPools.has(s.poolKey)) continue;
63345
- seenPools.add(s.poolKey);
63346
- const pooled = hubPool.get(s.poolKey);
63347
- if (pooled) announcePoolHosts(s.poolKey, pooled);
63776
+ if (STATELESS) {
63777
+ for (const key of hubPool.keys()) {
63778
+ const pooled = hubPool.get(key);
63779
+ if (!pooled) continue;
63780
+ if (now - (pooled.lastSeen ?? 0) > STATELESS_POOL_IDLE_MS) {
63781
+ hubPool.drop(key);
63782
+ continue;
63783
+ }
63784
+ if (pooled.needsBringUp) {
63785
+ void statelessPresenceBringUp(pooled.acp).then((ok4) => {
63786
+ if (ok4) pooled.needsBringUp = false;
63787
+ });
63788
+ } else {
63789
+ void statelessSweepBeat(pooled.acp, pooled, () => closePool(key, "auth revoked"));
63790
+ }
63791
+ }
63792
+ } else {
63793
+ const seenPools = /* @__PURE__ */ new Set();
63794
+ for (const s of sessions.values()) {
63795
+ if (seenPools.has(s.poolKey)) continue;
63796
+ seenPools.add(s.poolKey);
63797
+ const pooled = hubPool.get(s.poolKey);
63798
+ if (pooled) announcePoolHosts(s.poolKey, pooled);
63799
+ }
63348
63800
  }
63349
63801
  for (const [ip, e] of initHits) {
63350
63802
  if (now > e.resetAt) initHits.delete(ip);
@@ -63403,11 +63855,12 @@ var health = (_req, res) => res.status(draining ? 503 : 200).json({
63403
63855
  draining,
63404
63856
  service: "jefrichat-mcp-http",
63405
63857
  hub: HUB,
63858
+ stateless: STATELESS,
63406
63859
  sessions: sessions.size
63407
63860
  });
63408
63861
  app.get("/health", health);
63409
63862
  app.get("/mcp/health", health);
63410
- app.post("/mcp", async (req, res) => {
63863
+ var statefulPost = async (req, res) => {
63411
63864
  const sessionId = req.headers["mcp-session-id"];
63412
63865
  if (sessionId && sessions.has(sessionId)) {
63413
63866
  const s = sessions.get(sessionId);
@@ -63473,7 +63926,7 @@ app.post("/mcp", async (req, res) => {
63473
63926
  try {
63474
63927
  pooled = await acquirePooledConn(token, poolKey);
63475
63928
  } catch (e) {
63476
- if (e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED") {
63929
+ if (e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED" || e instanceof JefriHttpError && (e.status === 401 || e.status === 403)) {
63477
63930
  res.status(401).json({
63478
63931
  jsonrpc: "2.0",
63479
63932
  error: {
@@ -63498,7 +63951,7 @@ app.post("/mcp", async (req, res) => {
63498
63951
  return;
63499
63952
  }
63500
63953
  const mcp = new McpServer({ name: "jefrichat", version: "0.1.0" });
63501
- registerAcpTools(mcp, { ensureClient: async () => acp, inbox: pooled.inbox, serverUrl: HUB, local: false });
63954
+ registerAcpTools(mcp, { ensureClient: async () => acp, inbox: pooled.inbox, serverUrl: HUB, local: false, stateless: STATELESS });
63502
63955
  let mySid;
63503
63956
  mcp.server.oninitialized = () => {
63504
63957
  try {
@@ -63510,14 +63963,14 @@ app.post("/mcp", async (req, res) => {
63510
63963
  }
63511
63964
  };
63512
63965
  const transport = new StreamableHTTPServerTransport({
63513
- sessionIdGenerator: () => randomUUID(),
63966
+ sessionIdGenerator: () => randomUUID3(),
63514
63967
  onsessioninitialized: (sid) => {
63515
63968
  if (pooled.closed) {
63516
63969
  try {
63517
63970
  transport.close?.();
63518
63971
  } catch {
63519
63972
  }
63520
- log(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
63973
+ log2(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
63521
63974
  return;
63522
63975
  }
63523
63976
  const meName = acp.identity.username;
@@ -63542,19 +63995,19 @@ app.post("/mcp", async (req, res) => {
63542
63995
  transport.close?.();
63543
63996
  } catch {
63544
63997
  }
63545
- log(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
63998
+ log2(`session ${sid.slice(0, 8)}\u2026 refused (connection torn down during init)`);
63546
63999
  return;
63547
64000
  }
63548
64001
  sessions.set(sid, { transport, acp, lastActive: Date.now(), tokenHash: sha256(token), inflight: 0, poolKey });
63549
64002
  mySid = sid;
63550
- log(`session ${sid.slice(0, 8)}\u2026 open as @${acp.identity.username} (${sessions.size} active, ${pooled.refs.size} on this connection)`);
64003
+ log2(`session ${sid.slice(0, 8)}\u2026 open as @${acp.identity.username} (${sessions.size} active, ${pooled.refs.size} on this connection)`);
63551
64004
  }
63552
64005
  });
63553
64006
  transport.onclose = () => {
63554
64007
  const sid = transport.sessionId;
63555
64008
  if (sid) closeSession(sid, "client closed");
63556
64009
  };
63557
- const reservation = "init:" + randomUUID();
64010
+ const reservation = "init:" + randomUUID3();
63558
64011
  pooled.refs.add(reservation);
63559
64012
  try {
63560
64013
  await mcp.connect(transport);
@@ -63570,7 +64023,7 @@ app.post("/mcp", async (req, res) => {
63570
64023
  error: { code: -32001, message: "Session expired \u2014 reinitialize (your token is still valid)" },
63571
64024
  id: null
63572
64025
  });
63573
- });
64026
+ };
63574
64027
  var bySession = async (req, res) => {
63575
64028
  const sessionId = req.headers["mcp-session-id"];
63576
64029
  const s = sessionId ? sessions.get(sessionId) : void 0;
@@ -63588,8 +64041,58 @@ var bySession = async (req, res) => {
63588
64041
  touch(sessionId);
63589
64042
  await s.transport.handleRequest(req, res);
63590
64043
  };
63591
- app.get("/mcp", bySession);
63592
- app.delete("/mcp", bySession);
64044
+ if (STATELESS) {
64045
+ app.post(
64046
+ "/mcp",
64047
+ statelessMcpHandler({
64048
+ bearer,
64049
+ acquire: async (token) => {
64050
+ const poolKey = sha256(token).toString("hex");
64051
+ const pooled = await acquirePooledConn(token, poolKey);
64052
+ pooled.lastSeen = Date.now();
64053
+ return pooled;
64054
+ },
64055
+ // registerTools below closes over the POOLED entry via acp — the presence
64056
+ // resume/stop signal needs the pool, so it is looked up by identity of
64057
+ // the shared client (one pool owns exactly one client instance).
64058
+ classifyError: (e) => e instanceof JefriAuthError || e?.code === "ACP_AUTH_FAILED" || e instanceof JefriHttpError && (e.status === 401 || e.status === 403) ? "auth" : "hub",
64059
+ registerTools: (mcp, acp, hooks) => registerAcpTools(mcp, {
64060
+ ensureClient: async () => acp,
64061
+ inbox: [],
64062
+ serverUrl: HUB,
64063
+ local: false,
64064
+ stateless: true,
64065
+ deferUntilDelivered: hooks.deferUntilDelivered,
64066
+ // Round 26 #4a: a successful declaration RESUMES heartbeats (the
64067
+ // user came back — the hub cleared its sign-off marker, the pool
64068
+ // must clear its own), and a sign-off STOPS them proactively.
64069
+ onPresenceDeclared: (status) => {
64070
+ for (const key of hubPool.keys()) {
64071
+ const pooled = hubPool.get(key);
64072
+ if (pooled && pooled.acp === acp) {
64073
+ if (status === "offline") pooled.hbSignedOff = true;
64074
+ else {
64075
+ pooled.hbSignedOff = false;
64076
+ pooled.needsBringUp = false;
64077
+ }
64078
+ break;
64079
+ }
64080
+ }
64081
+ }
64082
+ }),
64083
+ isDraining: () => draining,
64084
+ tryAcquireInflight: (token) => tryAcquire(tokenInflight, sha256(token).toString("hex"), MAX_INFLIGHT_PER_TOKEN),
64085
+ releaseInflight: (token) => release(tokenInflight, sha256(token).toString("hex")),
64086
+ log: log2
64087
+ })
64088
+ );
64089
+ app.get("/mcp", statelessMethodNotAllowed);
64090
+ app.delete("/mcp", statelessMethodNotAllowed);
64091
+ } else {
64092
+ app.post("/mcp", statefulPost);
64093
+ app.get("/mcp", bySession);
64094
+ app.delete("/mcp", bySession);
64095
+ }
63593
64096
  var server = http.createServer(app).listen(PORT, () => {
63594
64097
  console.error(`
63595
64098
  \u{1F310} Jefri Chat remote MCP (HTTP) on http://localhost:${PORT}/mcp`);