Skip to content

Commit 961a7f0

Browse files
authored
rely on clickhouse client for readonly and limit for typescript-mcp template (#2968)
<!-- CURSOR_SUMMARY --> > [!NOTE] > Switches MCP template to rely on ClickHouse readonly mode and client-enforced limits, updates query tool schema, and removes custom SQL validation/limit utilities. > > - **MCP Server (`templates/typescript-mcp/app/apis/mcp.ts`)**: > - Add `clickhouseReadonlyQuery` to execute JSONEachRow queries with ClickHouse `readonly=2` and enforced `limit`. > - Update `query_clickhouse` tool: read-only description, increase `limit` max to `1000`, remove whitelist/blocklist validation and manual LIMIT handling; rely on DB-level readonly. > - Use direct SQL strings and apply high limit (`10000`) for catalog metadata queries; route result parsing through the new helper. > - Reorganize catalog type interfaces (`DataCatalogParams`, `ColumnInfo`, `TableInfo`, `DataCatalogResponse`). > - **Removal**: > - Delete `templates/typescript-mcp/app/apis/utils/sql.ts` and drop related imports (`validateQuery`, `applyLimitToQuery`, `Sql`, `sql`). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 83cc45e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8f46497 commit 961a7f0

File tree

2 files changed

+76
-285
lines changed

2 files changed

+76
-285
lines changed

templates/typescript-mcp/app/apis/mcp.ts

Lines changed: 76 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -16,48 +16,24 @@ import express from "express";
1616
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1717
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
1818
import { z } from "zod";
19-
import { WebApp, getMooseUtils, ApiUtil, Sql, sql } from "@514labs/moose-lib";
20-
import { validateQuery, applyLimitToQuery } from "./utils/sql";
19+
import { WebApp, getMooseUtils, ApiUtil } from "@514labs/moose-lib";
2120

2221
// TODO:
2322
// auth using getMooseUtils() jwt
2423

25-
/**
26-
* Parameters for the get_data_catalog tool
27-
*/
28-
interface DataCatalogParams {
29-
component_type?: "tables" | "materialized_views";
30-
search?: string;
31-
format?: "summary" | "detailed";
32-
}
33-
34-
/**
35-
* Column information from ClickHouse system.columns
36-
*/
37-
interface ColumnInfo {
38-
name: string;
39-
type: string;
40-
nullable: boolean;
41-
comment?: string;
42-
}
43-
44-
/**
45-
* Table information from ClickHouse system.tables
46-
*/
47-
interface TableInfo {
48-
name: string;
49-
engine: string;
50-
total_rows?: number;
51-
total_bytes?: number;
52-
columns: ColumnInfo[];
53-
}
54-
55-
/**
56-
* Catalog response structure
57-
*/
58-
interface DataCatalogResponse {
59-
tables?: Record<string, TableInfo>;
60-
materialized_views?: Record<string, TableInfo>;
24+
function clickhouseReadonlyQuery(
25+
client: ApiUtil["client"],
26+
sql: string,
27+
limit = 100,
28+
): ReturnType<ApiUtil["client"]["query"]["client"]["query"]> {
29+
return client.query.client.query({
30+
query: sql,
31+
format: "JSONEachRow",
32+
clickhouse_settings: {
33+
readonly: "2",
34+
limit: limit.toString(),
35+
},
36+
});
6137
}
6238

6339
/**
@@ -68,18 +44,19 @@ async function getTableColumns(
6844
dbName: string,
6945
tableName: string,
7046
): Promise<ColumnInfo[]> {
71-
const query = sql`
47+
const query = `
7248
SELECT
7349
name,
7450
type,
7551
type LIKE '%Nullable%' as nullable,
7652
comment
7753
FROM system.columns
78-
WHERE database = ${dbName} AND table = ${tableName}
54+
WHERE database = '${dbName}' AND table = '${tableName}'
7955
ORDER BY position
8056
`;
8157

82-
const result = await client.query.execute(query);
58+
// High limit for catalog queries - metadata tables are typically small
59+
const result = await clickhouseReadonlyQuery(client, query, 10000);
8360
const data = (await result.json()) as any[];
8461

8562
return data.map((row: any) => ({
@@ -102,7 +79,7 @@ async function getTablesAndMaterializedViews(
10279
tables: Array<{ name: string; engine: string }>;
10380
materializedViews: Array<{ name: string; engine: string }>;
10481
}> {
105-
const query = sql`
82+
const query = `
10683
SELECT
10784
name,
10885
engine,
@@ -111,11 +88,12 @@ async function getTablesAndMaterializedViews(
11188
ELSE 'table'
11289
END as component_type
11390
FROM system.tables
114-
WHERE database = ${dbName}
91+
WHERE database = '${dbName}'
11592
ORDER BY name
11693
`;
11794

118-
const result = await client.query.execute(query);
95+
// High limit for catalog queries - metadata tables are typically small
96+
const result = await clickhouseReadonlyQuery(client, query, 10000);
11997
const data = (await result.json()) as any[];
12098

12199
let filteredData = data;
@@ -233,6 +211,44 @@ async function formatCatalogDetailed(
233211
return JSON.stringify(catalog, null, 2);
234212
}
235213

214+
/**
215+
* Parameters for the get_data_catalog tool
216+
*/
217+
interface DataCatalogParams {
218+
component_type?: "tables" | "materialized_views";
219+
search?: string;
220+
format?: "summary" | "detailed";
221+
}
222+
223+
/**
224+
* Column information from ClickHouse system.columns
225+
*/
226+
interface ColumnInfo {
227+
name: string;
228+
type: string;
229+
nullable: boolean;
230+
comment?: string;
231+
}
232+
233+
/**
234+
* Table information from ClickHouse system.tables
235+
*/
236+
interface TableInfo {
237+
name: string;
238+
engine: string;
239+
total_rows?: number;
240+
total_bytes?: number;
241+
columns: ColumnInfo[];
242+
}
243+
244+
/**
245+
* Catalog response structure
246+
*/
247+
interface DataCatalogResponse {
248+
tables?: Record<string, TableInfo>;
249+
materialized_views?: Record<string, TableInfo>;
250+
}
251+
236252
// Create Express application
237253
const app = express();
238254
app.use(express.json());
@@ -251,30 +267,31 @@ const serverFactory = (mooseUtils: ApiUtil | null) => {
251267
/**
252268
* Register the query_clickhouse tool
253269
*
254-
* This tool allows AI assistants to execute SQL queries against your ClickHouse
255-
* database through the MCP protocol.
256-
*
257-
* Security features:
258-
* - Query whitelist: Only SELECT, SHOW, DESCRIBE, EXPLAIN queries permitted
259-
* - Query blocklist: Prevents INSERT, UPDATE, DELETE, DROP, CREATE, ALTER, etc.
260-
* - Row limit enforcement: Results automatically limited to maximum of 100 rows
270+
* Allows AI assistants to execute SQL queries against ClickHouse.
271+
* Results are limited to max 1000 rows to prevent excessive data transfer.
272+
* Security is enforced at the database level using ClickHouse readonly mode.
261273
*/
262274
server.registerTool(
263275
"query_clickhouse",
276+
/**
277+
* Type assertion needed here due to MCP SDK type limitations.
278+
* The SDK expects Record<string, ZodTypeAny> but our schema structure
279+
* doesn't match that exact type. Runtime validation still works correctly.
280+
*/
264281
{
265282
title: "Query ClickHouse Database",
266283
description:
267-
"Execute a SQL query against the ClickHouse OLAP database and return results as JSON",
284+
"Execute a read-only query against the ClickHouse OLAP database and return results as JSON. Use SELECT, SHOW, DESCRIBE, or EXPLAIN queries only. Data modification queries (INSERT, UPDATE, DELETE, ALTER, CREATE, etc.) are prohibited.",
268285
inputSchema: {
269286
query: z.string().describe("SQL query to execute against ClickHouse"),
270287
limit: z
271288
.number()
272289
.min(1)
273-
.max(100)
290+
.max(1000)
274291
.default(100)
275292
.optional()
276293
.describe(
277-
"Maximum number of rows to return (default: 100, max: 100)",
294+
"Maximum number of rows to return (default: 100, max: 1000)",
278295
),
279296
},
280297
outputSchema: {
@@ -283,7 +300,7 @@ const serverFactory = (mooseUtils: ApiUtil | null) => {
283300
.describe("Query results as array of row objects"),
284301
rowCount: z.number().describe("Number of rows returned"),
285302
},
286-
},
303+
} as any,
287304
async ({ query, limit = 100 }) => {
288305
try {
289306
// Check if MooseStack utilities are available
@@ -299,40 +316,14 @@ const serverFactory = (mooseUtils: ApiUtil | null) => {
299316
};
300317
}
301318

302-
// Validate query for security
303-
const validation = validateQuery(query);
304-
if (!validation.valid) {
305-
return {
306-
content: [
307-
{
308-
type: "text",
309-
text: `Security error: ${validation.error}`,
310-
},
311-
],
312-
isError: true,
313-
};
314-
}
315-
316319
const { client } = mooseUtils;
317320

318-
// Enforce maximum limit of 100
319-
const enforcedLimit = Math.min(limit, 100);
320-
321-
// Apply limit to the query using our dedicated function
322-
const finalQuery = applyLimitToQuery(
323-
query,
324-
enforcedLimit,
325-
validation.supportsLimit,
321+
const result = await clickhouseReadonlyQuery(
322+
client,
323+
query.trim(),
324+
limit,
326325
);
327326

328-
// Create a Sql object manually for dynamic query execution
329-
const sqlQuery: Sql = {
330-
strings: [finalQuery],
331-
values: [],
332-
} as any;
333-
334-
const result = await client.query.execute(sqlQuery);
335-
336327
// Parse the JSON response from ClickHouse
337328
const data = await result.json();
338329
const rows = Array.isArray(data) ? data : [];

0 commit comments

Comments
 (0)