@gizmodata/hardcharger-mcp 0.0.0 → 2.0.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/README.md CHANGED
@@ -1,5 +1,488 @@
1
- # @gizmodata/hardcharger-mcp
1
+ <p align="center">
2
+ <a href="https://github.com/gizmodata/hardcharger">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/hardcharger-banner-dark.png">
5
+ <img src="docs/assets/hardcharger-banner-light.png" alt="Hardcharger™" width="400">
6
+ </picture>
7
+ </a>
8
+ </p>
2
9
 
3
- Placeholder: Hardcharger MCP server: lets AI agents query a Hardcharger database. Coming soon from [GizmoData](https://gizmodata.com).
10
+ <p align="center">
11
+ <img src="https://img.shields.io/badge/npm-%40gizmodata%2Fhardcharger--mcp-cb3837.svg?logo=npm" alt="npm: @gizmodata/hardcharger-mcp">
12
+ <img src="https://img.shields.io/badge/MCP-server%20%2B%20Claude%20Desktop%20extension-d97757.svg" alt="MCP server and Claude Desktop extension">
13
+ <img src="https://img.shields.io/badge/TypeScript-ESM-3178C6.svg?logo=typescript&logoColor=white" alt="TypeScript, ESM">
14
+ <img src="https://img.shields.io/badge/Node-%E2%89%A522.19-339933.svg?logo=node.js&logoColor=white" alt="Node 22.19 or later">
15
+ <a href="https://github.com/gizmodata/warp-sql"><img src="https://img.shields.io/badge/protocol-Warp%20SQL%201.0.0-31a8db.svg" alt="Warp SQL protocol"></a>
16
+ <a href="https://github.com/gizmodata/hardcharger-js"><img src="https://img.shields.io/badge/built%20on-Hardcharger%20JS-31a8db.svg" alt="Built on the Hardcharger JavaScript client"></a>
17
+ <a href="https://github.com/gizmodata/hardcharger"><img src="https://img.shields.io/badge/server-Hardcharger-f15a24.svg" alt="Hardcharger"></a>
18
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache%202.0-brightgreen.svg" alt="Apache 2.0 license"></a>
19
+ <a href="https://gizmodata.com"><img src="https://img.shields.io/badge/by-GizmoData%E2%84%A2-f15a24.svg" alt="GizmoData™"></a>
20
+ </p>
4
21
 
5
- Hardcharger™ and GizmoData™ are trademarks of GizmoData LLC. Apache-2.0.
22
+ <h3 align="center">Explore and query Hardcharger™ from Claude Desktop, Claude Code and any MCP client.</h3>
23
+
24
+ ---
25
+
26
+ ## 🔌 What is it?
27
+
28
+ A [Model Context Protocol](https://modelcontextprotocol.io) server and Claude Desktop extension
29
+ for [Hardcharger™](https://github.com/gizmodata/hardcharger), GizmoData™'s DuckDB 2.0 server. It
30
+ lets Claude Desktop, Claude Code and any other MCP client explore your schema and run SQL against
31
+ a Hardcharger server you can reach from your machine (including private networks over VPN), or,
32
+ over Streamable HTTP, serves a whole team behind OAuth.
33
+
34
+ It speaks [Warp SQL](https://github.com/gizmodata/warp-sql), Hardcharger's HTTP protocol, through
35
+ the Hardcharger JavaScript client ([`@gizmodata/hardcharger`](https://github.com/gizmodata/hardcharger-js)):
36
+ pure JavaScript, no native driver, so one extension bundle serves every platform Claude Desktop
37
+ runs on. It is the Hardcharger port of
38
+ [gizmosql-mcp](https://github.com/gizmodata/gizmosql-mcp), with the same tools and settings
39
+ (`HARDCHARGER_*` instead of `GIZMOSQL_*`; see [CHANGELOG.md](CHANGELOG.md) for what changed).
40
+
41
+ > **Status: 2.0.0, with Hardcharger 2.0.0.** Built on `@gizmodata/hardcharger` 2.0.0 (hardcharger-js
42
+ > `a6e93a2`, on `@gizmodata/warp-sql` 1.0.0, Warp SQL protocol 1.0.0); tested against
43
+ > `hardcharger-server` 2.0.0-rc.1 and the image of Hardcharger's main branch.
44
+
45
+ ## 🧰 Tools
46
+
47
+ | Tool | What it does |
48
+ | --- | --- |
49
+ | `list_connections()` | Configured Hardcharger connections and which one is current (never credentials) |
50
+ | `use_connection(name)` | Makes a connection the default for subsequent calls |
51
+ | `list_catalogs` | Catalogs (attached databases) visible to the user |
52
+ | `list_schemas(catalog?, include_system?)` | Schemas, optionally in one catalog; system schemas (`information_schema`, `pg_catalog`, `pg_toast`, `pg_temp_*`) are hidden unless `include_system` is true |
53
+ | `list_tables(catalog?, schema?, like?)` | Tables and views with their type; `like` is a SQL LIKE pattern |
54
+ | `describe_table(table, schema?, catalog?)` | Columns, types, nullability, constraints, estimated row count |
55
+ | `use_schema(catalog?, schema?)` | Sets the session's default catalog/schema for unqualified table names |
56
+ | `run_query(sql, params?, max_rows?)` | Runs a query; returns a Markdown table plus structured JSON, capped at `max_rows` |
57
+ | `explain_query(sql)` | DuckDB `EXPLAIN` plan without executing |
58
+ | `execute_statement(sql, params?)` | DML/DDL with affected-row count. Only registered when writes are enabled |
59
+ | `server_info()` | Hardcharger/DuckDB versions, signed-in user and role, connection URI, limits, extension version |
60
+ | `login_sso(wait_seconds?)` | Browser sign-in through the server's identity provider. Only registered when SSO is enabled |
61
+
62
+ Every tool except `list_connections` and `use_connection` also accepts an optional `connection`
63
+ argument naming one of the configured servers (see [Multiple connections](#-multiple-connections)).
64
+ A resource template, `hardcharger://{connection}/schema/{catalog}/{schema}/{table}`, returns the
65
+ DDL of a table or view.
66
+
67
+ ### Query parameters
68
+
69
+ `run_query` and `execute_statement` accept a `params` array bound positionally to `?` (or `$1`,
70
+ `$2`, ...) placeholders. Values travel to the server as bound parameters, never as interpolated SQL
71
+ text, so use placeholders for any literal that comes from user input or from data returned by an
72
+ earlier query.
73
+
74
+ - Plain JSON values: string, number, boolean, `null`. The server types each value from where it is
75
+ used, so a date string compared with (or inserted into) a `DATE` or `TIMESTAMP` column is read as
76
+ a date.
77
+ - Explicit types: `{"type": "bigint", "value": "9223372036854775807"}` (exact beyond 2^53),
78
+ `{"type": "date", "value": "2024-01-02"}`, `{"type": "timestamp", ...}`,
79
+ `{"type": "binary", "value": "<base64>"}`, plus `string`, `number`, `boolean` and `null`. A date,
80
+ timestamp or binary parameter binds the row as Arrow data through a prepared statement.
81
+ - A placeholder whose type nothing gives, such as `SELECT ?`, is text unless typed or cast:
82
+ `SELECT ?::INTEGER`.
83
+
84
+ Example `run_query` arguments:
85
+
86
+ <!-- example: tool-call -->
87
+ ```json
88
+ {
89
+ "sql": "SELECT id, name FROM customers WHERE created_at >= ? AND region = ? LIMIT 20",
90
+ "params": ["2024-01-01", "EMEA"]
91
+ }
92
+ ```
93
+
94
+ ## 🚀 Installation
95
+
96
+ ### Claude Desktop extension (`.mcpb`)
97
+
98
+ 1. Download `hardcharger-mcp.mcpb` from this repository's
99
+ [Releases](https://github.com/gizmodata/hardcharger-mcp/releases) (each release also carries a
100
+ versioned `hardcharger-mcp-<version>.mcpb` and `.sha256` checksums), or build it with
101
+ `npm run build:mcpb` (see [Building and testing](#%EF%B8%8F-building-and-testing)).
102
+ 2. Double-click the file, or in Claude Desktop open **Settings → Extensions → Advanced settings →
103
+ Install Extension…** and pick it.
104
+ 3. Fill in the settings: the server's URI (`hardcharger://db.example.com`) and one credential.
105
+ Credentials are marked sensitive and kept in your operating system's keychain.
106
+ 4. In a chat, open the **+** menu, choose **Connectors** and make sure **Hardcharger** is on.
107
+
108
+ No Node.js installation is needed: Claude Desktop runs the extension with its own runtime, and the
109
+ bundle holds no native code.
110
+
111
+ ### Claude Code
112
+
113
+ <!-- example: claude-code -->
114
+ ```bash
115
+ claude mcp add hardcharger \
116
+ -e HARDCHARGER_URI='hardcharger://localhost?tls=false' \
117
+ -e HARDCHARGER_USERNAME=hardcharger \
118
+ -e HARDCHARGER_PASSWORD=change-me \
119
+ -- npx -y @gizmodata/hardcharger-mcp
120
+ ```
121
+
122
+ Use `-s user` to make it available in every project. Node.js 22.19 or newer is required when
123
+ running through `npx`. Installed globally (`npm install -g @gizmodata/hardcharger-mcp`), the command
124
+ is `hardcharger-mcp`: use `-- hardcharger-mcp` instead of `npx -y @gizmodata/hardcharger-mcp`.
125
+
126
+ ### `claude_desktop_config.json` (manual JSON)
127
+
128
+ <!-- example: desktop-json -->
129
+ ```json
130
+ {
131
+ "mcpServers": {
132
+ "hardcharger": {
133
+ "command": "npx",
134
+ "args": ["-y", "@gizmodata/hardcharger-mcp"],
135
+ "env": {
136
+ "HARDCHARGER_URI": "hardcharger://localhost?tls=false",
137
+ "HARDCHARGER_USERNAME": "hardcharger",
138
+ "HARDCHARGER_PASSWORD": "change-me"
139
+ }
140
+ }
141
+ }
142
+ }
143
+ ```
144
+
145
+ These examples connect to a local development server started with
146
+ `HARDCHARGER_ADMIN_USERNAME=hardcharger HARDCHARGER_ADMIN_PASSWORD=change-me hardcharger-server`
147
+ (the [Hardcharger README](https://github.com/gizmodata/hardcharger) has Docker and the
148
+ installers). A real server uses TLS: `hardcharger://db.example.com`.
149
+
150
+ ## 🔐 Signing in
151
+
152
+ Each connection takes one credential:
153
+
154
+ | Setting | Sign-in |
155
+ | --- | --- |
156
+ | `HARDCHARGER_USERNAME` + `HARDCHARGER_PASSWORD` | Password (any character, `:` and `@` included) |
157
+ | `HARDCHARGER_TOKEN` | A signed sign-in token the server trusts (`--token-allowed-issuer` with `--token-jwks-uri` or `--token-signature-verify-cert-path`), sent with Warp SQL's `token_exchange` grant. The same token also works as the password of user `token`. Its `role` claim (`admin`, `user`, `readonly`) decides what it may do |
158
+ | `HARDCHARGER_MTLS_CERT` + `HARDCHARGER_MTLS_KEY` | Client certificate sign-in (mutual TLS) on a server started with `--mtls-certificate-sign-in`: the certificate is the credential. With a password or token beside them, the certificate only admits the connection |
159
+ | `HARDCHARGER_ENABLE_SSO=true` | No stored credential: `login_sso` signs in through the server's identity provider in your browser |
160
+
161
+ A client certificate from a private CA usually comes with that CA for the server's certificate:
162
+
163
+ <!-- example: mtls -->
164
+ ```bash
165
+ HARDCHARGER_URI=hardcharger://localhost \
166
+ HARDCHARGER_MTLS_CERT=client.pem HARDCHARGER_MTLS_KEY=client-key.pem \
167
+ HARDCHARGER_TLS_ROOTS=server-ca.pem \
168
+ npx -y @gizmodata/hardcharger-mcp
169
+ ```
170
+
171
+ The identity is the certificate's e-mail SAN unless the server says otherwise; it must be one of
172
+ the server's `--mtls-admin-identities` or, under access control, a user of its store. Refusals keep
173
+ the server's codes: `unknown_certificate_identity` (401) for a certificate that names no user, and
174
+ `client_transport_error` when the handshake fails (an untrusted server certificate without
175
+ `HARDCHARGER_TLS_ROOTS` or `HARDCHARGER_TLS_SKIP_VERIFY`).
176
+
177
+ **Browser sign-in (`login_sso`).** On a server started with `--oauth-client-id` (and its identity
178
+ provider settings), `login_sso` opens the provider's login page through the server's Warp SQL
179
+ browser sign-in (on the connection's own URI and port), waits for you to finish, and connects with
180
+ the Hardcharger access token the server hands out, kept in memory only; when it expires, call
181
+ `login_sso` again. A refusal names its code (`access_denied`, `identity_provider_error`,
182
+ `invalid_credentials` for a user the server doesn't allow).
183
+ `HARDCHARGER_BROWSER` names the program that opens the page (default: `open`, `xdg-open`, or the
184
+ Windows URL handler), as in the Hardcharger CLI. It runs on the stdio transport only (over HTTP it
185
+ would open a browser on the server).
186
+
187
+ ## 🌐 Streamable HTTP (remote connector)
188
+
189
+ The same server can listen over Streamable HTTP for MCP clients that connect over the network,
190
+ such as a Claude.ai custom connector. The MCP endpoint is `/mcp`; `/healthz` answers health checks.
191
+ Put it behind TLS (an ingress or reverse proxy) before exposing it beyond localhost. Requests are
192
+ authenticated in one of two ways.
193
+
194
+ **OAuth (recommended).** Any OpenID Connect provider that issues signed JWT access tokens works:
195
+ Microsoft Entra ID, Okta (custom authorization server), Auth0, Keycloak, Cognito, Clerk. The server
196
+ is an OAuth 2.1 *resource server*: it verifies each bearer token against the provider's JWKS
197
+ (issuer, audience, signature, expiry) and serves the RFC 9728 metadata at
198
+ `/.well-known/oauth-protected-resource[/mcp]` so clients find the provider on their own. The token
199
+ is never forwarded; the Hardcharger connection uses the configured service credentials.
200
+
201
+ <!-- example: http-oauth -->
202
+ ```bash
203
+ HARDCHARGER_URI=hardcharger://hardcharger.internal.example.com \
204
+ HARDCHARGER_USERNAME=mcp_service HARDCHARGER_PASSWORD='service-password' \
205
+ HARDCHARGER_MCP_PUBLIC_URL=https://mcp.example.com/mcp \
206
+ HARDCHARGER_MCP_OAUTH_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0 \
207
+ HARDCHARGER_MCP_OAUTH_AUDIENCE=<application-client-id> \
208
+ HARDCHARGER_MCP_OAUTH_SCOPES='https://mcp.example.com/mcp/access_as_user openid profile email offline_access' \
209
+ HARDCHARGER_MCP_OAUTH_TOKEN_PROXY=true \
210
+ HARDCHARGER_MCP_OAUTH_AUTHORIZED_EMAILS='*@example.com' \
211
+ npx -y @gizmodata/hardcharger-mcp --transport http --host 0.0.0.0 --port 3000
212
+ ```
213
+
214
+ Then add `https://mcp.example.com/mcp` as a custom connector in Claude (Customize > Connectors).
215
+ Claude sends that URL as the OAuth `resource`, discovers the provider from the metadata, runs the
216
+ authorization-code flow with PKCE, and retries with the token. Each tool call is logged with the
217
+ caller's identity, and `server_info` reports it as `authenticated_user`. Each user gets their own
218
+ Hardcharger session, current connection and search path (`HARDCHARGER_MCP_SESSION_IDLE_SECONDS`,
219
+ `HARDCHARGER_MCP_MAX_SESSIONS`); the first result after an expired session says so.
220
+
221
+ Claude requests exactly the scopes named in `HARDCHARGER_MCP_OAUTH_SCOPES`, so include
222
+ `offline_access` alongside the API scope: it is what makes the provider issue a refresh token. The
223
+ server warns at startup when the scope list lacks it and logs every rejected token with the reason.
224
+
225
+ **Token proxy (required for Microsoft Entra ID).** Claude's refresh request carries only the OpenID
226
+ Connect scopes, and Entra refuses it with `AADSTS90009` because it names no resource, so the
227
+ connector stops working when the access token expires (about an hour). With
228
+ `HARDCHARGER_MCP_OAUTH_TOKEN_PROXY=true` the server publishes an authorization-server metadata
229
+ document at `<public origin>/oauth` and proxies the token endpoint at `<public origin>/oauth/token`:
230
+ `refresh_token` grants get the configured API scope and `offline_access` added, everything else
231
+ passes through, and sign-in still happens at the provider. See
232
+ [anthropics/claude-ai-mcp#840](https://github.com/anthropics/claude-ai-mcp/issues/840).
233
+
234
+ Provider notes: **Microsoft Entra ID** has no dynamic client registration, so the person adding the
235
+ connector enters the app registration's client ID and secret under *Advanced settings* (redirect URI
236
+ `https://claude.ai/api/mcp/auth_callback`, *Expose an API* with the MCP URL as Application ID URI
237
+ and a scope such as `access_as_user`, `requestedAccessTokenVersion` 2; v2 tokens carry the client ID
238
+ as `aud`). **Okta** needs a custom authorization server; **Auth0** an API with the audience;
239
+ **Keycloak** works out of the box; **Clerk** issues JWT access tokens by default. Providers with
240
+ opaque access tokens (Google) are not supported in this mode.
241
+
242
+ **Static bearer token.** For a quick shared secret instead of OAuth, set
243
+ `HARDCHARGER_MCP_BEARER_TOKEN`; every request must then send `Authorization: Bearer <token>`. The
244
+ two modes are mutually exclusive. With neither set the endpoint is unauthenticated, for local use
245
+ only.
246
+
247
+ <!-- example: http-bearer -->
248
+ ```bash
249
+ HARDCHARGER_URI='hardcharger://localhost?tls=false' \
250
+ HARDCHARGER_USERNAME=hardcharger HARDCHARGER_PASSWORD=change-me \
251
+ HARDCHARGER_MCP_BEARER_TOKEN=a-long-random-secret \
252
+ npx -y @gizmodata/hardcharger-mcp --transport http --port 3000
253
+ ```
254
+
255
+ ### Container image and Helm chart
256
+
257
+ [`Dockerfile`](Dockerfile) builds an image whose entrypoint runs the HTTP transport on port 3000, and
258
+ [`charts/hardcharger-mcp`](charts/hardcharger-mcp) deploys it with a ConfigMap for the
259
+ `HARDCHARGER_*` settings, a Secret (or `existingSecret`) for credentials, and an optional Ingress.
260
+ The release workflow builds both and attaches the packaged chart to the GitHub Release, and pushes
261
+ the image and chart to GHCR when the repository variable `IMAGE_PUBLISH_ENABLED` is `true`.
262
+
263
+ ```bash
264
+ helm upgrade --install hardcharger-mcp charts/hardcharger-mcp \
265
+ --namespace hardcharger-mcp --create-namespace --values values.yaml
266
+ ```
267
+
268
+ ## ⚙️ Configuration
269
+
270
+ All settings are environment variables; the extension's settings screen maps onto the same names.
271
+ Only `HARDCHARGER_*` names are read.
272
+
273
+ | Variable | Default | Description |
274
+ | --- | --- | --- |
275
+ | `HARDCHARGER_URI` | required | `hardcharger://host[:port][/prefix][?tls=false]`: TLS to port 31337 unless the URI says otherwise |
276
+ | `HARDCHARGER_CONNECTION_NAME` | `default` | Name of this connection (used with the `connection` tool argument) |
277
+ | `HARDCHARGER_USERNAME` / `HARDCHARGER_PASSWORD` | | Password sign-in |
278
+ | `HARDCHARGER_TOKEN` | | A signed sign-in token (the `token_exchange` grant) |
279
+ | `HARDCHARGER_MTLS_CERT` / `HARDCHARGER_MTLS_KEY` | | Client certificate and key (PEM files) for mutual TLS; alone, certificate sign-in |
280
+ | `HARDCHARGER_TLS_ROOTS` | system | CA bundle (PEM file) for the server's certificate |
281
+ | `HARDCHARGER_TLS_SKIP_VERIFY` | `false` | Accept self-signed or untrusted certificates |
282
+ | `HARDCHARGER_DEFAULT_CATALOG` | | Catalog for unqualified table names (optional) |
283
+ | `HARDCHARGER_DEFAULT_SCHEMA` | | Schema for unqualified table names (optional) |
284
+ | `HARDCHARGER_ALLOW_WRITES` | `false` | Register `execute_statement` and let `run_query` run non-read statements |
285
+ | `HARDCHARGER_MAX_ROWS` | `500` | Hard cap on rows returned by `run_query` |
286
+ | `HARDCHARGER_MAX_CELL_CHARS` | `200` | Cells longer than this are truncated with `…` |
287
+ | `HARDCHARGER_QUERY_TIMEOUT_SECONDS` | `60` | Per-statement timeout, enforced by the server; `0` disables it |
288
+ | `HARDCHARGER_ENABLE_SSO` | `false` | Register the `login_sso` tool (credentials may then be left empty) |
289
+ | `HARDCHARGER_BROWSER` | platform's | Program that opens the sign-in page for `login_sso` |
290
+ | `HARDCHARGER_MCP_BEARER_TOKEN` | | HTTP transport: static bearer token (mutually exclusive with OAuth) |
291
+ | `HARDCHARGER_MCP_PUBLIC_URL` | | HTTP transport with OAuth: public URL of the `/mcp` endpoint, the OAuth resource identifier |
292
+ | `HARDCHARGER_MCP_OAUTH_ISSUER` | | HTTP transport: OpenID Connect issuer URL; setting it enables OAuth |
293
+ | `HARDCHARGER_MCP_OAUTH_AUDIENCE` | public URL | Accepted `aud` values, comma-separated (Entra ID v2 tokens: the client ID) |
294
+ | `HARDCHARGER_MCP_OAUTH_SCOPES` | | Scopes advertised to clients; include `offline_access` so a refresh token is issued |
295
+ | `HARDCHARGER_MCP_OAUTH_AUTHORIZED_EMAILS` | | Glob allowlist of sign-in emails, e.g. `*@example.com` |
296
+ | `HARDCHARGER_MCP_OAUTH_TOKEN_PROXY` | `false` | Proxy the token endpoint so `refresh_token` grants carry the API scope (required for Entra ID) |
297
+ | `HARDCHARGER_MCP_OAUTH_JWKS_URI` | discovered | JWKS endpoint, when discovery from the issuer is not possible |
298
+ | `HARDCHARGER_MCP_OAUTH_USER_CLAIM` | `email,preferred_username,upn,name,sub` | Claims tried in order to name the caller |
299
+ | `HARDCHARGER_MCP_OAUTH_ALLOW_INSECURE` | `false` | Accept `http://` issuer and public URLs (local testing only) |
300
+ | `HARDCHARGER_MCP_SESSION_IDLE_SECONDS` | `1800` | HTTP transport with OAuth: close a user's session after this long without a request |
301
+ | `HARDCHARGER_MCP_MAX_SESSIONS` | `200` | HTTP transport with OAuth: cap on concurrent user sessions |
302
+ | `HARDCHARGER_2_URI`, `HARDCHARGER_3_URI`, … | | Additional connections, see below |
303
+ | `HARDCHARGER_CONNECTIONS` | | Comma-separated names of further connections, see below |
304
+
305
+ The default catalog and schema are optional session options: if one does not exist, the server
306
+ still starts and reports the problem (with the catalogs it found) in `server_info`. In a chat,
307
+ `use_schema` or a plain `USE catalog.schema` switches for the rest of the session.
308
+
309
+ ### 🔀 Multiple connections
310
+
311
+ - **Claude Desktop extension.** The settings screen has two extra slots, *Connection 2* and
312
+ *Connection 3*, each with its own name, URI, credentials and default catalog/schema. Leave a
313
+ slot's URI blank to disable it.
314
+ - **Environment variables.** The slots map to `HARDCHARGER_2_*` and `HARDCHARGER_3_*`
315
+ (`HARDCHARGER_2_URI`, `HARDCHARGER_2_NAME`, `HARDCHARGER_2_USERNAME`, `HARDCHARGER_2_PASSWORD`,
316
+ `HARDCHARGER_2_TOKEN`, ...). For any number of servers, list names in `HARDCHARGER_CONNECTIONS`
317
+ and define each one with the same variables prefixed by the upper-cased name (non-alphanumerics
318
+ become `_`; `mcp` is reserved):
319
+
320
+ <!-- example: multi -->
321
+ ```bash
322
+ HARDCHARGER_URI='hardcharger://localhost?tls=false' \
323
+ HARDCHARGER_USERNAME=hardcharger HARDCHARGER_PASSWORD=change-me \
324
+ HARDCHARGER_CONNECTIONS=staging,dev-eu \
325
+ HARDCHARGER_STAGING_URI=hardcharger://hardcharger.staging.example.com \
326
+ HARDCHARGER_STAGING_TOKEN='<a sign-in token>' \
327
+ HARDCHARGER_DEV_EU_URI='hardcharger://localhost?tls=false' \
328
+ HARDCHARGER_DEV_EU_USERNAME=hardcharger HARDCHARGER_DEV_EU_PASSWORD=change-me \
329
+ npx -y @gizmodata/hardcharger-mcp
330
+ ```
331
+
332
+ In a chat, `list_connections` shows what is configured, `use_connection` switches the default, and
333
+ every tool accepts a per-call `connection` argument. Connections open lazily; the row cap, timeout
334
+ and read-only settings apply to all of them, while each server's user privileges still govern what
335
+ that connection may do.
336
+
337
+ ## 🛡️ Read-only model and security
338
+
339
+ By default the server refuses anything that is not a read. `sql-guard.ts` classifies each statement
340
+ and only lets `SELECT`, `WITH … SELECT`, `FROM`, `VALUES`, `SHOW`, `DESCRIBE`, `SUMMARIZE`,
341
+ `EXPLAIN`, read-style `PRAGMA` and `USE` (search path only) through. CTEs that end in DML, `COPY`,
342
+ `ATTACH`, `INSTALL`, `LOAD`, `SET`, `CALL`, transactions and multi-statement input are rejected.
343
+ `HARDCHARGER_ALLOW_WRITES=true` lifts that and adds `execute_statement`, annotated as destructive so
344
+ clients ask before running it.
345
+
346
+ The guard is defense in depth; the real boundary is the privileges of the Hardcharger user the
347
+ server signs in as. A sign-in token with `role: readonly` can only read (Hardcharger refuses
348
+ anything else with `permission_denied`), and under Hardcharger's access control a store user's
349
+ grants decide what each table allows.
350
+
351
+ - Credentials never appear in tool output, `server_info` or error messages: every message that
352
+ reaches the client is redacted.
353
+ - The row cap is enforced by the server: reads run as `SELECT * FROM (<your query>) LIMIT
354
+ max_rows + 1`, over one result stream that is closed at the cap, which stops the statement.
355
+ - The timeout is the server's (Warp SQL `statementTimeoutMs`): it cancels a statement that runs too
356
+ long, `execute_statement` included, and the error says `statement_timeout`. A client-side cancel
357
+ a few seconds later is the backstop.
358
+ - Lost sessions: when Hardcharger evicts an idle session, the client opens a replacement with the
359
+ same catalog and schema (also those set by `use_schema` or `USE`), says so in the log, and counts
360
+ it in `server_info` (`session_replacements`); temporary tables and SQL-level settings of the old
361
+ session are gone. A session an administrator killed, a closed connection or a transport failure
362
+ reopens the connection once and retries (`reconnects`); a refused credential is never retried,
363
+ and neither is a session ended because the sign-in was withdrawn (`token_revoked`,
364
+ `credentials_changed`, `principal_dropped`).
365
+ - The server has no tools that touch the local filesystem or any network endpoint other than the
366
+ configured Hardcharger servers (and, for `login_sso`, the server's sign-in routes and your
367
+ identity provider in your browser).
368
+
369
+ ## 🧯 Troubleshooting
370
+
371
+ **`speaks plain HTTP, not TLS: add ?tls=false`.** A `hardcharger://` URI uses TLS unless it says
372
+ `?tls=false`; a development server started without `--tls-cert` needs it.
373
+
374
+ **TLS errors (`self-signed certificate`, `unable to verify`, `client_transport_error`).** Set
375
+ `HARDCHARGER_TLS_ROOTS` to the CA that issued the server's certificate, or (for a throwaway server)
376
+ `HARDCHARGER_TLS_SKIP_VERIFY=true`.
377
+
378
+ **`client_transport_error` to a `10.x`, `172.16.x` or `192.168.x` address.** The extension connects
379
+ directly from your machine: connect your VPN first, then start a new chat. For a server reachable
380
+ only through SSH or Kubernetes, forward the port and use `hardcharger://localhost:31337` (with
381
+ `HARDCHARGER_TLS_SKIP_VERIFY=true` if the certificate names the internal host):
382
+
383
+ ```bash
384
+ ssh -N -L 31337:hardcharger.internal:31337 bastion.example.com
385
+ kubectl port-forward svc/hardcharger 31337:31337
386
+ ```
387
+
388
+ **`invalid_credentials`, `unknown_certificate_identity`, `permission_denied`.** `server_info` shows
389
+ the user and role the server signed in. Username and password must both be set; a token's issuer,
390
+ audience and e-mail must be ones the server accepts; with SSO, run `login_sso` first.
391
+
392
+ **Tools do not appear in the chat.** Open the **+** menu, choose Connectors and enable Hardcharger.
393
+ After changing settings, start a new chat.
394
+
395
+ **Logs.** Claude Desktop writes the server's stderr to
396
+ `~/Library/Logs/Claude/mcp-server-hardcharger.log` on macOS and `%APPDATA%\Claude\logs\` on Windows.
397
+
398
+ ## 🛠️ Building and testing
399
+
400
+ Node 22.19 or later. For development and CI, `@gizmodata/hardcharger` and `@gizmodata/warp-sql` are
401
+ built from [gizmodata/hardcharger-js](https://github.com/gizmodata/hardcharger-js) and
402
+ [gizmodata/warp-sql-js](https://github.com/gizmodata/warp-sql-js) at the commits pinned in
403
+ [scripts/hardcharger-js.commit](scripts/hardcharger-js.commit) and
404
+ [scripts/warp-sql-js.commit](scripts/warp-sql-js.commit), and packed to `.clients/`, where
405
+ `package-lock.json` finds them, so the tests run against those exact clients. `package.json`, and
406
+ so the published package, names the npm package and its range (`^2.0.0`); the tarballs are a
407
+ stand-in for development and CI only (`scripts/check-lock.mjs` checks both).
408
+
409
+ ```bash
410
+ npm run clients # clone both clients (git access needed), build and pack the pinned commits
411
+ npm ci
412
+ npm run lint:ci # eslint and the lock's shape
413
+ npm run typecheck # src and tests
414
+ npm test # unit tests (no server)
415
+ npm run build:mcpb # build/hardcharger-mcp-<version>.mcpb + .sha256
416
+ docker build -t hardcharger-mcp . # container image for the HTTP transport
417
+ helm lint charts/hardcharger-mcp # Helm chart
418
+ ```
419
+
420
+ `HARDCHARGER_MCP_WARP_SQL_JS_REPO` and `HARDCHARGER_MCP_HARDCHARGER_JS_REPO` point `npm run clients`
421
+ at local checkouts.
422
+
423
+ The live suites start `hardcharger-server` themselves, on free loopback ports, with a fresh
424
+ administrator password that contains `:` and `@`, an empty home directory (DuckDB's secrets
425
+ directory included), and only `PATH`, `HOME`, `TMPDIR` and the password in its environment; the MCP
426
+ server under test gets `PATH`, `HOME` and the settings each test names, never your shell's. Without
427
+ the binary they fail; they never skip, and a run in which no test passed fails too.
428
+
429
+ ```bash
430
+ export HARDCHARGER_MCP_TEST_SERVER_BINARY=/path/to/hardcharger-server
431
+ npm run test:live # every tool over stdio and HTTP, OAuth sessions, every sign-in
432
+ # (password, token, login_sso with a throwaway identity provider,
433
+ # client certificates), lost sessions (eviction, KILL SESSION, restart)
434
+ npm run test:examples # this README's examples, installed from the packed tarball, on 31337
435
+ ```
436
+
437
+ Negative tests assert the error code (`invalid_credentials`, `permission_denied`,
438
+ `unknown_certificate_identity`, `statement_timeout`, `sql_error`, `client_transport_error`,
439
+ `invalid_option_value`), each beside a positive twin. CI runs everything against the image of
440
+ Hardcharger's main branch (`ghcr.io/gizmodata/hardcharger:main`), pulled with the workflow's
441
+ `GITHUB_TOKEN`; while the image is private the package must grant this repository read access
442
+ (package settings, "Manage Actions access"), or the live jobs fail. CI clones the clients with
443
+ `DEPLOY_KEY_WARP_SQL_JS` and `DEPLOY_KEY_HARDCHARGER_JS`, read-only deploy keys of those
444
+ repositories (needed while they are private).
445
+
446
+ ## 📦 Releasing
447
+
448
+ Releases follow [semantic versioning](https://semver.org) and [CHANGELOG.md](CHANGELOG.md): move the
449
+ `[Unreleased]` entries into a `## [X.Y.Z] - YYYY-MM-DD` section, set the same version in
450
+ `package.json`, `manifest.json` and `charts/hardcharger-mcp/Chart.yaml` (`version` and
451
+ `appVersion`), commit, and push `main` and `vX.Y.Z` together. `release.yml` runs the full CI on the
452
+ tagged commit, then makes a GitHub Release with the `.mcpb` bundles, their checksums, the Helm chart
453
+ and the npm tarball, and the CHANGELOG section as notes. Publishing to npm (public; trusted
454
+ publishing, OIDC) runs when the repository variable `NPM_PUBLISH_ENABLED` is `true`, after a check
455
+ that `@gizmodata/hardcharger` is on npm in the range `package.json` names; the image and chart go
456
+ to GHCR when `IMAGE_PUBLISH_ENABLED` is `true`. `npm publish` from a laptop refuses to run.
457
+
458
+ ## 🔒 Privacy Policy
459
+
460
+ This extension runs entirely on your computer and talks only to the Hardcharger servers you
461
+ configure (and, when you use `login_sso`, that server's sign-in routes and your identity provider in
462
+ your browser).
463
+
464
+ - **Data collection.** The extension collects nothing. It sends no telemetry, analytics or crash
465
+ reports to GizmoData or anyone else.
466
+ - **Usage and storage.** The SQL your MCP client sends and the rows the server returns pass through
467
+ the extension in memory and are handed back to the client; nothing is written to disk. Connection
468
+ settings are stored by Claude Desktop; credentials marked sensitive are kept in your operating
469
+ system's keychain. An SSO access token is held in process memory only.
470
+ - **Third-party sharing.** Data is shared only with the Hardcharger server you configured. What your
471
+ MCP client (for example Claude) does with tool results is governed by that client's own privacy
472
+ policy.
473
+ - **Data retention.** The extension retains no data between runs. Your Hardcharger server may log
474
+ queries according to its own configuration.
475
+ - **Contact.** Privacy questions: info@gizmodata.com. GizmoData's general privacy policy is at
476
+ <https://gizmodata.com/privacy-policy>.
477
+
478
+ ## 📄 License
479
+
480
+ [Apache License 2.0](LICENSE).
481
+
482
+ ---
483
+
484
+ <p align="center">
485
+ <a href="https://gizmodata.com"><img src="docs/assets/gizmodata-logo.png" alt="GizmoData™" width="56"></a><br>
486
+ <sub>Built by <a href="https://gizmodata.com">GizmoData™</a></sub><br>
487
+ <sub>Hardcharger™ and GizmoData™ are trademarks of GizmoData LLC.</sub>
488
+ </p>
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // Entrypoint: `hardcharger-mcp [--transport stdio|http] [--port N] [--host H]`.
3
+ import { parseArgs } from "node:util";
4
+ function usage() {
5
+ return [
6
+ "Usage: hardcharger-mcp [--transport stdio|http] [--port N] [--host H]",
7
+ "",
8
+ " --transport stdio (default) or http (Streamable HTTP on /mcp)",
9
+ " --port HTTP port (default 3000; http transport only)",
10
+ " --host HTTP bind address (default 127.0.0.1; http transport only)",
11
+ " --version print the version",
12
+ " --help show this help",
13
+ "",
14
+ "Configuration is read from HARDCHARGER_* environment variables (see README):",
15
+ "HARDCHARGER_URI=hardcharger://host[:port][?tls=false] and one credential.",
16
+ "HTTP transport authentication: HARDCHARGER_MCP_OAUTH_ISSUER (+ HARDCHARGER_MCP_PUBLIC_URL) for OAuth,",
17
+ "or HARDCHARGER_MCP_BEARER_TOKEN for a static token.",
18
+ ].join("\n");
19
+ }
20
+ async function main() {
21
+ const { values } = parseArgs({
22
+ args: process.argv.slice(2),
23
+ options: {
24
+ transport: { type: "string", default: "stdio" },
25
+ port: { type: "string", default: "3000" },
26
+ host: { type: "string", default: "127.0.0.1" },
27
+ help: { type: "boolean", default: false },
28
+ version: { type: "boolean", default: false },
29
+ },
30
+ strict: true,
31
+ });
32
+ if (values.help) {
33
+ console.log(usage());
34
+ return;
35
+ }
36
+ const { PACKAGE_VERSION } = await import("./version.js");
37
+ if (values.version) {
38
+ console.log(PACKAGE_VERSION);
39
+ return;
40
+ }
41
+ const transport = values.transport;
42
+ if (transport !== "stdio" && transport !== "http") {
43
+ console.error(`Unknown --transport "${transport}".\n\n${usage()}`);
44
+ process.exit(2);
45
+ }
46
+ const port = Number(values.port);
47
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
48
+ console.error(`Invalid --port "${values.port}".`);
49
+ process.exit(2);
50
+ }
51
+ const { parseConfig, ConfigError } = await import("./connection.js");
52
+ let config;
53
+ try {
54
+ config = parseConfig(process.env);
55
+ }
56
+ catch (err) {
57
+ if (err instanceof ConfigError) {
58
+ console.error(`[hardcharger-mcp] configuration error: ${err.message}`);
59
+ process.exit(2);
60
+ }
61
+ throw err;
62
+ }
63
+ const { startStdio, startHttp } = await import("./transports.js");
64
+ if (transport === "stdio") {
65
+ await startStdio(config);
66
+ }
67
+ else {
68
+ await startHttp(config, { host: values.host, port });
69
+ }
70
+ }
71
+ main().catch((err) => {
72
+ console.error(`[hardcharger-mcp] fatal: ${err instanceof Error ? err.stack ?? err.message : String(err)}`);
73
+ process.exit(1);
74
+ });
75
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}