@gozzle/cli 0.0.1-canary.3
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/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +38 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/clickhouse/client.d.ts +13 -0
- package/dist/src/clickhouse/client.js +27 -0
- package/dist/src/clickhouse/client.js.map +1 -0
- package/dist/src/clickhouse/identifier.d.ts +12 -0
- package/dist/src/clickhouse/identifier.js +38 -0
- package/dist/src/clickhouse/identifier.js.map +1 -0
- package/dist/src/clickhouse/introspection.d.ts +15 -0
- package/dist/src/clickhouse/introspection.js +88 -0
- package/dist/src/clickhouse/introspection.js.map +1 -0
- package/dist/src/clickhouse/table-inspection.d.ts +48 -0
- package/dist/src/clickhouse/table-inspection.js +162 -0
- package/dist/src/clickhouse/table-inspection.js.map +1 -0
- package/dist/src/config/clickhouse.d.ts +19 -0
- package/dist/src/config/clickhouse.js +24 -0
- package/dist/src/config/clickhouse.js.map +1 -0
- package/dist/src/mcp/server.d.ts +4 -0
- package/dist/src/mcp/server.js +36 -0
- package/dist/src/mcp/server.js.map +1 -0
- package/dist/src/shared/package-metadata.d.ts +4 -0
- package/dist/src/shared/package-metadata.js +33 -0
- package/dist/src/shared/package-metadata.js.map +1 -0
- package/dist/src/tools/connect.d.ts +2 -0
- package/dist/src/tools/connect.js +64 -0
- package/dist/src/tools/connect.js.map +1 -0
- package/dist/src/tools/health.d.ts +2 -0
- package/dist/src/tools/health.js +15 -0
- package/dist/src/tools/health.js.map +1 -0
- package/dist/src/tools/inspect-table.d.ts +4 -0
- package/dist/src/tools/inspect-table.js +77 -0
- package/dist/src/tools/inspect-table.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gozzle
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Gozzle
|
|
2
|
+
|
|
3
|
+
A safety harness for your ClickHouse, inside your own AI.
|
|
4
|
+
|
|
5
|
+
Gozzle is a local developer toolkit for ClickHouse. The AI reasons; Gozzle runs checks and produces proof.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
For early canary builds:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @gozzle/cli@canary
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then print the MCP config snippet:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gozzle init
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Add the printed config to Claude, Cursor, Codex, or another MCP host.
|
|
22
|
+
|
|
23
|
+
## Development
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm install
|
|
27
|
+
npm run build
|
|
28
|
+
npm test
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## ClickHouse Connection
|
|
32
|
+
|
|
33
|
+
Gozzle reads ClickHouse connection details from environment variables:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
GOZZLE_CLICKHOUSE_URL=http://localhost:8123
|
|
37
|
+
GOZZLE_CLICKHOUSE_USER=default
|
|
38
|
+
GOZZLE_CLICKHOUSE_PASSWORD=
|
|
39
|
+
GOZZLE_CLICKHOUSE_DATABASE=default
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `GOZZLE_` variables take precedence over the equivalent `CLICKHOUSE_` variables.
|
|
43
|
+
Use a read-only ClickHouse user; Gozzle does not need write access.
|
|
44
|
+
|
|
45
|
+
## Entry Points
|
|
46
|
+
|
|
47
|
+
- `gozzle`: CLI entrypoint.
|
|
48
|
+
- `gozzle-mcp`: MCP stdio server entrypoint.
|
|
49
|
+
|
|
50
|
+
## Canary Publishing
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm login
|
|
54
|
+
npm run build
|
|
55
|
+
npm test
|
|
56
|
+
npm publish --tag canary --access public
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For later canaries:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
npm version prerelease --preid canary
|
|
63
|
+
npm publish --tag canary --access public
|
|
64
|
+
```
|
package/dist/src/cli.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readPackageMetadata } from "./shared/package-metadata.js";
|
|
3
|
+
const metadata = readPackageMetadata();
|
|
4
|
+
const command = process.argv[2] ?? "help";
|
|
5
|
+
if (command === "version" || command === "--version" || command === "-v") {
|
|
6
|
+
console.log(metadata.version);
|
|
7
|
+
process.exit(0);
|
|
8
|
+
}
|
|
9
|
+
if (command === "init") {
|
|
10
|
+
printInit();
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
console.log(`gozzle ${metadata.version}`);
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log("Commands:");
|
|
16
|
+
console.log(" gozzle init Print MCP config for your AI host");
|
|
17
|
+
console.log(" gozzle version Print the CLI version");
|
|
18
|
+
console.log(" gozzle-mcp Start the MCP stdio server");
|
|
19
|
+
function printInit() {
|
|
20
|
+
console.log("Add this MCP server config to Claude, Cursor, Codex, or another MCP host:");
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log(JSON.stringify({
|
|
23
|
+
mcpServers: {
|
|
24
|
+
gozzle: {
|
|
25
|
+
command: "gozzle-mcp",
|
|
26
|
+
env: {
|
|
27
|
+
GOZZLE_CLICKHOUSE_URL: "https://your-cluster.clickhouse.cloud:8443",
|
|
28
|
+
GOZZLE_CLICKHOUSE_USER: "gozzle_readonly",
|
|
29
|
+
GOZZLE_CLICKHOUSE_PASSWORD: "replace-me",
|
|
30
|
+
GOZZLE_CLICKHOUSE_DATABASE: "default"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}, null, 2));
|
|
35
|
+
console.log("");
|
|
36
|
+
console.log("Use a read-only ClickHouse user. Gozzle does not need write access.");
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAEnE,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAC;AACvC,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC;AAE1C,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,KAAK,WAAW,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;IACzE,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC9B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,CAAC;IACZ,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,OAAO,CAAC,GAAG,CAAC,UAAU,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;AAC1C,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAChB,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;AACzB,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;AACtE,OAAO,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;AAC1D,OAAO,CAAC,GAAG,CAAC,iDAAiD,CAAC,CAAC;AAE/D,SAAS,SAAS;IAChB,OAAO,CAAC,GAAG,CAAC,2EAA2E,CAAC,CAAC;IACzF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CACT,IAAI,CAAC,SAAS,CACZ;QACE,UAAU,EAAE;YACV,MAAM,EAAE;gBACN,OAAO,EAAE,YAAY;gBACrB,GAAG,EAAE;oBACH,qBAAqB,EAAE,4CAA4C;oBACnE,sBAAsB,EAAE,iBAAiB;oBACzC,0BAA0B,EAAE,YAAY;oBACxC,0BAA0B,EAAE,SAAS;iBACtC;aACF;SACF;KACF,EACD,IAAI,EACJ,CAAC,CACF,CACF,CAAC;IACF,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAChB,OAAO,CAAC,GAAG,CAAC,qEAAqE,CAAC,CAAC;AACrF,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClickHouseConnectionConfig } from "../config/clickhouse.js";
|
|
2
|
+
export interface ClickHouseMetadataClient {
|
|
3
|
+
ping(): Promise<boolean>;
|
|
4
|
+
queryJson<T>(query: string): Promise<T[]>;
|
|
5
|
+
close(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare class ClickHouseHttpMetadataClient implements ClickHouseMetadataClient {
|
|
8
|
+
private readonly client;
|
|
9
|
+
constructor(config: ClickHouseConnectionConfig);
|
|
10
|
+
ping(): Promise<boolean>;
|
|
11
|
+
queryJson<T>(query: string): Promise<T[]>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { createClient } from "@clickhouse/client";
|
|
2
|
+
export class ClickHouseHttpMetadataClient {
|
|
3
|
+
client;
|
|
4
|
+
constructor(config) {
|
|
5
|
+
this.client = createClient({
|
|
6
|
+
url: config.url,
|
|
7
|
+
username: config.username,
|
|
8
|
+
password: config.password,
|
|
9
|
+
database: config.database
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
async ping() {
|
|
13
|
+
const result = await this.client.ping();
|
|
14
|
+
return result.success;
|
|
15
|
+
}
|
|
16
|
+
async queryJson(query) {
|
|
17
|
+
const resultSet = await this.client.query({
|
|
18
|
+
query,
|
|
19
|
+
format: "JSONEachRow"
|
|
20
|
+
});
|
|
21
|
+
return (await resultSet.json());
|
|
22
|
+
}
|
|
23
|
+
async close() {
|
|
24
|
+
await this.client.close();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/clickhouse/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAyB,MAAM,oBAAoB,CAAC;AAUzE,MAAM,OAAO,4BAA4B;IACtB,MAAM,CAAmB;IAE1C,YAAY,MAAkC;QAC5C,IAAI,CAAC,MAAM,GAAG,YAAY,CAAC;YACzB,GAAG,EAAE,MAAM,CAAC,GAAG;YACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;SAC1B,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACxC,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IAED,KAAK,CAAC,SAAS,CAAI,KAAa;QAC9B,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;YACxC,KAAK;YACL,MAAM,EAAE,aAAa;SACtB,CAAC,CAAC;QAEH,OAAO,CAAC,MAAM,SAAS,CAAC,IAAI,EAAE,CAAQ,CAAC;IACzC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAC5B,CAAC;CACF"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface TableIdentifier {
|
|
2
|
+
database?: string;
|
|
3
|
+
table: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ResolvedTableIdentifier {
|
|
6
|
+
database: string;
|
|
7
|
+
table: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseTableIdentifier(input: string): TableIdentifier;
|
|
10
|
+
export declare function resolveTableIdentifier(identifier: TableIdentifier, defaultDatabase: string): ResolvedTableIdentifier;
|
|
11
|
+
export declare function quoteIdentifier(identifier: string): string;
|
|
12
|
+
export declare function formatTableIdentifier(identifier: ResolvedTableIdentifier): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const IDENTIFIER_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2
|
+
export function parseTableIdentifier(input) {
|
|
3
|
+
const trimmed = input.trim();
|
|
4
|
+
if (trimmed === "") {
|
|
5
|
+
throw new Error("Table name is required.");
|
|
6
|
+
}
|
|
7
|
+
const parts = trimmed.split(".");
|
|
8
|
+
if (parts.length > 2) {
|
|
9
|
+
throw new Error("Use table or database.table format.");
|
|
10
|
+
}
|
|
11
|
+
const [database, table] = parts.length === 2 ? parts : [undefined, parts[0]];
|
|
12
|
+
if (database !== undefined) {
|
|
13
|
+
validateIdentifierPart(database, "database");
|
|
14
|
+
}
|
|
15
|
+
validateIdentifierPart(table, "table");
|
|
16
|
+
return {
|
|
17
|
+
database,
|
|
18
|
+
table
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function resolveTableIdentifier(identifier, defaultDatabase) {
|
|
22
|
+
return {
|
|
23
|
+
database: identifier.database ?? defaultDatabase,
|
|
24
|
+
table: identifier.table
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function quoteIdentifier(identifier) {
|
|
28
|
+
return `\`${identifier.replaceAll("`", "``")}\``;
|
|
29
|
+
}
|
|
30
|
+
export function formatTableIdentifier(identifier) {
|
|
31
|
+
return `${quoteIdentifier(identifier.database)}.${quoteIdentifier(identifier.table)}`;
|
|
32
|
+
}
|
|
33
|
+
function validateIdentifierPart(value, label) {
|
|
34
|
+
if (!IDENTIFIER_PATTERN.test(value)) {
|
|
35
|
+
throw new Error(`Invalid ${label} identifier "${value}". Use an unquoted ClickHouse identifier.`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=identifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identifier.js","sourceRoot":"","sources":["../../../src/clickhouse/identifier.ts"],"names":[],"mappings":"AAUA,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;AAEtD,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;IAE7B,IAAI,OAAO,KAAK,EAAE,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAEjC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACzD,CAAC;IAED,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC,GAAG,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAE7E,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,sBAAsB,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IAC/C,CAAC;IAED,sBAAsB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IAEvC,OAAO;QACL,QAAQ;QACR,KAAK;KACN,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,UAA2B,EAC3B,eAAuB;IAEvB,OAAO;QACL,QAAQ,EAAE,UAAU,CAAC,QAAQ,IAAI,eAAe;QAChD,KAAK,EAAE,UAAU,CAAC,KAAK;KACxB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,UAAkB;IAChD,OAAO,KAAK,UAAU,CAAC,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,UAAmC;IACvE,OAAO,GAAG,eAAe,CAAC,UAAU,CAAC,QAAQ,CAAC,IAAI,eAAe,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;AACxF,CAAC;AAED,SAAS,sBAAsB,CAAC,KAAa,EAAE,KAAa;IAC1D,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CACb,WAAW,KAAK,gBAAgB,KAAK,2CAA2C,CACjF,CAAC;IACJ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClickHouseConnectionConfig } from "../config/clickhouse.js";
|
|
2
|
+
import type { ClickHouseMetadataClient } from "./client.js";
|
|
3
|
+
export interface ClickHouseConnectionInfo {
|
|
4
|
+
connected: true;
|
|
5
|
+
version: string;
|
|
6
|
+
database: string;
|
|
7
|
+
currentUser: string;
|
|
8
|
+
hostName: string;
|
|
9
|
+
deployment: "cloud" | "self_hosted_or_unknown";
|
|
10
|
+
readonlySetting?: string;
|
|
11
|
+
writePrivileges: string[];
|
|
12
|
+
warnings: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function inspectClickHouseConnection(client: ClickHouseMetadataClient, config: ClickHouseConnectionConfig): Promise<ClickHouseConnectionInfo>;
|
|
15
|
+
export declare function detectDeployment(url: string): ClickHouseConnectionInfo["deployment"];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const WRITE_PRIVILEGES = new Set([
|
|
2
|
+
"INSERT",
|
|
3
|
+
"ALTER",
|
|
4
|
+
"CREATE",
|
|
5
|
+
"DROP",
|
|
6
|
+
"TRUNCATE",
|
|
7
|
+
"OPTIMIZE",
|
|
8
|
+
"SYSTEM",
|
|
9
|
+
"KILL QUERY"
|
|
10
|
+
]);
|
|
11
|
+
export async function inspectClickHouseConnection(client, config) {
|
|
12
|
+
const pingOk = await client.ping();
|
|
13
|
+
if (!pingOk) {
|
|
14
|
+
throw new Error("ClickHouse ping failed.");
|
|
15
|
+
}
|
|
16
|
+
const [serverInfo] = await client.queryJson(`
|
|
17
|
+
SELECT
|
|
18
|
+
version() AS version,
|
|
19
|
+
currentDatabase() AS database,
|
|
20
|
+
currentUser() AS current_user,
|
|
21
|
+
hostName() AS host_name
|
|
22
|
+
`);
|
|
23
|
+
if (!serverInfo) {
|
|
24
|
+
throw new Error("ClickHouse did not return server metadata.");
|
|
25
|
+
}
|
|
26
|
+
const warnings = [];
|
|
27
|
+
const readonlySetting = await readReadonlySetting(client, warnings);
|
|
28
|
+
const writePrivileges = await readWritePrivileges(client, warnings);
|
|
29
|
+
if (readonlySetting !== undefined && readonlySetting !== "1") {
|
|
30
|
+
warnings.push("Session readonly setting is not enabled. Use a read-only ClickHouse user for Gozzle.");
|
|
31
|
+
}
|
|
32
|
+
if (writePrivileges.length > 0) {
|
|
33
|
+
warnings.push(`Connected user appears to have write-capable grants: ${writePrivileges.join(", ")}. Gozzle only needs read-only access.`);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
connected: true,
|
|
37
|
+
version: String(serverInfo.version),
|
|
38
|
+
database: serverInfo.database,
|
|
39
|
+
currentUser: serverInfo.current_user,
|
|
40
|
+
hostName: serverInfo.host_name,
|
|
41
|
+
deployment: detectDeployment(config.url),
|
|
42
|
+
readonlySetting,
|
|
43
|
+
writePrivileges,
|
|
44
|
+
warnings
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function detectDeployment(url) {
|
|
48
|
+
const hostname = new URL(url).hostname;
|
|
49
|
+
return hostname.endsWith(".clickhouse.cloud")
|
|
50
|
+
? "cloud"
|
|
51
|
+
: "self_hosted_or_unknown";
|
|
52
|
+
}
|
|
53
|
+
async function readReadonlySetting(client, warnings) {
|
|
54
|
+
try {
|
|
55
|
+
const [row] = await client.queryJson(`
|
|
56
|
+
SELECT value
|
|
57
|
+
FROM system.settings
|
|
58
|
+
WHERE name = 'readonly'
|
|
59
|
+
LIMIT 1
|
|
60
|
+
`);
|
|
61
|
+
return row ? String(row.value) : undefined;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
warnings.push(`Could not inspect readonly setting: ${formatErrorMessage(error)}`);
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function readWritePrivileges(client, warnings) {
|
|
69
|
+
try {
|
|
70
|
+
const rows = await client.queryJson(`
|
|
71
|
+
SELECT DISTINCT access_type
|
|
72
|
+
FROM system.grants
|
|
73
|
+
WHERE user_name = currentUser()
|
|
74
|
+
ORDER BY access_type
|
|
75
|
+
`);
|
|
76
|
+
return rows
|
|
77
|
+
.map((row) => row.access_type)
|
|
78
|
+
.filter((accessType) => WRITE_PRIVILEGES.has(accessType));
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
warnings.push(`Could not inspect grants: ${formatErrorMessage(error)}`);
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function formatErrorMessage(error) {
|
|
86
|
+
return error instanceof Error ? error.message : String(error);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=introspection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"introspection.js","sourceRoot":"","sources":["../../../src/clickhouse/introspection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ClickHouseMetadataClient } from "./client.js";
|
|
2
|
+
import { type ResolvedTableIdentifier } from "./identifier.js";
|
|
3
|
+
export interface TableInspectionOptions {
|
|
4
|
+
table: string;
|
|
5
|
+
defaultDatabase: string;
|
|
6
|
+
}
|
|
7
|
+
export interface TableColumn {
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
defaultKind?: string;
|
|
11
|
+
defaultExpression?: string;
|
|
12
|
+
codecExpression?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TablePartsSummary {
|
|
15
|
+
activeParts: number;
|
|
16
|
+
rows: number;
|
|
17
|
+
bytesOnDisk: number;
|
|
18
|
+
partitions: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ReplacingMergeTreeDetails {
|
|
21
|
+
versionColumn?: string;
|
|
22
|
+
deletedColumn?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface TableInspection {
|
|
25
|
+
identifier: ResolvedTableIdentifier;
|
|
26
|
+
engine: string;
|
|
27
|
+
engineFull: string;
|
|
28
|
+
createStatement: string;
|
|
29
|
+
orderBy?: string;
|
|
30
|
+
partitionBy?: string;
|
|
31
|
+
primaryKey?: string;
|
|
32
|
+
sortingKey?: string;
|
|
33
|
+
totalRows: number;
|
|
34
|
+
totalBytes: number;
|
|
35
|
+
isDistributed: boolean;
|
|
36
|
+
isReplacingMergeTree: boolean;
|
|
37
|
+
replacingMergeTree?: ReplacingMergeTreeDetails;
|
|
38
|
+
columns: TableColumn[];
|
|
39
|
+
parts: TablePartsSummary;
|
|
40
|
+
eligibleChecks: {
|
|
41
|
+
verifyDedup: boolean;
|
|
42
|
+
dryRunMigration: boolean;
|
|
43
|
+
diagnoseQuery: boolean;
|
|
44
|
+
};
|
|
45
|
+
warnings: string[];
|
|
46
|
+
}
|
|
47
|
+
export declare function inspectTable(client: ClickHouseMetadataClient, options: TableInspectionOptions): Promise<TableInspection>;
|
|
48
|
+
export declare function extractClause(createStatement: string, clauseName: "ORDER BY" | "PARTITION BY"): string | undefined;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { formatTableIdentifier, parseTableIdentifier, resolveTableIdentifier } from "./identifier.js";
|
|
2
|
+
export async function inspectTable(client, options) {
|
|
3
|
+
const identifier = resolveTableIdentifier(parseTableIdentifier(options.table), options.defaultDatabase);
|
|
4
|
+
const fullTableName = formatTableIdentifier(identifier);
|
|
5
|
+
const databaseLiteral = quoteStringLiteral(identifier.database);
|
|
6
|
+
const tableLiteral = quoteStringLiteral(identifier.table);
|
|
7
|
+
const [showCreateRows, tableRows, columns, partsRows] = await Promise.all([
|
|
8
|
+
client.queryJson(`SHOW CREATE TABLE ${fullTableName}`),
|
|
9
|
+
client.queryJson(`
|
|
10
|
+
SELECT
|
|
11
|
+
engine,
|
|
12
|
+
engine_full,
|
|
13
|
+
sorting_key,
|
|
14
|
+
primary_key,
|
|
15
|
+
partition_key,
|
|
16
|
+
total_rows,
|
|
17
|
+
total_bytes
|
|
18
|
+
FROM system.tables
|
|
19
|
+
WHERE database = ${databaseLiteral}
|
|
20
|
+
AND name = ${tableLiteral}
|
|
21
|
+
LIMIT 1
|
|
22
|
+
`),
|
|
23
|
+
client.queryJson(`
|
|
24
|
+
SELECT
|
|
25
|
+
name,
|
|
26
|
+
type,
|
|
27
|
+
default_kind,
|
|
28
|
+
default_expression,
|
|
29
|
+
codec_expression
|
|
30
|
+
FROM system.columns
|
|
31
|
+
WHERE database = ${databaseLiteral}
|
|
32
|
+
AND table = ${tableLiteral}
|
|
33
|
+
ORDER BY position
|
|
34
|
+
`),
|
|
35
|
+
client.queryJson(`
|
|
36
|
+
SELECT
|
|
37
|
+
count() AS active_parts,
|
|
38
|
+
sum(rows) AS rows,
|
|
39
|
+
sum(bytes_on_disk) AS bytes_on_disk,
|
|
40
|
+
uniqExact(partition) AS partitions
|
|
41
|
+
FROM system.parts
|
|
42
|
+
WHERE database = ${databaseLiteral}
|
|
43
|
+
AND table = ${tableLiteral}
|
|
44
|
+
AND active
|
|
45
|
+
`)
|
|
46
|
+
]);
|
|
47
|
+
const [showCreate] = showCreateRows;
|
|
48
|
+
const [table] = tableRows;
|
|
49
|
+
const [parts] = partsRows;
|
|
50
|
+
if (!table) {
|
|
51
|
+
throw new Error(`Table not found: ${identifier.database}.${identifier.table}`);
|
|
52
|
+
}
|
|
53
|
+
const createStatement = showCreate?.statement ?? "";
|
|
54
|
+
const engineFull = table.engine_full || table.engine;
|
|
55
|
+
const isReplacingMergeTree = table.engine.includes("ReplacingMergeTree");
|
|
56
|
+
const isDistributed = table.engine === "Distributed";
|
|
57
|
+
const warnings = buildWarnings(table.engine, isReplacingMergeTree, isDistributed);
|
|
58
|
+
const replacingMergeTree = isReplacingMergeTree
|
|
59
|
+
? parseReplacingMergeTreeDetails(engineFull)
|
|
60
|
+
: undefined;
|
|
61
|
+
return {
|
|
62
|
+
identifier,
|
|
63
|
+
engine: table.engine,
|
|
64
|
+
engineFull,
|
|
65
|
+
createStatement,
|
|
66
|
+
orderBy: extractClause(createStatement, "ORDER BY"),
|
|
67
|
+
partitionBy: extractClause(createStatement, "PARTITION BY"),
|
|
68
|
+
primaryKey: normalizeOptional(table.primary_key),
|
|
69
|
+
sortingKey: normalizeOptional(table.sorting_key),
|
|
70
|
+
totalRows: toNumber(table.total_rows),
|
|
71
|
+
totalBytes: toNumber(table.total_bytes),
|
|
72
|
+
isDistributed,
|
|
73
|
+
isReplacingMergeTree,
|
|
74
|
+
replacingMergeTree,
|
|
75
|
+
columns: columns.map(toTableColumn),
|
|
76
|
+
parts: {
|
|
77
|
+
activeParts: toNumber(parts?.active_parts ?? 0),
|
|
78
|
+
rows: toNumber(parts?.rows ?? 0),
|
|
79
|
+
bytesOnDisk: toNumber(parts?.bytes_on_disk ?? 0),
|
|
80
|
+
partitions: toNumber(parts?.partitions ?? 0)
|
|
81
|
+
},
|
|
82
|
+
eligibleChecks: {
|
|
83
|
+
verifyDedup: isReplacingMergeTree && !isDistributed,
|
|
84
|
+
dryRunMigration: true,
|
|
85
|
+
diagnoseQuery: true
|
|
86
|
+
},
|
|
87
|
+
warnings
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
export function extractClause(createStatement, clauseName) {
|
|
91
|
+
const index = createStatement.indexOf(clauseName);
|
|
92
|
+
if (index === -1) {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
const start = index + clauseName.length;
|
|
96
|
+
const rest = createStatement.slice(start).trim();
|
|
97
|
+
const nextClauseIndex = findNextClauseIndex(rest);
|
|
98
|
+
const rawClause = nextClauseIndex === -1 ? rest : rest.slice(0, nextClauseIndex).trim();
|
|
99
|
+
return rawClause || undefined;
|
|
100
|
+
}
|
|
101
|
+
function findNextClauseIndex(input) {
|
|
102
|
+
const candidates = [
|
|
103
|
+
"\nORDER BY",
|
|
104
|
+
"\nPARTITION BY",
|
|
105
|
+
"\nPRIMARY KEY",
|
|
106
|
+
"\nSAMPLE BY",
|
|
107
|
+
"\nTTL",
|
|
108
|
+
"\nSETTINGS",
|
|
109
|
+
"\nCOMMENT"
|
|
110
|
+
];
|
|
111
|
+
const indexes = candidates
|
|
112
|
+
.map((candidate) => input.indexOf(candidate))
|
|
113
|
+
.filter((index) => index >= 0);
|
|
114
|
+
return indexes.length === 0 ? -1 : Math.min(...indexes);
|
|
115
|
+
}
|
|
116
|
+
function parseReplacingMergeTreeDetails(engineFull) {
|
|
117
|
+
const match = engineFull.match(/ReplacingMergeTree\s*\(([^)]*)\)/);
|
|
118
|
+
if (!match) {
|
|
119
|
+
return {};
|
|
120
|
+
}
|
|
121
|
+
const args = match[1]
|
|
122
|
+
.split(",")
|
|
123
|
+
.map((arg) => arg.trim())
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
return {
|
|
126
|
+
versionColumn: args[0],
|
|
127
|
+
deletedColumn: args[1]
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function buildWarnings(engine, isReplacingMergeTree, isDistributed) {
|
|
131
|
+
const warnings = [];
|
|
132
|
+
if (isReplacingMergeTree) {
|
|
133
|
+
warnings.push("ReplacingMergeTree table: queries without FINAL may expose duplicate rows.");
|
|
134
|
+
}
|
|
135
|
+
if (isDistributed) {
|
|
136
|
+
warnings.push("Distributed table: local checks may be advisory until shard topology is inspected.");
|
|
137
|
+
}
|
|
138
|
+
if (!engine.includes("MergeTree") && !isDistributed) {
|
|
139
|
+
warnings.push(`Unsupported or uncommon table engine for MVP checks: ${engine}.`);
|
|
140
|
+
}
|
|
141
|
+
return warnings;
|
|
142
|
+
}
|
|
143
|
+
function toTableColumn(row) {
|
|
144
|
+
return {
|
|
145
|
+
name: row.name,
|
|
146
|
+
type: row.type,
|
|
147
|
+
defaultKind: normalizeOptional(row.default_kind),
|
|
148
|
+
defaultExpression: normalizeOptional(row.default_expression),
|
|
149
|
+
codecExpression: normalizeOptional(row.codec_expression)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
function normalizeOptional(value) {
|
|
153
|
+
return value && value.trim() !== "" ? value : undefined;
|
|
154
|
+
}
|
|
155
|
+
function toNumber(value) {
|
|
156
|
+
const parsed = Number(value);
|
|
157
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
158
|
+
}
|
|
159
|
+
function quoteStringLiteral(value) {
|
|
160
|
+
return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=table-inspection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"table-inspection.js","sourceRoot":"","sources":["../../../src/clickhouse/table-inspection.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ClickHouseConnectionConfig {
|
|
2
|
+
url: string;
|
|
3
|
+
username: string;
|
|
4
|
+
password: string;
|
|
5
|
+
database?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ClickHouseConfigEnv {
|
|
8
|
+
CLICKHOUSE_URL?: string;
|
|
9
|
+
CLICKHOUSE_USER?: string;
|
|
10
|
+
CLICKHOUSE_USERNAME?: string;
|
|
11
|
+
CLICKHOUSE_PASSWORD?: string;
|
|
12
|
+
CLICKHOUSE_DATABASE?: string;
|
|
13
|
+
GOZZLE_CLICKHOUSE_URL?: string;
|
|
14
|
+
GOZZLE_CLICKHOUSE_USER?: string;
|
|
15
|
+
GOZZLE_CLICKHOUSE_USERNAME?: string;
|
|
16
|
+
GOZZLE_CLICKHOUSE_PASSWORD?: string;
|
|
17
|
+
GOZZLE_CLICKHOUSE_DATABASE?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function readClickHouseConfig(env?: ClickHouseConfigEnv): ClickHouseConnectionConfig;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function readClickHouseConfig(env = process.env) {
|
|
2
|
+
const url = firstNonEmpty(env.GOZZLE_CLICKHOUSE_URL, env.CLICKHOUSE_URL);
|
|
3
|
+
if (!url) {
|
|
4
|
+
throw new Error("Missing ClickHouse URL. Set GOZZLE_CLICKHOUSE_URL or CLICKHOUSE_URL.");
|
|
5
|
+
}
|
|
6
|
+
validateUrl(url);
|
|
7
|
+
return {
|
|
8
|
+
url,
|
|
9
|
+
username: firstNonEmpty(env.GOZZLE_CLICKHOUSE_USER, env.GOZZLE_CLICKHOUSE_USERNAME, env.CLICKHOUSE_USER, env.CLICKHOUSE_USERNAME) ?? "default",
|
|
10
|
+
password: firstNonEmpty(env.GOZZLE_CLICKHOUSE_PASSWORD, env.CLICKHOUSE_PASSWORD) ??
|
|
11
|
+
"",
|
|
12
|
+
database: firstNonEmpty(env.GOZZLE_CLICKHOUSE_DATABASE, env.CLICKHOUSE_DATABASE)
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function firstNonEmpty(...values) {
|
|
16
|
+
return values.find((value) => value !== undefined && value.trim() !== "");
|
|
17
|
+
}
|
|
18
|
+
function validateUrl(url) {
|
|
19
|
+
const parsed = new URL(url);
|
|
20
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
21
|
+
throw new Error("ClickHouse URL must use http or https.");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=clickhouse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clickhouse.js","sourceRoot":"","sources":["../../../src/config/clickhouse.ts"],"names":[],"mappings":"AAoBA,MAAM,UAAU,oBAAoB,CAClC,MAA2B,OAAO,CAAC,GAAG;IAEtC,MAAM,GAAG,GAAG,aAAa,CAAC,GAAG,CAAC,qBAAqB,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAEzE,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CACb,sEAAsE,CACvE,CAAC;IACJ,CAAC;IAED,WAAW,CAAC,GAAG,CAAC,CAAC;IAEjB,OAAO;QACL,GAAG;QACH,QAAQ,EACN,aAAa,CACX,GAAG,CAAC,sBAAsB,EAC1B,GAAG,CAAC,0BAA0B,EAC9B,GAAG,CAAC,eAAe,EACnB,GAAG,CAAC,mBAAmB,CACxB,IAAI,SAAS;QAChB,QAAQ,EACN,aAAa,CAAC,GAAG,CAAC,0BAA0B,EAAE,GAAG,CAAC,mBAAmB,CAAC;YACtE,EAAE;QACJ,QAAQ,EAAE,aAAa,CACrB,GAAG,CAAC,0BAA0B,EAC9B,GAAG,CAAC,mBAAmB,CACxB;KACF,CAAC;AACJ,CAAC;AAED,SAAS,aAAa,CACpB,GAAG,MAAiC;IAEpC,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAC5E,CAAC;AAED,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IAE5B,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAChE,MAAM,IAAI,KAAK,CAAC,wCAAwC,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { createConnectTool } from "../tools/connect.js";
|
|
7
|
+
import { createHealthTool } from "../tools/health.js";
|
|
8
|
+
import { createInspectTableTool } from "../tools/inspect-table.js";
|
|
9
|
+
import { readPackageMetadata } from "../shared/package-metadata.js";
|
|
10
|
+
export function createGozzleMcpServer() {
|
|
11
|
+
const metadata = readPackageMetadata();
|
|
12
|
+
const server = new McpServer({
|
|
13
|
+
name: "@gozzle/cli",
|
|
14
|
+
version: metadata.version
|
|
15
|
+
});
|
|
16
|
+
createHealthTool(server);
|
|
17
|
+
createConnectTool(server);
|
|
18
|
+
createInspectTableTool(server);
|
|
19
|
+
return server;
|
|
20
|
+
}
|
|
21
|
+
export async function startMcpServer() {
|
|
22
|
+
const server = createGozzleMcpServer();
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
}
|
|
26
|
+
// Resolve the entry path through symlinks: npm global bins (e.g. `gozzle-mcp`)
|
|
27
|
+
// and how Claude Code launches MCP servers invoke this file via a symlink, so
|
|
28
|
+
// process.argv[1] is the symlink while import.meta.url is the realpath.
|
|
29
|
+
const entry = process.argv[1];
|
|
30
|
+
if (entry && import.meta.url === pathToFileURL(realpathSync(entry)).href) {
|
|
31
|
+
startMcpServer().catch((error) => {
|
|
32
|
+
console.error(error);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../../src/mcp/server.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,MAAM,+BAA+B,CAAC;AAEpE,MAAM,UAAU,qBAAqB;IACnC,MAAM,QAAQ,GAAG,mBAAmB,EAAE,CAAC;IACvC,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC;QAC3B,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,QAAQ,CAAC,OAAO;KAC1B,CAAC,CAAC;IAEH,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACzB,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAC1B,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAE/B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc;IAClC,MAAM,MAAM,GAAG,qBAAqB,EAAE,CAAC;IACvC,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAClC,CAAC;AAED,+EAA+E;AAC/E,8EAA8E;AAC9E,wEAAwE;AACxE,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC9B,IAAI,KAAK,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,KAAK,aAAa,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACzE,cAAc,EAAE,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QACxC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
export function readPackageMetadata() {
|
|
5
|
+
const packageJsonPath = findPackageJson(dirname(fileURLToPath(import.meta.url)));
|
|
6
|
+
if (packageJsonPath) {
|
|
7
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8"));
|
|
8
|
+
if (typeof packageJson.version === "string") {
|
|
9
|
+
return {
|
|
10
|
+
version: packageJson.version
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
version: "0.0.1-canary.0"
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function findPackageJson(startDirectory) {
|
|
19
|
+
let currentDirectory = startDirectory;
|
|
20
|
+
for (let index = 0; index < 5; index += 1) {
|
|
21
|
+
const candidate = join(currentDirectory, "package.json");
|
|
22
|
+
if (existsSync(candidate)) {
|
|
23
|
+
return candidate;
|
|
24
|
+
}
|
|
25
|
+
const parentDirectory = dirname(currentDirectory);
|
|
26
|
+
if (parentDirectory === currentDirectory) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
currentDirectory = parentDirectory;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=package-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package-metadata.js","sourceRoot":"","sources":["../../../src/shared/package-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAMzC,MAAM,UAAU,mBAAmB;IACjC,MAAM,eAAe,GAAG,eAAe,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAEjF,IAAI,eAAe,EAAE,CAAC;QACpB,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,eAAe,EAAE,MAAM,CAAC,CAEnE,CAAC;QAEF,IAAI,OAAO,WAAW,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;YAC5C,OAAO;gBACL,OAAO,EAAE,WAAW,CAAC,OAAO;aAC7B,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE,gBAAgB;KAC1B,CAAC;AACJ,CAAC;AAED,SAAS,eAAe,CAAC,cAAsB;IAC7C,IAAI,gBAAgB,GAAG,cAAc,CAAC;IAEtC,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC1C,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,EAAE,cAAc,CAAC,CAAC;QAEzD,IAAI,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1B,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,MAAM,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;QAElD,IAAI,eAAe,KAAK,gBAAgB,EAAE,CAAC;YACzC,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,gBAAgB,GAAG,eAAe,CAAC;IACrC,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ClickHouseHttpMetadataClient } from "../clickhouse/client.js";
|
|
2
|
+
import { inspectClickHouseConnection } from "../clickhouse/introspection.js";
|
|
3
|
+
import { readClickHouseConfig } from "../config/clickhouse.js";
|
|
4
|
+
export function createConnectTool(server) {
|
|
5
|
+
server.registerTool("connect", {
|
|
6
|
+
title: "Connect to ClickHouse",
|
|
7
|
+
description: "Validate the configured ClickHouse connection and report read-only guardrails.",
|
|
8
|
+
inputSchema: {}
|
|
9
|
+
}, async () => {
|
|
10
|
+
let client;
|
|
11
|
+
try {
|
|
12
|
+
const config = readClickHouseConfig();
|
|
13
|
+
client = new ClickHouseHttpMetadataClient(config);
|
|
14
|
+
const info = await inspectClickHouseConnection(client, config);
|
|
15
|
+
return {
|
|
16
|
+
content: [
|
|
17
|
+
{
|
|
18
|
+
type: "text",
|
|
19
|
+
text: formatConnectionInfo(info)
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return {
|
|
26
|
+
isError: true,
|
|
27
|
+
content: [
|
|
28
|
+
{
|
|
29
|
+
type: "text",
|
|
30
|
+
text: `Gozzle could not connect to ClickHouse.\n\n${formatErrorMessage(error)}`
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
await client?.close();
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function formatConnectionInfo(info) {
|
|
41
|
+
const lines = [
|
|
42
|
+
"Connected read-only check complete.",
|
|
43
|
+
"No data leaves this machine.",
|
|
44
|
+
"",
|
|
45
|
+
`Version: ${info.version}`,
|
|
46
|
+
`Database: ${info.database}`,
|
|
47
|
+
`User: ${info.currentUser}`,
|
|
48
|
+
`Host: ${info.hostName}`,
|
|
49
|
+
`Deployment: ${info.deployment}`,
|
|
50
|
+
`Readonly setting: ${info.readonlySetting ?? "unknown"}`
|
|
51
|
+
];
|
|
52
|
+
if (info.writePrivileges.length > 0) {
|
|
53
|
+
lines.push(`Write-capable grants: ${info.writePrivileges.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
if (info.warnings.length > 0) {
|
|
56
|
+
lines.push("", "Warnings:");
|
|
57
|
+
lines.push(...info.warnings.map((warning) => `- ${warning}`));
|
|
58
|
+
}
|
|
59
|
+
return lines.join("\n");
|
|
60
|
+
}
|
|
61
|
+
function formatErrorMessage(error) {
|
|
62
|
+
return error instanceof Error ? error.message : String(error);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connect.js","sourceRoot":"","sources":["../../../src/tools/connect.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,4BAA4B,EAAE,MAAM,yBAAyB,CAAC;AACvE,OAAO,EAAE,2BAA2B,EAAE,MAAM,gCAAgC,CAAC;AAC7E,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,MAAM,UAAU,iBAAiB,CAAC,MAAiB;IACjD,MAAM,CAAC,YAAY,CACjB,SAAS,EACT;QACE,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EACT,gFAAgF;QAClF,WAAW,EAAE,EAAE;KAChB,EACD,KAAK,IAAI,EAAE;QACT,IAAI,MAAgD,CAAC;QAErD,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,oBAAoB,EAAE,CAAC;YACtC,MAAM,GAAG,IAAI,4BAA4B,CAAC,MAAM,CAAC,CAAC;YAClD,MAAM,IAAI,GAAG,MAAM,2BAA2B,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAE/D,OAAO;gBACL,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,oBAAoB,CAAC,IAAI,CAAC;qBACjC;iBACF;aACF,CAAC;QACJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO;gBACL,OAAO,EAAE,IAAI;gBACb,OAAO,EAAE;oBACP;wBACE,IAAI,EAAE,MAAM;wBACZ,IAAI,EAAE,8CAA8C,kBAAkB,CACpE,KAAK,CACN,EAAE;qBACJ;iBACF;aACF,CAAC;QACJ,CAAC;gBAAS,CAAC;YACT,MAAM,MAAM,EAAE,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC,CACF,CAAC;AACJ,CAAC;AAID,SAAS,oBAAoB,CAAC,IAAoB;IAChD,MAAM,KAAK,GAAG;QACZ,qCAAqC;QACrC,8BAA8B;QAC9B,EAAE;QACF,YAAY,IAAI,CAAC,OAAO,EAAE;QAC1B,aAAa,IAAI,CAAC,QAAQ,EAAE;QAC5B,SAAS,IAAI,CAAC,WAAW,EAAE;QAC3B,SAAS,IAAI,CAAC,QAAQ,EAAE;QACxB,eAAe,IAAI,CAAC,UAAU,EAAE;QAChC,qBAAqB,IAAI,CAAC,eAAe,IAAI,SAAS,EAAE;KACzD,CAAC;IAEF,IAAI,IAAI,CAAC,eAAe,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpC,KAAK,CAAC,IAAI,CAAC,yBAAyB,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,WAAW,CAAC,CAAC;QAC5B,KAAK,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,KAAK,OAAO,EAAE,CAAC,CAAC,CAAC;IAChE,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,kBAAkB,CAAC,KAAc;IACxC,OAAO,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function createHealthTool(server) {
|
|
2
|
+
server.registerTool("health", {
|
|
3
|
+
title: "Health Check",
|
|
4
|
+
description: "Confirm the Gozzle MCP server is running.",
|
|
5
|
+
inputSchema: {}
|
|
6
|
+
}, async () => ({
|
|
7
|
+
content: [
|
|
8
|
+
{
|
|
9
|
+
type: "text",
|
|
10
|
+
text: "Gozzle MCP server is running."
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../../src/tools/health.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,gBAAgB,CAAC,MAAiB;IAChD,MAAM,CAAC,YAAY,CACjB,QAAQ,EACR;QACE,KAAK,EAAE,cAAc;QACrB,WAAW,EAAE,2CAA2C;QACxD,WAAW,EAAE,EAAE;KAChB,EACD,KAAK,IAAI,EAAE,CAAC,CAAC;QACX,OAAO,EAAE;YACP;gBACE,IAAI,EAAE,MAAM;gBACZ,IAAI,EAAE,+BAA+B;aACtC;SACF;KACF,CAAC,CACH,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { type TableInspection } from "../clickhouse/table-inspection.js";
|
|
3
|
+
export declare function createInspectTableTool(server: McpServer): void;
|
|
4
|
+
export declare function formatTableInspection(inspection: TableInspection): string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ClickHouseHttpMetadataClient } from "../clickhouse/client.js";
|
|
3
|
+
import { inspectTable } from "../clickhouse/table-inspection.js";
|
|
4
|
+
import { readClickHouseConfig } from "../config/clickhouse.js";
|
|
5
|
+
export function createInspectTableTool(server) {
|
|
6
|
+
server.registerTool("inspect_table", {
|
|
7
|
+
title: "Inspect ClickHouse Table",
|
|
8
|
+
description: "Inspect a ClickHouse table's physical layout and eligible Gozzle checks.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
table: z
|
|
11
|
+
.string()
|
|
12
|
+
.min(1)
|
|
13
|
+
.describe("Table name in table or database.table format.")
|
|
14
|
+
}
|
|
15
|
+
}, async ({ table }) => {
|
|
16
|
+
let client;
|
|
17
|
+
try {
|
|
18
|
+
const config = readClickHouseConfig();
|
|
19
|
+
client = new ClickHouseHttpMetadataClient(config);
|
|
20
|
+
const inspection = await inspectTable(client, {
|
|
21
|
+
table,
|
|
22
|
+
defaultDatabase: config.database ?? "default"
|
|
23
|
+
});
|
|
24
|
+
return {
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: formatTableInspection(inspection)
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
return {
|
|
35
|
+
isError: true,
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `Gozzle could not inspect the table.\n\n${formatErrorMessage(error)}`
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await client?.close();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function formatTableInspection(inspection) {
|
|
50
|
+
const lines = [
|
|
51
|
+
`Table: ${inspection.identifier.database}.${inspection.identifier.table}`,
|
|
52
|
+
`Engine: ${inspection.engineFull}`,
|
|
53
|
+
`Order by: ${inspection.orderBy ?? inspection.sortingKey ?? "none"}`,
|
|
54
|
+
`Partition by: ${inspection.partitionBy ?? "none"}`,
|
|
55
|
+
`Primary key: ${inspection.primaryKey ?? "none"}`,
|
|
56
|
+
`Active parts: ${inspection.parts.activeParts}`,
|
|
57
|
+
`Rows: ${inspection.totalRows}`,
|
|
58
|
+
`Bytes on disk: ${inspection.totalBytes}`,
|
|
59
|
+
"",
|
|
60
|
+
"Eligible checks:",
|
|
61
|
+
`- verify_dedup: ${inspection.eligibleChecks.verifyDedup ? "yes" : "no"}`,
|
|
62
|
+
`- dry_run_migration: ${inspection.eligibleChecks.dryRunMigration ? "yes" : "no"}`,
|
|
63
|
+
`- diagnose_query: ${inspection.eligibleChecks.diagnoseQuery ? "yes" : "no"}`
|
|
64
|
+
];
|
|
65
|
+
if (inspection.replacingMergeTree) {
|
|
66
|
+
lines.push("", "ReplacingMergeTree:", `- version column: ${inspection.replacingMergeTree.versionColumn ?? "none"}`, `- deleted column: ${inspection.replacingMergeTree.deletedColumn ?? "none"}`);
|
|
67
|
+
}
|
|
68
|
+
if (inspection.warnings.length > 0) {
|
|
69
|
+
lines.push("", "Warnings:");
|
|
70
|
+
lines.push(...inspection.warnings.map((warning) => `- ${warning}`));
|
|
71
|
+
}
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
}
|
|
74
|
+
function formatErrorMessage(error) {
|
|
75
|
+
return error instanceof Error ? error.message : String(error);
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=inspect-table.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inspect-table.js","sourceRoot":"","sources":["../../../src/tools/inspect-table.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gozzle/cli",
|
|
3
|
+
"version": "0.0.1-canary.3",
|
|
4
|
+
"description": "A local safety harness and developer toolkit for ClickHouse.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"bin": {
|
|
9
|
+
"gozzle": "./dist/src/cli.js",
|
|
10
|
+
"gozzle-mcp": "./dist/src/mcp/server.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist/src",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public",
|
|
18
|
+
"tag": "canary"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc -p tsconfig.json",
|
|
22
|
+
"dev:mcp": "tsx src/mcp/server.ts",
|
|
23
|
+
"dev": "tsx src/cli.ts",
|
|
24
|
+
"lint": "tsc -p tsconfig.json --noEmit",
|
|
25
|
+
"prepublishOnly": "npm run build && npm test",
|
|
26
|
+
"smoke:mcp": "npm run build && node dist/scripts/mcp-smoke.js",
|
|
27
|
+
"test": "node --import tsx --test tests/**/*.test.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@clickhouse/client": "^1.21.0",
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.18.0",
|
|
32
|
+
"zod": "^3.25.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@types/node": "^22.15.0",
|
|
36
|
+
"tsx": "^4.20.0",
|
|
37
|
+
"typescript": "^5.8.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22"
|
|
41
|
+
}
|
|
42
|
+
}
|