#
tokens: 2389/50000 7/7 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

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

# Files

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

```
node_modules
dist

```

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

```markdown
# PostgreSQL Multi-Schema MCP Server

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.

## Key Features

- **Multi-Schema Support**: Explicitly specify which schemas to expose through command-line configuration
- **Schema Isolation**: Strict access control to only authorized schemas listed during server startup
- **Cross-Schema Discovery**: Unified view of tables across multiple schemas while maintaining schema boundaries
- **Metadata Security**: Filters system catalogs to only expose user-defined tables in specified schemas

## Components

### Tools

- **query**
  - Execute read-only SQL queries against the connected database
  - Input: `sql` (string): The SQL query to execute
  - All queries are executed within a READ ONLY transaction
  - Schema context maintained through search_path restriction

### Resources

The server provides schema information for each table across authorized schemas:

- **Table Schemas** (`postgres://<host>/<db_schema>/<table>/schema`)
  - JSON schema information for each table
  - Includes column names, data types, and type modifiers
  - Automatically discovered from database metadata
  - Multi-schema support with explicit schema allow-list

## Usage

The server requires a database URL and accepts a comma-separated list of schemas to expose:

```
npx -y mcp-server-postgres-multi-schema <database-url> [schemas]
```

- **database-url**: PostgreSQL connection string (e.g., `postgresql://localhost/mydb`)
- **schemas**: Comma-separated list of schemas to expose (defaults to 'public' if not specified)

### Examples

```bash
# Connect with default public schema
npx -y mcp-server-postgres-multi-schema postgresql://localhost/mydb

# Connect with multiple schemas
npx -y mcp-server-postgres-multi-schema postgresql://localhost/mydb public,analytics,staging
```

## Usage with Claude Desktop

Configure the "mcpServers" section in your `claude_desktop_config.json`:

### NPX

```json
{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-server-postgres-multi-schema",
        "postgresql://localhost/mydb",
        "public,audit"
      ]
    }
  }
}
```

## License

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.

```

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

```json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "."
  },
  "include": [
    "./**/*.ts"
  ]
}
```

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

```json
{
    "compilerOptions": {
        "target": "ES2022",
        "module": "Node16",
        "moduleResolution": "Node16",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "resolveJsonModule": true
    },
    "include": [
        "src/**/*"
    ],
    "exclude": [
        "node_modules"
    ]
}
```

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

```dockerfile
FROM node:22.12-alpine AS builder

COPY src/postgres /app
COPY tsconfig.json /tsconfig.json

WORKDIR /app

RUN --mount=type=cache,target=/root/.npm npm install

RUN --mount=type=cache,target=/root/.npm-production npm ci --ignore-scripts --omit-dev

FROM node:22-alpine AS release

COPY --from=builder /app/dist /app/dist
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/package-lock.json /app/package-lock.json

ENV NODE_ENV=production

WORKDIR /app

RUN npm ci --ignore-scripts --omit-dev

ENTRYPOINT ["node", "dist/index.js"]
```

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

```json
{
  "name": "mcp-server-postgres-multi-schema",
  "version": "0.1.3",
  "description": "MCP server for interacting with PostgreSQL databases, with support for multiple schemas",
  "license": "MIT",
  "author": "Harjot Singh Rana",
  "homepage": "https://github.com/HarjjotSinghh/mcp-server-postgres-multi-schema",
  "bugs": "https://github.com/HarjjotSinghh/mcp-server-postgres-multi-schema/issues",
  "repository": "https://github.com/HarjjotSinghh/mcp-server-postgres-multi-schema",
  "publishConfig": {
    "registry": "https://npm.pkg.github.com"
  },
  "type": "module",
  "bin": {
    "mcp-server-postgres-multi-schema": "dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "prepare": "npm run build",
    "watch": "tsc --watch"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.0.1",
    "pg": "^8.13.0"
  },
  "devDependencies": {
    "@types/pg": "^8.11.10",
    "shx": "^0.3.4",
    "typescript": "^5.6.2"
  }
}
```

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

```typescript
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ListToolsRequestSchema,
  ReadResourceRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import pg from "pg";

const server = new Server(
  {
    name: "mcp-server-postgres-multi-schema",
    version: "0.1.0",
  },
  {
    capabilities: {
      resources: {},
      tools: {},
    },
  },
);

const args = process.argv.slice(2);
if (args.length === 0) {
  console.error("Please provide a database URL as a command-line argument");
  console.error("Usage: npx -y mcp-server-postgres-multi-schema <database-url> [schemas]");
  console.error("Example: npx -y mcp-server-postgres-multi-schema postgresql://localhost/mydb \"public,custom_schema\"");
  process.exit(1);
}

const databaseUrl = args[0];
// Parse schemas from the second argument or default to 'public'
const schemas = args[1]
  ? args[1].split(',').map(schema => schema.trim())
  : ['public'];

console.log(`Connecting to database: ${databaseUrl}`);
console.log(`Using schemas: ${schemas.join(', ')}`);

const resourceBaseUrl = new URL(databaseUrl);
resourceBaseUrl.protocol = "postgres:";
resourceBaseUrl.password = "";

const pool = new pg.Pool({
  connectionString: databaseUrl,
});

const SCHEMA_PATH = "schema";

server.setRequestHandler(ListResourcesRequestSchema, async () => {
  const client = await pool.connect();
  try {
    // Build a parameterized query for multiple schemas
    const placeholders = schemas.map((_, i) => `$${i + 1}`).join(', ');
    const query = `
      SELECT table_schema, table_name 
      FROM information_schema.tables 
      WHERE table_schema IN (${placeholders})
      ORDER BY table_schema, table_name
    `;

    const result = await client.query(query, schemas);

    return {
      resources: result.rows.map((row) => ({
        uri: new URL(`${row.table_schema}/${row.table_name}/${SCHEMA_PATH}`, resourceBaseUrl).href,
        mimeType: "application/json",
        name: `"${row.table_name}" table in "${row.table_schema}" schema`,
      })),
    };
  } finally {
    client.release();
  }
});

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const resourceUrl = new URL(request.params.uri);

  const pathComponents = resourceUrl.pathname.split("/");
  const schemaPath = pathComponents.pop();
  const tableName = pathComponents.pop();
  const dbSchema = pathComponents.pop();

  if (schemaPath !== SCHEMA_PATH) {
    throw new Error("Invalid resource URI");
  }

  if (!dbSchema) {
    throw new Error("Schema is required");
  }

  if (!schemas.includes(dbSchema)) {
    throw new Error(`Schema "${dbSchema}" is not in the allowed schemas list`);
  }

  const client = await pool.connect();
  try {
    const result = await client.query(
      "SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2",
      [dbSchema, tableName],
    );

    return {
      contents: [
        {
          uri: request.params.uri,
          mimeType: "application/json",
          text: JSON.stringify(result.rows, null, 2),
        },
      ],
    };
  } finally {
    client.release();
  }
});

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "query",
        description: "Run a read-only SQL query",
        inputSchema: {
          type: "object",
          properties: {
            sql: { type: "string" },
          },
        },
      },
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "query") {
    // Ensure sql is provided
    if (!request.params.arguments || typeof request.params.arguments.sql !== 'string') {
      throw new Error("SQL query is required and must be a string");
    }

    const sql = request.params.arguments.sql;

    const client = await pool.connect();
    try {
      await client.query("BEGIN TRANSACTION READ ONLY");
      const result = await client.query(sql);
      return {
        content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
        isError: false,
      };
    } catch (error) {
      throw error;
    } finally {
      client
        .query("ROLLBACK")
        .catch((error) =>
          console.warn("Could not roll back transaction:", error),
        );

      client.release();
    }
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

async function runServer() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

runServer().catch(console.error);

```