#
tokens: 3282/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── bun.lock
├── Dockerfile
├── index.ts
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── tsconfig.base.json
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | node_modules
2 | dist
3 | 
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | # PostgreSQL Multi-Schema MCP Server
 2 | 
 3 | A Model Context Protocol server that provides read-only access to PostgreSQL databases with enhanced multi-schema support. This server enables LLMs to inspect database schemas across multiple namespaces and execute read-only queries while maintaining schema isolation.
 4 | 
 5 | ## Key Features
 6 | 
 7 | - **Multi-Schema Support**: Explicitly specify which schemas to expose through command-line configuration
 8 | - **Schema Isolation**: Strict access control to only authorized schemas listed during server startup
 9 | - **Cross-Schema Discovery**: Unified view of tables across multiple schemas while maintaining schema boundaries
10 | - **Metadata Security**: Filters system catalogs to only expose user-defined tables in specified schemas
11 | 
12 | ## Components
13 | 
14 | ### Tools
15 | 
16 | - **query**
17 |   - Execute read-only SQL queries against the connected database
18 |   - Input: `sql` (string): The SQL query to execute
19 |   - All queries are executed within a READ ONLY transaction
20 |   - Schema context maintained through search_path restriction
21 | 
22 | ### Resources
23 | 
24 | The server provides schema information for each table across authorized schemas:
25 | 
26 | - **Table Schemas** (`postgres://<host>/<db_schema>/<table>/schema`)
27 |   - JSON schema information for each table
28 |   - Includes column names, data types, and type modifiers
29 |   - Automatically discovered from database metadata
30 |   - Multi-schema support with explicit schema allow-list
31 | 
32 | ## Usage
33 | 
34 | The server requires a database URL and accepts a comma-separated list of schemas to expose:
35 | 
36 | ```
37 | npx -y mcp-server-postgres-multi-schema <database-url> [schemas]
38 | ```
39 | 
40 | - **database-url**: PostgreSQL connection string (e.g., `postgresql://localhost/mydb`)
41 | - **schemas**: Comma-separated list of schemas to expose (defaults to 'public' if not specified)
42 | 
43 | ### Examples
44 | 
45 | ```bash
46 | # Connect with default public schema
47 | npx -y mcp-server-postgres-multi-schema postgresql://localhost/mydb
48 | 
49 | # Connect with multiple schemas
50 | npx -y mcp-server-postgres-multi-schema postgresql://localhost/mydb public,analytics,staging
51 | ```
52 | 
53 | ## Usage with Claude Desktop
54 | 
55 | Configure the "mcpServers" section in your `claude_desktop_config.json`:
56 | 
57 | ### NPX
58 | 
59 | ```json
60 | {
61 |   "mcpServers": {
62 |     "postgres": {
63 |       "command": "npx",
64 |       "args": [
65 |         "-y",
66 |         "mcp-server-postgres-multi-schema",
67 |         "postgresql://localhost/mydb",
68 |         "public,audit"
69 |       ]
70 |     }
71 |   }
72 | }
73 | ```
74 | 
75 | ## License
76 | 
77 | This multi-schema MCP server is licensed under the MIT License. You may use, modify, and distribute the software according to the terms in the LICENSE file.
78 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "extends": "./tsconfig.base.json",
 3 |   "compilerOptions": {
 4 |     "outDir": "./dist",
 5 |     "rootDir": "."
 6 |   },
 7 |   "include": [
 8 |     "./**/*.ts"
 9 |   ]
10 | }
```

--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |     "compilerOptions": {
 3 |         "target": "ES2022",
 4 |         "module": "Node16",
 5 |         "moduleResolution": "Node16",
 6 |         "strict": true,
 7 |         "esModuleInterop": true,
 8 |         "skipLibCheck": true,
 9 |         "forceConsistentCasingInFileNames": true,
10 |         "resolveJsonModule": true
11 |     },
12 |     "include": [
13 |         "src/**/*"
14 |     ],
15 |     "exclude": [
16 |         "node_modules"
17 |     ]
18 | }
```

--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------

```dockerfile
 1 | FROM node:22.12-alpine AS builder
 2 | 
 3 | COPY src/postgres /app
 4 | COPY tsconfig.json /tsconfig.json
 5 | 
 6 | WORKDIR /app
 7 | 
 8 | RUN --mount=type=cache,target=/root/.npm npm install
 9 | 
10 | RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev
11 | 
12 | FROM node:22-alpine AS release
13 | 
14 | COPY --from=builder /app/dist /app/dist
15 | COPY --from=builder /app/package.json /app/package.json
16 | COPY --from=builder /app/package-lock.json /app/package-lock.json
17 | 
18 | ENV NODE_ENV=production
19 | 
20 | WORKDIR /app
21 | 
22 | RUN npm ci --ignore-scripts --omit-dev
23 | 
24 | ENTRYPOINT ["node", "dist/index.js"]
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "mcp-server-postgres-multi-schema",
 3 |   "version": "0.1.3",
 4 |   "description": "MCP server for interacting with PostgreSQL databases, with support for multiple schemas",
 5 |   "license": "MIT",
 6 |   "author": "Harjot Singh Rana",
 7 |   "homepage": "https://github.com/HarjjotSinghh/mcp-server-postgres-multi-schema",
 8 |   "bugs": "https://github.com/HarjjotSinghh/mcp-server-postgres-multi-schema/issues",
 9 |   "repository": "https://github.com/HarjjotSinghh/mcp-server-postgres-multi-schema",
10 |   "publishConfig": {
11 |     "registry": "https://npm.pkg.github.com"
12 |   },
13 |   "type": "module",
14 |   "bin": {
15 |     "mcp-server-postgres-multi-schema": "dist/index.js"
16 |   },
17 |   "files": [
18 |     "dist"
19 |   ],
20 |   "scripts": {
21 |     "build": "tsc && shx chmod +x dist/*.js",
22 |     "prepare": "npm run build",
23 |     "watch": "tsc --watch"
24 |   },
25 |   "dependencies": {
26 |     "@modelcontextprotocol/sdk": "1.0.1",
27 |     "pg": "^8.13.0"
28 |   },
29 |   "devDependencies": {
30 |     "@types/pg": "^8.11.10",
31 |     "shx": "^0.3.4",
32 |     "typescript": "^5.6.2"
33 |   }
34 | }
```

--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListResourcesRequestSchema,
  8 |   ListToolsRequestSchema,
  9 |   ReadResourceRequestSchema,
 10 | } from "@modelcontextprotocol/sdk/types.js";
 11 | import pg from "pg";
 12 | 
 13 | const server = new Server(
 14 |   {
 15 |     name: "mcp-server-postgres-multi-schema",
 16 |     version: "0.1.0",
 17 |   },
 18 |   {
 19 |     capabilities: {
 20 |       resources: {},
 21 |       tools: {},
 22 |     },
 23 |   },
 24 | );
 25 | 
 26 | const args = process.argv.slice(2);
 27 | if (args.length === 0) {
 28 |   console.error("Please provide a database URL as a command-line argument");
 29 |   console.error("Usage: npx -y mcp-server-postgres-multi-schema <database-url> [schemas]");
 30 |   console.error("Example: npx -y mcp-server-postgres-multi-schema postgresql://localhost/mydb \"public,custom_schema\"");
 31 |   process.exit(1);
 32 | }
 33 | 
 34 | const databaseUrl = args[0];
 35 | // Parse schemas from the second argument or default to 'public'
 36 | const schemas = args[1]
 37 |   ? args[1].split(',').map(schema => schema.trim())
 38 |   : ['public'];
 39 | 
 40 | console.log(`Connecting to database: ${databaseUrl}`);
 41 | console.log(`Using schemas: ${schemas.join(', ')}`);
 42 | 
 43 | const resourceBaseUrl = new URL(databaseUrl);
 44 | resourceBaseUrl.protocol = "postgres:";
 45 | resourceBaseUrl.password = "";
 46 | 
 47 | const pool = new pg.Pool({
 48 |   connectionString: databaseUrl,
 49 | });
 50 | 
 51 | const SCHEMA_PATH = "schema";
 52 | 
 53 | server.setRequestHandler(ListResourcesRequestSchema, async () => {
 54 |   const client = await pool.connect();
 55 |   try {
 56 |     // Build a parameterized query for multiple schemas
 57 |     const placeholders = schemas.map((_, i) => `$${i + 1}`).join(', ');
 58 |     const query = `
 59 |       SELECT table_schema, table_name 
 60 |       FROM information_schema.tables 
 61 |       WHERE table_schema IN (${placeholders})
 62 |       ORDER BY table_schema, table_name
 63 |     `;
 64 | 
 65 |     const result = await client.query(query, schemas);
 66 | 
 67 |     return {
 68 |       resources: result.rows.map((row) => ({
 69 |         uri: new URL(`${row.table_schema}/${row.table_name}/${SCHEMA_PATH}`, resourceBaseUrl).href,
 70 |         mimeType: "application/json",
 71 |         name: `"${row.table_name}" table in "${row.table_schema}" schema`,
 72 |       })),
 73 |     };
 74 |   } finally {
 75 |     client.release();
 76 |   }
 77 | });
 78 | 
 79 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
 80 |   const resourceUrl = new URL(request.params.uri);
 81 | 
 82 |   const pathComponents = resourceUrl.pathname.split("/");
 83 |   const schemaPath = pathComponents.pop();
 84 |   const tableName = pathComponents.pop();
 85 |   const dbSchema = pathComponents.pop();
 86 | 
 87 |   if (schemaPath !== SCHEMA_PATH) {
 88 |     throw new Error("Invalid resource URI");
 89 |   }
 90 | 
 91 |   if (!dbSchema) {
 92 |     throw new Error("Schema is required");
 93 |   }
 94 | 
 95 |   if (!schemas.includes(dbSchema)) {
 96 |     throw new Error(`Schema "${dbSchema}" is not in the allowed schemas list`);
 97 |   }
 98 | 
 99 |   const client = await pool.connect();
100 |   try {
101 |     const result = await client.query(
102 |       "SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2",
103 |       [dbSchema, tableName],
104 |     );
105 | 
106 |     return {
107 |       contents: [
108 |         {
109 |           uri: request.params.uri,
110 |           mimeType: "application/json",
111 |           text: JSON.stringify(result.rows, null, 2),
112 |         },
113 |       ],
114 |     };
115 |   } finally {
116 |     client.release();
117 |   }
118 | });
119 | 
120 | server.setRequestHandler(ListToolsRequestSchema, async () => {
121 |   return {
122 |     tools: [
123 |       {
124 |         name: "query",
125 |         description: "Run a read-only SQL query",
126 |         inputSchema: {
127 |           type: "object",
128 |           properties: {
129 |             sql: { type: "string" },
130 |           },
131 |         },
132 |       },
133 |     ],
134 |   };
135 | });
136 | 
137 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
138 |   if (request.params.name === "query") {
139 |     // Ensure sql is provided
140 |     if (!request.params.arguments || typeof request.params.arguments.sql !== 'string') {
141 |       throw new Error("SQL query is required and must be a string");
142 |     }
143 | 
144 |     const sql = request.params.arguments.sql;
145 | 
146 |     const client = await pool.connect();
147 |     try {
148 |       await client.query("BEGIN TRANSACTION READ ONLY");
149 |       const result = await client.query(sql);
150 |       return {
151 |         content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
152 |         isError: false,
153 |       };
154 |     } catch (error) {
155 |       throw error;
156 |     } finally {
157 |       client
158 |         .query("ROLLBACK")
159 |         .catch((error) =>
160 |           console.warn("Could not roll back transaction:", error),
161 |         );
162 | 
163 |       client.release();
164 |     }
165 |   }
166 |   throw new Error(`Unknown tool: ${request.params.name}`);
167 | });
168 | 
169 | async function runServer() {
170 |   const transport = new StdioServerTransport();
171 |   await server.connect(transport);
172 | }
173 | 
174 | runServer().catch(console.error);
175 | 
```