This commit is contained in:
zpc 2025-12-28 19:39:50 +08:00
parent 81d95862d4
commit 481eb2de17
16 changed files with 534 additions and 180 deletions

117
CLAUDE.md
View File

@ -24,6 +24,7 @@ MCP Database Server is a WebSocket/SSE-based database tooling service that expos
4. **Concurrency Model**: Per-client session isolation with independent connection pools 4. **Concurrency Model**: Per-client session isolation with independent connection pools
5. **Code Separation**: Complete separation from original STDIO-based codebase; this is a standalone server implementation 5. **Code Separation**: Complete separation from original STDIO-based codebase; this is a standalone server implementation
6. **Database Driver Abstraction** (v1.0.2): DatabaseDriver interface allows pluggable database backends (PostgreSQL, SQL Server) 6. **Database Driver Abstraction** (v1.0.2): DatabaseDriver interface allows pluggable database backends (PostgreSQL, SQL Server)
7. **Server Mode** (v1.0.3): Single server-level configuration with dynamic database discovery, eliminating per-database environment configuration
## Build and Development Commands ## Build and Development Commands
@ -217,6 +218,36 @@ Configuration uses `config/database.json` (see `config/database.example.json` fo
} }
``` ```
**Server Mode Example (v1.0.3+):**
Server mode allows configuring a single database server connection and dynamically accessing any database on that server, eliminating the need to configure each database as a separate environment.
```json
{
"environments": {
"pg-server": {
"type": "postgres",
"serverMode": true,
"connection": {
"host": "47.99.124.43",
"port": 5432,
"user": "postgres",
"password": "ENV:MCP_PG_PASSWORD",
"ssl": { "require": true }
},
"defaultSchema": "dbo",
"pool": { "max": 100 },
"mode": "ddl"
}
}
}
```
Key differences in server mode:
- `serverMode: true` - Enables server-level configuration
- `database` field is **optional** - Not required in connection config
- All tools accept optional `database` parameter to specify target database dynamically
### Configuration Fields ### Configuration Fields
**server** - Global server settings: **server** - Global server settings:
@ -230,6 +261,9 @@ Configuration uses `config/database.json` (see `config/database.example.json` fo
**environments** - Database connection configurations: **environments** - Database connection configurations:
- Each environment is an isolated connection pool with unique name - Each environment is an isolated connection pool with unique name
- `type`: Database type (`postgres` | `sqlserver`) - `type`: Database type (`postgres` | `sqlserver`)
- `serverMode`: Enable server-level connection (v1.0.3+, default: false)
- When `true`, `database` field becomes optional
- Connection pools are created dynamically per `(envName, database)` pair
- For PostgreSQL: - For PostgreSQL:
- `connection.ssl`: SSL configuration (`{ require: true }` or `false`) - `connection.ssl`: SSL configuration (`{ require: true }` or `false`)
- `searchPath`: Array of schemas for PostgreSQL search_path - `searchPath`: Array of schemas for PostgreSQL search_path
@ -300,6 +334,70 @@ await client.callTool('pg_query', {
}); });
``` ```
### Server Mode and Dynamic Database Discovery (v1.0.3+)
Server mode eliminates the need to configure each database as a separate environment. Instead, configure a single server-level connection and dynamically specify the database at query time.
**Scenario 1: Discover all databases on server**
```typescript
// List all databases on the server
const databases = await client.callTool('pg_discover_databases', {
environment: 'pg-server'
});
// Returns: ["shcis_drworks_cpoe_pg", "shcis_ipworkstation", "postgres", ...]
```
**Scenario 2: Query specific database dynamically**
```typescript
// List tables in a specific database
await client.callTool('pg_list_tables', {
environment: 'pg-server',
database: 'shcis_drworks_cpoe_pg',
schema: 'dbo'
});
// Execute query in a specific database
await client.callTool('pg_query', {
environment: 'pg-server',
database: 'shcis_ipworkstation',
sql: 'SELECT * FROM users LIMIT 10'
});
```
**Scenario 3: Mix traditional and server mode environments**
```json
{
"environments": {
"prod-drworks": {
"type": "postgres",
"connection": {
"host": "prod-db.example.com",
"database": "shcis_drworks_cpoe_pg",
"user": "readonly_user",
"password": "ENV:PROD_PASSWORD"
},
"mode": "readonly"
},
"dev-server": {
"type": "postgres",
"serverMode": true,
"connection": {
"host": "dev-db.example.com",
"user": "admin",
"password": "ENV:DEV_PASSWORD"
},
"mode": "ddl"
}
}
}
```
**Connection Pool Behavior in Server Mode:**
- Pools are cached by composite key: `envName:database`
- First query to a database creates a new pool
- Subsequent queries to the same database reuse the cached pool
- When no database is specified, defaults to `postgres` (PostgreSQL) or `master` (SQL Server)
## Key Implementation Notes ## Key Implementation Notes
### Security and Authentication ### Security and Authentication
@ -467,6 +565,7 @@ The server exposes 30+ PostgreSQL tools grouped by category:
**Metadata Tools**: **Metadata Tools**:
- `pg_list_environments` - List configured environments - `pg_list_environments` - List configured environments
- `pg_discover_databases` - List all databases on the server (v1.0.3+, for serverMode environments)
- `pg_list_schemas` - List schemas in environment - `pg_list_schemas` - List schemas in environment
- `pg_list_tables` - List tables in schema - `pg_list_tables` - List tables in schema
- `pg_describe_table` - Get table structure (columns, types, constraints) - `pg_describe_table` - Get table structure (columns, types, constraints)
@ -496,7 +595,7 @@ The server exposes 30+ PostgreSQL tools grouped by category:
- `pg_analyze_query` - Analyze query performance - `pg_analyze_query` - Analyze query performance
- `pg_check_connection` - Verify database connectivity - `pg_check_connection` - Verify database connectivity
All tools require `environment` parameter to specify which database to use. All tools require `environment` parameter to specify which database to use. In server mode (v1.0.3+), tools also accept an optional `database` parameter to dynamically target a specific database.
## Version History and Roadmap ## Version History and Roadmap
@ -603,6 +702,18 @@ cat changelog.json
- Convenience classes: `PostgresMcp`, `SqlServerMcp` - Convenience classes: `PostgresMcp`, `SqlServerMcp`
- Unit tests for both drivers (99+ tests) - Unit tests for both drivers (99+ tests)
### v1.0.3 (2024-12-28)
- **Server Mode Support**: Dynamic database discovery without per-database configuration
- New `serverMode` configuration option for server-level connections
- `database` field now optional in connection config when `serverMode: true`
- All MCP tools accept optional `database` parameter for dynamic database switching
- New `pg_discover_databases` tool to list all databases on a server
- ConnectionManager supports dynamic connection pools keyed by `(envName, database)`
- PostgreSQL driver: `buildListDatabasesQuery()` using `pg_database` catalog
- SQL Server driver: `buildListDatabasesQuery()` using `sys.databases`
- Configuration validation: `database` required unless `serverMode` is enabled
- Backward compatible: existing configurations work without modification
### Future Roadmap ### Future Roadmap
- ~~Multi-database support (SQL Server, MySQL adapters)~~ ✅ Completed in v1.0.2 - ~~Multi-database support (SQL Server, MySQL adapters)~~ ✅ Completed in v1.0.2
- mTLS authentication implementation - mTLS authentication implementation
@ -636,7 +747,7 @@ npm run test:coverage # Run tests with coverage report
**Build Image**: **Build Image**:
```bash ```bash
docker build -t mcp-database-server:1.0.1 . docker build -t mcp-database-server:1.0.3 .
``` ```
**Run Container**: **Run Container**:
@ -647,7 +758,7 @@ docker run -d \
-v $(pwd)/config/database.json:/app/config/database.json:ro \ -v $(pwd)/config/database.json:/app/config/database.json:ro \
-e MCP_AUTH_TOKEN=your-token \ -e MCP_AUTH_TOKEN=your-token \
-e MCP_DRWORKS_PASSWORD=your-password \ -e MCP_DRWORKS_PASSWORD=your-password \
mcp-database-server:1.0.1 mcp-database-server:1.0.3
``` ```
**Docker Compose**: **Docker Compose**:

View File

@ -1,6 +1,21 @@
{ {
"currentVersion": "1.0.2", "currentVersion": "1.0.3",
"changelog": [ "changelog": [
{
"version": "1.0.3",
"date": "2024-12-28",
"description": "服务器模式支持 - 动态数据库发现",
"changes": [
"新增 serverMode 配置项,支持服务器级别连接",
"database 字段在 serverMode 下变为可选",
"所有 MCP 工具新增可选 database 参数",
"ConnectionManager 支持按 (envName, database) 动态创建连接池",
"新增 buildListDatabasesQuery 驱动接口方法",
"PostgreSQL 和 SQL Server 驱动实现数据库列表查询",
"配置验证支持 serverMode 和 database 联合检查",
"向后兼容:现有配置无需修改即可继续使用"
]
},
{ {
"version": "1.0.2", "version": "1.0.2",
"date": "2024-12-27", "date": "2024-12-27",

View File

@ -0,0 +1,24 @@
## mcp 服务器支持sql server数据库
现在的配置文件格式为:
{
"type": "postgres",
"connection": {
"host": "47.99.124.43",
"port": 5432,
"database": "shcis_ipworkstation",
"user": "postgres",
"password": "passw0rd!",
"ssl":false
},
"defaultSchema": "dbo",
"searchPath": ["dbo"],
"pool": {
"max": 100,
"idleTimeoutMs": 10000,
"connectionTimeoutMs": 10000
},
"statementTimeoutMs": 60000,
"mode": "ddl"
}
由于项目的增多,导致服务器上的数据库会有十几个,如果每个库都需要配置,那么后期添加一个库就得修改一下配置。能不能我配置对应的服务器,直接读取所有的库呢?

View File

@ -42,7 +42,7 @@ const poolConfigSchema = z.object({
const postgresConnectionSchema = z.object({ const postgresConnectionSchema = z.object({
host: z.string().min(1), host: z.string().min(1),
port: z.number().min(1).max(65535).default(5432), port: z.number().min(1).max(65535).default(5432),
database: z.string().min(1), database: z.string().min(1).optional(), // Optional when serverMode is enabled
user: z.string().min(1), user: z.string().min(1),
password: z.string(), password: z.string(),
ssl: sslConfigSchema, ssl: sslConfigSchema,
@ -52,7 +52,7 @@ const postgresConnectionSchema = z.object({
const sqlServerConnectionSchema = z.object({ const sqlServerConnectionSchema = z.object({
host: z.string().min(1), host: z.string().min(1),
port: z.number().min(1).max(65535).default(1433), port: z.number().min(1).max(65535).default(1433),
database: z.string().min(1), database: z.string().min(1).optional(), // Optional when serverMode is enabled
user: z.string().min(1), user: z.string().min(1),
password: z.string(), password: z.string(),
encrypt: z.boolean().default(true), encrypt: z.boolean().default(true),
@ -61,9 +61,10 @@ const sqlServerConnectionSchema = z.object({
requestTimeout: z.number().min(0).optional(), requestTimeout: z.number().min(0).optional(),
}); });
// PostgreSQL Environment // PostgreSQL Environment (base schema without refine for discriminatedUnion)
const postgresEnvironmentSchema = z.object({ const postgresEnvironmentBaseSchema = z.object({
type: z.literal('postgres').optional(), type: z.literal('postgres').optional(),
serverMode: z.boolean().optional().default(false),
connection: postgresConnectionSchema, connection: postgresConnectionSchema,
defaultSchema: z.string().default(DEFAULT_POSTGRES_ENVIRONMENT.defaultSchema!), defaultSchema: z.string().default(DEFAULT_POSTGRES_ENVIRONMENT.defaultSchema!),
searchPath: z.array(z.string()).default(DEFAULT_POSTGRES_ENVIRONMENT.searchPath!), searchPath: z.array(z.string()).default(DEFAULT_POSTGRES_ENVIRONMENT.searchPath!),
@ -73,9 +74,10 @@ const postgresEnvironmentSchema = z.object({
mode: z.enum(['readonly', 'readwrite', 'ddl']).default(DEFAULT_POSTGRES_ENVIRONMENT.mode!), mode: z.enum(['readonly', 'readwrite', 'ddl']).default(DEFAULT_POSTGRES_ENVIRONMENT.mode!),
}); });
// SQL Server Environment // SQL Server Environment (base schema without refine for discriminatedUnion)
const sqlServerEnvironmentSchema = z.object({ const sqlServerEnvironmentBaseSchema = z.object({
type: z.literal('sqlserver'), type: z.literal('sqlserver'),
serverMode: z.boolean().optional().default(false),
connection: sqlServerConnectionSchema, connection: sqlServerConnectionSchema,
defaultSchema: z.string().default(DEFAULT_SQLSERVER_ENVIRONMENT.defaultSchema!), defaultSchema: z.string().default(DEFAULT_SQLSERVER_ENVIRONMENT.defaultSchema!),
pool: poolConfigSchema, pool: poolConfigSchema,
@ -84,13 +86,22 @@ const sqlServerEnvironmentSchema = z.object({
mode: z.enum(['readonly', 'readwrite', 'ddl']).default(DEFAULT_SQLSERVER_ENVIRONMENT.mode!), mode: z.enum(['readonly', 'readwrite', 'ddl']).default(DEFAULT_SQLSERVER_ENVIRONMENT.mode!),
}); });
// Combined Environment Schema (discriminated union) // Validation function for serverMode and database
const validateServerModeAndDatabase = (config: { serverMode?: boolean; connection: { database?: string } }) => {
// database is required unless serverMode is enabled
return config.serverMode === true || !!config.connection.database;
};
// Combined Environment Schema (discriminated union with validation)
const environmentConfigSchema = z.discriminatedUnion('type', [ const environmentConfigSchema = z.discriminatedUnion('type', [
postgresEnvironmentSchema.extend({ type: z.literal('postgres') }), postgresEnvironmentBaseSchema.extend({ type: z.literal('postgres') }),
sqlServerEnvironmentSchema, sqlServerEnvironmentBaseSchema,
]).or( ]).or(
// Allow environments without type (default to postgres for backward compatibility) // Allow environments without type (default to postgres for backward compatibility)
postgresEnvironmentSchema postgresEnvironmentBaseSchema
).refine(
validateServerModeAndDatabase,
{ message: 'connection.database is required unless serverMode is true' }
); );
// Auth Configuration // Auth Configuration

View File

@ -54,7 +54,10 @@ export type DatabaseType = 'postgres' | 'sqlserver';
export interface PostgresConnectionConfig { export interface PostgresConnectionConfig {
host: string; host: string;
port: number; port: number;
database: string; /**
* Database name. Required unless serverMode is enabled.
*/
database?: string;
user: string; user: string;
password: string; password: string;
ssl?: false | SSLConfig; ssl?: false | SSLConfig;
@ -62,6 +65,12 @@ export interface PostgresConnectionConfig {
export interface PostgresEnvironmentConfig { export interface PostgresEnvironmentConfig {
type?: 'postgres'; // Optional for backward compatibility type?: 'postgres'; // Optional for backward compatibility
/**
* Server mode: when true, the environment connects to the database server
* without specifying a particular database, allowing dynamic database switching.
* @default false
*/
serverMode?: boolean;
connection: PostgresConnectionConfig; connection: PostgresConnectionConfig;
defaultSchema?: string; defaultSchema?: string;
searchPath?: string[]; searchPath?: string[];
@ -76,7 +85,10 @@ export interface PostgresEnvironmentConfig {
export interface SqlServerConnectionConfig { export interface SqlServerConnectionConfig {
host: string; host: string;
port: number; port: number;
database: string; /**
* Database name. Required unless serverMode is enabled.
*/
database?: string;
user: string; user: string;
password: string; password: string;
encrypt?: boolean; encrypt?: boolean;
@ -87,6 +99,12 @@ export interface SqlServerConnectionConfig {
export interface SqlServerEnvironmentConfig { export interface SqlServerEnvironmentConfig {
type: 'sqlserver'; type: 'sqlserver';
/**
* Server mode: when true, the environment connects to the database server
* without specifying a particular database, allowing dynamic database switching.
* @default false
*/
serverMode?: boolean;
connection: SqlServerConnectionConfig; connection: SqlServerConnectionConfig;
defaultSchema?: string; defaultSchema?: string;
pool?: Partial<PoolConfig>; pool?: Partial<PoolConfig>;

View File

@ -1,6 +1,6 @@
import { DatabaseDriver } from '../drivers/database-driver.js'; import { DatabaseDriver } from '../drivers/database-driver.js';
import { buildSearchPath } from './utils.js'; import { buildSearchPath } from './utils.js';
import { EnvironmentConfig, QueryOptions, SchemaInput } from './types.js'; import { EnvironmentConfig, QueryOptions, SchemaInput, isPostgresConfig } from './types.js';
export type ClientCallback<T> = ( export type ClientCallback<T> = (
client: any, client: any,
@ -10,10 +10,11 @@ export type ClientCallback<T> = (
/** /**
* Connection Manager * Connection Manager
* Manages database connection pools using a database driver * Manages database connection pools using a database driver
* Supports dynamic database switching for serverMode environments
*/ */
export class ConnectionManager { export class ConnectionManager {
private readonly environments = new Map<string, EnvironmentConfig>(); private readonly environments = new Map<string, EnvironmentConfig>();
private readonly pools = new Map<string, any>(); private readonly pools = new Map<string, any>(); // key: "env" or "env:database"
constructor( constructor(
configs: EnvironmentConfig[], configs: EnvironmentConfig[],
@ -22,6 +23,13 @@ export class ConnectionManager {
configs.forEach((config) => this.environments.set(config.name, config)); configs.forEach((config) => this.environments.set(config.name, config));
} }
/**
* Build cache key for connection pool
*/
private buildPoolKey(envName: string, database?: string): string {
return database ? `${envName}:${database}` : envName;
}
getEnvironment(name: string): EnvironmentConfig { getEnvironment(name: string): EnvironmentConfig {
const env = this.environments.get(name); const env = this.environments.get(name);
if (!env) { if (!env) {
@ -30,14 +38,33 @@ export class ConnectionManager {
return env; return env;
} }
getPool(name: string): any { /**
if (this.pools.has(name)) { * Get connection pool for environment, optionally for a specific database
return this.pools.get(name); * @param name Environment name
* @param database Optional database name (overrides environment default)
*/
getPool(name: string, database?: string): any {
const key = this.buildPoolKey(name, database);
if (this.pools.has(key)) {
return this.pools.get(key);
} }
const env = this.getEnvironment(name); const env = this.getEnvironment(name);
const pool = this.driver.createConnectionPool(env.connection);
this.pools.set(name, pool); // Build connection config with optional database override
let connectionConfig = env.connection;
if (database) {
// Use specified database
connectionConfig = { ...env.connection, database };
} else if (!env.connection.database) {
// serverMode without specified database: use default system database
const defaultDb = isPostgresConfig(env) ? 'postgres' : 'master';
connectionConfig = { ...env.connection, database: defaultDb };
}
const pool = this.driver.createConnectionPool(connectionConfig);
this.pools.set(key, pool);
return pool; return pool;
} }
@ -48,7 +75,7 @@ export class ConnectionManager {
options?: QueryOptions, options?: QueryOptions,
): Promise<T> { ): Promise<T> {
const env = this.getEnvironment(envName); const env = this.getEnvironment(envName);
const pool = this.getPool(envName); const pool = this.getPool(envName, options?.database); // Support dynamic database
const client = await pool.connect(); const client = await pool.connect();
try { try {
@ -76,9 +103,9 @@ export class ConnectionManager {
} }
} }
async testConnection(envName: string): Promise<boolean> { async testConnection(envName: string, database?: string): Promise<boolean> {
try { try {
const pool = this.getPool(envName); const pool = this.getPool(envName, database);
return await this.driver.testConnection(pool); return await this.driver.testConnection(pool);
} catch (error) { } catch (error) {
return false; return false;

View File

@ -27,28 +27,24 @@ export class MetadataBrowser {
private readonly driver: DatabaseDriver private readonly driver: DatabaseDriver
) {} ) {}
async listDatabases(envName: string): Promise<string[]> { async listDatabases(envName: string, options?: QueryOptions): Promise<string[]> {
// Database listing is database-specific // Use driver-specific query for listing databases
// For PostgreSQL, we query pg_database const query = this.driver.buildListDatabasesQuery();
const query = `
SELECT datname
FROM pg_database
WHERE datistemplate = false
ORDER BY datname;
`;
const result = await this.connections.withClient( const result = await this.connections.withClient(
envName, envName,
undefined, undefined,
async (client) => { async (client) => {
return await this.driver.execute<{ datname: string }>(client, query); return await this.driver.execute<{ datname?: string; name?: string }>(client, query);
}, },
options,
); );
return result.rows.map((row) => row.datname); // PostgreSQL uses 'datname', SQL Server uses 'name'
return result.rows.map((row) => row.datname ?? row.name ?? '');
} }
async listSchemas(envName: string): Promise<string[]> { async listSchemas(envName: string, options?: QueryOptions): Promise<string[]> {
const query = this.driver.buildListSchemasQuery(); const query = this.driver.buildListSchemasQuery();
const result = await this.connections.withClient( const result = await this.connections.withClient(
@ -57,6 +53,7 @@ export class MetadataBrowser {
async (client) => { async (client) => {
return await this.driver.execute<{ schema_name: string }>(client, query); return await this.driver.execute<{ schema_name: string }>(client, query);
}, },
options,
); );
return result.rows.map((row) => row.schema_name); return result.rows.map((row) => row.schema_name);
@ -209,7 +206,7 @@ export class MetadataBrowser {
return result.rows; return result.rows;
} }
async listExtensions(envName: string): Promise<string[]> { async listExtensions(envName: string, options?: QueryOptions): Promise<string[]> {
// Extensions are PostgreSQL-specific // Extensions are PostgreSQL-specific
const query = ` const query = `
SELECT extname SELECT extname
@ -223,6 +220,7 @@ export class MetadataBrowser {
async (client) => { async (client) => {
return await this.driver.execute<{ extname: string }>(client, query); return await this.driver.execute<{ extname: string }>(client, query);
}, },
options,
); );
return result.rows.map((row) => row.extname); return result.rows.map((row) => row.extname);

View File

@ -21,7 +21,11 @@ export interface BaseConnectionOptions {
* PostgreSQL-specific connection options * PostgreSQL-specific connection options
*/ */
export interface PostgresConnectionOptions extends BaseConnectionOptions { export interface PostgresConnectionOptions extends BaseConnectionOptions {
database: string; /**
* Database name. Required unless serverMode is enabled.
* When serverMode is true, this can be omitted and specified dynamically per-query.
*/
database?: string;
port?: number; // Default: 5432 port?: number; // Default: 5432
ssl?: { ssl?: {
require?: boolean; require?: boolean;
@ -43,7 +47,11 @@ export interface PostgresConnectionOptions extends BaseConnectionOptions {
* SQL Server-specific connection options * SQL Server-specific connection options
*/ */
export interface SqlServerConnectionOptions extends BaseConnectionOptions { export interface SqlServerConnectionOptions extends BaseConnectionOptions {
database: string; /**
* Database name. Required unless serverMode is enabled.
* When serverMode is true, this can be omitted and specified dynamically per-query.
*/
database?: string;
port?: number; // Default: 1433 port?: number; // Default: 1433
encrypt?: boolean; encrypt?: boolean;
trustServerCertificate?: boolean; trustServerCertificate?: boolean;
@ -68,6 +76,12 @@ export interface PoolSettings {
*/ */
export interface BaseEnvironmentConfig { export interface BaseEnvironmentConfig {
name: string; name: string;
/**
* Server mode: when true, the environment connects to the database server
* without specifying a particular database, allowing dynamic database switching.
* @default false
*/
serverMode?: boolean;
defaultSchema?: string; defaultSchema?: string;
searchPath?: string[]; searchPath?: string[];
pool?: PoolSettings; pool?: PoolSettings;
@ -131,6 +145,11 @@ export function getDatabaseType(config: EnvironmentConfig): DatabaseType {
export interface QueryOptions { export interface QueryOptions {
schema?: SchemaInput; schema?: SchemaInput;
timeoutMs?: number; timeoutMs?: number;
/**
* Database name to use for this query.
* Overrides the environment's default database (useful with serverMode).
*/
database?: string;
} }
export interface PaginationOptions { export interface PaginationOptions {

View File

@ -175,6 +175,12 @@ export interface DatabaseDriver {
// ========== Metadata Queries ========== // ========== Metadata Queries ==========
/**
* Build query to list all databases on the server
* @returns SQL query
*/
buildListDatabasesQuery(): string;
/** /**
* Build query to list all schemas * Build query to list all schemas
* @returns SQL query * @returns SQL query

View File

@ -180,6 +180,15 @@ export class PostgresDriver implements DatabaseDriver {
// ========== Metadata Queries ========== // ========== Metadata Queries ==========
buildListDatabasesQuery(): string {
return `
SELECT datname
FROM pg_database
WHERE datistemplate = false
ORDER BY datname
`;
}
buildListSchemasQuery(): string { buildListSchemasQuery(): string {
return ` return `
SELECT schema_name SELECT schema_name

View File

@ -216,6 +216,16 @@ export class SqlServerDriver implements DatabaseDriver {
// ========== Metadata Queries ========== // ========== Metadata Queries ==========
buildListDatabasesQuery(): string {
return `
SELECT name
FROM sys.databases
WHERE state = 0
AND name NOT IN ('master', 'tempdb', 'model', 'msdb')
ORDER BY name
`;
}
buildListSchemasQuery(): string { buildListSchemasQuery(): string {
return ` return `
SELECT schema_name SELECT schema_name

159
src/tools/common-schemas.ts Normal file
View File

@ -0,0 +1,159 @@
/**
* Common Zod schemas for MCP tools
* Provides reusable schema definitions with database parameter support
*/
import { z } from 'zod';
// ========== Base Schemas ==========
/**
* Base environment schema with optional database parameter
*/
export const baseEnvSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default, useful with serverMode)'),
});
/**
* Schema with schema filter (for listing objects in a specific schema)
*/
export const schemaFilterSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
schema: z.string().optional().describe('Schema name to filter'),
});
/**
* Schema for table operations
*/
export const tableSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
table: z.string().describe('Table name'),
schema: z.string().optional().describe('Schema name'),
});
/**
* Schema for index filtering
*/
export const indexFilterSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
table: z.string().optional().describe('Table name to filter indexes'),
schema: z.string().optional().describe('Schema name'),
});
// ========== Query Schemas ==========
/**
* Basic query schema
*/
export const querySchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
sql: z.string().describe('SQL query to execute'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
/**
* Parameterized query schema
*/
export const paramQuerySchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
sql: z.string().describe('SQL query with $1, $2, etc. placeholders'),
params: z.array(z.any()).describe('Array of parameter values'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
/**
* Paginated query schema
*/
export const paginatedQuerySchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
sql: z.string().describe('SQL query (without LIMIT/OFFSET)'),
params: z.array(z.any()).optional().default([]).describe('Array of parameter values'),
limit: z.number().default(100).describe('Maximum number of rows to return'),
offset: z.number().default(0).describe('Number of rows to skip'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
/**
* EXPLAIN query schema
*/
export const explainSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
sql: z.string().describe('SQL query to explain'),
params: z.array(z.any()).optional().default([]).describe('Array of parameter values'),
analyze: z.boolean().optional().default(false).describe('Run EXPLAIN ANALYZE'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
// ========== Data Operation Schemas ==========
/**
* Execute statement schema
*/
export const executeSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
sql: z.string().describe('SQL statement to execute'),
params: z.array(z.any()).optional().default([]).describe('Array of parameter values'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
/**
* Bulk insert schema
*/
export const bulkInsertSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
table: z.string().describe('Target table name'),
rows: z.array(z.record(z.any())).describe('Array of row objects to insert'),
schema: z.string().optional().describe('Schema name'),
chunkSize: z.number().optional().default(500).describe('Number of rows per INSERT'),
});
/**
* Bulk upsert schema
*/
export const bulkUpsertSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
table: z.string().describe('Target table name'),
rows: z.array(z.record(z.any())).describe('Array of row objects to upsert'),
conflictColumns: z.array(z.string()).describe('Columns to use for conflict detection'),
updateColumns: z.array(z.string()).optional().describe('Columns to update on conflict'),
schema: z.string().optional().describe('Schema name'),
chunkSize: z.number().optional().default(200).describe('Number of rows per UPSERT'),
});
// ========== Transaction Schemas ==========
/**
* Transaction operation schema
*/
export const transactionSchema = z.object({
environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
});
// ========== Helper Functions ==========
/**
* Build QueryOptions from tool parameters
*/
export function buildQueryOptions(params: {
database?: string;
schema?: string;
timeoutMs?: number;
}): { database?: string; schema?: string; timeoutMs?: number } {
const options: { database?: string; schema?: string; timeoutMs?: number } = {};
if (params.database) options.database = params.database;
if (params.schema) options.schema = params.schema;
if (params.timeoutMs) options.timeoutMs = params.timeoutMs;
return options;
}

View File

@ -1,34 +1,17 @@
import { z } from 'zod'; import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { PostgresMcp } from '../core/index.js'; import { PostgresMcp } from '../core/index.js';
import {
executeSchema,
bulkInsertSchema,
bulkUpsertSchema,
buildQueryOptions,
} from './common-schemas.js';
const executeSchema = z.object({ // Export schema with database support
environment: z.string().describe('Database environment name'),
sql: z.string().describe('SQL statement to execute'),
params: z.array(z.any()).optional().default([]).describe('Array of parameter values'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
const bulkInsertSchema = z.object({
environment: z.string().describe('Database environment name'),
table: z.string().describe('Target table name'),
rows: z.array(z.record(z.any())).describe('Array of row objects to insert'),
schema: z.string().optional().describe('Schema name'),
chunkSize: z.number().optional().default(500).describe('Number of rows per INSERT statement'),
});
const bulkUpsertSchema = z.object({
environment: z.string().describe('Database environment name'),
table: z.string().describe('Target table name'),
rows: z.array(z.record(z.any())).describe('Array of row objects to upsert'),
conflictColumns: z.array(z.string()).describe('Columns to use for conflict detection'),
updateColumns: z.array(z.string()).optional().describe('Columns to update on conflict (defaults to all non-conflict columns)'),
schema: z.string().optional().describe('Schema name'),
chunkSize: z.number().optional().default(200).describe('Number of rows per UPSERT statement'),
});
const exportSchema = z.object({ const exportSchema = z.object({
environment: z.string().describe('Database environment name'), environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
sql: z.string().describe('SQL query to export'), sql: z.string().describe('SQL query to export'),
params: z.array(z.any()).optional().default([]).describe('Array of parameter values'), params: z.array(z.any()).optional().default([]).describe('Array of parameter values'),
schema: z.string().optional().describe('Schema to use for search_path'), schema: z.string().optional().describe('Schema to use for search_path'),
@ -39,13 +22,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_execute', 'pg_execute',
'Execute any SQL statement (INSERT, UPDATE, DELETE, etc.)', 'Execute any SQL statement (INSERT, UPDATE, DELETE, etc.)',
executeSchema.shape, executeSchema.shape,
async ({ environment, sql, params, schema }) => { async ({ environment, database, sql, params, schema }) => {
try { try {
const result = await pgMcp.queries.execute( const result = await pgMcp.queries.execute(
environment, environment,
sql, sql,
params || [], params || [],
schema ? { schema } : undefined buildQueryOptions({ database, schema })
); );
return { return {
content: [{ content: [{
@ -70,13 +53,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_bulk_insert', 'pg_bulk_insert',
'Insert multiple rows into a table efficiently', 'Insert multiple rows into a table efficiently',
bulkInsertSchema.shape, bulkInsertSchema.shape,
async ({ environment, table, rows, schema, chunkSize }) => { async ({ environment, database, table, rows, schema, chunkSize }) => {
try { try {
const results = await pgMcp.bulk.bulkInsert( const results = await pgMcp.bulk.bulkInsert(
environment, environment,
{ table, schema }, { table, schema },
rows, rows,
{ chunkSize, schema } { chunkSize, ...buildQueryOptions({ database, schema }) }
); );
const totalRowCount = results.reduce((sum, r) => sum + (r.rowCount ?? 0), 0); const totalRowCount = results.reduce((sum, r) => sum + (r.rowCount ?? 0), 0);
return { return {
@ -102,13 +85,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_bulk_upsert', 'pg_bulk_upsert',
'Upsert (INSERT ... ON CONFLICT UPDATE) multiple rows into a table', 'Upsert (INSERT ... ON CONFLICT UPDATE) multiple rows into a table',
bulkUpsertSchema.shape, bulkUpsertSchema.shape,
async ({ environment, table, rows, conflictColumns, updateColumns, schema, chunkSize }) => { async ({ environment, database, table, rows, conflictColumns, updateColumns, schema, chunkSize }) => {
try { try {
const results = await pgMcp.bulk.bulkUpsert( const results = await pgMcp.bulk.bulkUpsert(
environment, environment,
{ table, schema }, { table, schema },
rows, rows,
{ conflictColumns, updateColumns, chunkSize, schema } { conflictColumns, updateColumns, chunkSize, ...buildQueryOptions({ database, schema }) }
); );
const totalRowCount = results.reduce((sum, r) => sum + (r.rowCount ?? 0), 0); const totalRowCount = results.reduce((sum, r) => sum + (r.rowCount ?? 0), 0);
return { return {
@ -134,13 +117,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_export_csv', 'pg_export_csv',
'Export query results as CSV', 'Export query results as CSV',
exportSchema.shape, exportSchema.shape,
async ({ environment, sql, params, schema }) => { async ({ environment, database, sql, params, schema }) => {
try { try {
const csv = await pgMcp.bulk.exportToCsv( const csv = await pgMcp.bulk.exportToCsv(
environment, environment,
sql, sql,
params || [], params || [],
schema ? { schema } : undefined buildQueryOptions({ database, schema })
); );
return { return {
content: [{ type: 'text', text: csv }], content: [{ type: 'text', text: csv }],
@ -158,13 +141,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_export_json', 'pg_export_json',
'Export query results as JSON', 'Export query results as JSON',
exportSchema.shape, exportSchema.shape,
async ({ environment, sql, params, schema }) => { async ({ environment, database, sql, params, schema }) => {
try { try {
const json = await pgMcp.bulk.exportToJson( const json = await pgMcp.bulk.exportToJson(
environment, environment,
sql, sql,
params || [], params || [],
schema ? { schema } : undefined buildQueryOptions({ database, schema })
); );
return { return {
content: [{ type: 'text', text: json }], content: [{ type: 'text', text: json }],

View File

@ -1,18 +1,17 @@
import { z } from 'zod'; import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { PostgresMcp } from '../core/index.js'; import { PostgresMcp } from '../core/index.js';
import { baseEnvSchema, buildQueryOptions } from './common-schemas.js';
const envSchema = z.object({
environment: z.string().describe('Database environment name'),
});
const slowQueriesSchema = z.object({ const slowQueriesSchema = z.object({
environment: z.string().describe('Database environment name'), environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
limit: z.number().optional().default(20).describe('Maximum number of slow queries to return'), limit: z.number().optional().default(20).describe('Maximum number of slow queries to return'),
}); });
const vacuumSchema = z.object({ const vacuumSchema = z.object({
environment: z.string().describe('Database environment name'), environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
table: z.string().optional().describe('Table name (if not specified, vacuums entire database)'), table: z.string().optional().describe('Table name (if not specified, vacuums entire database)'),
schema: z.string().optional().describe('Schema name'), schema: z.string().optional().describe('Schema name'),
verbose: z.boolean().optional().default(false).describe('Show detailed progress'), verbose: z.boolean().optional().default(false).describe('Show detailed progress'),
@ -21,6 +20,7 @@ const vacuumSchema = z.object({
const analyzeSchema = z.object({ const analyzeSchema = z.object({
environment: z.string().describe('Database environment name'), environment: z.string().describe('Database environment name'),
database: z.string().optional().describe('Database name (overrides environment default)'),
table: z.string().describe('Table name to analyze'), table: z.string().describe('Table name to analyze'),
schema: z.string().optional().describe('Schema name'), schema: z.string().optional().describe('Schema name'),
}); });
@ -29,10 +29,10 @@ export function registerDiagnosticsTools(server: McpServer, pgMcp: PostgresMcp):
server.tool( server.tool(
'pg_active_connections', 'pg_active_connections',
'List active database connections and their current state', 'List active database connections and their current state',
envSchema.shape, baseEnvSchema.shape,
async ({ environment }) => { async ({ environment, database }) => {
try { try {
const connections = await pgMcp.diagnostics.getActiveConnections(environment); const connections = await pgMcp.diagnostics.getActiveConnections(environment, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(connections, null, 2) }], content: [{ type: 'text', text: JSON.stringify(connections, null, 2) }],
}; };
@ -48,10 +48,10 @@ export function registerDiagnosticsTools(server: McpServer, pgMcp: PostgresMcp):
server.tool( server.tool(
'pg_locks', 'pg_locks',
'Show current database locks and blocking queries', 'Show current database locks and blocking queries',
envSchema.shape, baseEnvSchema.shape,
async ({ environment }) => { async ({ environment, database }) => {
try { try {
const locks = await pgMcp.diagnostics.getLocks(environment); const locks = await pgMcp.diagnostics.getLocks(environment, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(locks, null, 2) }], content: [{ type: 'text', text: JSON.stringify(locks, null, 2) }],
}; };
@ -67,10 +67,10 @@ export function registerDiagnosticsTools(server: McpServer, pgMcp: PostgresMcp):
server.tool( server.tool(
'pg_replication_status', 'pg_replication_status',
'Show replication status and lag information', 'Show replication status and lag information',
envSchema.shape, baseEnvSchema.shape,
async ({ environment }) => { async ({ environment, database }) => {
try { try {
const status = await pgMcp.diagnostics.getReplicationStatus(environment); const status = await pgMcp.diagnostics.getReplicationStatus(environment, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(status, null, 2) }], content: [{ type: 'text', text: JSON.stringify(status, null, 2) }],
}; };
@ -87,9 +87,9 @@ export function registerDiagnosticsTools(server: McpServer, pgMcp: PostgresMcp):
'pg_slow_queries', 'pg_slow_queries',
'Get the slowest queries from pg_stat_statements', 'Get the slowest queries from pg_stat_statements',
slowQueriesSchema.shape, slowQueriesSchema.shape,
async ({ environment, limit }) => { async ({ environment, database, limit }) => {
try { try {
const queries = await pgMcp.diagnostics.getSlowQueries(environment, limit); const queries = await pgMcp.diagnostics.getSlowQueries(environment, limit, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(queries, null, 2) }], content: [{ type: 'text', text: JSON.stringify(queries, null, 2) }],
}; };
@ -106,13 +106,14 @@ export function registerDiagnosticsTools(server: McpServer, pgMcp: PostgresMcp):
'pg_vacuum', 'pg_vacuum',
'Run VACUUM on a table or the entire database', 'Run VACUUM on a table or the entire database',
vacuumSchema.shape, vacuumSchema.shape,
async ({ environment, table, schema, verbose, analyze }) => { async ({ environment, database, table, schema, verbose, analyze }) => {
try { try {
await pgMcp.diagnostics.triggerVacuum(environment, { await pgMcp.diagnostics.triggerVacuum(environment, {
table, table,
schema, schema,
verbose, verbose,
analyze, analyze,
...buildQueryOptions({ database }),
}); });
return { return {
content: [{ content: [{
@ -138,9 +139,9 @@ export function registerDiagnosticsTools(server: McpServer, pgMcp: PostgresMcp):
'pg_analyze', 'pg_analyze',
'Run ANALYZE on a table to update statistics', 'Run ANALYZE on a table to update statistics',
analyzeSchema.shape, analyzeSchema.shape,
async ({ environment, table, schema }) => { async ({ environment, database, table, schema }) => {
try { try {
await pgMcp.diagnostics.analyzeTable(environment, table, schema); await pgMcp.diagnostics.analyzeTable(environment, table, schema, buildQueryOptions({ database }));
return { return {
content: [{ content: [{
type: 'text', type: 'text',

View File

@ -1,27 +1,13 @@
import { z } from 'zod'; import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { PostgresMcp } from '../core/index.js'; import { PostgresMcp } from '../core/index.js';
import {
const envSchema = z.object({ baseEnvSchema,
environment: z.string().describe('Database environment name'), schemaFilterSchema,
}); tableSchema,
indexFilterSchema,
const schemaFilterSchema = z.object({ buildQueryOptions,
environment: z.string().describe('Database environment name'), } from './common-schemas.js';
schema: z.string().optional().describe('Schema name to filter'),
});
const tableSchema = z.object({
environment: z.string().describe('Database environment name'),
table: z.string().describe('Table name'),
schema: z.string().optional().describe('Schema name'),
});
const indexFilterSchema = z.object({
environment: z.string().describe('Database environment name'),
table: z.string().optional().describe('Table name to filter indexes'),
schema: z.string().optional().describe('Schema name'),
});
export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): void { export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): void {
server.tool( server.tool(
@ -39,10 +25,10 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
server.tool( server.tool(
'pg_test_connection', 'pg_test_connection',
'Test connection to a database environment', 'Test connection to a database environment',
envSchema.shape, baseEnvSchema.shape,
async ({ environment }) => { async ({ environment, database }) => {
try { try {
const pool = pgMcp.connections.getPool(environment); const pool = pgMcp.connections.getPool(environment, database);
const client = await pool.connect(); const client = await pool.connect();
const result = await client.query('SELECT version()'); const result = await client.query('SELECT version()');
client.release(); client.release();
@ -60,11 +46,11 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
server.tool( server.tool(
'pg_list_databases', 'pg_list_databases',
'List all databases in the PostgreSQL server', 'List all databases in the database server (useful with serverMode)',
envSchema.shape, baseEnvSchema.shape,
async ({ environment }) => { async ({ environment, database }) => {
try { try {
const databases = await pgMcp.metadata.listDatabases(environment); const databases = await pgMcp.metadata.listDatabases(environment, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(databases, null, 2) }], content: [{ type: 'text', text: JSON.stringify(databases, null, 2) }],
}; };
@ -80,10 +66,10 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
server.tool( server.tool(
'pg_list_schemas', 'pg_list_schemas',
'List all schemas in the database', 'List all schemas in the database',
envSchema.shape, baseEnvSchema.shape,
async ({ environment }) => { async ({ environment, database }) => {
try { try {
const schemas = await pgMcp.metadata.listSchemas(environment); const schemas = await pgMcp.metadata.listSchemas(environment, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(schemas, null, 2) }], content: [{ type: 'text', text: JSON.stringify(schemas, null, 2) }],
}; };
@ -100,9 +86,9 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
'pg_list_tables', 'pg_list_tables',
'List tables in the database, optionally filtered by schema', 'List tables in the database, optionally filtered by schema',
schemaFilterSchema.shape, schemaFilterSchema.shape,
async ({ environment, schema }) => { async ({ environment, database, schema }) => {
try { try {
const tables = await pgMcp.metadata.listTables(environment, schema); const tables = await pgMcp.metadata.listTables(environment, schema, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }], content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }],
}; };
@ -119,9 +105,9 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
'pg_list_views', 'pg_list_views',
'List views in the database, optionally filtered by schema', 'List views in the database, optionally filtered by schema',
schemaFilterSchema.shape, schemaFilterSchema.shape,
async ({ environment, schema }) => { async ({ environment, database, schema }) => {
try { try {
const views = await pgMcp.metadata.listViews(environment, schema); const views = await pgMcp.metadata.listViews(environment, schema, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(views, null, 2) }], content: [{ type: 'text', text: JSON.stringify(views, null, 2) }],
}; };
@ -138,9 +124,9 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
'pg_list_materialized_views', 'pg_list_materialized_views',
'List materialized views in the database, optionally filtered by schema', 'List materialized views in the database, optionally filtered by schema',
schemaFilterSchema.shape, schemaFilterSchema.shape,
async ({ environment, schema }) => { async ({ environment, database, schema }) => {
try { try {
const views = await pgMcp.metadata.listMaterializedViews(environment, schema); const views = await pgMcp.metadata.listMaterializedViews(environment, schema, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(views, null, 2) }], content: [{ type: 'text', text: JSON.stringify(views, null, 2) }],
}; };
@ -157,10 +143,10 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
'pg_list_indexes', 'pg_list_indexes',
'List indexes in the database, optionally filtered by table and schema', 'List indexes in the database, optionally filtered by table and schema',
indexFilterSchema.shape, indexFilterSchema.shape,
async ({ environment, table, schema }) => { async ({ environment, database, table, schema }) => {
try { try {
const tableId = table ? { schema: schema || 'public', name: table } : undefined; const tableId = table ? { schema: schema || 'public', name: table } : undefined;
const indexes = await pgMcp.metadata.listIndexes(environment, tableId); const indexes = await pgMcp.metadata.listIndexes(environment, tableId, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(indexes, null, 2) }], content: [{ type: 'text', text: JSON.stringify(indexes, null, 2) }],
}; };
@ -177,9 +163,9 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
'pg_list_sequences', 'pg_list_sequences',
'List sequences in the database, optionally filtered by schema', 'List sequences in the database, optionally filtered by schema',
schemaFilterSchema.shape, schemaFilterSchema.shape,
async ({ environment, schema }) => { async ({ environment, database, schema }) => {
try { try {
const sequences = await pgMcp.metadata.listSequences(environment, schema); const sequences = await pgMcp.metadata.listSequences(environment, schema, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(sequences, null, 2) }], content: [{ type: 'text', text: JSON.stringify(sequences, null, 2) }],
}; };
@ -195,10 +181,10 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
server.tool( server.tool(
'pg_list_extensions', 'pg_list_extensions',
'List installed PostgreSQL extensions', 'List installed PostgreSQL extensions',
envSchema.shape, baseEnvSchema.shape,
async ({ environment }) => { async ({ environment, database }) => {
try { try {
const extensions = await pgMcp.metadata.listExtensions(environment); const extensions = await pgMcp.metadata.listExtensions(environment, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(extensions, null, 2) }], content: [{ type: 'text', text: JSON.stringify(extensions, null, 2) }],
}; };
@ -215,9 +201,9 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
'pg_describe_table', 'pg_describe_table',
'Get detailed table structure including columns, constraints, and indexes', 'Get detailed table structure including columns, constraints, and indexes',
tableSchema.shape, tableSchema.shape,
async ({ environment, table, schema }) => { async ({ environment, database, table, schema }) => {
try { try {
const definition = await pgMcp.metadata.getTableDefinition(environment, table, schema); const definition = await pgMcp.metadata.getTableDefinition(environment, table, schema, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(definition, null, 2) }], content: [{ type: 'text', text: JSON.stringify(definition, null, 2) }],
}; };
@ -234,9 +220,9 @@ export function registerMetadataTools(server: McpServer, pgMcp: PostgresMcp): vo
'pg_show_search_path', 'pg_show_search_path',
'Show the current search_path for the session', 'Show the current search_path for the session',
schemaFilterSchema.shape, schemaFilterSchema.shape,
async ({ environment, schema }) => { async ({ environment, database, schema }) => {
try { try {
const searchPath = await pgMcp.metadata.describeSearchPath(environment, schema); const searchPath = await pgMcp.metadata.describeSearchPath(environment, schema, buildQueryOptions({ database }));
return { return {
content: [{ type: 'text', text: JSON.stringify(searchPath, null, 2) }], content: [{ type: 'text', text: JSON.stringify(searchPath, null, 2) }],
}; };

View File

@ -1,49 +1,26 @@
import { z } from 'zod'; import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { PostgresMcp } from '../core/index.js'; import { PostgresMcp } from '../core/index.js';
import {
const querySchema = z.object({ querySchema,
environment: z.string().describe('Database environment name'), paramQuerySchema,
sql: z.string().describe('SQL query to execute'), paginatedQuerySchema,
schema: z.string().optional().describe('Schema to use for search_path'), explainSchema,
}); buildQueryOptions,
} from './common-schemas.js';
const paramQuerySchema = z.object({
environment: z.string().describe('Database environment name'),
sql: z.string().describe('SQL query with $1, $2, etc. placeholders'),
params: z.array(z.any()).describe('Array of parameter values'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
const paginatedQuerySchema = z.object({
environment: z.string().describe('Database environment name'),
sql: z.string().describe('SQL query (without LIMIT/OFFSET)'),
params: z.array(z.any()).optional().default([]).describe('Array of parameter values'),
limit: z.number().default(100).describe('Maximum number of rows to return'),
offset: z.number().default(0).describe('Number of rows to skip'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
const explainSchema = z.object({
environment: z.string().describe('Database environment name'),
sql: z.string().describe('SQL query to explain'),
params: z.array(z.any()).optional().default([]).describe('Array of parameter values'),
analyze: z.boolean().optional().default(false).describe('Run EXPLAIN ANALYZE (actually executes the query)'),
schema: z.string().optional().describe('Schema to use for search_path'),
});
export function registerQueryTools(server: McpServer, pgMcp: PostgresMcp): void { export function registerQueryTools(server: McpServer, pgMcp: PostgresMcp): void {
server.tool( server.tool(
'pg_query', 'pg_query',
'Execute a SQL query and return results', 'Execute a SQL query and return results',
querySchema.shape, querySchema.shape,
async ({ environment, sql, schema }) => { async ({ environment, database, sql, schema }) => {
try { try {
const result = await pgMcp.queries.execute( const result = await pgMcp.queries.execute(
environment, environment,
sql, sql,
[], [],
schema ? { schema } : undefined buildQueryOptions({ database, schema })
); );
return { return {
content: [{ content: [{
@ -68,13 +45,13 @@ export function registerQueryTools(server: McpServer, pgMcp: PostgresMcp): void
'pg_query_with_params', 'pg_query_with_params',
'Execute a parameterized SQL query with $1, $2, etc. placeholders', 'Execute a parameterized SQL query with $1, $2, etc. placeholders',
paramQuerySchema.shape, paramQuerySchema.shape,
async ({ environment, sql, params, schema }) => { async ({ environment, database, sql, params, schema }) => {
try { try {
const result = await pgMcp.queries.execute( const result = await pgMcp.queries.execute(
environment, environment,
sql, sql,
params, params,
schema ? { schema } : undefined buildQueryOptions({ database, schema })
); );
return { return {
content: [{ content: [{
@ -99,14 +76,14 @@ export function registerQueryTools(server: McpServer, pgMcp: PostgresMcp): void
'pg_query_paginated', 'pg_query_paginated',
'Execute a SQL query with pagination (LIMIT/OFFSET)', 'Execute a SQL query with pagination (LIMIT/OFFSET)',
paginatedQuerySchema.shape, paginatedQuerySchema.shape,
async ({ environment, sql, params, limit, offset, schema }) => { async ({ environment, database, sql, params, limit, offset, schema }) => {
try { try {
const result = await pgMcp.queries.executePaginated( const result = await pgMcp.queries.executePaginated(
environment, environment,
sql, sql,
params || [], params || [],
{ limit, offset }, { limit, offset },
schema ? { schema } : undefined buildQueryOptions({ database, schema })
); );
return { return {
content: [{ content: [{
@ -132,13 +109,13 @@ export function registerQueryTools(server: McpServer, pgMcp: PostgresMcp): void
'pg_explain', 'pg_explain',
'Get the query execution plan (EXPLAIN)', 'Get the query execution plan (EXPLAIN)',
explainSchema.shape, explainSchema.shape,
async ({ environment, sql, params, analyze, schema }) => { async ({ environment, database, sql, params, analyze, schema }) => {
try { try {
const plan = await pgMcp.queries.explain( const plan = await pgMcp.queries.explain(
environment, environment,
sql, sql,
params || [], params || [],
{ analyze, schema } { analyze, ...buildQueryOptions({ database, schema }) }
); );
return { return {
content: [{ type: 'text', text: JSON.stringify(plan, null, 2) }], content: [{ type: 'text', text: JSON.stringify(plan, null, 2) }],