@seanhogg/builderforce-memory-mcp 2026.6.30 → 2026.6.34
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 +22 -2
- package/dist/backends/memory-store.d.ts +1 -1
- package/dist/bin/install.js +22 -0
- package/dist/bin/install.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/install/claude-hooks.d.ts +49 -0
- package/dist/install/claude-hooks.d.ts.map +1 -0
- package/dist/install/claude-hooks.js +255 -0
- package/dist/install/claude-hooks.js.map +1 -0
- package/dist/transports/auth.d.ts +36 -0
- package/dist/transports/auth.d.ts.map +1 -0
- package/dist/transports/auth.js +78 -0
- package/dist/transports/auth.js.map +1 -0
- package/dist/transports/http.d.ts +41 -7
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +79 -12
- package/dist/transports/http.js.map +1 -1
- package/package.json +3 -3
- package/src/backends/memory-store.ts +1 -1
- package/src/bin/install.ts +22 -0
- package/src/index.ts +5 -0
- package/src/install/claude-hooks.ts +267 -0
- package/src/transports/auth.ts +78 -0
- package/src/transports/http.ts +88 -15
package/src/transports/http.ts
CHANGED
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HTTP transport — the multi-tenant / networked path (builderforce.ai hosting,
|
|
3
|
-
* remote
|
|
3
|
+
* remote hosts). Stateless Streamable HTTP: one short-lived McpServer +
|
|
4
4
|
* transport per request, so it scales horizontally with no sticky sessions.
|
|
5
|
-
*
|
|
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
|
|
22
|
-
* request without a matching token is rejected 401.
|
|
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
|
|
29
|
-
res.writeHead(
|
|
30
|
-
res.end(JSON.stringify(
|
|
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
|
|
36
|
-
*
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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(
|
|
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
|
|