@hydradb/mcp 1.2.1 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Configuration for the remotely hostable HTTP transport.
3
+ *
4
+ * There are two configurations here, and they belong to two different people.
5
+ *
6
+ * - {@link resolveHttpServerConfig} is the OPERATOR's config: the port, the
7
+ * bind address, and the Host/Origin allowlists that decide who may reach the
8
+ * process at all. It is read once at startup from the environment.
9
+ *
10
+ * - {@link resolveRequestCredentials} is the CALLER's config: which Hydra DB
11
+ * account, database and collection a single request runs against. On a
12
+ * multi-tenant deployment (one process serving `mcp.hydradb.com` for many
13
+ * users) this MUST come per request, from headers, because the process has
14
+ * no single tenant of its own. It is resolved fresh on every `/mcp` call.
15
+ *
16
+ * The stdio server ({@link file://./index.ts}) has only the second concern and
17
+ * reads it from the environment via {@link file://./config.ts}; a hosted server
18
+ * cannot, because one env-configured key would make every user share one
19
+ * account. So the header path is the substantive new surface, and it falls back
20
+ * to the environment only to keep the single-tenant self-hosting story (set the
21
+ * env, expose the port, done) working unchanged.
22
+ */
23
+ import { DEFAULT_COLLECTION, nonNegativeInt, positiveInt, readEnv, resolveGraphConfig, } from "./config.js";
24
+ export const DEFAULT_PORT = 8080;
25
+ export const DEFAULT_BIND_ADDRESS = "127.0.0.1";
26
+ /** Split a comma-separated env var into trimmed, non-empty entries. */
27
+ export function parseList(value) {
28
+ if (!value)
29
+ return [];
30
+ return value
31
+ .split(",")
32
+ .map((v) => v.trim())
33
+ .filter((v) => v.length > 0);
34
+ }
35
+ /**
36
+ * A port, or the default when the value is absent or not a usable port.
37
+ *
38
+ * A typo'd `PORT` should not crash startup with a stack trace; it should fall
39
+ * back and let the startup banner show what it actually bound. Ports outside
40
+ * 1-65535 fall back for the same reason `parseInt("8080abc")` must not become
41
+ * `8080` silently — `Number` rejects the trailing garbage that `parseInt` keeps.
42
+ */
43
+ export function parsePort(raw, fallback = DEFAULT_PORT) {
44
+ const value = Number(raw);
45
+ return raw != null && Number.isInteger(value) && value >= 1 && value <= 65535
46
+ ? value
47
+ : fallback;
48
+ }
49
+ /**
50
+ * The set of `Host` headers this server answers to.
51
+ *
52
+ * Loopback names on the bound port are always included so a local run works with
53
+ * no configuration. A hosted deployment adds its public hostname(s) via
54
+ * `ALLOWED_HOSTS`. The bare (portless) loopback names cover clients that omit
55
+ * the port on the default HTTP/HTTPS port.
56
+ */
57
+ export function buildAllowedHosts(port, extra) {
58
+ return new Set([
59
+ `localhost:${port}`,
60
+ `127.0.0.1:${port}`,
61
+ `[::1]:${port}`,
62
+ "localhost",
63
+ "127.0.0.1",
64
+ "[::1]",
65
+ ...extra,
66
+ ].map((h) => h.toLowerCase()));
67
+ }
68
+ /**
69
+ * Interpret `TRUST_PROXY` for Express's `trust proxy` setting.
70
+ *
71
+ * Absent → `false` (trust nothing, the safe default). `true`/`false` → boolean.
72
+ * An integer → that many hops. Anything else is passed through as an Express
73
+ * preset or subnet list (e.g. `loopback`, `10.0.0.0/8`), so an operator can name
74
+ * exactly their proxy without this needing to know the syntax.
75
+ */
76
+ export function parseTrustProxy(raw) {
77
+ const value = raw?.trim();
78
+ if (!value)
79
+ return false;
80
+ if (value.toLowerCase() === "true")
81
+ return true;
82
+ if (value.toLowerCase() === "false")
83
+ return false;
84
+ if (/^\d+$/.test(value))
85
+ return Number(value);
86
+ return value;
87
+ }
88
+ export function resolveHttpServerConfig(env = process.env) {
89
+ const port = parsePort(env.PORT);
90
+ return {
91
+ port,
92
+ bindAddress: env.BIND_ADDRESS?.trim() || DEFAULT_BIND_ADDRESS,
93
+ allowedOrigins: parseList(env.ALLOWED_ORIGINS),
94
+ allowedHosts: buildAllowedHosts(port, parseList(env.ALLOWED_HOSTS)),
95
+ trustProxy: parseTrustProxy(env.TRUST_PROXY),
96
+ };
97
+ }
98
+ // --- Per-request tenant credentials ---
99
+ /**
100
+ * Header names a caller uses to select and authenticate against their tenant.
101
+ *
102
+ * Lowercased because Node lowercases incoming header names, and every lookup
103
+ * here goes through {@link headerValue} which reads them off `req.headers`.
104
+ *
105
+ * The API key is read from the standard `Authorization: Bearer <key>` first;
106
+ * `X-HydraDB-Api-Key` exists only for clients whose MCP config cannot set an
107
+ * `Authorization` header. Everything else — which database, which collection —
108
+ * has no standard header, so it takes an `X-HydraDB-*` one.
109
+ */
110
+ export const HEADER_AUTHORIZATION = "authorization";
111
+ export const HEADER_API_KEY = "x-hydradb-api-key";
112
+ export const HEADER_DATABASE = "x-hydradb-database";
113
+ export const HEADER_COLLECTION = "x-hydradb-collection";
114
+ export const HEADER_GRAPH_DATABASE = "x-hydradb-graph-database";
115
+ export const HEADER_GRAPH_COLLECTION = "x-hydradb-graph-collection";
116
+ /** One header value, taking the first when a header arrives repeated. */
117
+ function headerValue(headers, name) {
118
+ const raw = headers[name];
119
+ const value = Array.isArray(raw) ? raw[0] : raw;
120
+ const trimmed = value?.trim();
121
+ return trimmed ? trimmed : undefined;
122
+ }
123
+ /**
124
+ * The token from an `Authorization` header, accepting `Bearer <token>` or a bare
125
+ * token. The scheme is matched case-insensitively (RFC 7235 says it is
126
+ * case-insensitive, and clients send `bearer`, `Bearer` and `BEARER` in the
127
+ * wild); a bare value is accepted because some MCP hosts drop the scheme.
128
+ */
129
+ function bearerToken(authorization) {
130
+ if (!authorization)
131
+ return undefined;
132
+ const match = /^\s*Bearer\s+(.+)$/i.exec(authorization);
133
+ if (match)
134
+ return match[1].trim() || undefined;
135
+ return authorization.trim() || undefined;
136
+ }
137
+ /**
138
+ * Turn one request's headers (falling back to the environment) into the tenant
139
+ * credentials for that request.
140
+ *
141
+ * The API key and the database resolve TOGETHER, not field-by-field, which is
142
+ * the security-relevant part:
143
+ * - When the key comes from a request header, the request is
144
+ * "caller-authenticated" and its database MUST also come from a header. The
145
+ * env is NOT consulted for the database, so a caller who sends their own key
146
+ * but omits `X-HydraDB-Database` is refused (400) rather than silently run
147
+ * against the operator's env database. Likewise the graph scope defaults to
148
+ * the request's own database, never the operator's `HYDRADB_GRAPH_DATABASE`.
149
+ * - When there is no key header, the request is unauthenticated and falls back
150
+ * entirely to the operator's env credentials — the single-tenant self-host
151
+ * case, identical to the stdio server. A hosted, multi-tenant process sets
152
+ * no tenant env, so such a request is refused (401).
153
+ *
154
+ * Resolving field-by-field instead would let a caller's key pair with the
155
+ * operator's database (or graph namespace), mixing two identities into one
156
+ * request. `baseUrl`/`timeoutSeconds`/`maxRetries` are read from the environment
157
+ * only — they are the operator's, not the caller's (see {@link RequestCredentials}).
158
+ */
159
+ export function resolveRequestCredentials(headers, env = process.env) {
160
+ const headerKey = bearerToken(headerValue(headers, HEADER_AUTHORIZATION)) ??
161
+ headerValue(headers, HEADER_API_KEY);
162
+ // Whether the CALLER authenticated this request with their own key decides
163
+ // whether the operator's env is allowed to supply the rest of the identity.
164
+ const callerAuthenticated = headerKey != null;
165
+ const apiKey =
166
+ // The env fallback reuses the exact same canonical/legacy alias rules as
167
+ // the stdio server, so a self-host configured for stdio needs no new vars.
168
+ headerKey ?? readEnv(env, "HYDRADB_API_KEY", "HYDRA_DB_API_KEY", noopWarn);
169
+ if (!apiKey) {
170
+ return {
171
+ ok: false,
172
+ status: 401,
173
+ message: "Missing Hydra DB credentials. Send `Authorization: Bearer <api-key>` " +
174
+ "(or the `X-HydraDB-Api-Key` header). Get a key at https://app.hydradb.com.",
175
+ };
176
+ }
177
+ const headerDatabase = headerValue(headers, HEADER_DATABASE);
178
+ // A caller-authenticated request takes its database ONLY from the header;
179
+ // the env database belongs to the operator's identity, not the caller's.
180
+ const database = callerAuthenticated
181
+ ? headerDatabase
182
+ : (headerDatabase ?? readEnv(env, "HYDRADB_DATABASE", "HYDRA_DB_TENANT_ID", noopWarn));
183
+ if (!database) {
184
+ return {
185
+ ok: false,
186
+ status: 400,
187
+ message: "Missing Hydra DB database. Send the `X-HydraDB-Database` header naming " +
188
+ "the database (tenant) this request should run against.",
189
+ };
190
+ }
191
+ // Collection is a partition WITHIN the resolved database, not a cross-tenant
192
+ // boundary, so an env default is safe for either mode.
193
+ const collection = headerValue(headers, HEADER_COLLECTION) ??
194
+ readEnv(env, "HYDRADB_COLLECTION", "HYDRA_DB_SUB_TENANT_ID", noopWarn) ??
195
+ DEFAULT_COLLECTION;
196
+ // Operator-owned transport knobs. Same env vars, same parsing as resolveConfig.
197
+ const baseUrl = readEnv(env, "HYDRADB_BASE_URL", "HYDRA_DB_BASE_URL", noopWarn);
198
+ const timeoutSeconds = positiveInt(env.HYDRADB_TIMEOUT_SECONDS);
199
+ const maxRetries = nonNegativeInt(env.HYDRADB_MAX_RETRIES);
200
+ return {
201
+ ok: true,
202
+ credentials: {
203
+ apiKey,
204
+ database,
205
+ collection,
206
+ ...(baseUrl != null ? { baseUrl } : {}),
207
+ ...(timeoutSeconds != null ? { timeoutSeconds } : {}),
208
+ ...(maxRetries != null ? { maxRetries } : {}),
209
+ graph: resolveRequestGraphConfig(headers, env, database, callerAuthenticated),
210
+ },
211
+ };
212
+ }
213
+ /**
214
+ * Graph scope for one request: the operator's `enabled` flag, but the caller's
215
+ * database and collection.
216
+ *
217
+ * The graph database defaults to the request's OWN database, so a caller that
218
+ * names only `X-HydraDB-Database` gets a coherent graph scope without a second
219
+ * header. The operator's `HYDRADB_GRAPH_DATABASE` is honoured as that default
220
+ * ONLY for an unauthenticated (env-credential) request — for a caller-
221
+ * authenticated one it would pin every tenant to the operator's single graph
222
+ * namespace, the mixing this resolver exists to prevent. Header overrides always
223
+ * win, letting a caller address a graph in a different namespace than their
224
+ * memory database — which, because the two are genuinely separate stores, is a
225
+ * real need.
226
+ */
227
+ function resolveRequestGraphConfig(headers, env, database, callerAuthenticated) {
228
+ // For a caller-authenticated request the fallback database is the request's
229
+ // own, so `resolveGraphConfig`'s env default is deliberately not consulted.
230
+ const base = resolveGraphConfig(env, database);
231
+ const defaultDatabase = callerAuthenticated ? database : base.database;
232
+ return {
233
+ enabled: base.enabled,
234
+ database: headerValue(headers, HEADER_GRAPH_DATABASE) ?? defaultDatabase,
235
+ collection: headerValue(headers, HEADER_GRAPH_COLLECTION) ?? base.collection,
236
+ };
237
+ }
238
+ /**
239
+ * Credential resolution must not emit deprecation warnings.
240
+ *
241
+ * `readEnv` warns once per process when a legacy alias is used. On the header
242
+ * path the env is only a fallback and is read on EVERY request; routing its
243
+ * warning through the process-wide dedupe would still fire on the first request
244
+ * and, worse, tie a per-request code path to shared mutable warning state. The
245
+ * stdio/startup path already surfaces any legacy-alias warning, so silence here
246
+ * drops nothing.
247
+ */
248
+ function noopWarn() { }
249
+ /** A JSON-RPC error body, so every HTTP failure speaks the protocol's dialect. */
250
+ export function jsonRpcError(code, message) {
251
+ return { jsonrpc: "2.0", error: { code, message }, id: null };
252
+ }
253
+ //# sourceMappingURL=http-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-config.js","sourceRoot":"","sources":["../src/http-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EACN,kBAAkB,EAGlB,cAAc,EACd,WAAW,EACX,OAAO,EACP,kBAAkB,GAClB,MAAM,aAAa,CAAC;AAyBrB,MAAM,CAAC,MAAM,YAAY,GAAG,IAAI,CAAC;AACjC,MAAM,CAAC,MAAM,oBAAoB,GAAG,WAAW,CAAC;AAEhD,uEAAuE;AACvE,MAAM,UAAU,SAAS,CAAC,KAAyB;IAClD,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,CAAC;IACtB,OAAO,KAAK;SACV,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CAAC,GAAuB,EAAE,QAAQ,GAAG,YAAY;IACzE,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,GAAG,IAAI,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,KAAK,IAAI,KAAM;QAC7E,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,QAAQ,CAAC;AACb,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,KAAe;IAC9D,OAAO,IAAI,GAAG,CACb;QACC,aAAa,IAAI,EAAE;QACnB,aAAa,IAAI,EAAE;QACnB,SAAS,IAAI,EAAE;QACf,WAAW;QACX,WAAW;QACX,OAAO;QACP,GAAG,KAAK;KACR,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAC7B,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,GAAuB;IACtD,MAAM,KAAK,GAAG,GAAG,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,MAAM;QAAE,OAAO,IAAI,CAAC;IAChD,IAAI,KAAK,CAAC,WAAW,EAAE,KAAK,OAAO;QAAE,OAAO,KAAK,CAAC;IAClD,IAAI,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IAC9C,OAAO,KAAK,CAAC;AACd,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAiB,OAAO,CAAC,GAAG;IACnE,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO;QACN,IAAI;QACJ,WAAW,EAAE,GAAG,CAAC,YAAY,EAAE,IAAI,EAAE,IAAI,oBAAoB;QAC7D,cAAc,EAAE,SAAS,CAAC,GAAG,CAAC,eAAe,CAAC;QAC9C,YAAY,EAAE,iBAAiB,CAAC,IAAI,EAAE,SAAS,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACnE,UAAU,EAAE,eAAe,CAAC,GAAG,CAAC,WAAW,CAAC;KAC5C,CAAC;AACH,CAAC;AAED,yCAAyC;AAEzC;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAC;AACpD,MAAM,CAAC,MAAM,cAAc,GAAG,mBAAmB,CAAC;AAClD,MAAM,CAAC,MAAM,eAAe,GAAG,oBAAoB,CAAC;AACpD,MAAM,CAAC,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AACxD,MAAM,CAAC,MAAM,qBAAqB,GAAG,0BAA0B,CAAC;AAChE,MAAM,CAAC,MAAM,uBAAuB,GAAG,4BAA4B,CAAC;AAoCpE,yEAAyE;AACzE,SAAS,WAAW,CAAC,OAAuB,EAAE,IAAY;IACzD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAChD,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;AACtC,CAAC;AAED;;;;;GAKG;AACH,SAAS,WAAW,CAAC,aAAiC;IACrD,IAAI,CAAC,aAAa;QAAE,OAAO,SAAS,CAAC;IACrC,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;IACxD,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;IAC/C,OAAO,aAAa,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,yBAAyB,CACxC,OAAuB,EACvB,MAAiB,OAAO,CAAC,GAAG;IAE5B,MAAM,SAAS,GACd,WAAW,CAAC,WAAW,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAC;QACvD,WAAW,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IACtC,2EAA2E;IAC3E,4EAA4E;IAC5E,MAAM,mBAAmB,GAAG,SAAS,IAAI,IAAI,CAAC;IAE9C,MAAM,MAAM;IACX,yEAAyE;IACzE,2EAA2E;IAC3E,SAAS,IAAI,OAAO,CAAC,GAAG,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,QAAQ,CAAC,CAAC;IAC5E,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,OAAO;YACN,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,OAAO,EACN,uEAAuE;gBACvE,4EAA4E;SAC7E,CAAC;IACH,CAAC;IAED,MAAM,cAAc,GAAG,WAAW,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IAC7D,0EAA0E;IAC1E,yEAAyE;IACzE,MAAM,QAAQ,GAAG,mBAAmB;QACnC,CAAC,CAAC,cAAc;QAChB,CAAC,CAAC,CAAC,cAAc,IAAI,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,QAAQ,CAAC,CAAC,CAAC;IACxF,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,OAAO;YACN,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG;YACX,OAAO,EACN,yEAAyE;gBACzE,wDAAwD;SACzD,CAAC;IACH,CAAC;IAED,6EAA6E;IAC7E,uDAAuD;IACvD,MAAM,UAAU,GACf,WAAW,CAAC,OAAO,EAAE,iBAAiB,CAAC;QACvC,OAAO,CAAC,GAAG,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,QAAQ,CAAC;QACtE,kBAAkB,CAAC;IAEpB,gFAAgF;IAChF,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,QAAQ,CAAC,CAAC;IAChF,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IAChE,MAAM,UAAU,GAAG,cAAc,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAE3D,OAAO;QACN,EAAE,EAAE,IAAI;QACR,WAAW,EAAE;YACZ,MAAM;YACN,QAAQ;YACR,UAAU;YACV,GAAG,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACvC,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACrD,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7C,KAAK,EAAE,yBAAyB,CAAC,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,mBAAmB,CAAC;SAC7E;KACD,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,SAAS,yBAAyB,CACjC,OAAuB,EACvB,GAAc,EACd,QAAgB,EAChB,mBAA4B;IAE5B,4EAA4E;IAC5E,4EAA4E;IAC5E,MAAM,IAAI,GAAG,kBAAkB,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IAC/C,MAAM,eAAe,GAAG,mBAAmB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC;IACvE,OAAO;QACN,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,QAAQ,EAAE,WAAW,CAAC,OAAO,EAAE,qBAAqB,CAAC,IAAI,eAAe;QACxE,UAAU,EAAE,WAAW,CAAC,OAAO,EAAE,uBAAuB,CAAC,IAAI,IAAI,CAAC,UAAU;KAC5E,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,QAAQ,KAAU,CAAC;AAE5B,kFAAkF;AAClF,MAAM,UAAU,YAAY,CAAC,IAAY,EAAE,OAAe;IAKzD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AAC/D,CAAC"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The remotely hostable HTTP transport.
4
+ *
5
+ * This is the server behind a URL like `https://mcp.hydradb.com/mcp`: instead of
6
+ * every user installing and spawning the stdio binary ({@link file://./index.ts}),
7
+ * one process answers MCP over HTTP and each user points their client at the URL.
8
+ *
9
+ * It reuses the whole tool surface unchanged — {@link createHydraDBServer} builds
10
+ * exactly the same server the stdio path does. The only things this file adds are
11
+ * the ones a network endpoint needs and a pipe does not: a Host/Origin
12
+ * allowlist, CORS, and PER-REQUEST tenant credentials (see
13
+ * {@link file://./http-config.ts}), because one hosted process has no single
14
+ * ambient account to run as.
15
+ *
16
+ * Sessions are stateless: MCP's Protocol object binds to one transport, so a
17
+ * shared process serving many independent callers builds a fresh server +
18
+ * transport per request and tears it down when the response closes. That is the
19
+ * transport's documented stateless mode (`sessionIdGenerator: undefined`).
20
+ */
21
+ import { type Express } from "express";
22
+ import { buildAllowedHosts, type HttpServerConfig, parseList, parsePort, resolveHttpServerConfig } from "./http-config.js";
23
+ /**
24
+ * Build the Express app for the HTTP transport.
25
+ *
26
+ * Exported (and taking its config as an argument rather than reading the
27
+ * environment) so tests exercise the exact wiring production runs, against an
28
+ * arbitrary allowlist, with no process-global state.
29
+ */
30
+ export declare function createHttpApp(config: HttpServerConfig): Express;
31
+ export { buildAllowedHosts, parseList, parsePort, resolveHttpServerConfig };
package/dist/http.js ADDED
@@ -0,0 +1,350 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * The remotely hostable HTTP transport.
4
+ *
5
+ * This is the server behind a URL like `https://mcp.hydradb.com/mcp`: instead of
6
+ * every user installing and spawning the stdio binary ({@link file://./index.ts}),
7
+ * one process answers MCP over HTTP and each user points their client at the URL.
8
+ *
9
+ * It reuses the whole tool surface unchanged — {@link createHydraDBServer} builds
10
+ * exactly the same server the stdio path does. The only things this file adds are
11
+ * the ones a network endpoint needs and a pipe does not: a Host/Origin
12
+ * allowlist, CORS, and PER-REQUEST tenant credentials (see
13
+ * {@link file://./http-config.ts}), because one hosted process has no single
14
+ * ambient account to run as.
15
+ *
16
+ * Sessions are stateless: MCP's Protocol object binds to one transport, so a
17
+ * shared process serving many independent callers builds a fresh server +
18
+ * transport per request and tears it down when the response closes. That is the
19
+ * transport's documented stateless mode (`sessionIdGenerator: undefined`).
20
+ */
21
+ import cors from "cors";
22
+ import express from "express";
23
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
24
+ import { HydraDB } from "./hydra/index.js";
25
+ import { buildAllowedHosts, jsonRpcError, parseList, parsePort, resolveHttpServerConfig, resolveRequestCredentials, } from "./http-config.js";
26
+ import { logger } from "./logger.js";
27
+ import { awaitInFlight, beginShutdown, createHydraDBServer, inFlightCount, } from "./server.js";
28
+ /**
29
+ * The largest request body accepted before parsing.
30
+ *
31
+ * Sized above the tool layer's own ceilings — memory ingest caps `text` at 1M
32
+ * characters (~4 MB as UTF-8 with a JSON envelope), which is the biggest
33
+ * legitimate body — so a valid large ingest is not rejected at the door while an
34
+ * unbounded body cannot exhaust memory. Anything genuinely oversized is still
35
+ * refused by the per-tool checks with a message naming the real limit.
36
+ */
37
+ const MAX_REQUEST_BODY = "8mb";
38
+ /**
39
+ * Startup banners and security warnings go straight to stderr, not through
40
+ * `logger`.
41
+ *
42
+ * `logger` is gated by `HYDRADB_LOG_LEVEL`, which defaults to ERROR, so a
43
+ * `logger.warn` about a public bind would be invisible in the exact default
44
+ * configuration where it matters most. These lines must surface regardless of
45
+ * level — the same reason the deprecated-alias warnings bypass the logger — so
46
+ * they use `console.error` (stderr) directly. Per-request and lifecycle logging
47
+ * still goes through `logger`.
48
+ */
49
+ function banner(message) {
50
+ console.error(`[hydradb-mcp] ${message}`);
51
+ }
52
+ /** JSON-RPC error codes used for transport-level failures (spec: -32000 range). */
53
+ const JSONRPC_UNAUTHORIZED = -32001;
54
+ const JSONRPC_BAD_REQUEST = -32602;
55
+ const JSONRPC_INTERNAL_ERROR = -32603;
56
+ const JSONRPC_MISDIRECTED = -32000;
57
+ /**
58
+ * Build the Express app for the HTTP transport.
59
+ *
60
+ * Exported (and taking its config as an argument rather than reading the
61
+ * environment) so tests exercise the exact wiring production runs, against an
62
+ * arbitrary allowlist, with no process-global state.
63
+ */
64
+ export function createHttpApp(config) {
65
+ const { bindAddress, allowedOrigins, allowedHosts } = config;
66
+ const allowsAllOrigins = allowedOrigins.includes("*");
67
+ const app = express();
68
+ // Off by default so a direct client cannot spoof `X-Forwarded-*`; a
69
+ // deployment behind a known proxy sets TRUST_PROXY to recover the real client
70
+ // IP without trusting every hop. Only logging and `req.protocol` read it — the
71
+ // Host allowlist, not a proxy header, is the DNS-rebinding defence.
72
+ app.set("trust proxy", config.trustProxy);
73
+ // The Host allowlist is the actual defence; the fingerprinting header is noise.
74
+ app.disable("x-powered-by");
75
+ // --- Host header allowlist (runs first, before CORS) ---
76
+ //
77
+ // A DNS-rebinding defence: without it, a page the user visits can point a
78
+ // hostname it controls at 127.0.0.1 and drive a locally bound server. The
79
+ // check is the operator's ALLOWED_HOSTS plus loopback; a hosted deployment
80
+ // adds its public hostname. 421 Misdirected Request is the status for "this
81
+ // server does not answer to that authority".
82
+ app.use((req, res, next) => {
83
+ const host = req.headers.host;
84
+ if (!host || !allowedHosts.has(host.toLowerCase())) {
85
+ logger.warn("rejected request with disallowed Host header", {
86
+ host,
87
+ path: req.path,
88
+ });
89
+ res
90
+ .status(421)
91
+ .json(jsonRpcError(JSONRPC_MISDIRECTED, "Misdirected request"));
92
+ return;
93
+ }
94
+ next();
95
+ });
96
+ // --- CORS ---
97
+ //
98
+ // A distinct error type so the error handler below can tell an origin
99
+ // rejection apart from any other downstream failure and answer it with 403.
100
+ class CorsOriginNotAllowedError extends Error {
101
+ constructor(origin) {
102
+ super(`Origin ${origin} not allowed by CORS`);
103
+ this.origin = origin;
104
+ this.name = "CorsOriginNotAllowedError";
105
+ }
106
+ }
107
+ app.use(cors({
108
+ origin: (origin, callback) => {
109
+ // No Origin header: a non-browser client or a same-origin request.
110
+ // These are not subject to CORS and are allowed through.
111
+ if (!origin)
112
+ return callback(null, true);
113
+ // The opaque `null` origin (sandboxed iframe, file://) is only
114
+ // honoured when the operator lists it explicitly.
115
+ if (origin === "null") {
116
+ return allowedOrigins.includes("null")
117
+ ? callback(null, true)
118
+ : callback(new CorsOriginNotAllowedError("null"));
119
+ }
120
+ if (allowsAllOrigins || allowedOrigins.includes(origin)) {
121
+ return callback(null, true);
122
+ }
123
+ return callback(new CorsOriginNotAllowedError(origin));
124
+ },
125
+ // The client reads the session id and negotiated protocol version off
126
+ // the response; without exposing them a browser cannot complete a session.
127
+ exposedHeaders: ["Mcp-Session-Id", "Mcp-Protocol-Version"],
128
+ allowedHeaders: [
129
+ "Content-Type",
130
+ "Authorization",
131
+ "Mcp-Session-Id",
132
+ "Mcp-Protocol-Version",
133
+ "X-HydraDB-Api-Key",
134
+ "X-HydraDB-Database",
135
+ "X-HydraDB-Collection",
136
+ "X-HydraDB-Graph-Database",
137
+ "X-HydraDB-Graph-Collection",
138
+ ],
139
+ }));
140
+ // Turn a CORS origin rejection into an explicit 403 with a JSON-RPC body,
141
+ // mirroring the 421 the Host check emits. Placed right after cors so any
142
+ // other error still reaches Express's default handler unchanged.
143
+ app.use((err, req, res, next) => {
144
+ if (err instanceof CorsOriginNotAllowedError) {
145
+ logger.warn("rejected request with disallowed Origin", {
146
+ origin: err.origin,
147
+ path: req.path,
148
+ });
149
+ res
150
+ .status(403)
151
+ .json(jsonRpcError(JSONRPC_MISDIRECTED, "Origin not allowed"));
152
+ return;
153
+ }
154
+ next(err);
155
+ });
156
+ app.use(express.json({ limit: MAX_REQUEST_BODY }));
157
+ // --- The MCP endpoint ---
158
+ // Serves MCP at both the root `/` (e.g. https://mcp.hydradb.com) and `/mcp`
159
+ // so clients pointing at either URL connect seamlessly.
160
+ app.all(["/", "/mcp"], async (req, res) => {
161
+ // Who is this request for? On a hosted process the answer lives entirely
162
+ // in the request, so it is resolved here and a missing/incomplete answer
163
+ // is refused before any server is built.
164
+ const resolution = resolveRequestCredentials(req.headers, process.env);
165
+ if (!resolution.ok) {
166
+ // 401 gets a WWW-Authenticate header so a spec-compliant client knows
167
+ // how to authenticate rather than just seeing a bare refusal.
168
+ if (resolution.status === 401) {
169
+ res.setHeader("WWW-Authenticate", 'Bearer realm="Hydra DB MCP"');
170
+ }
171
+ res
172
+ .status(resolution.status)
173
+ .json(jsonRpcError(resolution.status === 401
174
+ ? JSONRPC_UNAUTHORIZED
175
+ : JSONRPC_BAD_REQUEST, resolution.message));
176
+ return;
177
+ }
178
+ const creds = resolution.credentials;
179
+ try {
180
+ const hydra = new HydraDB({
181
+ token: creds.apiKey,
182
+ database: creds.database,
183
+ collection: creds.collection,
184
+ ...(creds.baseUrl != null ? { baseUrl: creds.baseUrl } : {}),
185
+ ...(creds.timeoutSeconds != null
186
+ ? { timeoutSeconds: creds.timeoutSeconds }
187
+ : {}),
188
+ ...(creds.maxRetries != null ? { maxRetries: creds.maxRetries } : {}),
189
+ });
190
+ const server = createHydraDBServer(hydra, creds.graph);
191
+ // Stateless: this pair serves exactly this request and is discarded when
192
+ // the response closes. Tearing them down on `close` — which fires for a
193
+ // clean end AND for the error path below (it sends a response, which then
194
+ // closes) — is what frees the per-request state; without it a long-lived
195
+ // process leaks a server per call. It is the single teardown point, so
196
+ // nothing here double-closes. `close()` returns a promise, and a stray
197
+ // rejection would take the whole process down via `unhandledRejection`, so
198
+ // it is explicitly swallowed.
199
+ const transport = new StreamableHTTPServerTransport({
200
+ sessionIdGenerator: undefined,
201
+ enableJsonResponse: true,
202
+ });
203
+ res.on("close", () => {
204
+ transport.close().catch(() => { });
205
+ server.close().catch(() => { });
206
+ });
207
+ await server.connect(transport);
208
+ await transport.handleRequest(req, res, req.body);
209
+ }
210
+ catch (error) {
211
+ logger.error("error handling MCP request", {
212
+ error: error instanceof Error ? error.message : String(error),
213
+ });
214
+ // Do not close here: the `res.on("close")` handler above owns teardown,
215
+ // and the transport may still be mid-write. Only the transport writes the
216
+ // JSON-RPC body, so this responds solely when nothing has been sent yet
217
+ // AND the socket is still open — a client that aborted mid-request lands
218
+ // here too, and writing to its closed socket is pointless. Ending the
219
+ // response then triggers that one teardown.
220
+ if (!res.headersSent && !res.writableEnded) {
221
+ res
222
+ .status(500)
223
+ .json(jsonRpcError(JSONRPC_INTERNAL_ERROR, "Internal server error"));
224
+ }
225
+ }
226
+ });
227
+ // A liveness probe for load balancers and container orchestrators. It says
228
+ // nothing about Hydra DB reachability on purpose — credentials are per
229
+ // request, so there is no single upstream this endpoint could check.
230
+ app.get("/health", (_req, res) => {
231
+ res.json({ status: "ok", service: "hydradb-mcp" });
232
+ });
233
+ // `express.json` throws on a malformed body (`entity.parse.failed`) or one
234
+ // over the limit (`entity.too.large`). Registered after the routes so it
235
+ // catches those, it keeps every refusal on this server speaking JSON-RPC
236
+ // rather than letting Express answer with its default HTML error page. Any
237
+ // other error falls through to Express's default handler unchanged.
238
+ app.use((err, _req, res, next) => {
239
+ if (err.type === "entity.parse.failed") {
240
+ res
241
+ .status(400)
242
+ .json(jsonRpcError(JSONRPC_BAD_REQUEST, "Request body is not valid JSON"));
243
+ return;
244
+ }
245
+ if (err.type === "entity.too.large") {
246
+ res
247
+ .status(413)
248
+ .json(jsonRpcError(JSONRPC_BAD_REQUEST, `Request body exceeds the ${MAX_REQUEST_BODY} limit`));
249
+ return;
250
+ }
251
+ next(err);
252
+ });
253
+ if (bindAddress === "0.0.0.0" || bindAddress === "::") {
254
+ banner(`WARNING: BIND_ADDRESS=${bindAddress} exposes the server on all network interfaces — ` +
255
+ "set ALLOWED_HOSTS/ALLOWED_ORIGINS and put it behind TLS. See SECURITY.md.");
256
+ }
257
+ if (allowsAllOrigins) {
258
+ banner('WARNING: ALLOWED_ORIGINS contains "*" — any website may call this server. See SECURITY.md.');
259
+ }
260
+ return app;
261
+ }
262
+ /**
263
+ * Wire graceful shutdown for the HTTP server.
264
+ *
265
+ * The in-flight bookkeeping is shared with the stdio path (it is module state in
266
+ * {@link file://./server.js}), so this drains accepted tool calls the same way:
267
+ * stop accepting, close the listener, wait for running handlers, then exit. An
268
+ * ingest cut off mid-write leaves the caller unable to tell whether it
269
+ * committed, which under upsert is not answerable by retrying.
270
+ */
271
+ const SHUTDOWN_GRACE_MS = 10000;
272
+ function installLifecycle(httpServer) {
273
+ let shuttingDown = false;
274
+ const shutdown = async (signal) => {
275
+ if (shuttingDown) {
276
+ logger.warn(`${signal} received again — exiting immediately`);
277
+ process.exit(130);
278
+ }
279
+ shuttingDown = true;
280
+ beginShutdown();
281
+ logger.info(`${signal} received — shutting down`);
282
+ const timer = setTimeout(() => {
283
+ logger.warn(`in-flight work did not finish within ${SHUTDOWN_GRACE_MS}ms — exiting anyway`);
284
+ process.exit(0);
285
+ }, SHUTDOWN_GRACE_MS);
286
+ timer.unref();
287
+ // Stop accepting new connections, then drain the calls already running.
288
+ httpServer.close();
289
+ const pending = inFlightCount();
290
+ if (pending > 0) {
291
+ logger.info(`waiting for ${pending} in-flight tool call(s)`);
292
+ await awaitInFlight();
293
+ }
294
+ clearTimeout(timer);
295
+ process.exit(0);
296
+ };
297
+ process.on("SIGINT", () => void shutdown("SIGINT"));
298
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
299
+ process.on("unhandledRejection", (reason) => {
300
+ logger.error("unhandled promise rejection", {
301
+ error: reason instanceof Error ? (reason.stack ?? reason.message) : String(reason),
302
+ });
303
+ process.exit(1);
304
+ });
305
+ process.on("uncaughtException", (error) => {
306
+ logger.error("uncaught exception", { error: error.stack ?? error.message });
307
+ process.exit(1);
308
+ });
309
+ }
310
+ function main() {
311
+ const config = resolveHttpServerConfig();
312
+ // A public bind WITH tenant credentials in the env is the one genuinely
313
+ // dangerous combination: every unauthenticated request would run under that
314
+ // account. It is legitimate for a single-tenant self-host, so this warns
315
+ // rather than refuses — but a multi-tenant operator must see it.
316
+ const publicBind = config.bindAddress === "0.0.0.0" || config.bindAddress === "::";
317
+ if (publicBind && (process.env.HYDRADB_API_KEY || process.env.HYDRA_DB_API_KEY)) {
318
+ banner("WARNING: HYDRADB_API_KEY is set while binding publicly — every request that " +
319
+ "sends no `Authorization` header will run under this account. Unset it for a " +
320
+ "multi-tenant deployment so each caller must authenticate. See SECURITY.md.");
321
+ }
322
+ const app = createHttpApp(config);
323
+ const httpServer = app
324
+ .listen(config.port, config.bindAddress, () => {
325
+ // Startup banners: on stderr so an operator sees where it bound
326
+ // regardless of HYDRADB_LOG_LEVEL.
327
+ banner(`listening on http://${config.bindAddress}:${config.port}`);
328
+ banner(`allowed origins: ${config.allowedOrigins.length > 0
329
+ ? config.allowedOrigins.join(", ")
330
+ : "(none — cross-origin browser requests will be rejected)"}`);
331
+ })
332
+ .on("error", (error) => {
333
+ logger.error("HTTP server error", { error: error.message });
334
+ process.exit(1);
335
+ });
336
+ installLifecycle(httpServer);
337
+ }
338
+ // Only auto-start when run as a script, so tests can import `createHttpApp`
339
+ // without binding a port. Covers `node dist/http.js`, `tsx src/http.ts`, and
340
+ // the Docker entrypoint.
341
+ const invokedAsScript = import.meta.url === `file://${process.argv[1]}` ||
342
+ process.argv[1]?.endsWith("/http.js") ||
343
+ process.argv[1]?.endsWith("/http.ts");
344
+ if (invokedAsScript) {
345
+ main();
346
+ }
347
+ // Re-exported so callers importing the HTTP entry point get the config helpers
348
+ // from one place.
349
+ export { buildAllowedHosts, parseList, parsePort, resolveHttpServerConfig };
350
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"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"}