@futuretea/clickhouse-mcp-server 0.0.1 → 0.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/README.md +77 -6
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The MCP service has no authentication. It listens on `127.0.0.1` by default. Kee
|
|
|
8
8
|
|
|
9
9
|
## Quick start
|
|
10
10
|
|
|
11
|
-
1. Copy the example configuration and set the ClickHouse HTTP address.
|
|
11
|
+
1. Copy the example configuration and set the ClickHouse HTTP address. Use a read-only user; the built-in `default` user is rejected at startup. See [Service account](#service-account).
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
cp config.example.yaml config.yaml
|
|
@@ -51,9 +51,9 @@ All successful tool calls return JSON in MCP text content.
|
|
|
51
51
|
| `clickhouse_describe_table` | required `table`, optional `database` | column name, type, defaults, and comment |
|
|
52
52
|
| `clickhouse_query` | required `query`, optional positive integer `max_rows` | columns, display-string rows, returned `row_count`, and `truncated` |
|
|
53
53
|
|
|
54
|
-
`clickhouse_query` defaults to 1,000 rows. The server reads one additional row to report `truncated: true`; that extra row is not returned.
|
|
54
|
+
`clickhouse_query` defaults to 1,000 rows. The server reads one additional row to report `truncated: true`; that extra row is not returned. While output masking is off, each non-null cell is rendered as a string. While it is on, an unmatched cell keeps that display text, and a matched nested map or slice is JSON.
|
|
55
55
|
|
|
56
|
-
The query tool accepts exactly one statement. Semicolons in strings, quoted identifiers, and SQL comments are allowed; a second statement is rejected before it reaches ClickHouse. Every query is sent with ClickHouse `readonly=1
|
|
56
|
+
The query tool accepts exactly one statement. Semicolons in strings, quoted identifiers, and SQL comments are allowed; a second statement is rejected before it reaches ClickHouse. Every query is sent with ClickHouse `readonly=1`. That setting rejects writes, DDL, `SYSTEM` statements, user administration, and source table functions such as `file` and `url`. It does not hide in-flight SQL in `system.processes`, and it does not reject AI functions. Startup rejects accounts that can administer ClickHouse, read external sources, run global DDL, read other sessions' query text, or call AI functions. See [Service account](#service-account).
|
|
57
57
|
|
|
58
58
|
## Configuration
|
|
59
59
|
|
|
@@ -69,6 +69,7 @@ clickhouse:
|
|
|
69
69
|
secure: false
|
|
70
70
|
username: ""
|
|
71
71
|
password: ""
|
|
72
|
+
allow_unsafe_database_role: false
|
|
72
73
|
```
|
|
73
74
|
|
|
74
75
|
| Group | YAML key | Environment variable | Default | Meaning |
|
|
@@ -86,9 +87,75 @@ clickhouse:
|
|
|
86
87
|
| Tool filters | `disabled_tools` | `MCP_DISABLED_TOOLS` | empty | Comma-separated tool names to disable |
|
|
87
88
|
| Tool filters | `enabled_domains` | `MCP_ENABLED_DOMAINS` | empty | Comma-separated tool domains to enable |
|
|
88
89
|
| Tool filters | `disabled_domains` | `MCP_DISABLED_DOMAINS` | empty | Comma-separated tool domains to disable |
|
|
90
|
+
| Server | `allow_unsafe_database_role` | `MCP_ALLOW_UNSAFE_DATABASE_ROLE` | `false` | Skip only the startup unsafe-privilege rejection |
|
|
91
|
+
| Output | `masking` | `MCP_MASKING_JSON` | disabled | Optional output masking. A present variable replaces the whole object. |
|
|
89
92
|
|
|
90
93
|
The server never logs the configured password. `secure: true` protects the connection to ClickHouse; it does not add TLS or authentication to the MCP HTTP endpoint.
|
|
91
94
|
|
|
95
|
+
### Output masking
|
|
96
|
+
|
|
97
|
+
Masking stays off unless `masking.enabled` is `true`. An absent `masking` object leaves tool results, logs, and errors unchanged.
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
masking:
|
|
101
|
+
enabled: true
|
|
102
|
+
placeholder: "[masked]"
|
|
103
|
+
names: ["synthetic_field"]
|
|
104
|
+
name_patterns: ["token_.+"]
|
|
105
|
+
values: ["synthetic-token-value"]
|
|
106
|
+
value_patterns: ["sk_live_[0-9]+"]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`MCP_MASKING_JSON`, when set, replaces that whole object. An empty value is rejected. An unset variable leaves the YAML object in place. Other `MCP_*` variables do not merge into the object. `enabled: true` with no rules is rejected. Unknown JSON fields are rejected. The error text is `invalid masking config` and does not include the document.
|
|
110
|
+
|
|
111
|
+
`config check` prints `masking=enabled` or `masking=disabled`. The line is present when masking is off, as `masking=disabled source=default`. It does not print the rules. When masking is on, displayed configuration values are masked too.
|
|
112
|
+
|
|
113
|
+
Name patterns are parsed on their own and then anchored to the whole field name. A fragment such as `a)|(b` is rejected. A grouped pattern such as `(?:a)|(b)` matches only the whole names `a` and `b`. Value literals and value patterns are substring matches on the original text, including a one-character literal. RE2 returns the leftmost-first match and does not look for a longer alternative. A matched string becomes the placeholder. A matched number, boolean, or other non-string becomes JSON null. An original null stays null. A null in a result can be a database NULL or a redacted value, and the JSON type can change when a rule matches.
|
|
114
|
+
|
|
115
|
+
The server applies field rules to scanned driver values before `fmt.Sprint`. Unmatched cells keep the previous display text. Matched nested maps and slices become JSON. Strings and JSON text use those rules. ClickHouse does not decode a base64 string before masking it. A value rule that hits the original display text of a number, bool, time, duration, IP address, UUID, or geo value nulls the whole value. An unmatched value keeps that `fmt.Sprint` text. A JSON number keeps the digits from the document, including trailing zeros. A JSON value is counted again as its container, so it can exhaust the shared budget before a plain string of the same size. A name rule nulls the whole value. ClickHouse JSON and Variant values are walked. One set of size and node limits covers the whole tool result. An unknown value, a walk past the depth or node bound, or output past 1 MiB fails the result with `[clickhouse_masking_failed] result was not returned`. No partial result is returned.
|
|
116
|
+
|
|
117
|
+
This does not find every private value. Compressed payloads and encodings the server does not recognize are not rewritten in place. Query text sent to ClickHouse is not modified.
|
|
118
|
+
|
|
119
|
+
While masking is on, query, startup, setting, and connection errors omit driver and setting text. HTTP debug logs use a route class (`mcp`, `sse`, `message`, `healthz`, or `other`) instead of the request path. The placeholder and those fixed errors are not treated as stored data. A short literal that also occurs inside them can remain there.
|
|
120
|
+
|
|
121
|
+
### Service account
|
|
122
|
+
|
|
123
|
+
After the process connects, and before any tool runs a query, startup reads `SHOW GRANTS FINAL` for the current user. That statement includes privileges inherited through roles. Startup exits with `[clickhouse_unsafe_service_role] database service role is not permitted` when the account has any of:
|
|
124
|
+
|
|
125
|
+
- `ACCESS MANAGEMENT`, or a privilege in that group, including `CREATE USER`, `ALTER USER`, `DROP USER`, `CREATE ROLE`, `DROP ROLE`, `ROLE ADMIN`, row policies, quotas, settings profiles, and `SHOW ACCESS`
|
|
126
|
+
- `SYSTEM`, or any `SYSTEM …` privilege, such as `SYSTEM SHUTDOWN`
|
|
127
|
+
- `SOURCES`, `READ`, `WRITE`, or an external source privilege such as `FILE`, `URL`, `REMOTE`, `S3`, `YTSAURUS`, or `ARROW FLIGHT`
|
|
128
|
+
- global DDL or global `ALL` / `ALL PRIVILEGES` on `*.*`: `CREATE`, `ALTER`, `DROP`, `TRUNCATE`, `OPTIMIZE`, and their DDL children, including `ALTER HANDLER`
|
|
129
|
+
- similar administrative grants: `BACKUP`, `KILL QUERY`, `KILL TRANSACTION`, `MOVE PARTITION BETWEEN SHARDS`, `TABLE ENGINE`, `CLUSTER`, `NAMED COLLECTION ADMIN` (including named-collection secrets), `SET DEFINER`, `ALLOW SQL SECURITY NONE`, `SHOW CONNECTIONS`, and `SHOW HANDLERS`
|
|
130
|
+
- `AI FUNCTIONS`, or an individual AI function privilege such as `aiGenerate`. A later `ai*` privilege is rejected too. These lines are covered by unit tests. ClickHouse 26.5.6.64, 26.5.7.64, and 26.10.2.103 reject `GRANT AI FUNCTIONS` as a syntax error.
|
|
131
|
+
- `SELECT` or `ALL` on a scope that matches `system.processes`, `system.query_log`, `system.query_thread_log`, `system.query_views_log`, `system.user_query_log`, `system.query_cache`, `system.asynchronous_inserts`, `system.asynchronous_query_log`, or `system.users`. An unquoted trailing `*` is a prefix on that database or table name, so `system.proc*` and `sys*.*` match. A backtick-quoted `*` is a literal name. Matching is case sensitive.
|
|
132
|
+
|
|
133
|
+
`SELECT` on the account's own databases, plus `system.databases`, `system.tables`, and `system.columns`, starts with the default configuration. `SELECT` on `*.*` does not. `SHOW` by itself cannot read `system.processes`. DDL granted on one database, rather than on `*.*`, is not treated as a dangerous service role. Queries are still sent with `readonly=1`.
|
|
134
|
+
|
|
135
|
+
`allow_unsafe_database_role` defaults to `false`. Set it to `true` in the config file, or export `MCP_ALLOW_UNSAFE_DATABASE_ROLE=true`, only when you cannot provision a least-privilege account. The server logs a warning and skips this privilege rejection. A refused connection, a failed grant probe, and every other startup check still stop startup. The flag does not remove `readonly=1`.
|
|
136
|
+
|
|
137
|
+
`readonly=1` rejects writes, DDL, `SYSTEM` statements, `CREATE USER`, and `SELECT` from the `file` and `url` table functions. It does not reject `SELECT` from `system.processes`. On ClickHouse 26.5.6.64, `GRANT SELECT ON system.proc*` is printed unchanged and reads another session's in-flight SQL. The same wildcard inlined from a role does too. `system.data*` matches `system.databases` and does not match `system.processes`, so that narrower prefix still starts. `REVOKE SELECT ON system.processes` removes that one exact table and leaves a broader `GRANT SELECT ON *.*` in `SHOW GRANTS FINAL`.
|
|
138
|
+
|
|
139
|
+
AI functions are not a substitute for that grant check. Startup rejects `AI FUNCTIONS` and each `ai*` name when a server emits them. It also reads `allow_experimental_ai_functions` from `system.settings`. A value of enabled rejects the account only when `is_obsolete` is false. On 26.5.6.64 and 26.5.7.64 the setting is live. On 26.10.2.103 the same row is obsolete and does nothing, so a leftover value does not reject a scoped reader. The pinned integration image is 26.5.6.64, which has no AI privilege row. The integration script adds a live AI rejection only when that row exists.
|
|
140
|
+
|
|
141
|
+
`system.connections` is absent on 26.5.6.64, 26.5.7.64, and 26.10.2.103. `SHOW CONNECTIONS` and `SHOW HANDLERS` stay on the denylist as administrative visibility privileges. Enable the opt-out only for a ClickHouse instance you control.
|
|
142
|
+
|
|
143
|
+
The warning is logged at warn level. HTTP mode shows it at the default `info` log level. Stdio mode keeps logs disabled so MCP output stays intact.
|
|
144
|
+
|
|
145
|
+
`config check --connect` only pings ClickHouse. It does not run this privilege check. `mcp` and `tools call` do.
|
|
146
|
+
|
|
147
|
+
Do not use the ClickHouse `default` user. A standard installation grants that user `ALL ON *.*`.
|
|
148
|
+
|
|
149
|
+
```sql
|
|
150
|
+
CREATE USER mcp_reader IDENTIFIED WITH no_password;
|
|
151
|
+
GRANT SELECT ON analytics.* TO mcp_reader;
|
|
152
|
+
GRANT SELECT ON system.databases TO mcp_reader;
|
|
153
|
+
GRANT SELECT ON system.tables TO mcp_reader;
|
|
154
|
+
GRANT SELECT ON system.columns TO mcp_reader;
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The discovery tools read those three system tables. Grant `SELECT` on each database the query tool should read.
|
|
158
|
+
|
|
92
159
|
### Environment-only configuration check
|
|
93
160
|
|
|
94
161
|
For the smallest local setup, set just the ClickHouse HTTP address and inspect the effective configuration. This command does not start MCP or access the network.
|
|
@@ -98,7 +165,7 @@ export MCP_CLICKHOUSE_ADDRESS=127.0.0.1:8123
|
|
|
98
165
|
./bin/clickhouse-mcp-server config check
|
|
99
166
|
```
|
|
100
167
|
|
|
101
|
-
The command prints the
|
|
168
|
+
The command prints the 15 configuration fields in a stable order with their source (`flag`, `environment`, `YAML`, or `default`). Passwords are reported only as `set` or `unset`. `masking` is `enabled` or `disabled` and does not include the rules. `sse_base_url` removes query and fragment values and redacts URL userinfo before display.
|
|
102
169
|
|
|
103
170
|
Use `--connect` only when you intentionally want one ClickHouse HTTP(S) `Ping`; it opens, pings, and closes a short-lived connection without starting the MCP server.
|
|
104
171
|
|
|
@@ -155,10 +222,14 @@ Both commands launch the `mcp` command. Arguments after the package or image are
|
|
|
155
222
|
|
|
156
223
|
## Troubleshooting
|
|
157
224
|
|
|
225
|
+
- `[clickhouse_unsafe_service_role]`: the configured user can administer ClickHouse, read external sources, run global DDL, or call AI functions. Create a `SELECT`-only user, or set `allow_unsafe_database_role: true` only on an instance you control. The opt-out does not remove `readonly=1`.
|
|
158
226
|
- `clickhouse.address is required`: add a `host:port` value such as `127.0.0.1:8123` to the configuration or `MCP_CLICKHOUSE_ADDRESS`.
|
|
159
227
|
- Connection failure: check the address, `secure` value, ClickHouse HTTP port, and network reachability.
|
|
160
228
|
- Query rejection: verify the statement is a single read-only query and that the configured ClickHouse user has access to the database.
|
|
161
229
|
- `truncated: true`: lower the result size in SQL or increase `max_rows` deliberately.
|
|
230
|
+
- `invalid masking config`: the masking document is empty, unknown, or enabled with no rules. The message does not quote the document.
|
|
231
|
+
- `[clickhouse_masking_failed]`: the result could not be masked inside the bounds, or the value type is not supported. No partial result is returned.
|
|
232
|
+
- `[clickhouse_query_failed]`, `[clickhouse_startup_failed]`, and `[clickhouse_connect_failed]`: masking is on, so the server omits driver and setting text.
|
|
162
233
|
|
|
163
234
|
## Development
|
|
164
235
|
|
|
@@ -171,13 +242,13 @@ make ci
|
|
|
171
242
|
|
|
172
243
|
### Optional ClickHouse integration test
|
|
173
244
|
|
|
174
|
-
The default test suite has no external dependency. The opt-in integration test starts an isolated local ClickHouse Docker container to verify database discovery, parameterized table inspection, `SELECT`, and that `
|
|
245
|
+
The default test suite has no external dependency. The opt-in integration test starts an isolated local ClickHouse Docker container to verify database discovery, parameterized table inspection, `SELECT`, and that `CREATE TEMPORARY TABLE` is denied for the `SELECT`/`SHOW` account. ClickHouse returns `ACCESS_DENIED` (code 497) because that privilege can only be granted on `*.*`, which startup rejects. It also builds the public server binary, starts it in HTTP mode on loopback, checks `/healthz`, and calls `tools/list` and `tools/call` through `/mcp`.
|
|
175
246
|
|
|
176
247
|
```bash
|
|
177
248
|
make test-integration
|
|
178
249
|
```
|
|
179
250
|
|
|
180
|
-
The target requires Docker and `curl`, creates a random loopback port, waits for ClickHouse health, and removes its container on exit. Set `CLICKHOUSE_INTEGRATION_IMAGE` to test another isolated image. It runs the full MCP HTTP E2E test, the read-only toolset integration test, and `config check --connect`. It is local-only: neither `make test` nor the GitHub Actions CI workflow runs it.
|
|
251
|
+
The target requires Docker and `curl`, creates a random loopback port, waits for ClickHouse health, and removes its container on exit. Set `CLICKHOUSE_INTEGRATION_IMAGE` to test another isolated image. It creates ClickHouse user `mcp_reader` with `SELECT` on `system.databases`, `system.tables`, and `system.columns`, and runs the tests as that user. `SELECT` on `*.*` is rejected. It also grants `SELECT ON system.proc*` to `wild_reader` and expects startup to reject that account. It runs the full MCP HTTP E2E test, the read-only toolset integration test, and `config check --connect`. When `system.privileges` contains `AI FUNCTIONS`, it also creates scoped `ai_probe` with that privilege and expects startup to reject the account. ClickHouse 26.5.6.64, the default image, has no such row, so that extra check does not run there. It is local-only: neither `make test` nor the GitHub Actions CI workflow runs it.
|
|
181
252
|
|
|
182
253
|
The repository retains its template assets under `scripts/` and `templates/` for local reference; they are not part of the ClickHouse server runtime.
|
|
183
254
|
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futuretea/clickhouse-mcp-server",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Read-only ClickHouse Model Context Protocol server",
|
|
5
5
|
"main": "./bin/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"clickhouse-mcp-server": "bin/index.js"
|
|
8
8
|
},
|
|
9
9
|
"optionalDependencies": {
|
|
10
|
-
"@futuretea/clickhouse-mcp-server-darwin-amd64": "0.0
|
|
11
|
-
"@futuretea/clickhouse-mcp-server-darwin-arm64": "0.0
|
|
12
|
-
"@futuretea/clickhouse-mcp-server-linux-amd64": "0.0
|
|
13
|
-
"@futuretea/clickhouse-mcp-server-linux-arm64": "0.0
|
|
14
|
-
"@futuretea/clickhouse-mcp-server-windows-amd64": "0.0
|
|
10
|
+
"@futuretea/clickhouse-mcp-server-darwin-amd64": "0.2.0",
|
|
11
|
+
"@futuretea/clickhouse-mcp-server-darwin-arm64": "0.2.0",
|
|
12
|
+
"@futuretea/clickhouse-mcp-server-linux-amd64": "0.2.0",
|
|
13
|
+
"@futuretea/clickhouse-mcp-server-linux-arm64": "0.2.0",
|
|
14
|
+
"@futuretea/clickhouse-mcp-server-windows-amd64": "0.2.0"
|
|
15
15
|
},
|
|
16
16
|
"repository": {
|
|
17
17
|
"type": "git",
|