pg-lens-mcp 0.1.0-alpha
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/CHANGELOG.md +43 -0
- package/LICENSE +21 -0
- package/README.md +483 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +38 -0
- package/dist/db/pool.d.ts +22 -0
- package/dist/db/pool.js +66 -0
- package/dist/db/queries.d.ts +39 -0
- package/dist/db/queries.js +76 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +35 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.js +31 -0
- package/dist/tools/execute-query.d.ts +6 -0
- package/dist/tools/execute-query.js +34 -0
- package/dist/tools/explain-analyze.d.ts +6 -0
- package/dist/tools/explain-analyze.js +59 -0
- package/dist/tools/explain-query.d.ts +6 -0
- package/dist/tools/explain-query.js +43 -0
- package/dist/tools/get-table-data.d.ts +6 -0
- package/dist/tools/get-table-data.js +105 -0
- package/dist/tools/get-table-info.d.ts +6 -0
- package/dist/tools/get-table-info.js +130 -0
- package/dist/tools/list-schemas.d.ts +6 -0
- package/dist/tools/list-schemas.js +25 -0
- package/dist/tools/list-tables.d.ts +6 -0
- package/dist/tools/list-tables.js +34 -0
- package/dist/tools/search-column.d.ts +6 -0
- package/dist/tools/search-column.js +34 -0
- package/dist/utils/response.d.ts +39 -0
- package/dist/utils/response.js +99 -0
- package/dist/utils/validation.d.ts +25 -0
- package/dist/utils/validation.js +37 -0
- package/mcp-config.example.json +16 -0
- package/package.json +51 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL query builders and filter-to-WHERE clause converters
|
|
3
|
+
*/
|
|
4
|
+
export interface Filter {
|
|
5
|
+
column: string;
|
|
6
|
+
operator: "=" | "!=" | "<" | ">" | "<=" | ">=" | "LIKE" | "ILIKE" | "IN" | "IS NULL" | "IS NOT NULL";
|
|
7
|
+
value?: string | number | boolean | null | (string | number)[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Convert structured filters to parameterized WHERE clause
|
|
11
|
+
* Safe from SQL injection via parameterized queries
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildWhereClause(filters: Filter[]): {
|
|
14
|
+
whereClause: string;
|
|
15
|
+
params: unknown[];
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Build a paginated SELECT query with optional filters and ordering
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildSelectQuery(options: {
|
|
21
|
+
schema: string;
|
|
22
|
+
table: string;
|
|
23
|
+
columns?: string[];
|
|
24
|
+
filters?: Filter[];
|
|
25
|
+
orderBy?: string;
|
|
26
|
+
orderDirection?: "ASC" | "DESC";
|
|
27
|
+
limit: number;
|
|
28
|
+
offset: number;
|
|
29
|
+
}): {
|
|
30
|
+
query: string;
|
|
31
|
+
params: unknown[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Build COUNT query with same filters
|
|
35
|
+
*/
|
|
36
|
+
export declare function buildCountQuery(schema: string, table: string, filters?: Filter[]): {
|
|
37
|
+
query: string;
|
|
38
|
+
params: unknown[];
|
|
39
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL query builders and filter-to-WHERE clause converters
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Convert structured filters to parameterized WHERE clause
|
|
6
|
+
* Safe from SQL injection via parameterized queries
|
|
7
|
+
*/
|
|
8
|
+
export function buildWhereClause(filters) {
|
|
9
|
+
if (filters.length === 0) {
|
|
10
|
+
return { whereClause: "", params: [] };
|
|
11
|
+
}
|
|
12
|
+
const params = [];
|
|
13
|
+
const conditions = [];
|
|
14
|
+
filters.forEach((filter) => {
|
|
15
|
+
const columnName = `"${filter.column}"`;
|
|
16
|
+
if (filter.operator === "IS NULL" || filter.operator === "IS NOT NULL") {
|
|
17
|
+
conditions.push(`${columnName} ${filter.operator}`);
|
|
18
|
+
}
|
|
19
|
+
else if (filter.operator === "IN") {
|
|
20
|
+
if (!Array.isArray(filter.value)) {
|
|
21
|
+
throw new Error(`IN operator requires array value for column ${filter.column}`);
|
|
22
|
+
}
|
|
23
|
+
const placeholders = filter.value.map((val) => {
|
|
24
|
+
params.push(val);
|
|
25
|
+
return `$${params.length}`;
|
|
26
|
+
});
|
|
27
|
+
conditions.push(`${columnName} IN (${placeholders.join(", ")})`);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
if (filter.value === undefined) {
|
|
31
|
+
throw new Error(`Operator ${filter.operator} requires a value for column ${filter.column}`);
|
|
32
|
+
}
|
|
33
|
+
params.push(filter.value);
|
|
34
|
+
conditions.push(`${columnName} ${filter.operator} $${params.length}`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
whereClause: conditions.join(" AND "),
|
|
39
|
+
params,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build a paginated SELECT query with optional filters and ordering
|
|
44
|
+
*/
|
|
45
|
+
export function buildSelectQuery(options) {
|
|
46
|
+
const { schema, table, columns = ["*"], filters = [], orderBy, orderDirection = "ASC", limit, offset, } = options;
|
|
47
|
+
// Build column list
|
|
48
|
+
const columnList = columns[0] === "*" ? "*" : columns.map((c) => `"${c}"`).join(", ");
|
|
49
|
+
// Build WHERE clause
|
|
50
|
+
const { whereClause, params } = buildWhereClause(filters);
|
|
51
|
+
// Start building query
|
|
52
|
+
let query = `SELECT ${columnList} FROM "${schema}"."${table}"`;
|
|
53
|
+
// Add WHERE if filters exist
|
|
54
|
+
if (whereClause) {
|
|
55
|
+
query += ` WHERE ${whereClause}`;
|
|
56
|
+
}
|
|
57
|
+
// Add ORDER BY if specified
|
|
58
|
+
if (orderBy) {
|
|
59
|
+
query += ` ORDER BY "${orderBy}" ${orderDirection}`;
|
|
60
|
+
}
|
|
61
|
+
// Add pagination
|
|
62
|
+
params.push(limit, offset);
|
|
63
|
+
query += ` LIMIT $${params.length - 1} OFFSET $${params.length}`;
|
|
64
|
+
return { query, params };
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build COUNT query with same filters
|
|
68
|
+
*/
|
|
69
|
+
export function buildCountQuery(schema, table, filters = []) {
|
|
70
|
+
const { whereClause, params } = buildWhereClause(filters);
|
|
71
|
+
let query = `SELECT COUNT(*) as total FROM "${schema}"."${table}"`;
|
|
72
|
+
if (whereClause) {
|
|
73
|
+
query += ` WHERE ${whereClause}`;
|
|
74
|
+
}
|
|
75
|
+
return { query, params };
|
|
76
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PostgreSQL MCP Server
|
|
4
|
+
* Entry point - handles initialization and graceful shutdown
|
|
5
|
+
*/
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { createServer } from "./server.js";
|
|
8
|
+
import { healthCheck, shutdown } from "./db/pool.js";
|
|
9
|
+
async function main() {
|
|
10
|
+
try {
|
|
11
|
+
await healthCheck();
|
|
12
|
+
const server = createServer();
|
|
13
|
+
const transport = new StdioServerTransport();
|
|
14
|
+
await server.connect(transport);
|
|
15
|
+
console.error("✓ PostgreSQL MCP Server running on stdio");
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
console.error("✗ Fatal error during startup:", error);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// Graceful shutdown handlers
|
|
23
|
+
process.on("SIGINT", async () => {
|
|
24
|
+
await shutdown();
|
|
25
|
+
process.exit(0);
|
|
26
|
+
});
|
|
27
|
+
process.on("SIGTERM", async () => {
|
|
28
|
+
await shutdown();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
});
|
|
31
|
+
// Start the server
|
|
32
|
+
main().catch((error) => {
|
|
33
|
+
console.error("✗ Unhandled error:", error);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server setup and tool registration
|
|
3
|
+
*/
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { registerListTablesTool } from "./tools/list-tables.js";
|
|
6
|
+
import { registerListSchemasTool } from "./tools/list-schemas.js";
|
|
7
|
+
import { registerSearchColumnTool } from "./tools/search-column.js";
|
|
8
|
+
import { registerGetTableInfoTool } from "./tools/get-table-info.js";
|
|
9
|
+
import { registerGetTableDataTool } from "./tools/get-table-data.js";
|
|
10
|
+
import { registerExecuteQueryTool } from "./tools/execute-query.js";
|
|
11
|
+
import { registerExplainQueryTool } from "./tools/explain-query.js";
|
|
12
|
+
import { registerExplainAnalyzeTool } from "./tools/explain-analyze.js";
|
|
13
|
+
/**
|
|
14
|
+
* Create and configure the MCP server with all tools
|
|
15
|
+
*/
|
|
16
|
+
export function createServer() {
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: "postgres-server",
|
|
19
|
+
version: "1.0.0",
|
|
20
|
+
});
|
|
21
|
+
// Register all tools
|
|
22
|
+
registerListSchemasTool(server);
|
|
23
|
+
registerListTablesTool(server);
|
|
24
|
+
registerSearchColumnTool(server);
|
|
25
|
+
registerGetTableInfoTool(server);
|
|
26
|
+
registerGetTableDataTool(server);
|
|
27
|
+
registerExecuteQueryTool(server);
|
|
28
|
+
registerExplainQueryTool(server);
|
|
29
|
+
registerExplainAnalyzeTool(server);
|
|
30
|
+
return server;
|
|
31
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: execute_query
|
|
3
|
+
* Execute a read-only SQL query with database-enforced READ ONLY transaction
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { executeReadOnly } from "../db/pool.js";
|
|
7
|
+
import { formatQueryResult, formatError } from "../utils/response.js";
|
|
8
|
+
export function registerExecuteQueryTool(server) {
|
|
9
|
+
server.tool("execute_query", "Execute a read-only SQL query. Query runs in a READ ONLY transaction for safety.", {
|
|
10
|
+
query: z.string().describe("SQL query to execute"),
|
|
11
|
+
params: z
|
|
12
|
+
.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Query parameters for parameterized queries ($1, $2, etc.)"),
|
|
15
|
+
}, async ({ query, params }) => {
|
|
16
|
+
const trimmedQuery = query.trim().toUpperCase();
|
|
17
|
+
// Basic sanity check (still allow SELECT and WITH/CTE)
|
|
18
|
+
if (!trimmedQuery.startsWith("SELECT") &&
|
|
19
|
+
!trimmedQuery.startsWith("WITH") &&
|
|
20
|
+
!trimmedQuery.startsWith("EXPLAIN")) {
|
|
21
|
+
return formatError("Only SELECT, WITH (CTE), and EXPLAIN queries are allowed");
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
// Execute in READ ONLY transaction - PostgreSQL enforces no writes
|
|
25
|
+
const result = await executeReadOnly(query, params || []);
|
|
26
|
+
return formatQueryResult(result.rows, {
|
|
27
|
+
rowCount: result.rowCount,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return formatError(error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: explain_analyze
|
|
3
|
+
* Execute a query and get actual execution statistics
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { executeReadOnly } from "../db/pool.js";
|
|
7
|
+
import { formatSuccess, formatError } from "../utils/response.js";
|
|
8
|
+
export function registerExplainAnalyzeTool(server) {
|
|
9
|
+
server.tool("explain_analyze", "Execute a query with EXPLAIN ANALYZE to get actual timing and row statistics. ⚠️ WARNING: This actually executes the query, so it may be slow on large tables.", {
|
|
10
|
+
query: z.string().describe("SQL query to analyze"),
|
|
11
|
+
format: z
|
|
12
|
+
.enum(["text", "json"])
|
|
13
|
+
.optional()
|
|
14
|
+
.default("json")
|
|
15
|
+
.describe("Output format for the analysis"),
|
|
16
|
+
buffers: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.optional()
|
|
19
|
+
.default(false)
|
|
20
|
+
.describe("Include buffer usage statistics"),
|
|
21
|
+
timing: z
|
|
22
|
+
.boolean()
|
|
23
|
+
.optional()
|
|
24
|
+
.default(true)
|
|
25
|
+
.describe("Include timing information"),
|
|
26
|
+
verbose: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.optional()
|
|
29
|
+
.default(false)
|
|
30
|
+
.describe("Include verbose details"),
|
|
31
|
+
}, async ({ query, format, buffers, timing, verbose }) => {
|
|
32
|
+
try {
|
|
33
|
+
const options = [
|
|
34
|
+
"ANALYZE",
|
|
35
|
+
`FORMAT ${format?.toUpperCase() || "JSON"}`,
|
|
36
|
+
];
|
|
37
|
+
if (buffers)
|
|
38
|
+
options.push("BUFFERS");
|
|
39
|
+
if (!timing)
|
|
40
|
+
options.push("TIMING OFF");
|
|
41
|
+
if (verbose)
|
|
42
|
+
options.push("VERBOSE");
|
|
43
|
+
const explainQuery = `EXPLAIN (${options.join(", ")}) ${query}`;
|
|
44
|
+
// Execute in READ ONLY transaction - query runs but can't modify data
|
|
45
|
+
const result = await executeReadOnly(explainQuery);
|
|
46
|
+
// For JSON format, parse and format nicely
|
|
47
|
+
if (format === "json" && result.rows.length > 0) {
|
|
48
|
+
const row = result.rows[0];
|
|
49
|
+
return formatSuccess(row["QUERY PLAN"]);
|
|
50
|
+
}
|
|
51
|
+
// For text, return as-is
|
|
52
|
+
const analysis = result.rows.map((row) => row["QUERY PLAN"]).join("\n");
|
|
53
|
+
return formatSuccess(analysis);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
return formatError(error);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: explain_query
|
|
3
|
+
* Get the execution plan for a query without executing it
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { executeReadOnly } from "../db/pool.js";
|
|
7
|
+
import { formatSuccess, formatError } from "../utils/response.js";
|
|
8
|
+
export function registerExplainQueryTool(server) {
|
|
9
|
+
server.tool("explain_query", "Get the query execution plan (EXPLAIN) without actually running the query. Useful for understanding how PostgreSQL will execute a query.", {
|
|
10
|
+
query: z.string().describe("SQL query to analyze"),
|
|
11
|
+
format: z
|
|
12
|
+
.enum(["text", "json", "yaml"])
|
|
13
|
+
.optional()
|
|
14
|
+
.default("json")
|
|
15
|
+
.describe("Output format for the plan"),
|
|
16
|
+
verbose: z
|
|
17
|
+
.boolean()
|
|
18
|
+
.optional()
|
|
19
|
+
.default(false)
|
|
20
|
+
.describe("Include verbose details in the plan"),
|
|
21
|
+
}, async ({ query, format, verbose }) => {
|
|
22
|
+
try {
|
|
23
|
+
const options = [`FORMAT ${format?.toUpperCase() || "JSON"}`];
|
|
24
|
+
if (verbose) {
|
|
25
|
+
options.push("VERBOSE");
|
|
26
|
+
}
|
|
27
|
+
const explainQuery = `EXPLAIN (${options.join(", ")}) ${query}`;
|
|
28
|
+
// Execute in READ ONLY transaction for safety
|
|
29
|
+
const result = await executeReadOnly(explainQuery);
|
|
30
|
+
// For JSON format, parse and format nicely
|
|
31
|
+
if (format === "json" && result.rows.length > 0) {
|
|
32
|
+
const row = result.rows[0];
|
|
33
|
+
return formatSuccess(row["QUERY PLAN"]);
|
|
34
|
+
}
|
|
35
|
+
// For text/yaml, return as-is
|
|
36
|
+
const plan = result.rows.map((row) => row["QUERY PLAN"]).join("\n");
|
|
37
|
+
return formatSuccess(plan);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return formatError(error);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: get_table_data
|
|
3
|
+
* Query table data with structured filters and pagination
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { pool } from "../db/pool.js";
|
|
7
|
+
import { config } from "../config.js";
|
|
8
|
+
import { buildSelectQuery, buildCountQuery } from "../db/queries.js";
|
|
9
|
+
import { formatQueryResult, formatError } from "../utils/response.js";
|
|
10
|
+
import { validateTableExists, validateColumns } from "../utils/validation.js";
|
|
11
|
+
const FilterSchema = z.object({
|
|
12
|
+
column: z.string().describe("Column name to filter on"),
|
|
13
|
+
operator: z
|
|
14
|
+
.enum(["=", "!=", "<", ">", "<=", ">=", "LIKE", "ILIKE", "IN", "IS NULL", "IS NOT NULL"])
|
|
15
|
+
.describe("Comparison operator"),
|
|
16
|
+
value: z
|
|
17
|
+
.union([
|
|
18
|
+
z.string(),
|
|
19
|
+
z.number(),
|
|
20
|
+
z.boolean(),
|
|
21
|
+
z.null(),
|
|
22
|
+
z.array(z.union([z.string(), z.number()])),
|
|
23
|
+
])
|
|
24
|
+
.optional()
|
|
25
|
+
.describe("Value to compare (not needed for IS NULL/IS NOT NULL)"),
|
|
26
|
+
});
|
|
27
|
+
export function registerGetTableDataTool(server) {
|
|
28
|
+
server.tool("get_table_data", "Retrieve data from a table with optional structured filtering and pagination.", {
|
|
29
|
+
table_name: z.string().describe("Name of the table to query"),
|
|
30
|
+
schema: z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Schema name (default: public)"),
|
|
34
|
+
columns: z
|
|
35
|
+
.array(z.string())
|
|
36
|
+
.optional()
|
|
37
|
+
.describe("Specific columns to select (default: all)"),
|
|
38
|
+
filters: z
|
|
39
|
+
.array(FilterSchema)
|
|
40
|
+
.optional()
|
|
41
|
+
.describe("Structured filters for WHERE clause. Example: [{column: 'status', operator: '=', value: 'active'}]"),
|
|
42
|
+
limit: z
|
|
43
|
+
.number()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe("Maximum number of rows to return (default: 100, max: 1000)"),
|
|
46
|
+
offset: z
|
|
47
|
+
.number()
|
|
48
|
+
.optional()
|
|
49
|
+
.describe("Number of rows to skip (default: 0)"),
|
|
50
|
+
order_by: z
|
|
51
|
+
.string()
|
|
52
|
+
.optional()
|
|
53
|
+
.describe("Column name to order by"),
|
|
54
|
+
order_direction: z
|
|
55
|
+
.enum(["ASC", "DESC"])
|
|
56
|
+
.optional()
|
|
57
|
+
.describe("Sort direction (default: ASC)"),
|
|
58
|
+
}, async ({ table_name, schema, columns, filters, limit, offset, order_by, order_direction, }) => {
|
|
59
|
+
const targetSchema = schema || config.schema;
|
|
60
|
+
const effectiveLimit = Math.min(limit || 100, 1000);
|
|
61
|
+
const effectiveOffset = offset || 0;
|
|
62
|
+
try {
|
|
63
|
+
// Validate table exists
|
|
64
|
+
const tableExists = await validateTableExists(pool, targetSchema, table_name);
|
|
65
|
+
if (!tableExists) {
|
|
66
|
+
return formatError(`Table "${targetSchema}"."${table_name}" does not exist`);
|
|
67
|
+
}
|
|
68
|
+
// Validate columns if specified
|
|
69
|
+
if (columns && columns.length > 0) {
|
|
70
|
+
const validation = await validateColumns(pool, targetSchema, table_name, columns);
|
|
71
|
+
if (!validation.valid) {
|
|
72
|
+
return formatError(`Invalid columns: ${validation.invalidColumns.join(", ")}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Build queries
|
|
76
|
+
const { query, params } = buildSelectQuery({
|
|
77
|
+
schema: targetSchema,
|
|
78
|
+
table: table_name,
|
|
79
|
+
columns,
|
|
80
|
+
filters: filters,
|
|
81
|
+
orderBy: order_by,
|
|
82
|
+
orderDirection: order_direction,
|
|
83
|
+
limit: effectiveLimit,
|
|
84
|
+
offset: effectiveOffset,
|
|
85
|
+
});
|
|
86
|
+
const { query: countQuery, params: countParams } = buildCountQuery(targetSchema, table_name, filters);
|
|
87
|
+
// Execute queries in parallel
|
|
88
|
+
const [dataResult, countResult] = await Promise.all([
|
|
89
|
+
pool.query(query, params),
|
|
90
|
+
pool.query(countQuery, countParams),
|
|
91
|
+
]);
|
|
92
|
+
return formatQueryResult(dataResult.rows, {
|
|
93
|
+
schema: targetSchema,
|
|
94
|
+
table: table_name,
|
|
95
|
+
totalRows: parseInt(countResult.rows[0].total, 10),
|
|
96
|
+
returnedRows: dataResult.rows.length,
|
|
97
|
+
limit: effectiveLimit,
|
|
98
|
+
offset: effectiveOffset,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return formatError(error);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: get_table_info
|
|
3
|
+
* Get detailed table schema including columns, constraints, and indexes
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { pool } from "../db/pool.js";
|
|
7
|
+
import { config } from "../config.js";
|
|
8
|
+
import { formatSuccess, formatError } from "../utils/response.js";
|
|
9
|
+
export function registerGetTableInfoTool(server) {
|
|
10
|
+
server.tool("get_table_info", "Get detailed information about a table including columns, data types, and constraints.", {
|
|
11
|
+
table_name: z.string().describe("Name of the table to inspect"),
|
|
12
|
+
schema: z
|
|
13
|
+
.string()
|
|
14
|
+
.optional()
|
|
15
|
+
.describe("Schema name (default: public)"),
|
|
16
|
+
}, async ({ table_name, schema }) => {
|
|
17
|
+
const targetSchema = schema || config.schema;
|
|
18
|
+
// Get column information
|
|
19
|
+
const columnsQuery = `
|
|
20
|
+
SELECT
|
|
21
|
+
c.column_name,
|
|
22
|
+
c.data_type,
|
|
23
|
+
c.character_maximum_length,
|
|
24
|
+
c.numeric_precision,
|
|
25
|
+
c.numeric_scale,
|
|
26
|
+
c.is_nullable,
|
|
27
|
+
c.column_default,
|
|
28
|
+
c.udt_name
|
|
29
|
+
FROM information_schema.columns c
|
|
30
|
+
WHERE c.table_schema = $1
|
|
31
|
+
AND c.table_name = $2
|
|
32
|
+
ORDER BY c.ordinal_position
|
|
33
|
+
`;
|
|
34
|
+
// Get primary key information
|
|
35
|
+
const pkQuery = `
|
|
36
|
+
SELECT
|
|
37
|
+
kcu.column_name
|
|
38
|
+
FROM information_schema.table_constraints tc
|
|
39
|
+
JOIN information_schema.key_column_usage kcu
|
|
40
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
41
|
+
AND tc.table_schema = kcu.table_schema
|
|
42
|
+
WHERE tc.table_schema = $1
|
|
43
|
+
AND tc.table_name = $2
|
|
44
|
+
AND tc.constraint_type = 'PRIMARY KEY'
|
|
45
|
+
`;
|
|
46
|
+
// Get foreign key information
|
|
47
|
+
const fkQuery = `
|
|
48
|
+
SELECT
|
|
49
|
+
kcu.column_name,
|
|
50
|
+
ccu.table_name AS foreign_table,
|
|
51
|
+
ccu.column_name AS foreign_column
|
|
52
|
+
FROM information_schema.table_constraints tc
|
|
53
|
+
JOIN information_schema.key_column_usage kcu
|
|
54
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
55
|
+
AND tc.table_schema = kcu.table_schema
|
|
56
|
+
JOIN information_schema.constraint_column_usage ccu
|
|
57
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
58
|
+
AND ccu.table_schema = tc.table_schema
|
|
59
|
+
WHERE tc.table_schema = $1
|
|
60
|
+
AND tc.table_name = $2
|
|
61
|
+
AND tc.constraint_type = 'FOREIGN KEY'
|
|
62
|
+
`;
|
|
63
|
+
// Get indexes
|
|
64
|
+
const indexQuery = `
|
|
65
|
+
SELECT
|
|
66
|
+
i.relname AS index_name,
|
|
67
|
+
a.attname AS column_name,
|
|
68
|
+
ix.indisunique AS is_unique,
|
|
69
|
+
ix.indisprimary AS is_primary
|
|
70
|
+
FROM pg_class t
|
|
71
|
+
JOIN pg_index ix ON t.oid = ix.indrelid
|
|
72
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
73
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
74
|
+
JOIN pg_namespace n ON n.oid = t.relnamespace
|
|
75
|
+
WHERE t.relname = $2
|
|
76
|
+
AND n.nspname = $1
|
|
77
|
+
ORDER BY i.relname, a.attnum
|
|
78
|
+
`;
|
|
79
|
+
try {
|
|
80
|
+
const [columnsResult, pkResult, fkResult, indexResult] = await Promise.all([
|
|
81
|
+
pool.query(columnsQuery, [targetSchema, table_name]),
|
|
82
|
+
pool.query(pkQuery, [targetSchema, table_name]),
|
|
83
|
+
pool.query(fkQuery, [targetSchema, table_name]),
|
|
84
|
+
pool.query(indexQuery, [targetSchema, table_name]),
|
|
85
|
+
]);
|
|
86
|
+
const primaryKeys = pkResult.rows.map((row) => row.column_name);
|
|
87
|
+
const foreignKeys = fkResult.rows.reduce((acc, row) => {
|
|
88
|
+
acc[row.column_name] = {
|
|
89
|
+
references: `${row.foreign_table}.${row.foreign_column}`,
|
|
90
|
+
};
|
|
91
|
+
return acc;
|
|
92
|
+
}, {});
|
|
93
|
+
const columns = columnsResult.rows.map((row) => ({
|
|
94
|
+
name: row.column_name,
|
|
95
|
+
type: row.data_type,
|
|
96
|
+
udtName: row.udt_name,
|
|
97
|
+
maxLength: row.character_maximum_length,
|
|
98
|
+
precision: row.numeric_precision,
|
|
99
|
+
scale: row.numeric_scale,
|
|
100
|
+
nullable: row.is_nullable === "YES",
|
|
101
|
+
default: row.column_default,
|
|
102
|
+
isPrimaryKey: primaryKeys.includes(row.column_name),
|
|
103
|
+
foreignKey: foreignKeys[row.column_name] || null,
|
|
104
|
+
}));
|
|
105
|
+
// Group indexes
|
|
106
|
+
const indexes = indexResult.rows.reduce((acc, row) => {
|
|
107
|
+
if (!acc[row.index_name]) {
|
|
108
|
+
acc[row.index_name] = {
|
|
109
|
+
name: row.index_name,
|
|
110
|
+
columns: [],
|
|
111
|
+
isUnique: row.is_unique,
|
|
112
|
+
isPrimary: row.is_primary,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
acc[row.index_name].columns.push(row.column_name);
|
|
116
|
+
return acc;
|
|
117
|
+
}, {});
|
|
118
|
+
const tableInfo = {
|
|
119
|
+
schema: targetSchema,
|
|
120
|
+
table: table_name,
|
|
121
|
+
columns,
|
|
122
|
+
indexes: Object.values(indexes),
|
|
123
|
+
};
|
|
124
|
+
return formatSuccess(tableInfo);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
return formatError(error);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: list_schemas
|
|
3
|
+
* List all non-system schemas in the database
|
|
4
|
+
*/
|
|
5
|
+
import { pool } from "../db/pool.js";
|
|
6
|
+
import { formatSuccess, formatError } from "../utils/response.js";
|
|
7
|
+
export function registerListSchemasTool(server) {
|
|
8
|
+
server.tool("list_schemas", "List all non-system schemas in the database.", {}, async () => {
|
|
9
|
+
const query = `
|
|
10
|
+
SELECT
|
|
11
|
+
schema_name,
|
|
12
|
+
schema_owner
|
|
13
|
+
FROM information_schema.schemata
|
|
14
|
+
WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast')
|
|
15
|
+
ORDER BY schema_name
|
|
16
|
+
`;
|
|
17
|
+
try {
|
|
18
|
+
const result = await pool.query(query);
|
|
19
|
+
return formatSuccess(result.rows, { asTable: true });
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
return formatError(error);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|