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.
@@ -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,6 @@
1
+ /**
2
+ * Tool: search_column
3
+ * Find tables containing a column name pattern
4
+ */
5
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
6
+ export declare function registerSearchColumnTool(server: McpServer): void;
@@ -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
+ }