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,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: list_tables
|
|
3
|
+
* List all tables in a schema
|
|
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 registerListTablesTool(server) {
|
|
10
|
+
server.tool("list_tables", "List all tables in the database. Optionally filter by schema.", {
|
|
11
|
+
schema: z
|
|
12
|
+
.string()
|
|
13
|
+
.optional()
|
|
14
|
+
.describe("Schema name to filter tables (default: public)"),
|
|
15
|
+
}, async ({ schema }) => {
|
|
16
|
+
const targetSchema = schema || config.schema;
|
|
17
|
+
const query = `
|
|
18
|
+
SELECT
|
|
19
|
+
table_schema,
|
|
20
|
+
table_name,
|
|
21
|
+
table_type
|
|
22
|
+
FROM information_schema.tables
|
|
23
|
+
WHERE table_schema = $1
|
|
24
|
+
ORDER BY table_name
|
|
25
|
+
`;
|
|
26
|
+
try {
|
|
27
|
+
const result = await pool.query(query, [targetSchema]);
|
|
28
|
+
return formatSuccess(result.rows, { asTable: true });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return formatError(error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool: search_column
|
|
3
|
+
* Find tables containing a column name pattern
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { pool } from "../db/pool.js";
|
|
7
|
+
import { formatSuccess, formatError } from "../utils/response.js";
|
|
8
|
+
export function registerSearchColumnTool(server) {
|
|
9
|
+
server.tool("search_column", "Find tables containing a column name pattern (case-insensitive search).", {
|
|
10
|
+
column_pattern: z
|
|
11
|
+
.string()
|
|
12
|
+
.describe("Partial or full column name to search for"),
|
|
13
|
+
}, async ({ column_pattern }) => {
|
|
14
|
+
const query = `
|
|
15
|
+
SELECT
|
|
16
|
+
table_schema,
|
|
17
|
+
table_name,
|
|
18
|
+
column_name,
|
|
19
|
+
data_type,
|
|
20
|
+
is_nullable,
|
|
21
|
+
column_default
|
|
22
|
+
FROM information_schema.columns
|
|
23
|
+
WHERE column_name ILIKE $1
|
|
24
|
+
ORDER BY table_schema, table_name, column_name
|
|
25
|
+
`;
|
|
26
|
+
try {
|
|
27
|
+
const result = await pool.query(query, [`%${column_pattern}%`]);
|
|
28
|
+
return formatSuccess(result.rows, { asTable: true });
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return formatError(error);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP response formatting utilities
|
|
3
|
+
*
|
|
4
|
+
* Token-optimized responses using markdown tables instead of verbose JSON
|
|
5
|
+
*/
|
|
6
|
+
interface McpTextContent {
|
|
7
|
+
type: "text";
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
interface McpResponse {
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
content: McpTextContent[];
|
|
13
|
+
isError?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Format database rows as a compact markdown table
|
|
17
|
+
* Significantly reduces token usage compared to JSON
|
|
18
|
+
*/
|
|
19
|
+
export declare function formatTableAsMarkdown(rows: Record<string, unknown>[]): string;
|
|
20
|
+
/**
|
|
21
|
+
* Format metadata (table schema, etc.) as compact summary
|
|
22
|
+
*/
|
|
23
|
+
export declare function formatMeta(data: Record<string, unknown>): string;
|
|
24
|
+
/**
|
|
25
|
+
* Format a successful response
|
|
26
|
+
*/
|
|
27
|
+
export declare function formatSuccess(data: unknown, options?: {
|
|
28
|
+
asTable?: boolean;
|
|
29
|
+
asMeta?: boolean;
|
|
30
|
+
}): McpResponse;
|
|
31
|
+
/**
|
|
32
|
+
* Format an error response
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatError(error: unknown): McpResponse;
|
|
35
|
+
/**
|
|
36
|
+
* Format a response with row count metadata and table data
|
|
37
|
+
*/
|
|
38
|
+
export declare function formatQueryResult(rows: Record<string, unknown>[], metadata?: Record<string, unknown>): McpResponse;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP response formatting utilities
|
|
3
|
+
*
|
|
4
|
+
* Token-optimized responses using markdown tables instead of verbose JSON
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Format database rows as a compact markdown table
|
|
8
|
+
* Significantly reduces token usage compared to JSON
|
|
9
|
+
*/
|
|
10
|
+
export function formatTableAsMarkdown(rows) {
|
|
11
|
+
if (rows.length === 0) {
|
|
12
|
+
return "_No results_";
|
|
13
|
+
}
|
|
14
|
+
const headers = Object.keys(rows[0]);
|
|
15
|
+
const headerRow = `| ${headers.join(" | ")} |`;
|
|
16
|
+
const separator = `| ${headers.map(() => "---").join(" | ")} |`;
|
|
17
|
+
const dataRows = rows
|
|
18
|
+
.map((row) => {
|
|
19
|
+
const values = headers.map((h) => {
|
|
20
|
+
const value = row[h];
|
|
21
|
+
if (value === null || value === undefined)
|
|
22
|
+
return "NULL";
|
|
23
|
+
if (typeof value === "object")
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
return String(value);
|
|
26
|
+
});
|
|
27
|
+
return `| ${values.join(" | ")} |`;
|
|
28
|
+
})
|
|
29
|
+
.join("\n");
|
|
30
|
+
return `${headerRow}\n${separator}\n${dataRows}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Format metadata (table schema, etc.) as compact summary
|
|
34
|
+
*/
|
|
35
|
+
export function formatMeta(data) {
|
|
36
|
+
return Object.entries(data)
|
|
37
|
+
.map(([key, value]) => `**${key}**: ${JSON.stringify(value, null, 2)}`)
|
|
38
|
+
.join("\n");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Format a successful response
|
|
42
|
+
*/
|
|
43
|
+
export function formatSuccess(data, options = {}) {
|
|
44
|
+
let text;
|
|
45
|
+
if (options.asTable && Array.isArray(data)) {
|
|
46
|
+
text = formatTableAsMarkdown(data);
|
|
47
|
+
}
|
|
48
|
+
else if (options.asMeta && typeof data === "object" && data !== null) {
|
|
49
|
+
text = formatMeta(data);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
text = JSON.stringify(data, null, 2);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
content: [
|
|
56
|
+
{
|
|
57
|
+
type: "text",
|
|
58
|
+
text,
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Format an error response
|
|
65
|
+
*/
|
|
66
|
+
export function formatError(error) {
|
|
67
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: "text",
|
|
72
|
+
text: `❌ **Error**: ${errorMessage}`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Format a response with row count metadata and table data
|
|
80
|
+
*/
|
|
81
|
+
export function formatQueryResult(rows, metadata) {
|
|
82
|
+
const parts = [];
|
|
83
|
+
if (metadata) {
|
|
84
|
+
parts.push("### Query Result\n");
|
|
85
|
+
Object.entries(metadata).forEach(([key, value]) => {
|
|
86
|
+
parts.push(`- **${key}**: ${value}`);
|
|
87
|
+
});
|
|
88
|
+
parts.push("");
|
|
89
|
+
}
|
|
90
|
+
parts.push(formatTableAsMarkdown(rows));
|
|
91
|
+
return {
|
|
92
|
+
content: [
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
text: parts.join("\n"),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for database identifiers and inputs
|
|
3
|
+
*/
|
|
4
|
+
import type { Pool } from "pg";
|
|
5
|
+
/**
|
|
6
|
+
* Basic identifier validation for table/column names
|
|
7
|
+
* Note: PostgreSQL allows quoted identifiers with special characters,
|
|
8
|
+
* so we validate existence rather than format
|
|
9
|
+
*/
|
|
10
|
+
export declare const IDENTIFIER_REGEX: RegExp;
|
|
11
|
+
/**
|
|
12
|
+
* Validate that a table exists in the specified schema
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateTableExists(pool: Pool, schema: string, tableName: string): Promise<boolean>;
|
|
15
|
+
/**
|
|
16
|
+
* Validate that a schema exists
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateSchemaExists(pool: Pool, schema: string): Promise<boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Validate column names against a table
|
|
21
|
+
*/
|
|
22
|
+
export declare function validateColumns(pool: Pool, schema: string, tableName: string, columns: string[]): Promise<{
|
|
23
|
+
valid: boolean;
|
|
24
|
+
invalidColumns: string[];
|
|
25
|
+
}>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation utilities for database identifiers and inputs
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Basic identifier validation for table/column names
|
|
6
|
+
* Note: PostgreSQL allows quoted identifiers with special characters,
|
|
7
|
+
* so we validate existence rather than format
|
|
8
|
+
*/
|
|
9
|
+
export const IDENTIFIER_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
10
|
+
/**
|
|
11
|
+
* Validate that a table exists in the specified schema
|
|
12
|
+
*/
|
|
13
|
+
export async function validateTableExists(pool, schema, tableName) {
|
|
14
|
+
const result = await pool.query(`SELECT 1 FROM information_schema.tables
|
|
15
|
+
WHERE table_schema = $1 AND table_name = $2`, [schema, tableName]);
|
|
16
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validate that a schema exists
|
|
20
|
+
*/
|
|
21
|
+
export async function validateSchemaExists(pool, schema) {
|
|
22
|
+
const result = await pool.query(`SELECT 1 FROM information_schema.schemata WHERE schema_name = $1`, [schema]);
|
|
23
|
+
return result.rowCount !== null && result.rowCount > 0;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Validate column names against a table
|
|
27
|
+
*/
|
|
28
|
+
export async function validateColumns(pool, schema, tableName, columns) {
|
|
29
|
+
const result = await pool.query(`SELECT column_name FROM information_schema.columns
|
|
30
|
+
WHERE table_schema = $1 AND table_name = $2`, [schema, tableName]);
|
|
31
|
+
const validColumns = new Set(result.rows.map((row) => row.column_name));
|
|
32
|
+
const invalidColumns = columns.filter((col) => !validColumns.has(col));
|
|
33
|
+
return {
|
|
34
|
+
valid: invalidColumns.length === 0,
|
|
35
|
+
invalidColumns,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mcpServers": {
|
|
3
|
+
"postgres": {
|
|
4
|
+
"command": "node",
|
|
5
|
+
"args": ["/home/yohann/Lab/MCP/postgres-server/dist/index.js"],
|
|
6
|
+
"env": {
|
|
7
|
+
"DB_HOST": "localhost",
|
|
8
|
+
"DB_PORT": "5432",
|
|
9
|
+
"DB_DATABASE": "your_database",
|
|
10
|
+
"DB_USERNAME": "your_username",
|
|
11
|
+
"DB_PASSWORD": "your_password",
|
|
12
|
+
"DB_SCHEMA": "public"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pg-lens-mcp",
|
|
3
|
+
"version": "0.1.0-alpha",
|
|
4
|
+
"description": "Secure PostgreSQL integration for AI assistants via Model Context Protocol",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"pg-lens-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"start": "node dist/index.js",
|
|
13
|
+
"dev": "tsx src/index.ts"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"postgres",
|
|
18
|
+
"postgresql",
|
|
19
|
+
"database",
|
|
20
|
+
"ai",
|
|
21
|
+
"claude",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"anthropic",
|
|
24
|
+
"sql",
|
|
25
|
+
"read-only"
|
|
26
|
+
],
|
|
27
|
+
"author": "Yohann",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/YhannHommet/pg-lens-mcp.git"
|
|
32
|
+
},
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/YhannHommet/pg-lens-mcp/issues"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/YhannHommet/pg-lens-mcp#readme",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
42
|
+
"pg": "^8.13.0",
|
|
43
|
+
"zod": "^3.24.0"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.0.0",
|
|
47
|
+
"@types/pg": "^8.11.0",
|
|
48
|
+
"tsx": "^4.19.0",
|
|
49
|
+
"typescript": "^5.7.0"
|
|
50
|
+
}
|
|
51
|
+
}
|