#!/usr/bin/env node
/**
* postgres-mcp - CLI Entry Point
*
* Command-line interface for the PostgreSQL MCP server.
* Supports stdio, HTTP, and SSE transports with OAuth 2.0 authentication.
*/
import { Command } from "commander";
import { PostgresAdapter } from "./adapters/postgresql/index.js";
import { PostgresMcpServer } from "./server/McpServer.js";
import { parseToolFilter, getFilterSummary } from "./filtering/ToolFilter.js";
import { logger } from "./utils/logger.js";
import { HttpTransport, type HttpTransportConfig } from "./transports/http.js";
import {
OAuthResourceServer,
TokenValidator,
AuthorizationServerDiscovery,
ALL_SCOPES,
} from "./auth/index.js";
import type {
DatabaseConfig,
OAuthConfig,
TransportType,
} from "./types/index.js";
const VERSION = "0.1.0";
interface CliOptions {
postgres?: string;
host?: string;
pgPort?: number;
user?: string;
password?: string;
database?: string;
ssl?: boolean;
poolMax?: number;
toolFilter?: string;
logLevel?:
| "debug"
| "info"
| "notice"
| "warning"
| "error"
| "critical"
| "alert"
| "emergency";
transport?: TransportType;
port?: number;
oauthEnabled?: boolean;
oauthIssuer?: string;
oauthAudience?: string;
oauthJwksUri?: string;
oauthClockTolerance?: number;
}
interface ListToolsOptions {
filter?: string;
group?: string;
}
const program = new Command();
program
.name("postgres-mcp")
.description(
"PostgreSQL MCP Server - Full-featured database tools for AI with OAuth 2.0",
)
.version(VERSION);
program
// Connection options
.option(
"--postgres <url>",
"PostgreSQL connection string (postgres://user:pass@host:port/database)",
)
.option("--host <host>", "PostgreSQL host (default: localhost)")
.option("--pg-port <port>", "PostgreSQL port (default: 5432)", parseInt)
.option("--user <user>", "PostgreSQL username")
.option("--password <password>", "PostgreSQL password")
.option("--database <database>", "PostgreSQL database name")
.option("--ssl", "Enable SSL connection")
.option(
"--pool-max <size>",
"Maximum pool connections (default: 10)",
parseInt,
)
// Server options
.option(
"--transport, -t <type>",
"Transport type: stdio, http, sse (default: stdio)",
)
.option(
"--port, -p <port>",
"HTTP port for http/sse transports (default: 3000)",
parseInt,
)
.option(
"--tool-filter <filter>",
'Tool filter string (e.g., "-vector,-postgis")',
)
.option(
"--log-level <level>",
"Log level: debug, info, notice, warning, error, critical, alert, emergency (default: info)",
)
// OAuth options
.option("--oauth-enabled, -o", "Enable OAuth 2.0 authentication")
.option("--oauth-issuer <url>", "Authorization server URL (issuer)")
.option("--oauth-audience <aud>", "Expected token audience")
.option(
"--oauth-jwks-uri <url>",
"JWKS URI (auto-discovered from issuer if not set)",
)
.option(
"--oauth-clock-tolerance <seconds>",
"Clock tolerance in seconds (default: 60)",
parseInt,
)
.action(async (options: CliOptions) => {
// Set log level
const logLevel =
options.logLevel ?? (process.env["LOG_LEVEL"] as typeof options.logLevel);
if (logLevel) {
logger.setLevel(logLevel);
}
// Build database config
const dbConfig = buildDatabaseConfig(options);
// Build OAuth config
const oauthConfig = await buildOAuthConfig(options);
// Create adapter and connect
const adapter = new PostgresAdapter();
try {
await adapter.connect(dbConfig);
// Get tool filter from option or environment
const toolFilter =
options.toolFilter ??
process.env["POSTGRES_TOOL_FILTER"] ??
process.env["MCP_TOOL_FILTER"];
if (toolFilter) {
const filterConfig = parseToolFilter(toolFilter);
logger.info(getFilterSummary(filterConfig));
}
// Log OAuth status
if (oauthConfig?.enabled) {
logger.info("OAuth 2.0 authentication enabled", {
issuer: oauthConfig.issuer,
});
}
// Determine transport type
const transport = (options.transport ??
process.env["MCP_TRANSPORT"] ??
"stdio") as TransportType;
if (transport === "http" || transport === "sse") {
// Start with HTTP transport
await startHttpServer(adapter, toolFilter, oauthConfig, options);
} else {
// Start with stdio transport (default)
await startStdioServer(adapter, toolFilter);
}
} catch (error) {
logger.error("Failed to start server", {
error: error instanceof Error ? error.message : String(error),
});
await adapter.disconnect();
process.exit(1);
}
});
/**
* Build database configuration from CLI options and environment
*/
function buildDatabaseConfig(options: CliOptions): DatabaseConfig {
const config: DatabaseConfig = {
type: "postgresql",
};
// Parse connection string or individual options
if (options.postgres) {
const url = new URL(options.postgres);
config.host = url.hostname;
config.port = parseInt(url.port, 10) || 5432;
config.username = url.username;
config.password = url.password;
config.database = url.pathname.slice(1); // Remove leading /
if (
url.searchParams.get("ssl") === "true" ||
url.searchParams.get("sslmode") === "require"
) {
config.options = { ssl: true };
}
} else {
config.host =
options.host ??
process.env["PGHOST"] ??
process.env["POSTGRES_HOST"] ??
"localhost";
config.port =
options.pgPort ??
parseInt(
process.env["PGPORT"] ?? process.env["POSTGRES_PORT"] ?? "5432",
10,
);
config.username =
options.user ??
process.env["PGUSER"] ??
process.env["POSTGRES_USER"] ??
"postgres";
config.password =
options.password ??
process.env["PGPASSWORD"] ??
process.env["POSTGRES_PASSWORD"] ??
"";
config.database =
options.database ??
process.env["PGDATABASE"] ??
process.env["POSTGRES_DATABASE"] ??
"postgres";
if (options.ssl) {
config.options = { ssl: true };
}
}
// Pool configuration
if (options.poolMax !== undefined && options.poolMax > 0) {
config.pool = { max: options.poolMax };
}
return config;
}
/**
* Build OAuth configuration from CLI options and environment
*/
async function buildOAuthConfig(
options: CliOptions,
): Promise<OAuthConfig | undefined> {
// Check if OAuth is enabled
const oauthEnabled =
options.oauthEnabled ?? process.env["OAUTH_ENABLED"] === "true";
if (!oauthEnabled) {
return undefined;
}
const issuer = options.oauthIssuer ?? process.env["OAUTH_ISSUER"];
const audience = options.oauthAudience ?? process.env["OAUTH_AUDIENCE"];
let jwksUri = options.oauthJwksUri ?? process.env["OAUTH_JWKS_URI"];
const clockTolerance =
options.oauthClockTolerance ??
(process.env["OAUTH_CLOCK_TOLERANCE"]
? parseInt(process.env["OAUTH_CLOCK_TOLERANCE"], 10)
: 60);
// Auto-discover JWKS URI if not provided
if (!jwksUri && issuer) {
try {
const discovery = new AuthorizationServerDiscovery({
authServerUrl: issuer,
});
jwksUri = await discovery.getJwksUri();
logger.debug("JWKS URI discovered from issuer", { jwksUri });
} catch (error) {
logger.warn("Failed to discover JWKS URI, OAuth may not work correctly", {
error: String(error),
});
}
}
// Build OAuth config (we already checked oauthEnabled at function start)
const oauthConfig: OAuthConfig = {
enabled: true,
clockTolerance,
};
if (issuer) oauthConfig.authorizationServerUrl = issuer;
if (issuer) oauthConfig.issuer = issuer;
if (audience) oauthConfig.audience = audience;
if (jwksUri) oauthConfig.jwksUri = jwksUri;
return oauthConfig;
}
/**
* Start the server with stdio transport
*/
async function startStdioServer(
adapter: PostgresAdapter,
toolFilter?: string,
): Promise<void> {
const server = new PostgresMcpServer({
name: "postgres-mcp",
version: VERSION,
adapter,
toolFilter,
});
// Handle shutdown
const shutdown = (): void => {
logger.info("Shutting down...");
void server
.stop()
.then(() => adapter.disconnect())
.then(() => process.exit(0));
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
await server.start();
}
/**
* Start the server with HTTP transport
*/
async function startHttpServer(
adapter: PostgresAdapter,
toolFilter: string | undefined,
oauthConfig: OAuthConfig | undefined,
options: CliOptions,
): Promise<void> {
const port = options.port ?? parseInt(process.env["PORT"] ?? "3000", 10);
const host = process.env["HOST"] ?? "localhost";
// Create OAuth components if enabled
let resourceServer: OAuthResourceServer | undefined;
let tokenValidator: TokenValidator | undefined;
if (
oauthConfig?.enabled &&
oauthConfig.issuer &&
oauthConfig.jwksUri &&
oauthConfig.audience
) {
resourceServer = new OAuthResourceServer({
resource: `http://${host}:${String(port)}`,
authorizationServers: [oauthConfig.issuer],
scopesSupported: [...ALL_SCOPES],
});
tokenValidator = new TokenValidator({
jwksUri: oauthConfig.jwksUri,
issuer: oauthConfig.issuer,
audience: oauthConfig.audience,
clockTolerance: oauthConfig.clockTolerance,
});
}
// Create MCP server
const mcpServer = new PostgresMcpServer({
name: "postgres-mcp",
version: VERSION,
adapter,
toolFilter,
});
// Build HTTP transport config
const transportConfig: HttpTransportConfig = {
port,
host,
publicPaths: oauthConfig?.publicPaths ?? ["/health", "/.well-known/*"],
};
if (resourceServer) transportConfig.resourceServer = resourceServer;
if (tokenValidator) transportConfig.tokenValidator = tokenValidator;
// Create HTTP transport with OAuth
const httpTransport = new HttpTransport(transportConfig, (transport) => {
// Connect MCP server to the transport when client connects
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
void mcpServer.getMcpServer().connect(transport as any);
});
// Handle shutdown
const shutdown = (): void => {
logger.info("Shutting down...");
void httpTransport
.stop()
.then(() => mcpServer.stop())
.then(() => adapter.disconnect())
.then(() => process.exit(0));
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
// Start HTTP server
await httpTransport.start();
logger.info(
`PostgreSQL MCP Server started on http://${host}:${String(port)}`,
);
if (oauthConfig?.enabled) {
logger.info(
"OAuth 2.0 protected resource metadata available at /.well-known/oauth-protected-resource",
);
}
}
// List tools command
program
.command("list-tools")
.description("List all available tools")
.option("--filter <filter>", "Apply tool filter")
.option("--group <group>", "Filter by tool group")
// eslint-disable-next-line @typescript-eslint/require-await
.action(async (options: ListToolsOptions) => {
const adapter = new PostgresAdapter();
const tools = adapter.getToolDefinitions();
const filterConfig = parseToolFilter(options.filter);
let filteredTools = tools;
if (options.group) {
filteredTools = tools.filter((t) => t.group === options.group);
}
filteredTools = filteredTools.filter((t) =>
filterConfig.enabledTools.has(t.name),
);
// Use stderr for all output - stdout is reserved for MCP protocol
console.error(
`\nPostgreSQL MCP Tools (${String(filteredTools.length)}/${String(tools.length)}):\n`,
);
// Group by category
const grouped = new Map<string, typeof tools>();
for (const tool of filteredTools) {
const groupTools = grouped.get(tool.group) ?? [];
groupTools.push(tool);
grouped.set(tool.group, groupTools);
}
for (const [group, groupTools] of grouped) {
console.error(`[${group}] (${String(groupTools.length)})`);
for (const tool of groupTools) {
const desc = tool.description.split(".")[0] ?? "";
console.error(` - ${tool.name}: ${desc}`);
}
console.error("");
}
});
// Print tool count
program
.command("info")
.description("Show server information")
// eslint-disable-next-line @typescript-eslint/require-await
.action(async () => {
const adapter = new PostgresAdapter();
const tools = adapter.getToolDefinitions();
const resources = adapter.getResourceDefinitions();
const prompts = adapter.getPromptDefinitions();
const groups = adapter.getSupportedToolGroups();
// Use stderr for all output - stdout is reserved for MCP protocol
console.error("\nPostgreSQL MCP Server");
console.error("=====================");
console.error(`Version: ${VERSION}`);
console.error(`Tools: ${String(tools.length)}`);
console.error(`Resources: ${String(resources.length)}`);
console.error(`Prompts: ${String(prompts.length)}`);
console.error(`Tool Groups: ${groups.join(", ")}`);
console.error("\nTransports: stdio (default), http, sse");
console.error("OAuth 2.0: Supported (RFC 9728/8414)");
console.error("\nCapabilities:");
const caps = adapter.getCapabilities();
for (const [cap, enabled] of Object.entries(caps)) {
console.error(` ${cap}: ${enabled ? "✓" : "✗"}`);
}
});
program.parse();