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
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)
7. **Server Mode** (v1.0.3): Single server-level configuration with dynamic database discovery, eliminating per-database environment configuration
## 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
**server** - Global server settings:
@ -230,6 +261,9 @@ Configuration uses `config/database.json` (see `config/database.example.json` fo
**environments** - Database connection configurations:
- Each environment is an isolated connection pool with unique name
- `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:
- `connection.ssl`: SSL configuration (`{ require: true }` or `false`)
- `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
### Security and Authentication
@ -467,6 +565,7 @@ The server exposes 30+ PostgreSQL tools grouped by category:
**Metadata Tools**:
- `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_tables` - List tables in schema
- `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_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
@ -603,6 +702,18 @@ cat changelog.json
- Convenience classes: `PostgresMcp`, `SqlServerMcp`
- 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
- ~~Multi-database support (SQL Server, MySQL adapters)~~ ✅ Completed in v1.0.2
- mTLS authentication implementation
@ -636,7 +747,7 @@ npm run test:coverage # Run tests with coverage report
**Build Image**:
```bash
docker build -t mcp-database-server:1.0.1 .
docker build -t mcp-database-server:1.0.3 .
```
**Run Container**:
@ -647,7 +758,7 @@ docker run -d \
-v $(pwd)/config/database.json:/app/config/database.json:ro \
-e MCP_AUTH_TOKEN=your-token \
-e MCP_DRWORKS_PASSWORD=your-password \
mcp-database-server:1.0.1
mcp-database-server:1.0.3
```
**Docker Compose**:

View File

@ -1,6 +1,21 @@
{
"currentVersion": "1.0.2",
"currentVersion": "1.0.3",
"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",
"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({
host: z.string().min(1),
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),
password: z.string(),
ssl: sslConfigSchema,
@ -52,7 +52,7 @@ const postgresConnectionSchema = z.object({
const sqlServerConnectionSchema = z.object({
host: z.string().min(1),
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),
password: z.string(),
encrypt: z.boolean().default(true),
@ -61,9 +61,10 @@ const sqlServerConnectionSchema = z.object({
requestTimeout: z.number().min(0).optional(),
});
// PostgreSQL Environment
const postgresEnvironmentSchema = z.object({
// PostgreSQL Environment (base schema without refine for discriminatedUnion)
const postgresEnvironmentBaseSchema = z.object({
type: z.literal('postgres').optional(),
serverMode: z.boolean().optional().default(false),
connection: postgresConnectionSchema,
defaultSchema: z.string().default(DEFAULT_POSTGRES_ENVIRONMENT.defaultSchema!),
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!),
});
// SQL Server Environment
const sqlServerEnvironmentSchema = z.object({
// SQL Server Environment (base schema without refine for discriminatedUnion)
const sqlServerEnvironmentBaseSchema = z.object({
type: z.literal('sqlserver'),
serverMode: z.boolean().optional().default(false),
connection: sqlServerConnectionSchema,
defaultSchema: z.string().default(DEFAULT_SQLSERVER_ENVIRONMENT.defaultSchema!),
pool: poolConfigSchema,
@ -84,13 +86,22 @@ const sqlServerEnvironmentSchema = z.object({
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', [
postgresEnvironmentSchema.extend({ type: z.literal('postgres') }),
sqlServerEnvironmentSchema,
postgresEnvironmentBaseSchema.extend({ type: z.literal('postgres') }),
sqlServerEnvironmentBaseSchema,
]).or(
// 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

View File

@ -54,7 +54,10 @@ export type DatabaseType = 'postgres' | 'sqlserver';
export interface PostgresConnectionConfig {
host: string;
port: number;
database: string;
/**
* Database name. Required unless serverMode is enabled.
*/
database?: string;
user: string;
password: string;
ssl?: false | SSLConfig;
@ -62,6 +65,12 @@ export interface PostgresConnectionConfig {
export interface PostgresEnvironmentConfig {
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;
defaultSchema?: string;
searchPath?: string[];
@ -76,7 +85,10 @@ export interface PostgresEnvironmentConfig {
export interface SqlServerConnectionConfig {
host: string;
port: number;
database: string;
/**
* Database name. Required unless serverMode is enabled.
*/
database?: string;
user: string;
password: string;
encrypt?: boolean;
@ -87,6 +99,12 @@ export interface SqlServerConnectionConfig {
export interface SqlServerEnvironmentConfig {
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;
defaultSchema?: string;
pool?: Partial<PoolConfig>;

View File

@ -1,6 +1,6 @@
import { DatabaseDriver } from '../drivers/database-driver.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> = (
client: any,
@ -10,10 +10,11 @@ export type ClientCallback<T> = (
/**
* Connection Manager
* Manages database connection pools using a database driver
* Supports dynamic database switching for serverMode environments
*/
export class ConnectionManager {
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(
configs: EnvironmentConfig[],
@ -22,6 +23,13 @@ export class ConnectionManager {
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 {
const env = this.environments.get(name);
if (!env) {
@ -30,14 +38,33 @@ export class ConnectionManager {
return env;
}
getPool(name: string): any {
if (this.pools.has(name)) {
return this.pools.get(name);
/**
* Get connection pool for environment, optionally for a specific database
* @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 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;
}
@ -48,7 +75,7 @@ export class ConnectionManager {
options?: QueryOptions,
): Promise<T> {
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();
try {
@ -76,9 +103,9 @@ export class ConnectionManager {
}
}
async testConnection(envName: string): Promise<boolean> {
async testConnection(envName: string, database?: string): Promise<boolean> {
try {
const pool = this.getPool(envName);
const pool = this.getPool(envName, database);
return await this.driver.testConnection(pool);
} catch (error) {
return false;

View File

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

View File

@ -21,7 +21,11 @@ export interface BaseConnectionOptions {
* PostgreSQL-specific connection options
*/
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
ssl?: {
require?: boolean;
@ -43,7 +47,11 @@ export interface PostgresConnectionOptions extends BaseConnectionOptions {
* SQL Server-specific connection options
*/
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
encrypt?: boolean;
trustServerCertificate?: boolean;
@ -68,6 +76,12 @@ export interface PoolSettings {
*/
export interface BaseEnvironmentConfig {
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;
searchPath?: string[];
pool?: PoolSettings;
@ -131,6 +145,11 @@ export function getDatabaseType(config: EnvironmentConfig): DatabaseType {
export interface QueryOptions {
schema?: SchemaInput;
timeoutMs?: number;
/**
* Database name to use for this query.
* Overrides the environment's default database (useful with serverMode).
*/
database?: string;
}
export interface PaginationOptions {

View File

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

View File

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

View File

@ -216,6 +216,16 @@ export class SqlServerDriver implements DatabaseDriver {
// ========== 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 {
return `
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 { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { PostgresMcp } from '../core/index.js';
import {
executeSchema,
bulkInsertSchema,
bulkUpsertSchema,
buildQueryOptions,
} from './common-schemas.js';
const executeSchema = z.object({
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'),
});
// Export schema with database support
const exportSchema = 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 export'),
params: z.array(z.any()).optional().default([]).describe('Array of parameter values'),
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',
'Execute any SQL statement (INSERT, UPDATE, DELETE, etc.)',
executeSchema.shape,
async ({ environment, sql, params, schema }) => {
async ({ environment, database, sql, params, schema }) => {
try {
const result = await pgMcp.queries.execute(
environment,
sql,
params || [],
schema ? { schema } : undefined
buildQueryOptions({ database, schema })
);
return {
content: [{
@ -70,13 +53,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_bulk_insert',
'Insert multiple rows into a table efficiently',
bulkInsertSchema.shape,
async ({ environment, table, rows, schema, chunkSize }) => {
async ({ environment, database, table, rows, schema, chunkSize }) => {
try {
const results = await pgMcp.bulk.bulkInsert(
environment,
{ table, schema },
rows,
{ chunkSize, schema }
{ chunkSize, ...buildQueryOptions({ database, schema }) }
);
const totalRowCount = results.reduce((sum, r) => sum + (r.rowCount ?? 0), 0);
return {
@ -102,13 +85,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_bulk_upsert',
'Upsert (INSERT ... ON CONFLICT UPDATE) multiple rows into a table',
bulkUpsertSchema.shape,
async ({ environment, table, rows, conflictColumns, updateColumns, schema, chunkSize }) => {
async ({ environment, database, table, rows, conflictColumns, updateColumns, schema, chunkSize }) => {
try {
const results = await pgMcp.bulk.bulkUpsert(
environment,
{ table, schema },
rows,
{ conflictColumns, updateColumns, chunkSize, schema }
{ conflictColumns, updateColumns, chunkSize, ...buildQueryOptions({ database, schema }) }
);
const totalRowCount = results.reduce((sum, r) => sum + (r.rowCount ?? 0), 0);
return {
@ -134,13 +117,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_export_csv',
'Export query results as CSV',
exportSchema.shape,
async ({ environment, sql, params, schema }) => {
async ({ environment, database, sql, params, schema }) => {
try {
const csv = await pgMcp.bulk.exportToCsv(
environment,
sql,
params || [],
schema ? { schema } : undefined
buildQueryOptions({ database, schema })
);
return {
content: [{ type: 'text', text: csv }],
@ -158,13 +141,13 @@ export function registerDataTools(server: McpServer, pgMcp: PostgresMcp): void {
'pg_export_json',
'Export query results as JSON',
exportSchema.shape,
async ({ environment, sql, params, schema }) => {
async ({ environment, database, sql, params, schema }) => {
try {
const json = await pgMcp.bulk.exportToJson(
environment,
sql,
params || [],
schema ? { schema } : undefined
buildQueryOptions({ database, schema })
);
return {
content: [{ type: 'text', text: json }],

View File

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

View File

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

View File

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