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