@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 +37 -0
- package/README.md +52 -8
- package/dist/broker.d.ts +82 -0
- package/dist/broker.js +278 -0
- package/dist/broker.js.map +1 -0
- package/dist/connection.d.ts +51 -4
- package/dist/connection.js +65 -5
- package/dist/connection.js.map +1 -1
- package/dist/oauth.d.ts +27 -0
- package/dist/oauth.js +60 -11
- package/dist/oauth.js.map +1 -1
- package/dist/params.d.ts +3 -3
- package/dist/registry.d.ts +36 -5
- package/dist/registry.js +84 -11
- package/dist/registry.js.map +1 -1
- package/dist/server.js +48 -13
- package/dist/server.js.map +1 -1
- package/dist/sessions.d.ts +2 -0
- package/dist/sessions.js +5 -3
- package/dist/sessions.js.map +1 -1
- package/dist/transports.d.ts +3 -0
- package/dist/transports.js +39 -10
- package/dist/transports.js.map +1 -1
- package/package.json +2 -2
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
|
|
42
|
-
> `
|
|
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
|
|
241
|
-
|
|
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.
|
|
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
|
package/dist/broker.d.ts
ADDED
|
@@ -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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"}
|
package/dist/connection.d.ts
CHANGED
|
@@ -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>;
|