@hydradb/mcp 1.2.1 → 1.3.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,147 @@ All notable changes to this project are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
9
+
10
+ ## [1.3.0] - 2026-08-28
11
+
12
+ ### Added — OAuth resource server ("Sign in with HydraDB")
13
+
14
+ The hosted server can now take part in the MCP authorization flow (PRO-1790).
15
+ Given `HYDRADB_OAUTH_ISSUER`, `HYDRADB_MCP_PUBLIC_URL` and
16
+ `HYDRADB_OAUTH_INTROSPECTION_SECRET`, it serves an RFC 9728 Protected Resource
17
+ Metadata document at `/.well-known/oauth-protected-resource`, points every
18
+ `401` at it through `WWW-Authenticate`, and accepts `hmat_` access tokens
19
+ issued by the HydraDB dashboard, exchanging each (memoised, bounded by the
20
+ token's own expiry) for the API key and database the user approved on the
21
+ consent screen. Tokens minted for another audience are refused; an unreachable
22
+ issuer answers `503` rather than a `401` blaming the caller.
23
+
24
+ Entirely additive. With the three variables unset the server registers no new
25
+ routes and sends no new headers, and every existing path — API keys in
26
+ `Authorization` or `X-HydraDB-Api-Key`, the `X-HydraDB-Database` and collection
27
+ and graph headers, and the single-tenant environment fallback — behaves exactly
28
+ as before whether OAuth is configured or not.
29
+
30
+ ## [1.2.2] - 2026-08-19
31
+
32
+ ### Added — per-call database and collection scope overrides
33
+
34
+ Every context tool (`hydradb_query`, `hydradb_ingest`, `hydradb_list`,
35
+ `hydradb_inspect`, `hydradb_delete`, `hydradb_status`, and deprecated aliases)
36
+ now accepts optional `database` and `collection` arguments directly in its payload.
37
+ This allows multi-tenant SaaS agents to dynamically switch tenant/collection scopes
38
+ per request within a single persistent MCP session, while falling back to the session
39
+ defaults when omitted.
40
+
41
+ ### Added — remote HTTP transport (hosted server)
42
+
43
+ The server can now run as a long-lived **HTTP endpoint** that many clients reach
44
+ at one URL (`https://mcp.hydradb.com`), alongside the existing stdio (`npx`) binary.
45
+ This is what a hosted deployment runs, and what `npm run start:http` or the new
46
+ Docker image runs locally — nothing to install per user.
47
+
48
+ The tool surface is unchanged: the HTTP server builds the exact same server as
49
+ stdio via `createHydraDBServer`. Only connection and auth are new.
50
+
51
+ - **Root endpoint and backward-compatible alias.** The server serves the MCP
52
+ Streamable HTTP transport directly on `/` and `/mcp`.
53
+ - **Per-request, multi-tenant credentials.** A hosted process has no single
54
+ ambient account, so each request selects its tenant with headers —
55
+ `Authorization: Bearer <api-key>` (or `X-HydraDB-Api-Key`) and
56
+ `X-HydraDB-Database`, with optional `X-HydraDB-Collection` and
57
+ `X-HydraDB-Graph-*`. A request that authenticates nobody is refused (`401`);
58
+ one that names no database is `400`. A single-tenant self-host may instead set
59
+ `HYDRADB_API_KEY`/`HYDRADB_DATABASE` in the environment and clients send no
60
+ credentials — the header path falls back to it.
61
+ - **Safe-by-default network posture.** Binds loopback unless `BIND_ADDRESS` is
62
+ set; a `Host` allowlist (`ALLOWED_HOSTS`, loopback always) answers stray
63
+ authorities with `421`; CORS is closed until `ALLOWED_ORIGINS` lists an
64
+ origin. `Base URL`/timeout/retries stay operator-only env settings and are
65
+ never read from a request header.
66
+ - New: `src/http.ts` (Express app + lifecycle), `src/http-config.ts`
67
+ (operator + per-request config resolution), `Dockerfile`, `.dockerignore`,
68
+ `npm run start:http` / `dev:http`, and a `hydradb-mcp-http` bin. Runtime deps
69
+ `express` and `cors` were added.
70
+
71
+ ### Added — graph (BYOG) tools
72
+
73
+ Hydra DB's **graph database** offering is now reachable from the MCP. Previously
74
+ this server exposed only the application offering (memory and knowledge); the
75
+ property graphs users model and own end to end, queried in Cypher, had no client
76
+ surface at all despite being [documented](https://docs.hydradb.com/essentials/v2/graph-collections-byog)
77
+ and live.
78
+
79
+ Three new tools, all additive — nothing about the existing six changed:
80
+
81
+ | Tool | Annotation | What it does |
82
+ |---|---|---|
83
+ | `hydradb_graph_query` | `destructiveHint` | Cypher, reads and writes alike |
84
+ | `hydradb_graph_collections` | `readOnlyHint` | List the graphs in a graph database |
85
+ | `hydradb_graph_admin` | `destructiveHint` | Create a graph database; drop a collection or database |
86
+
87
+ This covers [Neo4j's MCP server](https://github.com/neo4j-contrib/mcp-neo4j)
88
+ `read_neo4j_cypher` and `write_neo4j_cypher`, plus the graph database lifecycle
89
+ its Aura server covers. Two of its capabilities are deliberately **not**
90
+ reproduced:
91
+
92
+ **One Cypher tool, not a read/write pair.** Neo4j splits `read_neo4j_cypher`
93
+ from `write_neo4j_cypher` so a host can auto-approve one and gate the other.
94
+ That split is only sound if the read/write classifier is right on every query,
95
+ and any classifier over Cypher text is a heuristic — Neo4j's own is a substring
96
+ scan that refuses `MATCH (p:Person) WHERE p.name = "CREATE something" RETURN
97
+ p.name`, a query HydraDB accepts and that mutates nothing. Rather than ship a
98
+ tool whose contract ("this one never writes") rests on a heuristic, there is one
99
+ tool, annotated destructive, and the host gates the whole graph surface. No
100
+ classifier ships at all.
101
+
102
+ **No schema tool.** Neo4j's `get_neo4j_schema` runs `CALL apoc.meta.schema()`,
103
+ which HydraDB rejects outright. A derived equivalent is not part of the product,
104
+ so none ships. Callers discover a collection's structure the same way they query
105
+ it — `MATCH (n) UNWIND labels(n) AS l RETURN l, count(*) AS c ORDER BY l` — and
106
+ the tool description says so.
107
+
108
+ Also worth recording: **`EXPLAIN` is not a preview.** `EXPLAIN MATCH (p:Person)
109
+ RETURN p` returns live rows rather than a plan, so it is documented as something
110
+ not to reach for.
111
+
112
+ The 256 KiB body cap IS enforced before upload, since the remote `413` arrives
113
+ only after the whole oversized batch has been sent — that is a transport fact
114
+ the client owns, not a rule about what Cypher means.
115
+
116
+ Registered by default, with one switch: `HYDRADB_MCP_GRAPH_TOOLS=0` withholds
117
+ all three, for memory-only users who do not want the extra tool definitions in
118
+ every conversation.
119
+
120
+ **The client does not inspect your Cypher.** No read/write classification, no
121
+ read-only mode, no local pre-rejection of unsupported constructs. All of those
122
+ would put a second, worse implementation of the server's rules inside a client,
123
+ able only to agree with the server or to be wrong — and being wrong means
124
+ refusing a query HydraDB would have run. The server rejects unsupported
125
+ constructs before executing anything (verified: a query mixing `CREATE` with a
126
+ procedure call leaves the node count unchanged) and its messages are more
127
+ specific than the ones this server used to produce. Queries are sent verbatim.
128
+
129
+ Withholding the tools is the only lockdown offered, because it is the only one
130
+ that is actually a guarantee.
131
+
132
+ New configuration: `HYDRADB_GRAPH_DATABASE` (falls back to `HYDRADB_DATABASE`)
133
+ and `HYDRADB_GRAPH_COLLECTION` (defaults to `default`). A graph database is a
134
+ different namespace from the memory database, so every graph tool also accepts
135
+ `database` and `collection` per call.
136
+
137
+ ### Internal
138
+
139
+ - `HydraDB.graph` is a hand-rolled HTTP path, not an SDK call: `@hydradb/sdk`
140
+ at the pinned 2.1.2 has no `byog` resource, so the endpoints are unreachable
141
+ through it. It sits behind the same wrapper surface, unwraps the same envelope
142
+ by shape and raises the same `HydraWrapperError`, so callers cannot tell which
143
+ methods go through the SDK. When the SDK grows a `byog` resource, that one file
144
+ is replaced and nothing above it changes. The exact SDK pin is unaffected —
145
+ there is no generated name to be insulated from yet.
146
+ - `responseError()` added to the wrapper's error module so failures from the
147
+ non-SDK path are formatted through the same code as SDK failures, keeping the
148
+ error code, the server's message and the request id.
8
149
  ## [1.2.1] - 2026-08-17
9
150
 
10
151
  ### Fixed
package/README.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  MCP (Model Context Protocol) server for [Hydra DB](https://hydradb.com), the state-of-the-art agentic memory. Provides tools for storing, recalling, and managing memories with knowledge-graph enriched context.
4
4
 
5
+ Run it two ways, same tools either way:
6
+
7
+ - **Local (stdio)** — the `npx @hydradb/mcp` binary each client spawns. No server to operate; credentials live in the client's config. This is the default and everything below the [Configuration](#configuration) section documents it.
8
+ - **Remote (HTTP)** — one hosted process behind a URL like `https://mcp.hydradb.com` that many clients point at, with nothing to install. See [**Remote / hosted server**](#remote--hosted-server).
9
+
5
10
  ## Available Tools
6
11
 
7
12
  | Tool | What it does |
@@ -16,6 +21,44 @@ MCP (Model Context Protocol) server for [Hydra DB](https://hydradb.com), the sta
16
21
  Ids flow between these: `hydradb_query` and `hydradb_list` emit them;
17
22
  `hydradb_inspect`, `hydradb_delete` and `hydradb_status` accept them.
18
23
 
24
+ ### Graph tools (Cypher)
25
+
26
+ Hydra DB also runs **property graphs you model and own end to end**, queried in [Cypher](https://docs.hydradb.com/essentials/v2/graph-collections-byog). This is a different product surface from the memory and knowledge above, and nothing crosses between them: `hydradb_query` cannot see graph data, and `hydradb_graph_query` cannot see memories.
27
+
28
+ | Tool | What it does |
29
+ |---|---|
30
+ | `hydradb_graph_query` | Run Cypher — reads **and** writes |
31
+ | `hydradb_graph_collections` | List the graphs in a graph database |
32
+ | `hydradb_graph_admin` | Create a graph database; drop a collection or a database |
33
+
34
+ `hydradb_graph_query` is annotated `destructiveHint`, because it runs arbitrary Cypher and `DELETE` is as reachable through it as `MATCH`. There is deliberately **no** separate read-only Cypher tool and **no read-only mode**: both would mean classifying Cypher text client-side to decide what to refuse, which is a heuristic — a promise the server can keep and a client cannot. This server does not inspect your query at all; it sends it and reports what HydraDB says. To lock the graph surface down, withhold the tools (below) — that is a real guarantee.
35
+
36
+ ```jsonc
37
+ // Everyone Alice knows within four hops
38
+ {"query": "MATCH (a:Person {name:$n})-[:KNOWS*1..4]->(r) RETURN DISTINCT r.name AS name",
39
+ "params": {"n": "Alice"}}
40
+
41
+ // Bulk load, re-runnable after a failure
42
+ {"query": "UNWIND $rows AS row MERGE (p:Person {ext_id: row.ext_id}) SET p += row",
43
+ "params": {"rows": [{"ext_id": "a", "name": "Alice"}]}}
44
+ ```
45
+
46
+ **Differences from Neo4j** worth knowing before you write Cypher. Each is rejected *before* execution, so a rejected query changes nothing and fails identically on retry:
47
+
48
+ - Procedure calls (`CALL db.*`, `CALL apoc.*`) are rejected **by the server**, before it executes anything. `CALL { ... }` subqueries are fine. There is no schema tool and no `apoc.meta.schema()`; to learn a collection's structure, query it — `MATCH (n) UNWIND labels(n) AS l RETURN l, count(*) AS c ORDER BY l`.
49
+ - `LOAD CSV` is rejected — pass data through `params` instead.
50
+ - Existence checks are bare pattern predicates (`WHERE (p)-[:KNOWS]->()`); `EXISTS { ... }` and `exists()` are not accepted.
51
+ - `shortestPath` belongs in `RETURN`/`WITH`, not `MATCH p = ...`, and must be directed.
52
+ - `EXPLAIN`/`PROFILE` **execute** the query rather than planning it — do not use them to preview one.
53
+
54
+ Collections auto-create on first write, so there is no create-collection call. Requests are capped at 256 KiB (enforced locally, before upload) and large result sets are truncated server-side — paginate with `ORDER BY ... SKIP $offset LIMIT $limit`.
55
+
56
+ To turn the graph tools off entirely:
57
+
58
+ ```
59
+ HYDRADB_MCP_GRAPH_TOOLS=0 # withhold all three
60
+ ```
61
+
19
62
  ### Deprecated aliases
20
63
 
21
64
  The previous `hydra_db_*` tool names are **no longer registered by default** as
@@ -145,6 +188,14 @@ Checks whether ingested sources have finished indexing.
145
188
  | `HYDRADB_TIMEOUT_SECONDS` | Per-attempt request timeout | `30` |
146
189
  | `HYDRADB_MAX_RETRIES` | Retries per request (0 disables) | `2` |
147
190
  | `HYDRADB_MCP_LEGACY_TOOLS` | Register the deprecated `hydra_db_*` tools | *off* |
191
+ | `HYDRADB_GRAPH_DATABASE` | Default graph database for the Cypher tools | `HYDRADB_DATABASE` |
192
+ | `HYDRADB_GRAPH_COLLECTION` | Default graph collection | `default` |
193
+ | `HYDRADB_MCP_GRAPH_TOOLS` | Register the graph tools (`0` withholds them) | *on* |
194
+
195
+ A graph database is a **different namespace** from the memory database: the same
196
+ name can exist as both, and Cypher aimed at the wrong one reads an empty graph
197
+ rather than failing. Every graph tool also takes `database` and `collection`
198
+ per call, overriding these defaults.
148
199
 
149
200
  The legacy `HYDRA_DB_*` names — `HYDRA_DB_API_KEY`, `HYDRA_DB_TENANT_ID`,
150
201
  `HYDRA_DB_SUB_TENANT_ID`, `HYDRA_DB_BASE_URL`, `HYDRA_DB_LOG_LEVEL` — remain
@@ -230,6 +281,133 @@ To partition data, set the `HYDRADB_COLLECTION` environment variable:
230
281
  }
231
282
  ```
232
283
 
284
+ ## Remote / hosted server
285
+
286
+ Everything above spawns the server locally over stdio. The same server also runs
287
+ as a long-lived **HTTP endpoint** that many clients reach at one URL — nothing to
288
+ install or update per user. This is what powers a hosted deployment like
289
+ `https://mcp.hydradb.com`, and what you run yourself with `npm run start:http`
290
+ or the Docker image.
291
+
292
+ The tool surface is identical; only how a client connects and authenticates
293
+ changes.
294
+
295
+ ### Point a client at a URL
296
+
297
+ MCP clients that support a remote (`streamable-http`) server take a URL and
298
+ headers instead of a command:
299
+
300
+ ```jsonc
301
+ {
302
+ "mcpServers": {
303
+ "hydradb": {
304
+ "url": "https://mcp.hydradb.com",
305
+ "headers": {
306
+ "Authorization": "Bearer YOUR_HYDRADB_API_KEY",
307
+ "X-HydraDB-Database": "your-database"
308
+ }
309
+ }
310
+ }
311
+ }
312
+ ```
313
+
314
+ Every request carries its own credentials, so one hosted process serves any
315
+ number of independent users. The headers a request may send:
316
+
317
+ | Header | Maps to | Required |
318
+ | --- | --- | --- |
319
+ | `Authorization: Bearer <key>` | Hydra DB API key (`X-HydraDB-Api-Key` also accepted) | Yes\* |
320
+ | `X-HydraDB-Database` | Default database (tenant scope) | Yes\* |
321
+ | `X-HydraDB-Collection` | Default collection (sub-tenant); defaults to `hydra-db-mcp` | No |
322
+ | `X-HydraDB-Graph-Database` | Default graph database for the Cypher tools; defaults to the request's database | No |
323
+ | `X-HydraDB-Graph-Collection` | Default graph collection; defaults to `default` | No |
324
+
325
+ \* Unless the server was started with `HYDRADB_API_KEY` / `HYDRADB_DATABASE` in
326
+ its environment (single-tenant self-host, below), in which case a request may
327
+ omit them and fall back to the server's own credentials. A request that supplies
328
+ neither a header nor a server-side default is refused: `401` with no key, `400`
329
+ with a key but no database.
330
+
331
+ Every tool additionally accepts optional `database` and `collection` parameters
332
+ directly in its arguments (e.g. `hydradb_query` with `{"query": "...", "database": "tenant_b"}`),
333
+ allowing multi-tenant agents to switch tenant scope per tool call while falling
334
+ back to the session defaults when omitted.
335
+
336
+ `Base URL`, request timeout and retry count are
337
+ **operator** settings read from the server's environment and are never taken
338
+ from a request header.
339
+
340
+ ### Run the HTTP server
341
+
342
+ **Node:**
343
+
344
+ ```bash
345
+ npm ci && npm run build
346
+ # Single-tenant: the server holds one account; clients send no credentials.
347
+ HYDRADB_API_KEY=your-key HYDRADB_DATABASE=your-database npm run start:http
348
+ # Multi-tenant: no account in the env; every client sends its own headers.
349
+ BIND_ADDRESS=0.0.0.0 ALLOWED_HOSTS=mcp.hydradb.com npm run start:http
350
+ ```
351
+
352
+ **Docker:**
353
+
354
+ ```bash
355
+ docker build -t hydradb-mcp .
356
+ # Single-tenant
357
+ docker run -p 8080:8080 -e HYDRADB_API_KEY=your-key -e HYDRADB_DATABASE=your-database hydradb-mcp
358
+ # Multi-tenant (clients authenticate per request)
359
+ docker run -p 8080:8080 -e ALLOWED_HOSTS=mcp.hydradb.com hydradb-mcp
360
+ ```
361
+
362
+ The primary MCP endpoint is `/` (with `/mcp` supported as an alias); `GET /health` is an unauthenticated liveness probe.
363
+ The image binds `0.0.0.0` inside the container (the host controls exposure with
364
+ `-p`) and runs as an unprivileged user.
365
+
366
+ ### Sign in with HydraDB (OAuth)
367
+
368
+ With three extra variables the hosted server also speaks the MCP authorization flow: a client that arrives with no credentials gets a `401` pointing at `/.well-known/oauth-protected-resource`, discovers the HydraDB dashboard as its authorization server, opens the browser, and the user signs in and picks a database. No key is ever pasted into a client.
369
+
370
+ | Variable | Description |
371
+ | --- | --- |
372
+ | `HYDRADB_OAUTH_ISSUER` | The authorization server, e.g. `https://app.hydradb.com`. Must match its `NEXTAUTH_URL` exactly |
373
+ | `HYDRADB_MCP_PUBLIC_URL` | This server's public URL as clients see it, e.g. `https://mcp.hydradb.com`. Tokens minted for any other audience are refused |
374
+ | `HYDRADB_OAUTH_INTROSPECTION_SECRET` | Shared secret the issuer's `/api/oauth/introspect` expects; must equal the dashboard's `MCP_INTROSPECTION_SECRET` |
375
+
376
+ All three are required together. With any of them missing, OAuth stays off and the server behaves exactly as before: no new routes, no new headers. Token introspection answers are memoised for up to 30 seconds, so disconnecting an app from the dashboard takes effect within that window.
377
+
378
+ OAuth is purely additive. API keys in `Authorization` / `X-HydraDB-Api-Key` headers, the `X-HydraDB-Database` header and the single-tenant environment fallback all keep working unchanged on the same URL.
379
+
380
+ ### Server environment variables
381
+
382
+
383
+ These configure the HTTP process itself (the stdio server ignores them). All
384
+ the `HYDRADB_*` variables from [Environment Variables](#environment-variables)
385
+ also apply — as the single-tenant default and as operator settings.
386
+
387
+ | Variable | Description | Default |
388
+ | --- | --- | --- |
389
+ | `PORT` | Port to listen on | `8080` |
390
+ | `BIND_ADDRESS` | Interface to bind (`0.0.0.0` to accept off-host) | `127.0.0.1` |
391
+ | `ALLOWED_HOSTS` | Extra `Host` headers to accept (comma-separated); loopback always allowed | *(loopback only)* |
392
+ | `ALLOWED_ORIGINS` | CORS origins for browser clients (comma-separated; `*` allows any) | *(none)* |
393
+ | `TRUST_PROXY` | Express `trust proxy` when behind a reverse proxy: a hop count, `true`, or a subnet preset (`loopback`) | *off* |
394
+
395
+ ### Security
396
+
397
+ The defaults are safe for local use and must be widened deliberately for a
398
+ public deployment — see [SECURITY.md](SECURITY.md):
399
+
400
+ - **Bind loopback by default.** `BIND_ADDRESS` stays `127.0.0.1` until you set
401
+ otherwise; `0.0.0.0` exposes the server on every interface and logs a warning.
402
+ - **Host allowlist.** Requests whose `Host` is not loopback or in `ALLOWED_HOSTS`
403
+ get `421 Misdirected Request` — a DNS-rebinding defence. Add your public
404
+ hostname when binding publicly.
405
+ - **CORS is closed by default.** No cross-origin browser request is accepted
406
+ until you list its origin in `ALLOWED_ORIGINS`. Non-browser clients (no
407
+ `Origin` header) are unaffected.
408
+ - **Terminate TLS in front.** Run the server behind a reverse proxy / load
409
+ balancer that handles HTTPS; do not expose plain HTTP to the internet.
410
+
233
411
  ## How It Works
234
412
 
235
413
  The server talks to Hydra DB through the generated [`@hydradb/sdk`](https://www.npmjs.com/package/@hydradb/sdk)
@@ -257,6 +435,13 @@ For development with auto-reload:
257
435
  HYDRADB_API_KEY=your-key HYDRADB_DATABASE=your-database npm run dev
258
436
  ```
259
437
 
438
+ To run the HTTP transport locally (see [Remote / hosted server](#remote--hosted-server)):
439
+
440
+ ```bash
441
+ HYDRADB_API_KEY=your-key HYDRADB_DATABASE=your-database npm run dev:http
442
+ # then: curl localhost:8080/health
443
+ ```
444
+
260
445
  ## Testing
261
446
 
262
447
  ```bash
package/dist/config.d.ts CHANGED
@@ -14,9 +14,31 @@ export interface HydraDBConfig {
14
14
  baseUrl?: string;
15
15
  timeoutSeconds?: number;
16
16
  maxRetries?: number;
17
+ graph: GraphConfig;
18
+ }
19
+ /**
20
+ * Scope and gating for the BYOG graph tools.
21
+ *
22
+ * A graph database is a DIFFERENT namespace from the memory database: the same
23
+ * name can exist as both, and a Cypher query aimed at the wrong one silently
24
+ * reads an empty graph rather than failing. So the graph scope is configured
25
+ * separately, and every graph tool also takes a per-call override.
26
+ */
27
+ export interface GraphConfig {
28
+ /** Whether the graph tools are registered at all. */
29
+ enabled: boolean;
30
+ /**
31
+ * Default graph database. Falls back to HYDRADB_DATABASE — convenient when
32
+ * one name is used for both, and harmless otherwise because the tools accept
33
+ * `database` per call.
34
+ */
35
+ database: string;
36
+ /** Default graph collection. */
37
+ collection: string;
17
38
  }
18
39
  export type EnvSource = Record<string, string | undefined>;
19
40
  export type WarnFn = (message: string) => void;
41
+ export declare const DEFAULT_COLLECTION = "hydra-db-mcp";
20
42
  /**
21
43
  * Read a canonical `HYDRADB_*` variable, falling back to the legacy `HYDRA_DB_*`
22
44
  * spelling with a once-per-process warning. Exported so every variable this
@@ -26,3 +48,21 @@ export type WarnFn = (message: string) => void;
26
48
  */
27
49
  export declare function readEnv(env: EnvSource, canonical: string, deprecated: string, warn?: WarnFn): string | undefined;
28
50
  export declare function resolveConfig(env?: EnvSource, warn?: WarnFn): HydraDBConfig;
51
+ /**
52
+ * Graph scope and gating, resolved on its own.
53
+ *
54
+ * Separate from `resolveConfig` because it must be resolvable WITHOUT an API key
55
+ * or a memory database: tests construct a server around an injected client and
56
+ * never set those, and making the graph tools depend on them would mean the
57
+ * graph surface could not be exercised without a full credential set.
58
+ */
59
+ export declare function resolveGraphConfig(env?: EnvSource, fallbackDatabase?: string): GraphConfig;
60
+ /**
61
+ * Numeric overrides are ignored rather than fatal when malformed.
62
+ *
63
+ * A typo'd timeout should not stop the server from starting — falling back to
64
+ * the built-in default keeps it running, and the alternative (exit 1 on a
65
+ * cosmetic env var) is worse than the misconfiguration.
66
+ */
67
+ export declare function positiveInt(raw: string | undefined): number | undefined;
68
+ export declare function nonNegativeInt(raw: string | undefined): number | undefined;
package/dist/config.js CHANGED
@@ -7,7 +7,26 @@
7
7
  * deprecated alias emits exactly one stderr warning per process naming the
8
8
  * canonical replacement.
9
9
  */
10
- const DEFAULT_COLLECTION = "hydra-db-mcp";
10
+ const DEFAULT_GRAPH_COLLECTION = "default";
11
+ /**
12
+ * Env flags accept the usual spellings, not just `1`.
13
+ *
14
+ * Someone writing `HYDRADB_MCP_GRAPH_TOOLS=false` means it, and silently
15
+ * ignoring that would register tools an operator believed they had withheld.
16
+ * An unrecognised value falls back rather than throwing, so a typo cannot stop
17
+ * the server starting.
18
+ */
19
+ function flag(raw, fallback) {
20
+ if (raw == null || raw.trim() === "")
21
+ return fallback;
22
+ const value = raw.trim().toLowerCase();
23
+ if (["1", "true", "yes", "on"].includes(value))
24
+ return true;
25
+ if (["0", "false", "no", "off"].includes(value))
26
+ return false;
27
+ return fallback;
28
+ }
29
+ export const DEFAULT_COLLECTION = "hydra-db-mcp";
11
30
  // Process-lifetime dedupe so each deprecated alias warns at most once.
12
31
  const warnedOnce = new Set();
13
32
  function defaultWarn(message) {
@@ -61,6 +80,24 @@ export function resolveConfig(env = process.env, warn = defaultWarn) {
61
80
  baseUrl,
62
81
  ...(timeoutSeconds != null ? { timeoutSeconds } : {}),
63
82
  ...(maxRetries != null ? { maxRetries } : {}),
83
+ graph: resolveGraphConfig(env, database),
84
+ };
85
+ }
86
+ /**
87
+ * Graph scope and gating, resolved on its own.
88
+ *
89
+ * Separate from `resolveConfig` because it must be resolvable WITHOUT an API key
90
+ * or a memory database: tests construct a server around an injected client and
91
+ * never set those, and making the graph tools depend on them would mean the
92
+ * graph surface could not be exercised without a full credential set.
93
+ */
94
+ export function resolveGraphConfig(env = process.env, fallbackDatabase = "") {
95
+ return {
96
+ enabled: flag(env.HYDRADB_MCP_GRAPH_TOOLS, true),
97
+ // Not `readEnv` — these are new names with no legacy spelling to alias,
98
+ // and inventing a deprecated one would be noise.
99
+ database: env.HYDRADB_GRAPH_DATABASE?.trim() || fallbackDatabase,
100
+ collection: env.HYDRADB_GRAPH_COLLECTION?.trim() || DEFAULT_GRAPH_COLLECTION,
64
101
  };
65
102
  }
66
103
  /**
@@ -70,11 +107,11 @@ export function resolveConfig(env = process.env, warn = defaultWarn) {
70
107
  * the built-in default keeps it running, and the alternative (exit 1 on a
71
108
  * cosmetic env var) is worse than the misconfiguration.
72
109
  */
73
- function positiveInt(raw) {
110
+ export function positiveInt(raw) {
74
111
  const value = Number(raw);
75
112
  return raw != null && Number.isInteger(value) && value > 0 ? value : undefined;
76
113
  }
77
- function nonNegativeInt(raw) {
114
+ export function nonNegativeInt(raw) {
78
115
  const value = Number(raw);
79
116
  return raw != null && Number.isInteger(value) && value >= 0 ? value : undefined;
80
117
  }
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAiCH,MAAM,wBAAwB,GAAG,SAAS,CAAC;AAE3C;;;;;;;GAOG;AACH,SAAS,IAAI,CAAC,GAAuB,EAAE,QAAiB;IACvD,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,QAAQ,CAAC;IACtD,MAAM,KAAK,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACvC,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC;IAC5D,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,QAAQ,CAAC;AACjB,CAAC;AAKD,MAAM,CAAC,MAAM,kBAAkB,GAAG,cAAc,CAAC;AAEjD,uEAAuE;AACvE,MAAM,UAAU,GAAG,IAAI,GAAG,EAAU,CAAC;AACrC,SAAS,WAAW,CAAC,OAAe;IACnC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;AACxB,CAAC;AACD,SAAS,QAAQ,CAAC,OAAe,EAAE,IAAY;IAC9C,IAAI,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC;QAAE,OAAO;IACpC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IACxB,IAAI,CAAC,OAAO,CAAC,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,OAAO,CACtB,GAAc,EACd,SAAiB,EACjB,UAAkB,EAClB,OAAe,WAAW;IAE1B,MAAM,cAAc,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,cAAc,IAAI,IAAI,IAAI,cAAc,KAAK,EAAE,EAAE,CAAC;QACrD,OAAO,cAAc,CAAC;IACvB,CAAC;IACD,MAAM,eAAe,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC;IACxC,IAAI,eAAe,IAAI,IAAI,IAAI,eAAe,KAAK,EAAE,EAAE,CAAC;QACvD,QAAQ,CACP,sCAAsC,UAAU,uBAAuB,SAAS,WAAW,EAC3F,IAAI,CACJ,CAAC;QACF,OAAO,eAAe,CAAC;IACxB,CAAC;IACD,OAAO,SAAS,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,aAAa,CAC5B,MAAiB,OAAO,CAAC,GAAG,EAC5B,OAAe,WAAW;IAE1B,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,IAAI,CAAC,CAAC;IACzE,IAAI,CAAC,MAAM,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CACd,6FAA6F,CAC7F,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,IAAI,CAAC,CAAC;IAC9E,IAAI,CAAC,QAAQ,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACd,gGAAgG,CAChG,CAAC;IACH,CAAC;IAED,MAAM,UAAU,GACf,OAAO,CAAC,GAAG,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,IAAI,CAAC;QAClE,kBAAkB,CAAC;IAEpB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,IAAI,CAAC,CAAC;IAE5E,wEAAwE;IACxE,kEAAkE;IAClE,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IAChE,MAAM,UAAU,GAAG,cAAc,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAE3D,OAAO;QACN,MAAM;QACN,QAAQ;QACR,UAAU;QACV,OAAO;QACP,GAAG,CAAC,cAAc,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACrD,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,KAAK,EAAE,kBAAkB,CAAC,GAAG,EAAE,QAAQ,CAAC;KACxC,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CACjC,MAAiB,OAAO,CAAC,GAAG,EAC5B,gBAAgB,GAAG,EAAE;IAErB,OAAO;QACN,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,uBAAuB,EAAE,IAAI,CAAC;QAChD,wEAAwE;QACxE,iDAAiD;QACjD,QAAQ,EAAE,GAAG,CAAC,sBAAsB,EAAE,IAAI,EAAE,IAAI,gBAAgB;QAChE,UAAU,EAAE,GAAG,CAAC,wBAAwB,EAAE,IAAI,EAAE,IAAI,wBAAwB;KAC5E,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,GAAuB;IAClD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,GAAG,IAAI,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAAuB;IACrD,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1B,OAAO,GAAG,IAAI,IAAI,IAAI,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AACjF,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Rendering and local limits for the BYOG (Bring Your Own Graph) tool.
3
+ *
4
+ * Deliberately contains NO Cypher analysis. An earlier version lexed the query
5
+ * to classify reads vs writes and to pre-reject constructs the server refuses,
6
+ * which meant a second, worse implementation of the server's own rules living
7
+ * in a client: it could only ever agree with the server or be wrong, and being
8
+ * wrong meant refusing a query HydraDB would have run.
9
+ *
10
+ * The server is the authority on what Cypher is valid and permitted. It rejects
11
+ * unsupported constructs before executing anything — verified: a query mixing
12
+ * CREATE with a procedure call leaves the node count unchanged — and its
13
+ * messages are more specific than the ones this file used to produce.
14
+ *
15
+ * What is left is the work a client genuinely owns: turning the server's row
16
+ * objects into something readable, and the two limits that are cheaper to check
17
+ * here than to discover from a remote error.
18
+ */
19
+ /**
20
+ * The documented request-body ceiling for `POST /byog/query`.
21
+ *
22
+ * Enforced before the request goes out. The server answers an oversize body
23
+ * with 413 — but only after the whole thing has been uploaded, which on a bulk
24
+ * load is the slowest possible way to learn the batch was too big.
25
+ */
26
+ export declare const MAX_BODY_BYTES: number;
27
+ /** Collection names the server accepts. Rejecting locally names the rule. */
28
+ export declare const COLLECTION_PATTERN: RegExp;
29
+ type Row = Record<string, unknown>;
30
+ /**
31
+ * One returned value, rendered the way a graph user reads it.
32
+ *
33
+ * A node comes back as a flat object mixing its properties with `id` and
34
+ * `labels`; dumping that as raw JSON makes the caller do the separating. This
35
+ * renders `(:Person {name: "Alice"})` instead, which is both shorter and the
36
+ * notation the query was written in.
37
+ */
38
+ export declare function renderValue(value: unknown): string;
39
+ /**
40
+ * Rows as a table, bounded.
41
+ *
42
+ * A traversal can return far more than the caller can use, and unlike the
43
+ * memory tools there is no server-side relevance ranking to lean on — the query
44
+ * asked for exactly this. So the ceiling is on the rendering, and what was
45
+ * dropped is stated rather than silently cut.
46
+ */
47
+ export declare function renderRows(rows: Row[], opts?: {
48
+ maxRows?: number;
49
+ maxChars?: number;
50
+ }): string;
51
+ export {};