From 481eb2de17e9ea923794b8d19dca2f9d7b8b1e60 Mon Sep 17 00:00:00 2001 From: zpc Date: Sun, 28 Dec 2025 19:39:50 +0800 Subject: [PATCH] v1.0.3 --- CLAUDE.md | 117 +++++++++++++- changelog.json | 17 ++- docs/1.0.3版本需求/1.0.3版本需求.md | 24 +++ src/config/loader.ts | 31 ++-- src/config/types.ts | 22 ++- src/core/connection-manager.ts | 47 ++++-- src/core/metadata-browser.ts | 24 ++- src/core/types.ts | 23 ++- src/drivers/database-driver.ts | 6 + src/drivers/postgres/postgres-driver.ts | 9 ++ src/drivers/sqlserver/sqlserver-driver.ts | 10 ++ src/tools/common-schemas.ts | 159 ++++++++++++++++++++ src/tools/data.ts | 53 +++---- src/tools/diagnostics.ts | 37 ++--- src/tools/metadata.ts | 82 +++++----- src/tools/query.ts | 53 ++----- 16 files changed, 534 insertions(+), 180 deletions(-) create mode 100644 docs/1.0.3版本需求/1.0.3版本需求.md create mode 100644 src/tools/common-schemas.ts diff --git a/CLAUDE.md b/CLAUDE.md index c05a071..4f0b930 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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**: diff --git a/changelog.json b/changelog.json index 0c5f52e..d17071c 100644 --- a/changelog.json +++ b/changelog.json @@ -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", diff --git a/docs/1.0.3版本需求/1.0.3版本需求.md b/docs/1.0.3版本需求/1.0.3版本需求.md new file mode 100644 index 0000000..85db91b --- /dev/null +++ b/docs/1.0.3版本需求/1.0.3版本需求.md @@ -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" + } + +由于项目的增多,导致服务器上的数据库会有十几个,如果每个库都需要配置,那么后期添加一个库就得修改一下配置。能不能我配置对应的服务器,直接读取所有的库呢? \ No newline at end of file diff --git a/src/config/loader.ts b/src/config/loader.ts index ef193a8..f818cc0 100644 --- a/src/config/loader.ts +++ b/src/config/loader.ts @@ -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 diff --git a/src/config/types.ts b/src/config/types.ts index b534c96..f32d5dc 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -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; diff --git a/src/core/connection-manager.ts b/src/core/connection-manager.ts index 1ff7eb9..7de6189 100644 --- a/src/core/connection-manager.ts +++ b/src/core/connection-manager.ts @@ -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 = ( client: any, @@ -10,10 +10,11 @@ export type ClientCallback = ( /** * 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(); - private readonly pools = new Map(); + private readonly pools = new Map(); // 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 { 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 { + async testConnection(envName: string, database?: string): Promise { try { - const pool = this.getPool(envName); + const pool = this.getPool(envName, database); return await this.driver.testConnection(pool); } catch (error) { return false; diff --git a/src/core/metadata-browser.ts b/src/core/metadata-browser.ts index 1674924..88550f2 100644 --- a/src/core/metadata-browser.ts +++ b/src/core/metadata-browser.ts @@ -27,28 +27,24 @@ export class MetadataBrowser { private readonly driver: DatabaseDriver ) {} - async listDatabases(envName: string): Promise { - // 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 { + // 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 { + async listSchemas(envName: string, options?: QueryOptions): Promise { 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 { + async listExtensions(envName: string, options?: QueryOptions): Promise { // 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); diff --git a/src/core/types.ts b/src/core/types.ts index 41e5fbf..9d4a499 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -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 { diff --git a/src/drivers/database-driver.ts b/src/drivers/database-driver.ts index 9fdd81f..180daf7 100644 --- a/src/drivers/database-driver.ts +++ b/src/drivers/database-driver.ts @@ -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 diff --git a/src/drivers/postgres/postgres-driver.ts b/src/drivers/postgres/postgres-driver.ts index 12a7b19..ba4245c 100644 --- a/src/drivers/postgres/postgres-driver.ts +++ b/src/drivers/postgres/postgres-driver.ts @@ -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 diff --git a/src/drivers/sqlserver/sqlserver-driver.ts b/src/drivers/sqlserver/sqlserver-driver.ts index dd3af02..cf8a543 100644 --- a/src/drivers/sqlserver/sqlserver-driver.ts +++ b/src/drivers/sqlserver/sqlserver-driver.ts @@ -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 diff --git a/src/tools/common-schemas.ts b/src/tools/common-schemas.ts new file mode 100644 index 0000000..341fdf8 --- /dev/null +++ b/src/tools/common-schemas.ts @@ -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; +} diff --git a/src/tools/data.ts b/src/tools/data.ts index f619b07..a0c0ca2 100644 --- a/src/tools/data.ts +++ b/src/tools/data.ts @@ -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 }], diff --git a/src/tools/diagnostics.ts b/src/tools/diagnostics.ts index 0c749da..fc60e9f 100644 --- a/src/tools/diagnostics.ts +++ b/src/tools/diagnostics.ts @@ -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', diff --git a/src/tools/metadata.ts b/src/tools/metadata.ts index 18752aa..e861592 100644 --- a/src/tools/metadata.ts +++ b/src/tools/metadata.ts @@ -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) }], }; diff --git a/src/tools/query.ts b/src/tools/query.ts index e576541..c72da91 100644 --- a/src/tools/query.ts +++ b/src/tools/query.ts @@ -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) }],