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

```
├── .gitignore
├── LICENSE
├── package-lock.json
├── package.json
├── README.md
├── smithery.yaml
├── src
│   └── index.ts
└── tsconfig.json
```

# Files

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

```
1 | node_modules/
2 | build/
3 | .env
4 | *.log
5 | .DS_Store
6 | .idea/
7 | .vscode/
```

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

```markdown
  1 | # MCP PostgreSQL Server
  2 | 
  3 | A Model Context Protocol server that provides PostgreSQL database operations. This server enables AI models to interact with PostgreSQL databases through a standardized interface.
  4 | 
  5 | ## Installation
  6 | 
  7 | ### Manual Installation
  8 | 
  9 | ```bash
 10 | npm install mcp-postgres-server
 11 | ```
 12 | 
 13 | Or run directly with:
 14 | 
 15 | ```bash
 16 | npx mcp-postgres-server
 17 | ```
 18 | 
 19 | ## Configuration
 20 | 
 21 | The server requires the following environment variables:
 22 | 
 23 | ```json
 24 | {
 25 |   "mcpServers": {
 26 |     "postgres": {
 27 |       "type": "stdio",
 28 |       "command": "npx",
 29 |       "args": ["-y", "mcp-postgres-server"],
 30 |       "env": {
 31 |         "PG_HOST": "your_host",
 32 |         "PG_PORT": "5432",
 33 |         "PG_USER": "your_user",
 34 |         "PG_PASSWORD": "your_password",
 35 |         "PG_DATABASE": "your_database"
 36 |       }
 37 |     }
 38 |   }
 39 | }
 40 | ```
 41 | 
 42 | ## Available Tools
 43 | 
 44 | ### 1. connect_db
 45 | 
 46 | Establish connection to PostgreSQL database using provided credentials.
 47 | 
 48 | ```javascript
 49 | use_mcp_tool({
 50 |   server_name: "postgres",
 51 |   tool_name: "connect_db",
 52 |   arguments: {
 53 |     host: "localhost",
 54 |     port: 5432,
 55 |     user: "your_user",
 56 |     password: "your_password",
 57 |     database: "your_database"
 58 |   }
 59 | });
 60 | ```
 61 | 
 62 | ### 2. query
 63 | 
 64 | Execute SELECT queries with optional prepared statement parameters. Supports both PostgreSQL-style ($1, $2) and MySQL-style (?) parameter placeholders.
 65 | 
 66 | ```javascript
 67 | use_mcp_tool({
 68 |   server_name: "postgres",
 69 |   tool_name: "query",
 70 |   arguments: {
 71 |     sql: "SELECT * FROM users WHERE id = $1",
 72 |     params: [1]
 73 |   }
 74 | });
 75 | ```
 76 | 
 77 | ### 3. execute
 78 | 
 79 | Execute INSERT, UPDATE, or DELETE queries with optional prepared statement parameters. Supports both PostgreSQL-style ($1, $2) and MySQL-style (?) parameter placeholders.
 80 | 
 81 | ```javascript
 82 | use_mcp_tool({
 83 |   server_name: "postgres",
 84 |   tool_name: "execute",
 85 |   arguments: {
 86 |     sql: "INSERT INTO users (name, email) VALUES ($1, $2)",
 87 |     params: ["John Doe", "[email protected]"]
 88 |   }
 89 | });
 90 | ```
 91 | 
 92 | ### 4. list_schemas
 93 | 
 94 | List all schemas in the connected database.
 95 | 
 96 | ```javascript
 97 | use_mcp_tool({
 98 |   server_name: "postgres",
 99 |   tool_name: "list_schemas",
100 |   arguments: {}
101 | });
102 | ```
103 | 
104 | ### 5. list_tables
105 | 
106 | List tables in the connected database. Accepts an optional schema parameter (defaults to 'public').
107 | 
108 | ```javascript
109 | // List tables in the 'public' schema (default)
110 | use_mcp_tool({
111 |   server_name: "postgres",
112 |   tool_name: "list_tables",
113 |   arguments: {}
114 | });
115 | 
116 | // List tables in a specific schema
117 | use_mcp_tool({
118 |   server_name: "postgres",
119 |   tool_name: "list_tables",
120 |   arguments: {
121 |     schema: "my_schema"
122 |   }
123 | });
124 | ```
125 | 
126 | ### 6. describe_table
127 | 
128 | Get the structure of a specific table. Accepts an optional schema parameter (defaults to 'public').
129 | 
130 | ```javascript
131 | // Describe a table in the 'public' schema (default)
132 | use_mcp_tool({
133 |   server_name: "postgres",
134 |   tool_name: "describe_table",
135 |   arguments: {
136 |     table: "users"
137 |   }
138 | });
139 | 
140 | // Describe a table in a specific schema
141 | use_mcp_tool({
142 |   server_name: "postgres",
143 |   tool_name: "describe_table",
144 |   arguments: {
145 |     table: "users",
146 |     schema: "my_schema"
147 |   }
148 | });
149 | ```
150 | 
151 | ## Features
152 | 
153 | * Secure connection handling with automatic cleanup
154 | * Prepared statement support for query parameters
155 | * Support for both PostgreSQL-style ($1, $2) and MySQL-style (?) parameter placeholders
156 | * Comprehensive error handling and validation
157 | * TypeScript support
158 | * Automatic connection management
159 | * Supports PostgreSQL-specific syntax and features
160 | * Multi-schema support for database operations
161 | 
162 | ## Security
163 | 
164 | * Uses prepared statements to prevent SQL injection
165 | * Supports secure password handling through environment variables
166 | * Validates queries before execution
167 | * Automatically closes connections when done
168 | 
169 | ## Error Handling
170 | 
171 | The server provides detailed error messages for common issues:
172 | 
173 | * Connection failures
174 | * Invalid queries
175 | * Missing parameters
176 | * Database errors
177 | 
178 | ## License
179 | 
180 | MIT
```

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

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

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

```json
 1 | {
 2 |   "name": "mcp-postgres-server",
 3 |   "version": "0.1.3",
 4 |   "description": "A Model Context Protocol server for PostgreSQL database operations",
 5 |   "type": "module",
 6 |   "bin": {
 7 |     "mcp-postgres": "./build/index.js"
 8 |   },
 9 |   "files": [
10 |     "build"
11 |   ],
12 |   "scripts": {
13 |     "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
14 |     "prepare": "npm run build",
15 |     "watch": "tsc --watch",
16 |     "inspector": "npx @modelcontextprotocol/inspector build/index.js"
17 |   },
18 |   "keywords": [
19 |     "mcp",
20 |     "model-context-protocol",
21 |     "postgres",
22 |     "postgresql",
23 |     "database",
24 |     "claude",
25 |     "anthropic"
26 |   ],
27 |   "author": "",
28 |   "license": "MIT",
29 |   "dependencies": {
30 |     "@modelcontextprotocol/sdk": "0.6.0",
31 |     "dotenv": "^16.4.7",
32 |     "pg": "^8.11.3"
33 |   },
34 |   "devDependencies": {
35 |     "@types/node": "^20.11.24",
36 |     "@types/pg": "^8.10.7",
37 |     "typescript": "^5.3.3"
38 |   },
39 |   "repository": {
40 |     "type": "git",
41 |     "url": "https://github.com/antonorlov/mcp-postgres-server.git"
42 |   },
43 |   "publishConfig": {
44 |     "access": "public"
45 |   }
46 | }
```

--------------------------------------------------------------------------------
/smithery.yaml:
--------------------------------------------------------------------------------

```yaml
 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
 2 | 
 3 | startCommand:
 4 |   type: stdio
 5 |   configSchema:
 6 |     # JSON Schema defining the configuration options for the MCP.
 7 |     type: object
 8 |     required:
 9 |       - pgHost
10 |       - pgUser
11 |       - pgPassword
12 |       - pgDatabase
13 |     properties:
14 |       pgHost:
15 |         type: string
16 |         description: Hostname for the PostgreSQL server
17 |       pgPort:
18 |         type: number
19 |         description: Port for the PostgreSQL server
20 |         default: 5432
21 |       pgUser:
22 |         type: string
23 |         description: PostgreSQL user name
24 |       pgPassword:
25 |         type: string
26 |         description: PostgreSQL user's password
27 |       pgDatabase:
28 |         type: string
29 |         description: PostgreSQL database name
30 |   commandFunction:
31 |     # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
32 |     |-
33 |     (config) => ({
34 |       command: 'node',
35 |       args: ['build/index.js'],
36 |       env: {
37 |         PG_HOST: config.pgHost,
38 |         PG_PORT: config.pgPort || 5432,
39 |         PG_USER: config.pgUser,
40 |         PG_PASSWORD: config.pgPassword,
41 |         PG_DATABASE: config.pgDatabase
42 |       }
43 |     })
44 |   exampleConfig:
45 |     pgHost: localhost
46 |     pgPort: 5432
47 |     pgUser: example_user
48 |     pgPassword: example_password
49 |     pgDatabase: example_db
```

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

```typescript
  1 | #!/usr/bin/env node
  2 | import { Server } from '@modelcontextprotocol/sdk/server/index.js';
  3 | import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
  4 | import {
  5 |   CallToolRequestSchema,
  6 |   ErrorCode,
  7 |   ListToolsRequestSchema,
  8 |   McpError,
  9 | } from '@modelcontextprotocol/sdk/types.js';
 10 | import pg from 'pg';
 11 | const { Client } = pg;
 12 | import { config } from 'dotenv';
 13 | 
 14 | // Load environment variables
 15 | config();
 16 | 
 17 | interface DatabaseConfig {
 18 |   host: string;
 19 |   port: number;
 20 |   user: string;
 21 |   password: string;
 22 |   database: string;
 23 | }
 24 | 
 25 | // Type guard for error objects
 26 | function isErrorWithMessage(error: unknown): error is { message: string } {
 27 |   return (
 28 |     typeof error === 'object' &&
 29 |     error !== null &&
 30 |     'message' in error &&
 31 |     typeof (error as Record<string, unknown>).message === 'string'
 32 |   );
 33 | }
 34 | 
 35 | // Helper to get error message
 36 | function getErrorMessage(error: unknown): string {
 37 |   if (isErrorWithMessage(error)) {
 38 |     return error.message;
 39 |   }
 40 |   return String(error);
 41 | }
 42 | 
 43 | // Helper to convert ? parameters to $1, $2, etc.
 44 | function convertToNamedParams(query: string): string {
 45 |   let paramIndex = 0;
 46 |   return query.replace(/\?/g, () => `$${++paramIndex}`);
 47 | }
 48 | 
 49 | class PostgresServer {
 50 |   private server: Server;
 51 |   private client: pg.Client | null = null;
 52 |   private config: DatabaseConfig | null = null;
 53 | 
 54 |   constructor() {
 55 |     this.server = new Server(
 56 |       {
 57 |         name: 'postgres-server',
 58 |         version: '1.0.0',
 59 |       },
 60 |       {
 61 |         capabilities: {
 62 |           tools: {},
 63 |         },
 64 |       }
 65 |     );
 66 | 
 67 |     this.setupToolHandlers();
 68 |     
 69 |     // Error handling
 70 |     this.server.onerror = (error) => console.error('[MCP Error]', error);
 71 |     const handleTermination = async () => {
 72 |       try {
 73 |         await this.cleanup();
 74 |       } catch (error) {
 75 |         console.error('Error during cleanup:', error);
 76 |         process.exit(1);
 77 |       }
 78 |       process.exit(0);
 79 |     };
 80 |     process.on('SIGINT', handleTermination);
 81 |     process.stdin.on('close', handleTermination);
 82 |   }
 83 | 
 84 |   private async cleanup() {
 85 |     if (this.client) {
 86 |       await this.client.end();
 87 |     }
 88 |     await this.server.close();
 89 |   }
 90 | 
 91 |   private async ensureConnection() {
 92 |     if (!this.config) {
 93 |       // Try to use environment variables if no explicit config was provided
 94 |       const envConfig = this.getEnvConfig();
 95 |       
 96 |       if (envConfig) {
 97 |         this.config = envConfig;
 98 |         console.error('[MCP Info] Using database config from environment variables');
 99 |       } else {
100 |         throw new McpError(
101 |           ErrorCode.InvalidRequest,
102 |           'Database configuration not set. Use connect_db tool first or set environment variables.'
103 |         );
104 |       }
105 |     }
106 | 
107 |     if (!this.client) {
108 |       try {
109 |         this.client = new Client(this.config);
110 |         await this.client.connect();
111 |       } catch (error) {
112 |         throw new McpError(
113 |           ErrorCode.InternalError,
114 |           `Failed to connect to database: ${getErrorMessage(error)}`
115 |         );
116 |       }
117 |     }
118 |   }
119 |   
120 |   private getEnvConfig(): DatabaseConfig | null {
121 |     const { PG_HOST, PG_USER, PG_PASSWORD, PG_DATABASE, PG_PORT } = process.env;
122 |     
123 |     if (PG_HOST && PG_USER && PG_PASSWORD && PG_DATABASE) {
124 |       return {
125 |         host: PG_HOST,
126 |         port: PG_PORT ? parseInt(PG_PORT, 10) : 5432,
127 |         user: PG_USER,
128 |         password: PG_PASSWORD,
129 |         database: PG_DATABASE
130 |       };
131 |     }
132 |     
133 |     return null;
134 |   }
135 | 
136 |   private setupToolHandlers() {
137 |     this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
138 |       tools: [
139 |         {
140 |           name: 'connect_db',
141 |           description: 'Connect to PostgreSQL database. NOTE: Default connection exists - only use when requested or if other commands fail',
142 |           inputSchema: {
143 |             type: 'object',
144 |             properties: {
145 |               host: {
146 |                 type: 'string',
147 |                 description: 'Database host',
148 |               },
149 |               port: {
150 |                 type: 'number',
151 |                 description: 'Database port (default: 5432)',
152 |               },
153 |               user: {
154 |                 type: 'string',
155 |                 description: 'Database user',
156 |               },
157 |               password: {
158 |                 type: 'string',
159 |                 description: 'Database password',
160 |               },
161 |               database: {
162 |                 type: 'string',
163 |                 description: 'Database name',
164 |               },
165 |             },
166 |             required: ['host', 'user', 'password', 'database'],
167 |           },
168 |         },
169 |         {
170 |           name: 'query',
171 |           description: 'Execute a SELECT query',
172 |           inputSchema: {
173 |             type: 'object',
174 |             properties: {
175 |               sql: {
176 |                 type: 'string',
177 |                 description: 'SQL SELECT query (use $1, $2, etc. for parameters)',
178 |               },
179 |               params: {
180 |                 type: 'array',
181 |                 items: {
182 |                   type: ['string', 'number', 'boolean', 'null'],
183 |                 },
184 |                 description: 'Query parameters (optional)',
185 |               },
186 |             },
187 |             required: ['sql'],
188 |           },
189 |         },
190 |         {
191 |           name: 'execute',
192 |           description: 'Execute an INSERT, UPDATE, or DELETE query',
193 |           inputSchema: {
194 |             type: 'object',
195 |             properties: {
196 |               sql: {
197 |                 type: 'string',
198 |                 description: 'SQL query (INSERT, UPDATE, DELETE) (use $1, $2, etc. for parameters)',
199 |               },
200 |               params: {
201 |                 type: 'array',
202 |                 items: {
203 |                   type: ['string', 'number', 'boolean', 'null'],
204 |                 },
205 |                 description: 'Query parameters (optional)',
206 |               },
207 |             },
208 |             required: ['sql'],
209 |           },
210 |         },
211 |         {
212 |           name: 'list_schemas',
213 |           description: 'List all schemas in the database',
214 |           inputSchema: {
215 |             type: 'object',
216 |             properties: {},
217 |             required: [],
218 |           },
219 |         },
220 |         {
221 |           name: 'list_tables',
222 |           description: 'List tables in the database',
223 |           inputSchema: {
224 |             type: 'object',
225 |             properties: {
226 |               schema: {
227 |                 type: 'string',
228 |                 description: 'Schema name (default: public)',
229 |               },
230 |             },
231 |             required: [],
232 |           },
233 |         },
234 |         {
235 |           name: 'describe_table',
236 |           description: 'Get table structure',
237 |           inputSchema: {
238 |             type: 'object',
239 |             properties: {
240 |               table: {
241 |                 type: 'string',
242 |                 description: 'Table name',
243 |               },
244 |               schema: {
245 |                 type: 'string',
246 |                 description: 'Schema name (default: public)',
247 |               },
248 |             },
249 |             required: ['table'],
250 |           },
251 |         },
252 |       ],
253 |     }));
254 |   
255 |     this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
256 |       switch (request.params.name) {
257 |         case 'connect_db':
258 |           return await this.handleConnectDb(request.params.arguments);
259 |         case 'query':
260 |           return await this.handleQuery(request.params.arguments);
261 |         case 'execute':
262 |           return await this.handleExecute(request.params.arguments);
263 |         case 'list_schemas':
264 |           return await this.handleListSchemas();
265 |         case 'list_tables':
266 |           return await this.handleListTables(request.params.arguments);
267 |         case 'describe_table':
268 |           return await this.handleDescribeTable(request.params.arguments);
269 |         default:
270 |           throw new McpError(
271 |             ErrorCode.MethodNotFound,
272 |             `Unknown tool: ${request.params.name}`
273 |           );
274 |       }
275 |     });
276 |   }
277 | 
278 |   private async handleConnectDb(args: any) {
279 |     if (!args.host || !args.user || !args.password || !args.database) {
280 |       throw new McpError(
281 |         ErrorCode.InvalidParams,
282 |         'Missing required database configuration parameters'
283 |       );
284 |     }
285 | 
286 |     // Close existing connection if any
287 |     if (this.client) {
288 |       await this.client.end();
289 |       this.client = null;
290 |     }
291 | 
292 |     this.config = {
293 |       host: args.host,
294 |       port: args.port || 5432,
295 |       user: args.user,
296 |       password: args.password,
297 |       database: args.database,
298 |     };
299 | 
300 |     try {
301 |       await this.ensureConnection();
302 |       return {
303 |         content: [
304 |           {
305 |             type: 'text',
306 |             text: 'Successfully connected to PostgreSQL database',
307 |           },
308 |         ],
309 |       };
310 |     } catch (error) {
311 |       throw new McpError(
312 |         ErrorCode.InternalError,
313 |         `Failed to connect to database: ${getErrorMessage(error)}`
314 |       );
315 |     }
316 |   }
317 | 
318 |   private async handleQuery(args: any) {
319 |     await this.ensureConnection();
320 | 
321 |     if (!args.sql) {
322 |       throw new McpError(ErrorCode.InvalidParams, 'SQL query is required');
323 |     }
324 | 
325 |     if (!args.sql.trim().toUpperCase().startsWith('SELECT')) {
326 |       throw new McpError(
327 |         ErrorCode.InvalidParams,
328 |         'Only SELECT queries are allowed with query tool'
329 |       );
330 |     }
331 | 
332 |     try {
333 |       // Convert ? parameters to $1, $2, etc. if needed
334 |       const sql = args.sql.includes('?') ? convertToNamedParams(args.sql) : args.sql;
335 |       const result = await this.client!.query(sql, args.params || []);
336 |       
337 |       return {
338 |         content: [
339 |           {
340 |             type: 'text',
341 |             text: JSON.stringify(result.rows, null, 2),
342 |           },
343 |         ],
344 |       };
345 |     } catch (error) {
346 |       throw new McpError(
347 |         ErrorCode.InternalError,
348 |         `Query execution failed: ${getErrorMessage(error)}`
349 |       );
350 |     }
351 |   }
352 | 
353 |   private async handleExecute(args: any) {
354 |     await this.ensureConnection();
355 | 
356 |     if (!args.sql) {
357 |       throw new McpError(ErrorCode.InvalidParams, 'SQL query is required');
358 |     }
359 | 
360 |     const sql = args.sql.trim().toUpperCase();
361 |     if (sql.startsWith('SELECT')) {
362 |       throw new McpError(
363 |         ErrorCode.InvalidParams,
364 |         'Use query tool for SELECT statements'
365 |       );
366 |     }
367 | 
368 |     try {
369 |       // Convert ? parameters to $1, $2, etc. if needed
370 |       const preparedSql = args.sql.includes('?') ? convertToNamedParams(args.sql) : args.sql;
371 |       const result = await this.client!.query(preparedSql, args.params || []);
372 |       
373 |       return {
374 |         content: [
375 |           {
376 |             type: 'text',
377 |             text: JSON.stringify({
378 |               rowCount: result.rowCount,
379 |               command: result.command,
380 |             }, null, 2),
381 |           },
382 |         ],
383 |       };
384 |     } catch (error) {
385 |       throw new McpError(
386 |         ErrorCode.InternalError,
387 |         `Query execution failed: ${getErrorMessage(error)}`
388 |       );
389 |     }
390 |   }
391 | 
392 |   private async handleListSchemas() {
393 |     await this.ensureConnection();
394 |   
395 |     try {
396 |       const result = await this.client!.query(`
397 |         SELECT schema_name
398 |         FROM information_schema.schemata
399 |         ORDER BY schema_name
400 |       `);
401 |       
402 |       return {
403 |         content: [
404 |           {
405 |             type: 'text',
406 |             text: JSON.stringify(result.rows, null, 2),
407 |           },
408 |         ],
409 |       };
410 |     } catch (error) {
411 |       throw new McpError(
412 |         ErrorCode.InternalError,
413 |         `Failed to list schemas: ${getErrorMessage(error)}`
414 |       );
415 |     }
416 |   }
417 | 
418 |   private async handleListTables(args: any = {}) {
419 |     await this.ensureConnection();
420 |   
421 |     const schema = args.schema || 'public';
422 |   
423 |     try {
424 |       const result = await this.client!.query(`
425 |         SELECT table_name
426 |         FROM information_schema.tables
427 |         WHERE table_schema = $1
428 |         ORDER BY table_name
429 |       `, [schema]);
430 |       
431 |       return {
432 |         content: [
433 |           {
434 |             type: 'text',
435 |             text: JSON.stringify(result.rows, null, 2),
436 |           },
437 |         ],
438 |       };
439 |     } catch (error) {
440 |       throw new McpError(
441 |         ErrorCode.InternalError,
442 |         `Failed to list tables: ${getErrorMessage(error)}`
443 |       );
444 |     }
445 |   }
446 | 
447 |   private async handleDescribeTable(args: any) {
448 |     await this.ensureConnection();
449 |   
450 |     if (!args.table) {
451 |       throw new McpError(ErrorCode.InvalidParams, 'Table name is required');
452 |     }
453 |   
454 |     const schema = args.schema || 'public';
455 |   
456 |     try {
457 |       const result = await this.client!.query(`
458 |         SELECT 
459 |           c.column_name, 
460 |           c.data_type, 
461 |           c.is_nullable, 
462 |           c.column_default,
463 |           CASE 
464 |             WHEN pk.constraint_type = 'PRIMARY KEY' THEN true 
465 |             ELSE false 
466 |           END AS is_primary_key,
467 |           c.character_maximum_length
468 |         FROM 
469 |           information_schema.columns c
470 |         LEFT JOIN (
471 |           SELECT 
472 |             tc.constraint_type, 
473 |             kcu.column_name, 
474 |             kcu.table_name,
475 |             kcu.table_schema
476 |           FROM 
477 |             information_schema.table_constraints tc
478 |           JOIN 
479 |             information_schema.key_column_usage kcu
480 |           ON 
481 |             tc.constraint_name = kcu.constraint_name
482 |           WHERE 
483 |             tc.constraint_type = 'PRIMARY KEY'
484 |         ) pk
485 |         ON 
486 |           c.column_name = pk.column_name
487 |           AND c.table_name = pk.table_name
488 |           AND c.table_schema = pk.table_schema
489 |         WHERE 
490 |           c.table_schema = $1 
491 |           AND c.table_name = $2
492 |         ORDER BY 
493 |           c.ordinal_position
494 |       `, [schema, args.table]);
495 |       
496 |       return {
497 |         content: [
498 |           {
499 |             type: 'text',
500 |             text: JSON.stringify(result.rows, null, 2),
501 |           },
502 |         ],
503 |       };
504 |     } catch (error) {
505 |       throw new McpError(
506 |         ErrorCode.InternalError,
507 |         `Failed to describe table: ${getErrorMessage(error)}`
508 |       );
509 |     }
510 |   }
511 | 
512 |   async run() {
513 |     const transport = new StdioServerTransport();
514 |     await this.server.connect(transport);
515 |     console.error('PostgreSQL MCP server running on stdio');
516 |   }
517 | }
518 | 
519 | const server = new PostgresServer();
520 | server.run().catch(console.error);
```