@seanhogg/builderforce-memory-mcp 2026.6.31 → 2026.6.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,9 @@ tool core over a pluggable `MemoryBackend`, three transports:
7
7
  |---|---|---|---|
8
8
  | **In-process (Claude Agent SDK)** | `createMemoryMcpServer(backend)` | the returned `type:"sdk"` object | the consuming product is TS and runs `@anthropic-ai/claude-agent-sdk` in-process — lowest latency |
9
9
  | **stdio** | `runStdio(backend)` / `npx @seanhogg/builderforce-memory-mcp` | `{ type:"stdio", command, args }` | any language / separate process; decouples the SSM+IndexedDB deps from the consumer |
10
- | **HTTP (Streamable)** | `createMemoryHttpHandler(backend, { authToken })` | `{ type:"http", url, headers }` | multi-tenant / networked (builderforce.ai hosting, remote claws) |
10
+ | **HTTP (Streamable)** | `createMemoryHttpHandler(backend, { authToken })` | `{ type:"http", url, headers }` | multi-tenant / networked (builderforce.ai hosting, remote agents) |
11
+
12
+ > **Technical report & peer review.** This transport is reviewed in the Evermind technical report's adversarial [`PEER-REVIEW.md`](../../publication/evermind/PEER-REVIEW.md). Note before exposing the HTTP handler publicly: the `authToken` check is a **non-constant-time** string compare (timing channel) and the handler shares **one `backend` with no per-tenant isolation** — a single token reads every tenant's facts. Despite the "multi-tenant" note above, real isolation (token→namespace mapping, constant-time compare, rate limiting) is **not yet implemented**. Tracked as `EVM-3` in the Builderforce.ai Consolidated Gap Register.
11
13
 
12
14
  ## Why this saves tokens
13
15
 
@@ -67,7 +69,7 @@ for await (const msg of query({
67
69
 
68
70
  The local backend defaults to **lexical (Jaccard)** recall. For SSM-embedding
69
71
  cosine recall, pass an SSM runtime — e.g. reuse the agent-runtime's already-loaded
70
- hippocampus instead of standing up a second model:
72
+ Evermind runtime instead of standing up a second model:
71
73
 
72
74
  ```ts
73
75
  const backend = await createLocalMemoryStoreBackend({ runtime: ssmMemoryService.runtime });
@@ -47,7 +47,7 @@ export interface LocalBackendOptions {
47
47
  /**
48
48
  * Optional SSMRuntime for embedding-based recall. Omit for lexical (Jaccard)
49
49
  * recall. In the agent-runtime, pass `ssmMemoryService.runtime` here to reuse
50
- * the already-loaded hippocampus instead of standing up a second model.
50
+ * the already-loaded Evermind runtime instead of standing up a second model.
51
51
  */
52
52
  runtime?: unknown;
53
53
  /**
package/dist/index.d.ts CHANGED
@@ -18,6 +18,7 @@ export type { McpServerOptions } from "./transports/mcp-server.js";
18
18
  export { runStdio } from "./transports/stdio.js";
19
19
  export { createMemoryHttpHandler } from "./transports/http.js";
20
20
  export type { HttpHandlerOptions } from "./transports/http.js";
21
+ export { hashToken, timingSafeEqualStr, buildTenantIndex, bearerToken, RateLimiter } from "./transports/auth.js";
21
22
  export { buildServerSpec, MCP_PACKAGE, MCP_BIN, RUNTIME_PEERS } from "./install/server-spec.js";
22
23
  export type { StdioServerSpec, ServerSpecOptions } from "./install/server-spec.js";
23
24
  export { installMemoryServer } from "./install/install.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG5E,OAAO,EAAE,kBAAkB,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAC/F,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEhF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAChG,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC/G,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG7E,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACpH,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAG5E,OAAO,EAAE,kBAAkB,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAC/F,YAAY,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG7E,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAEhF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,YAAY,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAEnE,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAGjH,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAChG,YAAY,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AACnF,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAC3D,YAAY,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC/G,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACjE,YAAY,EAAE,WAAW,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAG7E,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACpH,YAAY,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}
package/dist/index.js CHANGED
@@ -15,6 +15,7 @@ export { createMemoryMcpServer } from "./transports/sdk.js";
15
15
  export { buildMcpServer } from "./transports/mcp-server.js";
16
16
  export { runStdio } from "./transports/stdio.js";
17
17
  export { createMemoryHttpHandler } from "./transports/http.js";
18
+ export { hashToken, timingSafeEqualStr, buildTenantIndex, bearerToken, RateLimiter } from "./transports/auth.js";
18
19
  // ── Multi-host installer (wire the stdio server into any MCP-capable agent) ───
19
20
  export { buildServerSpec, MCP_PACKAGE, MCP_BIN, RUNTIME_PEERS } from "./install/server-spec.js";
20
21
  export { installMemoryServer } from "./install/install.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,0FAA0F;AAC1F,OAAO,EAAE,kBAAkB,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAG/F,iFAAiF;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,+EAA+E;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG5D,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAG/D,iFAAiF;AACjF,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEhG,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEjE,qFAAqF;AACrF,4FAA4F;AAC5F,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH,0FAA0F;AAC1F,OAAO,EAAE,kBAAkB,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAG/F,iFAAiF;AACjF,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9C,+EAA+E;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAG5D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAG5D,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAEjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEjH,iFAAiF;AACjF,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEhG,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAEjE,qFAAqF;AACrF,4FAA4F;AAC5F,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * transports/auth.ts — auth + rate-limiting primitives for the HTTP transport.
3
+ *
4
+ * The networked MCP surface is multi-tenant: one bearer token must map to ONE
5
+ * tenant's memory namespace, the comparison must not leak the secret through a
6
+ * timing side channel, and a noisy or hostile caller must not be able to hammer
7
+ * the server. These helpers provide exactly those three pieces, dependency-free
8
+ * (node:crypto only).
9
+ */
10
+ /** SHA-256 hex digest of a token — the stable, non-reversible lookup key. */
11
+ export declare function hashToken(token: string): string;
12
+ /**
13
+ * Constant-time string equality. Hashes both sides to fixed-length digests
14
+ * first, so neither the comparison nor the input length leaks the secret.
15
+ */
16
+ export declare function timingSafeEqualStr(a: string, b: string): boolean;
17
+ /**
18
+ * Build a tenant lookup keyed by token HASH (never the raw token). Resolving a
19
+ * presented token is a hash + map lookup — no per-character secret comparison,
20
+ * so there is no early-return timing channel on the token value.
21
+ */
22
+ export declare function buildTenantIndex<B>(tenants: Record<string, B>): Map<string, B>;
23
+ /** Fixed-window rate limiter. Memory-bounded by pruning expired windows. */
24
+ export declare class RateLimiter {
25
+ private readonly windowMs;
26
+ private readonly max;
27
+ private readonly now;
28
+ private readonly _hits;
29
+ constructor(windowMs: number, max: number, now?: () => number);
30
+ /** Returns true when the call is allowed; false when the window is exhausted. */
31
+ check(key: string): boolean;
32
+ private _prune;
33
+ }
34
+ /** Extract the bearer token from an Authorization header value, or undefined. */
35
+ export declare function bearerToken(header: string | string[] | undefined): string | undefined;
36
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/transports/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,6EAA6E;AAC7E,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAIhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,CAM9E;AAED,4EAA4E;AAC5E,qBAAa,WAAW;IAIhB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,GAAG;IALxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAyD;gBAG1D,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,GAAG,GAAE,MAAM,MAAyB;IAGzD,iFAAiF;IACjF,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAa3B,OAAO,CAAC,MAAM;CAKjB;AAED,iFAAiF;AACjF,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAKrF"}
@@ -0,0 +1,78 @@
1
+ /**
2
+ * transports/auth.ts — auth + rate-limiting primitives for the HTTP transport.
3
+ *
4
+ * The networked MCP surface is multi-tenant: one bearer token must map to ONE
5
+ * tenant's memory namespace, the comparison must not leak the secret through a
6
+ * timing side channel, and a noisy or hostile caller must not be able to hammer
7
+ * the server. These helpers provide exactly those three pieces, dependency-free
8
+ * (node:crypto only).
9
+ */
10
+ import { createHash, timingSafeEqual } from "node:crypto";
11
+ /** SHA-256 hex digest of a token — the stable, non-reversible lookup key. */
12
+ export function hashToken(token) {
13
+ return createHash("sha256").update(token, "utf8").digest("hex");
14
+ }
15
+ /**
16
+ * Constant-time string equality. Hashes both sides to fixed-length digests
17
+ * first, so neither the comparison nor the input length leaks the secret.
18
+ */
19
+ export function timingSafeEqualStr(a, b) {
20
+ const ha = createHash("sha256").update(a, "utf8").digest();
21
+ const hb = createHash("sha256").update(b, "utf8").digest();
22
+ return timingSafeEqual(ha, hb);
23
+ }
24
+ /**
25
+ * Build a tenant lookup keyed by token HASH (never the raw token). Resolving a
26
+ * presented token is a hash + map lookup — no per-character secret comparison,
27
+ * so there is no early-return timing channel on the token value.
28
+ */
29
+ export function buildTenantIndex(tenants) {
30
+ const index = new Map();
31
+ for (const [token, backend] of Object.entries(tenants)) {
32
+ if (token)
33
+ index.set(hashToken(token), backend);
34
+ }
35
+ return index;
36
+ }
37
+ /** Fixed-window rate limiter. Memory-bounded by pruning expired windows. */
38
+ export class RateLimiter {
39
+ windowMs;
40
+ max;
41
+ now;
42
+ _hits = new Map();
43
+ constructor(windowMs, max, now = () => Date.now()) {
44
+ this.windowMs = windowMs;
45
+ this.max = max;
46
+ this.now = now;
47
+ }
48
+ /** Returns true when the call is allowed; false when the window is exhausted. */
49
+ check(key) {
50
+ const t = this.now();
51
+ const entry = this._hits.get(key);
52
+ if (!entry || t >= entry.resetAt) {
53
+ if (this._hits.size > 10_000)
54
+ this._prune(t);
55
+ this._hits.set(key, { count: 1, resetAt: t + this.windowMs });
56
+ return true;
57
+ }
58
+ if (entry.count >= this.max)
59
+ return false;
60
+ entry.count++;
61
+ return true;
62
+ }
63
+ _prune(t) {
64
+ for (const [k, v] of this._hits) {
65
+ if (t >= v.resetAt)
66
+ this._hits.delete(k);
67
+ }
68
+ }
69
+ }
70
+ /** Extract the bearer token from an Authorization header value, or undefined. */
71
+ export function bearerToken(header) {
72
+ const value = Array.isArray(header) ? header[0] : header;
73
+ if (!value)
74
+ return undefined;
75
+ const m = /^Bearer\s+(.+)$/i.exec(value.trim());
76
+ return m ? m[1].trim() : undefined;
77
+ }
78
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/transports/auth.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,22 @@
1
1
  /**
2
2
  * HTTP transport — the multi-tenant / networked path (builderforce.ai hosting,
3
- * remote claws). Stateless Streamable HTTP: one short-lived McpServer +
3
+ * remote hosts). Stateless Streamable HTTP: one short-lived McpServer +
4
4
  * transport per request, so it scales horizontally with no sticky sessions.
5
- * Bearer-token auth gates every request.
5
+ *
6
+ * Auth model (pick one, in precedence order):
7
+ * • `tenants` — a `{ token: backend }` map. The bearer token selects the
8
+ * tenant's OWN backend (namespace isolation); an unknown token is 401. This
9
+ * is the production multi-tenant mode — one token can never read another
10
+ * tenant's facts.
11
+ * • `resolveBackend` — a function `(token) => backend` for dynamic tenant
12
+ * resolution (e.g. a gateway lookup).
13
+ * • `authToken` — a single shared secret guarding the one positional backend
14
+ * (single-tenant / reference hosting).
15
+ * • none — open, only valid behind a trusted gateway that already authed.
16
+ *
17
+ * Token comparison is constant-time (hash + timingSafeEqual / hash-keyed lookup),
18
+ * and an optional fixed-window rate limit (per tenant, or per client IP when
19
+ * unauthenticated) caps abuse.
6
20
  *
7
21
  * Consumed from the Claude Agent SDK as:
8
22
  * mcpServers: {
@@ -15,16 +29,36 @@ import type { MemoryBackend } from "../backend.js";
15
29
  import { type McpServerOptions } from "./mcp-server.js";
16
30
  export interface HttpHandlerOptions extends McpServerOptions {
17
31
  /**
18
- * Shared secret required in `Authorization: Bearer <token>`. When set, every
19
- * request without a matching token is rejected 401. Omit only behind a trusted
20
- * gateway that has already authenticated the caller.
32
+ * Shared secret required in `Authorization: Bearer <token>` for the single
33
+ * positional backend. Every request without a matching token is rejected 401.
34
+ * Omit only behind a trusted gateway that has already authenticated the caller.
21
35
  */
22
36
  authToken?: string;
37
+ /**
38
+ * Multi-tenant map: bearer token → that tenant's backend. When set, the token
39
+ * selects the backend (namespace isolation) and the positional `backend` is
40
+ * only used as a fallback when no token-auth modes match. Lookups are keyed by
41
+ * token hash, never the raw token.
42
+ */
43
+ tenants?: Record<string, MemoryBackend>;
44
+ /**
45
+ * Dynamic tenant resolver: `(token) => backend | undefined`. Used when tenants
46
+ * live in an external store. Returning undefined → 401.
47
+ */
48
+ resolveBackend?: (token: string) => MemoryBackend | undefined | Promise<MemoryBackend | undefined>;
49
+ /** Fixed-window rate limit applied per tenant (or per client IP if open). */
50
+ rateLimit?: {
51
+ windowMs: number;
52
+ max: number;
53
+ };
54
+ /** Injectable clock (ms) for deterministic rate-limit tests. */
55
+ now?: () => number;
23
56
  }
24
57
  /**
25
58
  * Returns a `(req, res) => Promise<void>` handler you mount on any Node HTTP
26
- * server (or Express). Per request it spins up a stateless MCP server bound to
27
- * `backend`, handles the MCP exchange, and tears down on response close.
59
+ * server (or Express). Per request it authenticates, resolves the tenant
60
+ * backend, rate-limits, spins up a stateless MCP server bound to that backend,
61
+ * handles the MCP exchange, and tears down on response close.
28
62
  */
29
63
  export declare function createMemoryHttpHandler(backend: MemoryBackend, opts?: HttpHandlerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
30
64
  //# sourceMappingURL=http.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAExE,MAAM,WAAW,kBAAmB,SAAQ,gBAAgB;IACxD;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAOD;;;;GAIG;AACH,wBAAgB,uBAAuB,CACnC,OAAO,EAAE,aAAa,EACtB,IAAI,GAAE,kBAAuB,GAC9B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAuB9D"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,EAAkB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGxE,MAAM,WAAW,kBAAmB,SAAQ,gBAAgB;IACxD;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACxC;;;OAGG;IACH,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,GAAG,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IACnG,6EAA6E;IAC7E,SAAS,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,gEAAgE;IAChE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAcD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACnC,OAAO,EAAE,aAAa,EACtB,IAAI,GAAE,kBAAuB,GAC9B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAyD9D"}
@@ -1,8 +1,22 @@
1
1
  /**
2
2
  * HTTP transport — the multi-tenant / networked path (builderforce.ai hosting,
3
- * remote claws). Stateless Streamable HTTP: one short-lived McpServer +
3
+ * remote hosts). Stateless Streamable HTTP: one short-lived McpServer +
4
4
  * transport per request, so it scales horizontally with no sticky sessions.
5
- * Bearer-token auth gates every request.
5
+ *
6
+ * Auth model (pick one, in precedence order):
7
+ * • `tenants` — a `{ token: backend }` map. The bearer token selects the
8
+ * tenant's OWN backend (namespace isolation); an unknown token is 401. This
9
+ * is the production multi-tenant mode — one token can never read another
10
+ * tenant's facts.
11
+ * • `resolveBackend` — a function `(token) => backend` for dynamic tenant
12
+ * resolution (e.g. a gateway lookup).
13
+ * • `authToken` — a single shared secret guarding the one positional backend
14
+ * (single-tenant / reference hosting).
15
+ * • none — open, only valid behind a trusted gateway that already authed.
16
+ *
17
+ * Token comparison is constant-time (hash + timingSafeEqual / hash-keyed lookup),
18
+ * and an optional fixed-window rate limit (per tenant, or per client IP when
19
+ * unauthenticated) caps abuse.
6
20
  *
7
21
  * Consumed from the Claude Agent SDK as:
8
22
  * mcpServers: {
@@ -12,25 +26,78 @@
12
26
  */
13
27
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
14
28
  import { buildMcpServer } from "./mcp-server.js";
15
- function unauthorized(res) {
16
- res.writeHead(401, { "content-type": "application/json" });
17
- res.end(JSON.stringify({ error: "unauthorized" }));
29
+ import { RateLimiter, bearerToken, buildTenantIndex, hashToken, timingSafeEqualStr } from "./auth.js";
30
+ function send(res, status, body) {
31
+ res.writeHead(status, { "content-type": "application/json" });
32
+ res.end(JSON.stringify(body));
33
+ }
34
+ /** First-hop client IP, for rate-limiting unauthenticated callers. */
35
+ function clientIp(req) {
36
+ const fwd = req.headers["x-forwarded-for"];
37
+ const first = Array.isArray(fwd) ? fwd[0] : fwd?.split(",")[0];
38
+ return (first?.trim() || req.socket?.remoteAddress || "unknown");
18
39
  }
19
40
  /**
20
41
  * Returns a `(req, res) => Promise<void>` handler you mount on any Node HTTP
21
- * server (or Express). Per request it spins up a stateless MCP server bound to
22
- * `backend`, handles the MCP exchange, and tears down on response close.
42
+ * server (or Express). Per request it authenticates, resolves the tenant
43
+ * backend, rate-limits, spins up a stateless MCP server bound to that backend,
44
+ * handles the MCP exchange, and tears down on response close.
23
45
  */
24
46
  export function createMemoryHttpHandler(backend, opts = {}) {
47
+ const tenantIndex = opts.tenants ? buildTenantIndex(opts.tenants) : undefined;
48
+ const limiter = opts.rateLimit
49
+ ? new RateLimiter(opts.rateLimit.windowMs, opts.rateLimit.max, opts.now ?? (() => Date.now()))
50
+ : undefined;
25
51
  return async (req, res) => {
26
- if (opts.authToken) {
27
- const header = req.headers["authorization"];
28
- if (header !== `Bearer ${opts.authToken}`) {
29
- unauthorized(res);
52
+ const token = bearerToken(req.headers["authorization"]);
53
+ // ── Resolve the per-request backend + a rate-limit key ────────────────
54
+ let resolved;
55
+ let rateKey;
56
+ if (tenantIndex) {
57
+ // Multi-tenant: token → that tenant's backend (hash-keyed lookup).
58
+ if (!token) {
59
+ send(res, 401, { error: "unauthorized" });
60
+ return;
61
+ }
62
+ resolved = tenantIndex.get(hashToken(token));
63
+ if (!resolved) {
64
+ send(res, 401, { error: "unauthorized" });
65
+ return;
66
+ }
67
+ rateKey = `t:${hashToken(token)}`;
68
+ }
69
+ else if (opts.resolveBackend) {
70
+ if (!token) {
71
+ send(res, 401, { error: "unauthorized" });
30
72
  return;
31
73
  }
74
+ resolved = await opts.resolveBackend(token);
75
+ if (!resolved) {
76
+ send(res, 401, { error: "unauthorized" });
77
+ return;
78
+ }
79
+ rateKey = `t:${hashToken(token)}`;
80
+ }
81
+ else if (opts.authToken) {
82
+ // Single shared secret, constant-time compare.
83
+ if (!token || !timingSafeEqualStr(token, opts.authToken)) {
84
+ send(res, 401, { error: "unauthorized" });
85
+ return;
86
+ }
87
+ resolved = backend;
88
+ rateKey = `t:${hashToken(token)}`;
89
+ }
90
+ else {
91
+ // Open (trusted gateway): rate-limit by client IP.
92
+ resolved = backend;
93
+ rateKey = `ip:${clientIp(req)}`;
94
+ }
95
+ // ── Rate limit ────────────────────────────────────────────────────────
96
+ if (limiter && !limiter.check(rateKey)) {
97
+ send(res, 429, { error: "rate_limited" });
98
+ return;
32
99
  }
33
- const server = buildMcpServer(backend, opts);
100
+ const server = buildMcpServer(resolved, opts);
34
101
  // Stateless: no session id generator → a fresh transport per request.
35
102
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
36
103
  res.on("close", () => {
@@ -1 +1 @@
1
- {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAEnG,OAAO,EAAE,cAAc,EAAyB,MAAM,iBAAiB,CAAC;AAWxE,SAAS,YAAY,CAAC,GAAmB;IACrC,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;AACvD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CACnC,OAAsB,EACtB,OAA2B,EAAE;IAE7B,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACtB,IAAI,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,eAAe,CAAC,CAAC;YAC5C,IAAI,MAAM,KAAK,UAAU,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;gBACxC,YAAY,CAAC,GAAG,CAAC,CAAC;gBAClB,OAAO;YACX,CAAC;QACL,CAAC;QAED,MAAM,MAAM,GAAG,cAAc,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;QAC7C,sEAAsE;QACtE,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC,EAAE,kBAAkB,EAAE,SAAS,EAAE,CAAC,CAAC;QAEvF,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACjB,KAAK,SAAS,CAAC,KAAK,EAAE,CAAC;YACvB,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC,CAAC,CAAC;QAEH,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAChC,8EAA8E;QAC9E,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;IAC5C,CAAC,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/transports/http.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanhogg/builderforce-memory-mcp",
3
- "version": "2026.6.31",
3
+ "version": "2026.6.35",
4
4
  "description": "BuilderForce Agent Memory — MCP transport. Exposes @seanhogg/builderforce-memory to any MCP client (Claude Agent SDK in-process, stdio, or HTTP) over a pluggable MemoryBackend, with token-saving fetch-on-demand recall.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -73,7 +73,7 @@
73
73
  "fake-indexeddb": "^6.0.0",
74
74
  "typescript": "^6.0.3",
75
75
  "@typescript/native-preview": "7.0.0-dev.20260627.2",
76
- "@seanhogg/builderforce-memory": "2026.6.31"
76
+ "@seanhogg/builderforce-memory": "2026.6.35"
77
77
  },
78
78
  "engines": {
79
79
  "node": ">=18.0.0"
@@ -83,7 +83,7 @@
83
83
  },
84
84
  "scripts": {
85
85
  "build": "tsc",
86
- "test": "tsc && node --test tests/install.test.mjs tests/tools.test.mjs",
86
+ "test": "tsc && node --test tests/install.test.mjs tests/tools.test.mjs tests/http.test.mjs",
87
87
  "lint": "eslint src/",
88
88
  "typecheck:native": "tsgo --noEmit"
89
89
  }
@@ -81,7 +81,7 @@ export interface LocalBackendOptions {
81
81
  /**
82
82
  * Optional SSMRuntime for embedding-based recall. Omit for lexical (Jaccard)
83
83
  * recall. In the agent-runtime, pass `ssmMemoryService.runtime` here to reuse
84
- * the already-loaded hippocampus instead of standing up a second model.
84
+ * the already-loaded Evermind runtime instead of standing up a second model.
85
85
  */
86
86
  runtime?: unknown;
87
87
  /**
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ export { runStdio } from "./transports/stdio.js";
29
29
 
30
30
  export { createMemoryHttpHandler } from "./transports/http.js";
31
31
  export type { HttpHandlerOptions } from "./transports/http.js";
32
+ export { hashToken, timingSafeEqualStr, buildTenantIndex, bearerToken, RateLimiter } from "./transports/auth.js";
32
33
 
33
34
  // ── Multi-host installer (wire the stdio server into any MCP-capable agent) ───
34
35
  export { buildServerSpec, MCP_PACKAGE, MCP_BIN, RUNTIME_PEERS } from "./install/server-spec.js";
@@ -0,0 +1,78 @@
1
+ /**
2
+ * transports/auth.ts — auth + rate-limiting primitives for the HTTP transport.
3
+ *
4
+ * The networked MCP surface is multi-tenant: one bearer token must map to ONE
5
+ * tenant's memory namespace, the comparison must not leak the secret through a
6
+ * timing side channel, and a noisy or hostile caller must not be able to hammer
7
+ * the server. These helpers provide exactly those three pieces, dependency-free
8
+ * (node:crypto only).
9
+ */
10
+
11
+ import { createHash, timingSafeEqual } from "node:crypto";
12
+
13
+ /** SHA-256 hex digest of a token — the stable, non-reversible lookup key. */
14
+ export function hashToken(token: string): string {
15
+ return createHash("sha256").update(token, "utf8").digest("hex");
16
+ }
17
+
18
+ /**
19
+ * Constant-time string equality. Hashes both sides to fixed-length digests
20
+ * first, so neither the comparison nor the input length leaks the secret.
21
+ */
22
+ export function timingSafeEqualStr(a: string, b: string): boolean {
23
+ const ha = createHash("sha256").update(a, "utf8").digest();
24
+ const hb = createHash("sha256").update(b, "utf8").digest();
25
+ return timingSafeEqual(ha, hb);
26
+ }
27
+
28
+ /**
29
+ * Build a tenant lookup keyed by token HASH (never the raw token). Resolving a
30
+ * presented token is a hash + map lookup — no per-character secret comparison,
31
+ * so there is no early-return timing channel on the token value.
32
+ */
33
+ export function buildTenantIndex<B>(tenants: Record<string, B>): Map<string, B> {
34
+ const index = new Map<string, B>();
35
+ for (const [token, backend] of Object.entries(tenants)) {
36
+ if (token) index.set(hashToken(token), backend);
37
+ }
38
+ return index;
39
+ }
40
+
41
+ /** Fixed-window rate limiter. Memory-bounded by pruning expired windows. */
42
+ export class RateLimiter {
43
+ private readonly _hits = new Map<string, { count: number; resetAt: number }>();
44
+
45
+ constructor(
46
+ private readonly windowMs: number,
47
+ private readonly max: number,
48
+ private readonly now: () => number = () => Date.now(),
49
+ ) {}
50
+
51
+ /** Returns true when the call is allowed; false when the window is exhausted. */
52
+ check(key: string): boolean {
53
+ const t = this.now();
54
+ const entry = this._hits.get(key);
55
+ if (!entry || t >= entry.resetAt) {
56
+ if (this._hits.size > 10_000) this._prune(t);
57
+ this._hits.set(key, { count: 1, resetAt: t + this.windowMs });
58
+ return true;
59
+ }
60
+ if (entry.count >= this.max) return false;
61
+ entry.count++;
62
+ return true;
63
+ }
64
+
65
+ private _prune(t: number): void {
66
+ for (const [k, v] of this._hits) {
67
+ if (t >= v.resetAt) this._hits.delete(k);
68
+ }
69
+ }
70
+ }
71
+
72
+ /** Extract the bearer token from an Authorization header value, or undefined. */
73
+ export function bearerToken(header: string | string[] | undefined): string | undefined {
74
+ const value = Array.isArray(header) ? header[0] : header;
75
+ if (!value) return undefined;
76
+ const m = /^Bearer\s+(.+)$/i.exec(value.trim());
77
+ return m ? m[1]!.trim() : undefined;
78
+ }
@@ -1,8 +1,22 @@
1
1
  /**
2
2
  * HTTP transport — the multi-tenant / networked path (builderforce.ai hosting,
3
- * remote claws). Stateless Streamable HTTP: one short-lived McpServer +
3
+ * remote hosts). Stateless Streamable HTTP: one short-lived McpServer +
4
4
  * transport per request, so it scales horizontally with no sticky sessions.
5
- * Bearer-token auth gates every request.
5
+ *
6
+ * Auth model (pick one, in precedence order):
7
+ * • `tenants` — a `{ token: backend }` map. The bearer token selects the
8
+ * tenant's OWN backend (namespace isolation); an unknown token is 401. This
9
+ * is the production multi-tenant mode — one token can never read another
10
+ * tenant's facts.
11
+ * • `resolveBackend` — a function `(token) => backend` for dynamic tenant
12
+ * resolution (e.g. a gateway lookup).
13
+ * • `authToken` — a single shared secret guarding the one positional backend
14
+ * (single-tenant / reference hosting).
15
+ * • none — open, only valid behind a trusted gateway that already authed.
16
+ *
17
+ * Token comparison is constant-time (hash + timingSafeEqual / hash-keyed lookup),
18
+ * and an optional fixed-window rate limit (per tenant, or per client IP when
19
+ * unauthenticated) caps abuse.
6
20
  *
7
21
  * Consumed from the Claude Agent SDK as:
8
22
  * mcpServers: {
@@ -15,40 +29,99 @@ import type { IncomingMessage, ServerResponse } from "node:http";
15
29
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
16
30
  import type { MemoryBackend } from "../backend.js";
17
31
  import { buildMcpServer, type McpServerOptions } from "./mcp-server.js";
32
+ import { RateLimiter, bearerToken, buildTenantIndex, hashToken, timingSafeEqualStr } from "./auth.js";
18
33
 
19
34
  export interface HttpHandlerOptions extends McpServerOptions {
20
35
  /**
21
- * Shared secret required in `Authorization: Bearer <token>`. When set, every
22
- * request without a matching token is rejected 401. Omit only behind a trusted
23
- * gateway that has already authenticated the caller.
36
+ * Shared secret required in `Authorization: Bearer <token>` for the single
37
+ * positional backend. Every request without a matching token is rejected 401.
38
+ * Omit only behind a trusted gateway that has already authenticated the caller.
24
39
  */
25
40
  authToken?: string;
41
+ /**
42
+ * Multi-tenant map: bearer token → that tenant's backend. When set, the token
43
+ * selects the backend (namespace isolation) and the positional `backend` is
44
+ * only used as a fallback when no token-auth modes match. Lookups are keyed by
45
+ * token hash, never the raw token.
46
+ */
47
+ tenants?: Record<string, MemoryBackend>;
48
+ /**
49
+ * Dynamic tenant resolver: `(token) => backend | undefined`. Used when tenants
50
+ * live in an external store. Returning undefined → 401.
51
+ */
52
+ resolveBackend?: (token: string) => MemoryBackend | undefined | Promise<MemoryBackend | undefined>;
53
+ /** Fixed-window rate limit applied per tenant (or per client IP if open). */
54
+ rateLimit?: { windowMs: number; max: number };
55
+ /** Injectable clock (ms) for deterministic rate-limit tests. */
56
+ now?: () => number;
26
57
  }
27
58
 
28
- function unauthorized(res: ServerResponse): void {
29
- res.writeHead(401, { "content-type": "application/json" });
30
- res.end(JSON.stringify({ error: "unauthorized" }));
59
+ function send(res: ServerResponse, status: number, body: unknown): void {
60
+ res.writeHead(status, { "content-type": "application/json" });
61
+ res.end(JSON.stringify(body));
62
+ }
63
+
64
+ /** First-hop client IP, for rate-limiting unauthenticated callers. */
65
+ function clientIp(req: IncomingMessage): string {
66
+ const fwd = req.headers["x-forwarded-for"];
67
+ const first = Array.isArray(fwd) ? fwd[0] : fwd?.split(",")[0];
68
+ return (first?.trim() || req.socket?.remoteAddress || "unknown");
31
69
  }
32
70
 
33
71
  /**
34
72
  * Returns a `(req, res) => Promise<void>` handler you mount on any Node HTTP
35
- * server (or Express). Per request it spins up a stateless MCP server bound to
36
- * `backend`, handles the MCP exchange, and tears down on response close.
73
+ * server (or Express). Per request it authenticates, resolves the tenant
74
+ * backend, rate-limits, spins up a stateless MCP server bound to that backend,
75
+ * handles the MCP exchange, and tears down on response close.
37
76
  */
38
77
  export function createMemoryHttpHandler(
39
78
  backend: MemoryBackend,
40
79
  opts: HttpHandlerOptions = {},
41
80
  ): (req: IncomingMessage, res: ServerResponse) => Promise<void> {
81
+ const tenantIndex = opts.tenants ? buildTenantIndex(opts.tenants) : undefined;
82
+ const limiter = opts.rateLimit
83
+ ? new RateLimiter(opts.rateLimit.windowMs, opts.rateLimit.max, opts.now ?? (() => Date.now()))
84
+ : undefined;
85
+
42
86
  return async (req, res) => {
43
- if (opts.authToken) {
44
- const header = req.headers["authorization"];
45
- if (header !== `Bearer ${opts.authToken}`) {
46
- unauthorized(res);
87
+ const token = bearerToken(req.headers["authorization"]);
88
+
89
+ // ── Resolve the per-request backend + a rate-limit key ────────────────
90
+ let resolved: MemoryBackend | undefined;
91
+ let rateKey: string;
92
+
93
+ if (tenantIndex) {
94
+ // Multi-tenant: token → that tenant's backend (hash-keyed lookup).
95
+ if (!token) { send(res, 401, { error: "unauthorized" }); return; }
96
+ resolved = tenantIndex.get(hashToken(token));
97
+ if (!resolved) { send(res, 401, { error: "unauthorized" }); return; }
98
+ rateKey = `t:${hashToken(token)}`;
99
+ } else if (opts.resolveBackend) {
100
+ if (!token) { send(res, 401, { error: "unauthorized" }); return; }
101
+ resolved = await opts.resolveBackend(token);
102
+ if (!resolved) { send(res, 401, { error: "unauthorized" }); return; }
103
+ rateKey = `t:${hashToken(token)}`;
104
+ } else if (opts.authToken) {
105
+ // Single shared secret, constant-time compare.
106
+ if (!token || !timingSafeEqualStr(token, opts.authToken)) {
107
+ send(res, 401, { error: "unauthorized" });
47
108
  return;
48
109
  }
110
+ resolved = backend;
111
+ rateKey = `t:${hashToken(token)}`;
112
+ } else {
113
+ // Open (trusted gateway): rate-limit by client IP.
114
+ resolved = backend;
115
+ rateKey = `ip:${clientIp(req)}`;
116
+ }
117
+
118
+ // ── Rate limit ────────────────────────────────────────────────────────
119
+ if (limiter && !limiter.check(rateKey)) {
120
+ send(res, 429, { error: "rate_limited" });
121
+ return;
49
122
  }
50
123
 
51
- const server = buildMcpServer(backend, opts);
124
+ const server = buildMcpServer(resolved, opts);
52
125
  // Stateless: no session id generator → a fresh transport per request.
53
126
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
54
127