@gizmodata/hardcharger-mcp 2.1.0 → 2.2.1

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
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.1] - 2026-11-04
11
+
12
+ ### Changed
13
+ - Built on `@gizmodata/hardcharger` 2.1.1 and `@gizmodata/chargewire` 2.1.0 (the dependency range is
14
+ now `^2.1.1`). A query that returns no rows with a LIST column no longer fails when the client
15
+ sets its unread result aside ([gizmodata/hardcharger#23](https://github.com/gizmodata/hardcharger/issues/23)),
16
+ and a session Hardcharger 2.7.0 ends for an idle transaction is replaced by the client.
17
+ - `server_info` reports the server's `hardcharger.idle_in_transaction_session_timeout` among its settings.
18
+
19
+ ### Fixed
20
+ - With writes enabled, a statement that found its transaction ended for sitting idle past the server's
21
+ `idle_in_transaction_session_timeout` (`session_ended`, reason `idle_in_transaction`, Hardcharger
22
+ 2.7.0) was retried on a new connection, where it ran in autocommit and committed on its own while
23
+ the transaction's earlier statements had been rolled back. It now fails with that error and is not
24
+ retried; the next statement runs on the new session. A live test covers it, beside a busy
25
+ transaction that commits.
26
+
27
+ ## [2.2.0] - 2026-10-30
28
+
29
+ ### Added
30
+ - Per-person credentials through a connection broker (`HARDCHARGER_MCP_BROKER_URL`,
31
+ `HARDCHARGER_MCP_BROKER_TOKEN`, HTTP transport with OAuth). The server then holds no Hardcharger
32
+ credentials: a broker, such as the control plane that runs the servers, lists the servers each
33
+ signed-in person may reach and mints them short-lived sign-in tokens, which the client exchanges
34
+ (token_exchange). Every statement runs as that person, under the server's grants, row policies and
35
+ masking. The person's token is presented to the broker as an RFC 8693 subject token, never as its
36
+ credential. The broker also checks each request's access token (`/introspect`, answers reused for 60 s), so
37
+ opaque access tokens such as Clerk's work and this server needs no provider keys; a broker outage answers 503,
38
+ not 401. Writes need the broker's per-server `allow_writes` as well as `HARDCHARGER_ALLOW_WRITES`.
39
+ Protocol: `docs/connection-broker.md`.
40
+ - `list_connections` and `server_info` report whether each connection accepts writes
41
+ (`allow_writes`), and a brokered connection's description.
42
+
43
+ ### Fixed
44
+ - The README said Clerk issues JWT access tokens by default; it issues opaque ones unless the instance turns JWT
45
+ access tokens on.
46
+
10
47
  ## [2.1.0] - 2026-10-29
11
48
 
12
49
  ### Changed
package/README.md CHANGED
@@ -38,8 +38,8 @@ runs on. It is the Hardcharger port of
38
38
  [gizmosql-mcp](https://github.com/gizmodata/gizmosql-mcp), with the same tools and settings
39
39
  (`HARDCHARGER_*` instead of `GIZMOSQL_*`; see [CHANGELOG.md](CHANGELOG.md) for what changed).
40
40
 
41
- > **Status: 2.1.0, with Hardcharger 2.3.0.** Built on `@gizmodata/hardcharger` 2.1.0 (hardcharger-js
42
- > `1a524f6`, on `@gizmodata/chargewire` 2.0.0, chargewire-js `1ea10dd`, Chargewire protocol 1.0.0);
41
+ > **Status: 2.2.1, with Hardcharger 2.7.0.** Built on `@gizmodata/hardcharger` 2.1.1 (hardcharger-js
42
+ > `3849b40`, on `@gizmodata/chargewire` 2.1.0, chargewire-js `8a4ed78`, Chargewire protocol 1.0.0);
43
43
  > tested against the image of Hardcharger's main branch and against Hardcharger 2.2.0, released
44
44
  > before the protocol was renamed to Chargewire.
45
45
 
@@ -197,7 +197,8 @@ Microsoft Entra ID, Okta (custom authorization server), Auth0, Keycloak, Cognito
197
197
  is an OAuth 2.1 *resource server*: it verifies each bearer token against the provider's JWKS
198
198
  (issuer, audience, signature, expiry) and serves the RFC 9728 metadata at
199
199
  `/.well-known/oauth-protected-resource[/mcp]` so clients find the provider on their own. The token
200
- is never forwarded; the Hardcharger connection uses the configured service credentials.
200
+ is never forwarded; the Hardcharger connection uses the configured service credentials, or, with a
201
+ [connection broker](#-per-person-credentials-connection-broker), each person's own.
201
202
 
202
203
  <!-- example: http-oauth -->
203
204
  ```bash
@@ -237,8 +238,10 @@ connector enters the app registration's client ID and secret under *Advanced set
237
238
  `https://claude.ai/api/mcp/auth_callback`, *Expose an API* with the MCP URL as Application ID URI
238
239
  and a scope such as `access_as_user`, `requestedAccessTokenVersion` 2; v2 tokens carry the client ID
239
240
  as `aud`). **Okta** needs a custom authorization server; **Auth0** an API with the audience;
240
- **Keycloak** works out of the box; **Clerk** issues JWT access tokens by default. Providers with
241
- opaque access tokens (Google) are not supported in this mode.
241
+ **Keycloak** works out of the box; **Clerk** issues opaque access tokens unless the instance has
242
+ *JWT access tokens* turned on (checked 2026-10-30), so turn that on for this mode. Providers with
243
+ opaque access tokens (Google) are not supported in this mode; with a connection broker they are,
244
+ because the broker checks the tokens.
242
245
 
243
246
  **Static bearer token.** For a quick shared secret instead of OAuth, set
244
247
  `HARDCHARGER_MCP_BEARER_TOKEN`; every request must then send `Authorization: Bearer <token>`. The
@@ -253,6 +256,38 @@ HARDCHARGER_MCP_BEARER_TOKEN=a-long-random-secret \
253
256
  npx -y @gizmodata/hardcharger-mcp --transport http --port 3000
254
257
  ```
255
258
 
259
+ ### 🪪 Per-person credentials (connection broker)
260
+
261
+ With a connection broker, the server holds no Hardcharger credentials at all. A broker, usually
262
+ the control plane that runs your Hardcharger servers (GizmoData Cloud is one), tells it which
263
+ servers each signed-in person may reach and mints that person a short-lived sign-in token for
264
+ each one. The client exchanges the token (Chargewire token_exchange), so every statement runs
265
+ **as that person**: the server's access control (grants, row policies, masking) decides what
266
+ they may see and change, and its audit log names them. One connector serves everyone, and each
267
+ person sees only their own servers in `list_connections`.
268
+
269
+ ```bash
270
+ HARDCHARGER_MCP_PUBLIC_URL=https://mcp.example.com/mcp \
271
+ HARDCHARGER_MCP_OAUTH_ISSUER=https://login.example.com \
272
+ HARDCHARGER_MCP_BROKER_URL=https://control-plane.example.com/api/mcp-broker \
273
+ HARDCHARGER_MCP_BROKER_TOKEN=this-servers-own-broker-credential \
274
+ HARDCHARGER_ALLOW_WRITES=true \
275
+ npx -y @gizmodata/hardcharger-mcp --transport http --host 0.0.0.0 --port 3000
276
+ ```
277
+
278
+ The broker also checks each request's access token (`/introspect`), so the provider's tokens may be
279
+ opaque (Clerk's are by default) and this server needs none of the provider's keys or credentials;
280
+ `HARDCHARGER_MCP_OAUTH_ISSUER` only names the authorization server in the metadata clients read.
281
+ It needs OAuth (each request must carry a verified person) and the HTTP transport, and takes no
282
+ `HARDCHARGER_URI`: a configured connection's credentials would be shared by every user. The
283
+ person's access token is never the broker credential: the server authenticates to the broker
284
+ with `HARDCHARGER_MCP_BROKER_TOKEN` and presents the person's token as an RFC 8693 subject token.
285
+ Writes need both `HARDCHARGER_ALLOW_WRITES=true` (the server-wide ceiling) and the broker's
286
+ `allow_writes` for that person and server. Table resources are listed only for servers the
287
+ person has already used, so a resource listing never signs in to every server they can reach.
288
+ The protocol, for anyone writing a broker, is in
289
+ [docs/connection-broker.md](docs/connection-broker.md).
290
+
256
291
  ### Container image and Helm chart
257
292
 
258
293
  [`Dockerfile`](Dockerfile) builds an image whose entrypoint runs the HTTP transport on port 3000, and
@@ -300,6 +335,10 @@ Only `HARDCHARGER_*` names are read.
300
335
  | `HARDCHARGER_MCP_OAUTH_ALLOW_INSECURE` | `false` | Accept `http://` issuer and public URLs (local testing only) |
301
336
  | `HARDCHARGER_MCP_SESSION_IDLE_SECONDS` | `1800` | HTTP transport with OAuth: close a user's session after this long without a request |
302
337
  | `HARDCHARGER_MCP_MAX_SESSIONS` | `200` | HTTP transport with OAuth: cap on concurrent user sessions |
338
+ | `HARDCHARGER_MCP_BROKER_URL` | | HTTP transport with OAuth: a connection broker supplies each person's servers and sign-in tokens ([protocol](docs/connection-broker.md)); no `HARDCHARGER_URI` then |
339
+ | `HARDCHARGER_MCP_BROKER_TOKEN` | | This server's own credential at the broker (required with the URL) |
340
+ | `HARDCHARGER_MCP_BROKER_CACHE_SECONDS` | `60` | How long a person's server list is reused (`list_connections` always asks again) |
341
+ | `HARDCHARGER_MCP_BROKER_ALLOW_INSECURE` | `false` | Accept a plain `http` broker URL (local testing) |
303
342
  | `HARDCHARGER_2_URI`, `HARDCHARGER_3_URI`, … | | Additional connections, see below |
304
343
  | `HARDCHARGER_CONNECTIONS` | | Comma-separated names of further connections, see below |
305
344
 
@@ -347,7 +386,8 @@ clients ask before running it.
347
386
  The guard is defense in depth; the real boundary is the privileges of the Hardcharger user the
348
387
  server signs in as. A sign-in token with `role: readonly` can only read (Hardcharger refuses
349
388
  anything else with `permission_denied`), and under Hardcharger's access control a store user's
350
- grants decide what each table allows.
389
+ grants decide what each table allows. With a connection broker that user is the person asking,
390
+ not a shared service account.
351
391
 
352
392
  - Credentials never appear in tool output, `server_info` or error messages: every message that
353
393
  reaches the client is redacted.
@@ -362,7 +402,11 @@ grants decide what each table allows.
362
402
  session are gone. A session an administrator killed, a closed connection or a transport failure
363
403
  reopens the connection once and retries (`reconnects`); a refused credential is never retried,
364
404
  and neither is a session ended because the sign-in was withdrawn (`token_revoked`,
365
- `credentials_changed`, `principal_dropped`).
405
+ `credentials_changed`, `principal_dropped`). With writes enabled, a transaction (`BEGIN` through
406
+ `execute_statement`) left idle past the server's `idle_in_transaction_session_timeout`
407
+ (Hardcharger 2.7.0) is rolled back by the server: the next statement fails with
408
+ `session_ended` and `idle_in_transaction` and is not retried, so it can't commit on its own
409
+ outside the transaction, and the statement after it runs on a new session in autocommit.
366
410
  - The server has no tools that touch the local filesystem or any network endpoint other than the
367
411
  configured Hardcharger servers (and, for `login_sso`, the server's sign-in routes and your
368
412
  identity provider in your browser).
@@ -404,7 +448,7 @@ built from [gizmodata/hardcharger-js](https://github.com/gizmodata/hardcharger-j
404
448
  [scripts/hardcharger-js.commit](scripts/hardcharger-js.commit) and
405
449
  [scripts/chargewire-js.commit](scripts/chargewire-js.commit), and packed to `.clients/`, where
406
450
  `package-lock.json` finds them, so the tests run against those exact clients. `package.json`, and
407
- so the published package, names the npm package and its range (`^2.1.0`); the tarballs are a
451
+ so the published package, names the npm package and its range (`^2.1.1`); the tarballs are a
408
452
  stand-in for development and CI only (`scripts/check-lock.mjs` checks both).
409
453
 
410
454
  ```bash
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ import { type BrokerConfig, type McpConfig } from "./connection.js";
3
+ import type { IntrospectionResult } from "./oauth.js";
4
+ import { ConnectionRegistry } from "./registry.js";
5
+ /** RFC 8693 token type of the caller's OAuth access token. */
6
+ export declare const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
7
+ declare const connectionSchema: z.ZodObject<{
8
+ id: z.ZodString;
9
+ name: z.ZodString;
10
+ label: z.ZodOptional<z.ZodNullable<z.ZodString>>;
11
+ uri: z.ZodString;
12
+ default_catalog: z.ZodOptional<z.ZodNullable<z.ZodString>>;
13
+ default_schema: z.ZodOptional<z.ZodNullable<z.ZodString>>;
14
+ allow_writes: z.ZodDefault<z.ZodBoolean>;
15
+ }, z.core.$strip>;
16
+ export type BrokeredConnection = z.infer<typeof connectionSchema>;
17
+ /** A broker request that failed; `message` is safe to show the caller. */
18
+ export declare class BrokerError extends Error {
19
+ readonly status?: number | undefined;
20
+ constructor(message: string, status?: number | undefined);
21
+ }
22
+ export interface BrokerHooks {
23
+ /** Test hook: the fetch implementation. */
24
+ fetch?: typeof fetch;
25
+ /** Test hook: clock (ms). */
26
+ now?: () => number;
27
+ }
28
+ /** Talks to the broker's two endpoints. Stateless; one instance serves every caller. */
29
+ export declare class BrokerClient {
30
+ readonly config: BrokerConfig;
31
+ private readonly fetchImpl;
32
+ constructor(config: BrokerConfig, hooks?: BrokerHooks);
33
+ /** The servers the caller may reach. */
34
+ listConnections(subjectToken: string): Promise<{
35
+ connections: unknown[];
36
+ message?: string;
37
+ }>;
38
+ /**
39
+ * Whether the caller's access token is active, and whose it is: the broker verifies the tokens of
40
+ * the provider it trusts, opaque ones included, so this server needs no provider credentials.
41
+ */
42
+ introspect(subjectToken: string): Promise<IntrospectionResult>;
43
+ /** A sign-in token for the caller on one server, and its lifetime in seconds. */
44
+ issueToken(subjectToken: string, connectionId: string): Promise<{
45
+ token: string;
46
+ expiresIn: number;
47
+ }>;
48
+ private post;
49
+ }
50
+ /**
51
+ * One caller's connections, as the broker lists them. Refreshed on a request when the list is
52
+ * older than HARDCHARGER_MCP_BROKER_CACHE_SECONDS (and by list_connections at once); servers
53
+ * the broker no longer lists are closed. Each connection signs in with a token the broker
54
+ * mints for this caller.
55
+ */
56
+ export declare class BrokeredRegistry extends ConnectionRegistry {
57
+ readonly broker: BrokerClient;
58
+ private callerToken;
59
+ private listedAt;
60
+ private refreshing;
61
+ private lastProblem;
62
+ private emptyMessage;
63
+ private readonly clock;
64
+ constructor(config: McpConfig, broker: BrokerClient, log: (message: string) => void, hooks?: Pick<BrokerHooks, "now">);
65
+ now(): number;
66
+ /** The caller's current OAuth access token (verified by the transport), presented to the broker. */
67
+ subjectToken(): string;
68
+ /** Records the token of the request being served: the newest one the client sent. */
69
+ setSubjectToken(token: string): void;
70
+ /** Asks the broker again when the list is stale (or always, with `force`). Never throws. */
71
+ refresh({ force }?: {
72
+ force?: boolean;
73
+ }): Promise<void>;
74
+ private load;
75
+ /** The broker's own words when it listed nothing, or why it could not be asked. */
76
+ status(): string | undefined;
77
+ protected noConnectionsMessage(): string;
78
+ /** Only servers this caller already uses: listing every table would sign in to every server they can reach. */
79
+ resourceConnections(): import("./connection.js").HardchargerConnection[];
80
+ secrets(): Array<string | undefined>;
81
+ }
82
+ export {};
package/dist/broker.js ADDED
@@ -0,0 +1,278 @@
1
+ // Connection broker: per-user connections and sign-in tokens for the hosted HTTP transport.
2
+ //
3
+ // With HARDCHARGER_MCP_BROKER_URL, this server holds no Hardcharger credentials of its own. A
4
+ // broker (a control plane such as GizmoData Cloud) knows which Hardcharger servers each person
5
+ // may reach and mints short-lived sign-in tokens for them; the client exchanges such a token
6
+ // (Chargewire token_exchange), so every statement runs as the person who asked, under that
7
+ // person's grants. The protocol is in docs/connection-broker.md.
8
+ //
9
+ // The caller's OAuth access token is never forwarded as a credential (MCP forbids token
10
+ // passthrough). Each broker request authenticates this server with its own credential and
11
+ // presents the caller's token as an RFC 8693 subject token, which the broker verifies and
12
+ // maps to a person. Only the broker decides what that person may reach.
13
+ import { z } from "zod";
14
+ import { parseHardchargerUri, } from "./connection.js";
15
+ import { ConnectionRegistry } from "./registry.js";
16
+ /** RFC 8693 token type of the caller's OAuth access token. */
17
+ export const ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
18
+ /** A minted token is reused until this share of its lifetime has passed. */
19
+ const TOKEN_REUSE_FRACTION = 0.8;
20
+ /** Connection names the tools accept (the same rule as configured connections). */
21
+ const NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
22
+ const connectionSchema = z.object({
23
+ id: z.string().min(1),
24
+ name: z.string().min(1),
25
+ label: z.string().nullish(),
26
+ uri: z.string().min(1),
27
+ default_catalog: z.string().nullish(),
28
+ default_schema: z.string().nullish(),
29
+ allow_writes: z.boolean().default(false),
30
+ });
31
+ const listSchema = z.object({
32
+ connections: z.array(z.unknown()),
33
+ /** Shown to the caller when the list is empty (e.g. how to create a server). */
34
+ message: z.string().nullish(),
35
+ });
36
+ const introspectionSchema = z.object({
37
+ active: z.boolean(),
38
+ sub: z.string().nullish(),
39
+ email: z.string().nullish(),
40
+ name: z.string().nullish(),
41
+ });
42
+ const tokenSchema = z.object({
43
+ token: z.string().min(1),
44
+ expires_in: z.number().positive(),
45
+ });
46
+ /** A broker request that failed; `message` is safe to show the caller. */
47
+ export class BrokerError extends Error {
48
+ status;
49
+ constructor(message, status) {
50
+ super(message);
51
+ this.status = status;
52
+ this.name = "BrokerError";
53
+ }
54
+ }
55
+ /** The broker's error text from a JSON body (`error_description`, `message` or a string `detail`). */
56
+ function brokerDetail(body) {
57
+ if (!body || typeof body !== "object")
58
+ return undefined;
59
+ for (const key of ["error_description", "message", "detail"]) {
60
+ const value = body[key];
61
+ if (typeof value === "string" && value.trim() !== "")
62
+ return value.trim();
63
+ }
64
+ return undefined;
65
+ }
66
+ /** Talks to the broker's two endpoints. Stateless; one instance serves every caller. */
67
+ export class BrokerClient {
68
+ config;
69
+ fetchImpl;
70
+ constructor(config, hooks = {}) {
71
+ this.config = config;
72
+ this.fetchImpl = hooks.fetch ?? fetch;
73
+ }
74
+ /** The servers the caller may reach. */
75
+ async listConnections(subjectToken) {
76
+ const body = listSchema.safeParse(await this.post("/connections", subjectToken));
77
+ if (!body.success)
78
+ throw new BrokerError("The connection broker answered the connection list with an unexpected body.");
79
+ return { connections: body.data.connections, message: body.data.message ?? undefined };
80
+ }
81
+ /**
82
+ * Whether the caller's access token is active, and whose it is: the broker verifies the tokens of
83
+ * the provider it trusts, opaque ones included, so this server needs no provider credentials.
84
+ */
85
+ async introspect(subjectToken) {
86
+ const body = introspectionSchema.safeParse(await this.post("/introspect", subjectToken));
87
+ if (!body.success)
88
+ throw new BrokerError("The connection broker answered the token check with an unexpected body.");
89
+ const { active, sub, email, name } = body.data;
90
+ return { active, sub: sub ?? undefined, email: email ?? undefined, name: name ?? undefined };
91
+ }
92
+ /** A sign-in token for the caller on one server, and its lifetime in seconds. */
93
+ async issueToken(subjectToken, connectionId) {
94
+ const body = tokenSchema.safeParse(await this.post(`/connections/${encodeURIComponent(connectionId)}/token`, subjectToken));
95
+ if (!body.success)
96
+ throw new BrokerError("The connection broker answered the token request with an unexpected body.");
97
+ return { token: body.data.token, expiresIn: body.data.expires_in };
98
+ }
99
+ async post(path, subjectToken) {
100
+ let res;
101
+ try {
102
+ res = await this.fetchImpl(`${this.config.url}${path}`, {
103
+ method: "POST",
104
+ headers: {
105
+ authorization: `Bearer ${this.config.credential}`,
106
+ "content-type": "application/json",
107
+ accept: "application/json",
108
+ },
109
+ body: JSON.stringify({ subject_token: subjectToken, subject_token_type: ACCESS_TOKEN_TYPE }),
110
+ signal: AbortSignal.timeout(this.config.timeoutMs),
111
+ redirect: "error",
112
+ });
113
+ }
114
+ catch (err) {
115
+ const reason = err instanceof Error && err.name === "TimeoutError" ? `no answer within ${this.config.timeoutMs} ms` : "unreachable";
116
+ throw new BrokerError(`The connection broker is ${reason}; try again shortly.`);
117
+ }
118
+ let body;
119
+ try {
120
+ body = await res.json();
121
+ }
122
+ catch {
123
+ body = undefined;
124
+ }
125
+ if (res.ok)
126
+ return body;
127
+ const detail = brokerDetail(body);
128
+ if (res.status === 401) {
129
+ // The broker refused this server's own credential (or found the caller's token invalid).
130
+ throw new BrokerError(detail ?? "The connection broker refused this request; reconnect the connector, or ask the administrator to check this server's broker credential.", 401);
131
+ }
132
+ throw new BrokerError(detail ?? `The connection broker answered HTTP ${res.status}.`, res.status);
133
+ }
134
+ }
135
+ /** Sign-in tokens for one caller on one server, minted by the broker and reused for most of their life. */
136
+ class BrokeredTokenSource {
137
+ registry;
138
+ connectionId;
139
+ cached;
140
+ inFlight;
141
+ issued = new Set();
142
+ constructor(registry, connectionId) {
143
+ this.registry = registry;
144
+ this.connectionId = connectionId;
145
+ }
146
+ async token() {
147
+ const now = this.registry.now();
148
+ if (this.cached && now < this.cached.reuseUntil)
149
+ return this.cached.token;
150
+ this.inFlight ??= this.registry.broker
151
+ .issueToken(this.registry.subjectToken(), this.connectionId)
152
+ .then(({ token, expiresIn }) => {
153
+ this.cached = { token, reuseUntil: this.registry.now() + expiresIn * 1000 * TOKEN_REUSE_FRACTION };
154
+ this.issued.add(token);
155
+ return token;
156
+ })
157
+ .finally(() => {
158
+ this.inFlight = undefined;
159
+ });
160
+ return this.inFlight;
161
+ }
162
+ secrets() {
163
+ return [...this.issued];
164
+ }
165
+ }
166
+ /**
167
+ * One caller's connections, as the broker lists them. Refreshed on a request when the list is
168
+ * older than HARDCHARGER_MCP_BROKER_CACHE_SECONDS (and by list_connections at once); servers
169
+ * the broker no longer lists are closed. Each connection signs in with a token the broker
170
+ * mints for this caller.
171
+ */
172
+ export class BrokeredRegistry extends ConnectionRegistry {
173
+ broker;
174
+ callerToken = "";
175
+ listedAt;
176
+ refreshing;
177
+ lastProblem;
178
+ emptyMessage;
179
+ clock;
180
+ constructor(config, broker, log, hooks = {}) {
181
+ super(config, log, { connections: [], allowEmpty: true });
182
+ this.broker = broker;
183
+ this.clock = hooks.now ?? (() => Date.now());
184
+ }
185
+ now() {
186
+ return this.clock();
187
+ }
188
+ /** The caller's current OAuth access token (verified by the transport), presented to the broker. */
189
+ subjectToken() {
190
+ return this.callerToken;
191
+ }
192
+ /** Records the token of the request being served: the newest one the client sent. */
193
+ setSubjectToken(token) {
194
+ this.callerToken = token;
195
+ }
196
+ /** Asks the broker again when the list is stale (or always, with `force`). Never throws. */
197
+ async refresh({ force = false } = {}) {
198
+ const ttl = (this.config.broker?.cacheSeconds ?? 0) * 1000;
199
+ if (!force && this.listedAt !== undefined && this.now() - this.listedAt < ttl)
200
+ return;
201
+ this.refreshing ??= this.load().finally(() => {
202
+ this.refreshing = undefined;
203
+ });
204
+ await this.refreshing;
205
+ }
206
+ async load() {
207
+ let listed;
208
+ try {
209
+ listed = await this.broker.listConnections(this.callerToken);
210
+ }
211
+ catch (err) {
212
+ // Keep what the caller had; say why on the next tool call that finds nothing.
213
+ this.lastProblem = err instanceof Error ? err.message : String(err);
214
+ this.log(`[hardcharger-mcp] connection broker: ${this.redact(this.lastProblem)}`);
215
+ return;
216
+ }
217
+ const configs = [];
218
+ const seen = new Set();
219
+ for (const raw of listed.connections) {
220
+ const parsed = connectionSchema.safeParse(raw);
221
+ if (!parsed.success) {
222
+ this.log("[hardcharger-mcp] connection broker: skipped a connection with an unexpected shape");
223
+ continue;
224
+ }
225
+ const c = parsed.data;
226
+ if (!NAME_PATTERN.test(c.name) || seen.has(c.name.toLowerCase())) {
227
+ this.log(`[hardcharger-mcp] connection broker: skipped connection ${JSON.stringify(c.name)} (invalid or repeated name)`);
228
+ continue;
229
+ }
230
+ let target;
231
+ try {
232
+ target = parseHardchargerUri(`connection ${c.name}`, c.uri);
233
+ }
234
+ catch (err) {
235
+ this.log(`[hardcharger-mcp] connection broker: skipped ${c.name}: ${err instanceof Error ? err.message : String(err)}`);
236
+ continue;
237
+ }
238
+ seen.add(c.name.toLowerCase());
239
+ configs.push({
240
+ name: c.name,
241
+ ...target,
242
+ tlsSkipVerify: false,
243
+ defaultCatalog: c.default_catalog ?? undefined,
244
+ defaultSchema: c.default_schema ?? undefined,
245
+ queryTimeoutSeconds: this.config.queryTimeoutSeconds,
246
+ id: c.id,
247
+ label: c.label ?? undefined,
248
+ allowWrites: c.allow_writes,
249
+ tokenSource: new BrokeredTokenSource(this, c.id),
250
+ });
251
+ }
252
+ this.lastProblem = undefined;
253
+ this.emptyMessage = listed.message;
254
+ this.listedAt = this.now();
255
+ await this.replaceConnections(configs);
256
+ }
257
+ /** The broker's own words when it listed nothing, or why it could not be asked. */
258
+ status() {
259
+ if (this.lastProblem !== undefined)
260
+ return this.lastProblem;
261
+ if (this.names().length === 0)
262
+ return this.noConnectionsMessage();
263
+ return undefined;
264
+ }
265
+ noConnectionsMessage() {
266
+ if (this.lastProblem !== undefined)
267
+ return this.lastProblem;
268
+ return this.emptyMessage ?? "You have no Hardcharger servers to connect to.";
269
+ }
270
+ /** Only servers this caller already uses: listing every table would sign in to every server they can reach. */
271
+ resourceConnections() {
272
+ return this.all().filter((c) => c.connectedAt !== null);
273
+ }
274
+ secrets() {
275
+ return [...super.secrets(), this.callerToken];
276
+ }
277
+ }
278
+ //# sourceMappingURL=broker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broker.js","sourceRoot":"","sources":["../src/broker.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,17 @@ import { Table } from "apache-arrow";
3
3
  import type { OAuthConfig } from "./oauth.js";
4
4
  import type { BoundParams } from "./params.js";
5
5
  /** How a connection signs in. */
6
- export type AuthMethod = "password" | "token" | "mtls" | "sso" | "none";
6
+ export type AuthMethod = "password" | "token" | "mtls" | "sso" | "brokered" | "none";
7
+ /**
8
+ * Where a brokered connection gets its sign-in tokens: the connection broker mints one for the
9
+ * calling user each time the client signs in (see broker.ts).
10
+ */
11
+ export interface TokenSource {
12
+ /** A signed sign-in token for this user and server, for the token_exchange grant. */
13
+ token(): Promise<string>;
14
+ /** Tokens this source has handed out or holds, for redaction. */
15
+ secrets(): string[];
16
+ }
7
17
  /** Settings for one Hardcharger server. */
8
18
  export interface ConnectionConfig {
9
19
  /** Unique, case-insensitive name used in tool arguments (e.g. "default", "prod"). */
@@ -31,6 +41,18 @@ export interface ConnectionConfig {
31
41
  defaultSchema?: string;
32
42
  /** Per-statement timeout applied to this connection's statements (0 = none). */
33
43
  queryTimeoutSeconds: number;
44
+ /** Brokered connections: the broker's id for this server. */
45
+ id?: string;
46
+ /** Brokered connections: a human description (e.g. organization / project / cluster). */
47
+ label?: string;
48
+ /**
49
+ * Brokered connections: whether the broker lets this user write through this server. Writes
50
+ * also need HARDCHARGER_ALLOW_WRITES, which stays the server-wide ceiling. Unset means the
51
+ * server-wide setting alone decides.
52
+ */
53
+ allowWrites?: boolean;
54
+ /** Brokered connections: the per-user sign-in tokens (token_exchange grant). */
55
+ tokenSource?: TokenSource;
34
56
  }
35
57
  /** Whole-server settings plus every configured connection (the first is the initial default). */
36
58
  export interface McpConfig {
@@ -49,6 +71,22 @@ export interface McpConfig {
49
71
  mcpMaxSessions: number;
50
72
  /** Enables the optional `login_sso` tool (the server's OAuth browser sign-in). */
51
73
  enableSso: boolean;
74
+ /**
75
+ * HTTP transport with OAuth: a connection broker supplies each caller's connections and
76
+ * sign-in tokens (HARDCHARGER_MCP_BROKER_URL); `connections` is then empty.
77
+ */
78
+ broker?: BrokerConfig;
79
+ }
80
+ /** Settings for the connection broker (see broker.ts and docs/connection-broker.md). */
81
+ export interface BrokerConfig {
82
+ /** Base URL of the broker's API: `<url>/connections` and `<url>/connections/<id>/token`. */
83
+ url: string;
84
+ /** This MCP server's own credential at the broker, sent as a bearer token. */
85
+ credential: string;
86
+ /** How long a caller's connection list is reused before the broker is asked again. */
87
+ cacheSeconds: number;
88
+ /** Timeout of one broker request. */
89
+ timeoutMs: number;
52
90
  }
53
91
  export declare const DEFAULTS: {
54
92
  readonly maxRows: 500;
@@ -57,6 +95,8 @@ export declare const DEFAULTS: {
57
95
  readonly connectionName: "default";
58
96
  readonly sessionIdleSeconds: 1800;
59
97
  readonly maxSessions: 200;
98
+ readonly brokerCacheSeconds: 60;
99
+ readonly brokerTimeoutMs: 10000;
60
100
  };
61
101
  /** Extra connection slots the Claude Desktop manifest exposes (`HARDCHARGER_2_*`, `HARDCHARGER_3_*`). */
62
102
  export declare const MANIFEST_SLOTS: readonly ["2", "3"];
@@ -92,6 +132,12 @@ export declare function parseHardchargerUri(name: string, raw: string): {
92
132
  * Slots without a URI are ignored. At least one connection is required.
93
133
  */
94
134
  export declare function parseConfig(env?: NodeJS.ProcessEnv): McpConfig;
135
+ /**
136
+ * Reads the connection broker settings (HTTP transport with OAuth). Returns undefined unless
137
+ * HARDCHARGER_MCP_BROKER_URL is set. A plain http URL needs
138
+ * HARDCHARGER_MCP_BROKER_ALLOW_INSECURE=true (local testing).
139
+ */
140
+ export declare function parseBroker(env: NodeJS.ProcessEnv): BrokerConfig | undefined;
95
141
  /** Default claim precedence used to name an authenticated caller. */
96
142
  export declare const DEFAULT_USER_CLAIMS: readonly ["email", "preferred_username", "upn", "name", "sub"];
97
143
  /**
@@ -102,7 +148,7 @@ export declare const DEFAULT_USER_CLAIMS: readonly ["email", "preferred_username
102
148
  */
103
149
  export declare function parseOAuth(env: NodeJS.ProcessEnv): OAuthConfig | undefined;
104
150
  /** The sign-in a connection's settings call for (before any login_sso). */
105
- export declare function authMethod(config: Pick<ConnectionConfig, "username" | "token" | "mtlsCertFile">): AuthMethod;
151
+ export declare function authMethod(config: Pick<ConnectionConfig, "username" | "token" | "mtlsCertFile" | "tokenSource">): AuthMethod;
106
152
  /** Replaces every configured secret in `text` with `***`. */
107
153
  export declare function redactSecrets(text: string, secrets: Array<string | undefined>): string;
108
154
  export declare class QueryTimeoutError extends Error {
@@ -125,7 +171,8 @@ export declare function errorCode(err: unknown): string | undefined;
125
171
  * this server doesn't know), a closed connection, a transport failure, and a lost session
126
172
  * the client could not replace. A refused credential is never retried, and neither is a
127
173
  * session ended because the sign-in was withdrawn (`token_revoked`, `credentials_changed`,
128
- * `principal_dropped`).
174
+ * `principal_dropped`), nor one ended for an idle transaction (`idle_in_transaction`: the
175
+ * client replaced that session itself, and the failed statement must not run again).
129
176
  */
130
177
  export declare function isConnectionLost(err: unknown): boolean;
131
178
  export interface SessionOverrides {
@@ -151,7 +198,7 @@ export interface RunContext {
151
198
  statementTimeoutMs: number | undefined;
152
199
  }
153
200
  /** Setting names read from hardcharger_settings() and reported by server_info. */
154
- export declare const SERVER_SETTINGS_OF_INTEREST: readonly ["hardcharger.version", "hardcharger.read_only", "hardcharger.max_sessions", "hardcharger.session_idle_timeout", "hardcharger.query_timeout", "hardcharger.memory_limit", "hardcharger.max_concurrent_statements", "hardcharger.instance_id", "hardcharger.cluster_id"];
201
+ export declare const SERVER_SETTINGS_OF_INTEREST: readonly ["hardcharger.version", "hardcharger.read_only", "hardcharger.max_sessions", "hardcharger.session_idle_timeout", "hardcharger.idle_in_transaction_session_timeout", "hardcharger.query_timeout", "hardcharger.memory_limit", "hardcharger.max_concurrent_statements", "hardcharger.instance_id", "hardcharger.cluster_id"];
155
202
  export interface ConnectionHooks {
156
203
  /** Test hook: opens the client connection (defaults to `connect` from @gizmodata/hardcharger). */
157
204
  connect?: (options: ConnectOptions) => Promise<Connection>;