@gizmodata/hardcharger-mcp 2.0.0 → 2.2.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 +44 -14
- package/README.md +67 -23
- 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 +52 -6
- package/dist/connection.js +55 -7
- 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/params.js +1 -1
- package/dist/params.js.map +1 -1
- package/dist/registry.d.ts +36 -5
- package/dist/registry.js +84 -11
- package/dist/registry.js.map +1 -1
- package/dist/server.js +51 -16
- 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/sso.d.ts +2 -2
- package/dist/sso.js +4 -4
- package/dist/sso.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 +3 -3
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 {
|
|
@@ -114,7 +160,7 @@ export interface CappedResult {
|
|
|
114
160
|
/** True when more rows existed beyond `maxRows` (the extra row is not in `table`). */
|
|
115
161
|
truncated: boolean;
|
|
116
162
|
}
|
|
117
|
-
/** The error's Hardcharger (
|
|
163
|
+
/** The error's Hardcharger (Chargewire) code, if it is one of the client's errors. */
|
|
118
164
|
export declare function errorCode(err: unknown): string | undefined;
|
|
119
165
|
/**
|
|
120
166
|
* Does this error mean the connection (or the server's session behind it) is gone in a
|
|
@@ -159,7 +205,7 @@ export interface ConnectionHooks {
|
|
|
159
205
|
/**
|
|
160
206
|
* One Hardcharger connection per configured server, opened lazily and serialized so a
|
|
161
207
|
* single statement is in flight at a time. Timeouts are enforced by the server
|
|
162
|
-
* (`statementTimeoutMs`,
|
|
208
|
+
* (`statementTimeoutMs`, Chargewire §6.9) with a client-side abort as backstop that cancels
|
|
163
209
|
* the statement on the server; only if that fails to settle the call is the connection
|
|
164
210
|
* dropped and reopened. The client replaces a session the server lost and replays its
|
|
165
211
|
* catalog and schema; a connection that is gone altogether (a killed session, a closed
|
|
@@ -187,7 +233,7 @@ export declare class HardchargerConnection {
|
|
|
187
233
|
/** The search path (catalog, schema) applied to every session. */
|
|
188
234
|
currentSearchPath(): SearchPath;
|
|
189
235
|
/**
|
|
190
|
-
* Switches the session's default catalog and/or schema (
|
|
236
|
+
* Switches the session's default catalog and/or schema (Chargewire session options, which
|
|
191
237
|
* the client replays on a replacement session). Throws when the server refuses it.
|
|
192
238
|
*/
|
|
193
239
|
useSchema(path: SearchPath): Promise<SearchPath>;
|
|
@@ -225,7 +271,7 @@ export declare class HardchargerConnection {
|
|
|
225
271
|
/** Opens a fresh client and applies the session settings. */
|
|
226
272
|
private open;
|
|
227
273
|
/**
|
|
228
|
-
* The search path, as
|
|
274
|
+
* The search path, as Chargewire session options. A catalog or schema the server does not
|
|
229
275
|
* have is reported (with the catalogs it does have) instead of failing the connection;
|
|
230
276
|
* the server's default then applies.
|
|
231
277
|
*/
|
package/dist/connection.js
CHANGED
|
@@ -10,6 +10,8 @@ export const DEFAULTS = {
|
|
|
10
10
|
connectionName: "default",
|
|
11
11
|
sessionIdleSeconds: 1800,
|
|
12
12
|
maxSessions: 200,
|
|
13
|
+
brokerCacheSeconds: 60,
|
|
14
|
+
brokerTimeoutMs: 10_000,
|
|
13
15
|
};
|
|
14
16
|
/** Extra connection slots the Claude Desktop manifest exposes (`HARDCHARGER_2_*`, `HARDCHARGER_3_*`). */
|
|
15
17
|
export const MANIFEST_SLOTS = ["2", "3"];
|
|
@@ -170,7 +172,13 @@ export function parseConfig(env = process.env) {
|
|
|
170
172
|
if (c)
|
|
171
173
|
connections.push(c);
|
|
172
174
|
}
|
|
173
|
-
|
|
175
|
+
const broker = parseBroker(env);
|
|
176
|
+
if (broker !== undefined && connections.length > 0) {
|
|
177
|
+
throw new ConfigError("HARDCHARGER_MCP_BROKER_URL supplies every caller's connections; remove " +
|
|
178
|
+
`${connections.map((c) => c.name).join(", ")} (HARDCHARGER_URI and the other connection settings): a configured ` +
|
|
179
|
+
"connection's credentials would be shared by every user of this server.");
|
|
180
|
+
}
|
|
181
|
+
if (connections.length === 0 && broker === undefined) {
|
|
174
182
|
throw new ConfigError("HARDCHARGER_URI is required: the Hardcharger server's URI, e.g. hardcharger://db.example.com " +
|
|
175
183
|
"(TLS to port 31337), or hardcharger://localhost?tls=false for a server without TLS.");
|
|
176
184
|
}
|
|
@@ -187,6 +195,10 @@ export function parseConfig(env = process.env) {
|
|
|
187
195
|
if (mcpBearerToken !== undefined && mcpOAuth !== undefined) {
|
|
188
196
|
throw new ConfigError("HARDCHARGER_MCP_BEARER_TOKEN and HARDCHARGER_MCP_OAUTH_ISSUER are mutually exclusive: pick one way to authenticate HTTP clients.");
|
|
189
197
|
}
|
|
198
|
+
if (broker !== undefined && mcpOAuth === undefined) {
|
|
199
|
+
throw new ConfigError("HARDCHARGER_MCP_BROKER_URL needs HARDCHARGER_MCP_OAUTH_ISSUER: the broker issues credentials per signed-in user, " +
|
|
200
|
+
"so every request must carry a verified identity (HTTP transport with OAuth).");
|
|
201
|
+
}
|
|
190
202
|
return {
|
|
191
203
|
connections,
|
|
192
204
|
allowWrites: parseBoolean("HARDCHARGER_ALLOW_WRITES", env.HARDCHARGER_ALLOW_WRITES, false),
|
|
@@ -204,6 +216,35 @@ export function parseConfig(env = process.env) {
|
|
|
204
216
|
max: 100000,
|
|
205
217
|
}),
|
|
206
218
|
enableSso,
|
|
219
|
+
broker,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Reads the connection broker settings (HTTP transport with OAuth). Returns undefined unless
|
|
224
|
+
* HARDCHARGER_MCP_BROKER_URL is set. A plain http URL needs
|
|
225
|
+
* HARDCHARGER_MCP_BROKER_ALLOW_INSECURE=true (local testing).
|
|
226
|
+
*/
|
|
227
|
+
export function parseBroker(env) {
|
|
228
|
+
const allowHttp = parseBoolean("HARDCHARGER_MCP_BROKER_ALLOW_INSECURE", env.HARDCHARGER_MCP_BROKER_ALLOW_INSECURE, false);
|
|
229
|
+
const url = parseHttpsUrl("HARDCHARGER_MCP_BROKER_URL", env.HARDCHARGER_MCP_BROKER_URL, { allowHttp });
|
|
230
|
+
if (url === undefined) {
|
|
231
|
+
for (const key of Object.keys(env)) {
|
|
232
|
+
if (key.startsWith("HARDCHARGER_MCP_BROKER_") && key !== "HARDCHARGER_MCP_BROKER_ALLOW_INSECURE" && !isUnset(env[key])) {
|
|
233
|
+
throw new ConfigError(`${key} is set but HARDCHARGER_MCP_BROKER_URL is not; the URL enables the connection broker.`);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
const credential = nonEmpty(env.HARDCHARGER_MCP_BROKER_TOKEN);
|
|
239
|
+
if (credential === undefined) {
|
|
240
|
+
throw new ConfigError("HARDCHARGER_MCP_BROKER_TOKEN is required with HARDCHARGER_MCP_BROKER_URL: this server's own credential at the " +
|
|
241
|
+
"broker. Callers' tokens are presented to the broker as subject tokens, never as its credential.");
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
url,
|
|
245
|
+
credential,
|
|
246
|
+
cacheSeconds: parseInteger("HARDCHARGER_MCP_BROKER_CACHE_SECONDS", env.HARDCHARGER_MCP_BROKER_CACHE_SECONDS, DEFAULTS.brokerCacheSeconds, { min: 0, max: 3600 }),
|
|
247
|
+
timeoutMs: DEFAULTS.brokerTimeoutMs,
|
|
207
248
|
};
|
|
208
249
|
}
|
|
209
250
|
/** Splits a comma- or whitespace-separated list, dropping blanks. */
|
|
@@ -274,6 +315,8 @@ export function parseOAuth(env) {
|
|
|
274
315
|
}
|
|
275
316
|
/** The sign-in a connection's settings call for (before any login_sso). */
|
|
276
317
|
export function authMethod(config) {
|
|
318
|
+
if (config.tokenSource !== undefined)
|
|
319
|
+
return "brokered";
|
|
277
320
|
if (config.username !== undefined)
|
|
278
321
|
return "password";
|
|
279
322
|
if (config.token !== undefined)
|
|
@@ -306,12 +349,12 @@ export class QueryTimeoutError extends Error {
|
|
|
306
349
|
const CLIENT_DEADLINE_GRACE_MS = 5000;
|
|
307
350
|
/** If an abort does not settle the call, drop the connection after this. */
|
|
308
351
|
const HARD_RESET_GRACE_MS = 15000;
|
|
309
|
-
/** The error's Hardcharger (
|
|
352
|
+
/** The error's Hardcharger (Chargewire) code, if it is one of the client's errors. */
|
|
310
353
|
export function errorCode(err) {
|
|
311
354
|
return err instanceof HardchargerError ? err.code : undefined;
|
|
312
355
|
}
|
|
313
356
|
/**
|
|
314
|
-
* `session_ended` reasons after which the user's sign-in itself no longer holds (
|
|
357
|
+
* `session_ended` reasons after which the user's sign-in itself no longer holds (Chargewire
|
|
315
358
|
* §4.2, §5.6): a revoked token, changed credentials, a dropped user. Nothing signs in again
|
|
316
359
|
* on its own after these; the error is the answer.
|
|
317
360
|
*/
|
|
@@ -365,7 +408,7 @@ function readPem(what, file) {
|
|
|
365
408
|
/**
|
|
366
409
|
* One Hardcharger connection per configured server, opened lazily and serialized so a
|
|
367
410
|
* single statement is in flight at a time. Timeouts are enforced by the server
|
|
368
|
-
* (`statementTimeoutMs`,
|
|
411
|
+
* (`statementTimeoutMs`, Chargewire §6.9) with a client-side abort as backstop that cancels
|
|
369
412
|
* the statement on the server; only if that fails to settle the call is the connection
|
|
370
413
|
* dropped and reopened. The client replaces a session the server lost and replays its
|
|
371
414
|
* catalog and schema; a connection that is gone altogether (a killed session, a closed
|
|
@@ -400,7 +443,7 @@ export class HardchargerConnection {
|
|
|
400
443
|
return { ...this.searchPath };
|
|
401
444
|
}
|
|
402
445
|
/**
|
|
403
|
-
* Switches the session's default catalog and/or schema (
|
|
446
|
+
* Switches the session's default catalog and/or schema (Chargewire session options, which
|
|
404
447
|
* the client replays on a replacement session). Throws when the server refuses it.
|
|
405
448
|
*/
|
|
406
449
|
async useSchema(path) {
|
|
@@ -435,7 +478,7 @@ export class HardchargerConnection {
|
|
|
435
478
|
}
|
|
436
479
|
/** Secrets to redact from any message that might reach a client. */
|
|
437
480
|
secrets() {
|
|
438
|
-
return [this.config.password, this.config.token, this.overrides.accessToken];
|
|
481
|
+
return [this.config.password, this.config.token, this.overrides.accessToken, ...(this.config.tokenSource?.secrets() ?? [])];
|
|
439
482
|
}
|
|
440
483
|
redact(text) {
|
|
441
484
|
return redactSecrets(text, this.secrets());
|
|
@@ -513,6 +556,11 @@ export class HardchargerConnection {
|
|
|
513
556
|
if (this.overrides.accessToken !== undefined) {
|
|
514
557
|
options.token = this.overrides.accessToken;
|
|
515
558
|
}
|
|
559
|
+
else if (cfg.tokenSource !== undefined) {
|
|
560
|
+
const source = cfg.tokenSource;
|
|
561
|
+
// Called for every new sign-in: a fresh token from the broker, for this user.
|
|
562
|
+
options.subjectToken = () => source.token();
|
|
563
|
+
}
|
|
516
564
|
else if (cfg.username !== undefined) {
|
|
517
565
|
options.username = cfg.username;
|
|
518
566
|
options.password = cfg.password ?? "";
|
|
@@ -548,7 +596,7 @@ export class HardchargerConnection {
|
|
|
548
596
|
return client;
|
|
549
597
|
}
|
|
550
598
|
/**
|
|
551
|
-
* The search path, as
|
|
599
|
+
* The search path, as Chargewire session options. A catalog or schema the server does not
|
|
552
600
|
* have is reported (with the catalogs it does have) instead of failing the connection;
|
|
553
601
|
* the server's default then applies.
|
|
554
602
|
*/
|