/**
* mysql-mcp - MCP Server
*
* Main MCP server implementation with adapter registration,
* tool filtering, and transport handling.
*/
import { McpServer as SdkMcpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { generateInstructions } from '../constants/ServerInstructions.js';
import type { DatabaseAdapter } from '../adapters/DatabaseAdapter.js';
import type { McpServerConfig, TransportType, DatabaseConfig, ToolFilterConfig } from '../types/index.js';
import { parseToolFilter, getFilterSummary } from '../filtering/ToolFilter.js';
import { logger } from '../utils/logger.js';
import { mcpLogger } from '../logging/McpLogging.js';
import { progressFactory } from '../progress/ProgressReporter.js';
import { OAuthResourceServer } from '../auth/OAuthResourceServer.js';
import { TokenValidator } from '../auth/TokenValidator.js';
/**
* Default server configuration
*/
export const DEFAULT_CONFIG: McpServerConfig = {
name: 'mysql-mcp',
version: '0.1.0',
transport: 'stdio',
databases: []
};
/**
* MySQL MCP Server
*/
export class McpServer {
private server: SdkMcpServer;
private adapters = new Map<string, DatabaseAdapter>();
private config: McpServerConfig;
private toolFilter: ToolFilterConfig;
private started = false;
private activeTransport: { stop(): Promise<void> } | null = null;
constructor(config: Partial<McpServerConfig> = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
this.toolFilter = parseToolFilter(this.config.toolFilter);
// Generate dynamic instructions based on enabled tools
const instructions = generateInstructions(
this.toolFilter.enabledTools,
[], // Resources will be added when adapter is registered
[] // Prompts will be added when adapter is registered
);
this.server = new SdkMcpServer(
{
name: this.config.name,
version: this.config.version
},
{
capabilities: {
logging: {}
},
instructions
}
);
// Initialize MCP protocol logging so clients can receive log messages
mcpLogger.setServer(this.server);
// Initialize MCP protocol progress reporting for long-running operations
progressFactory.setServer(this.server);
// Log tool filter summary
if (this.toolFilter.rules.length > 0) {
logger.info(getFilterSummary(this.toolFilter));
}
// Log server initialization with capabilities
logger.info('MCP Server initialized', {
name: this.config.name,
version: this.config.version,
toolFilter: this.config.toolFilter ?? 'none',
capabilities: ['logging']
});
}
/**
* Register a database adapter
*/
registerAdapter(adapter: DatabaseAdapter, alias?: string): void {
const key = alias ?? `${adapter.type}:default`;
if (this.adapters.has(key)) {
logger.warn(`Adapter already registered: ${key}`);
return;
}
this.adapters.set(key, adapter);
// Get counts before registration
const allTools = adapter.getToolDefinitions();
const allResources = adapter.getResourceDefinitions();
const allPrompts = adapter.getPromptDefinitions();
// Register adapter's tools, resources, and prompts
adapter.registerTools(this.server, this.toolFilter.enabledTools);
adapter.registerResources(this.server);
adapter.registerPrompts(this.server);
// Count enabled tools
const enabledToolCount = allTools.filter(t => this.toolFilter.enabledTools.has(t.name)).length;
logger.info(`Registered adapter: ${adapter.name} (${key})`);
logger.info(` Tools: ${enabledToolCount}/${allTools.length} enabled`);
logger.info(` Resources: ${allResources.length}`);
logger.info(` Prompts: ${allPrompts.length}`);
mcpLogger.info(`Database adapter registered: ${adapter.name} (${enabledToolCount} tools, ${allResources.length} resources, ${allPrompts.length} prompts)`);
}
/**
* Get a registered adapter by key
*/
getAdapter(key: string): DatabaseAdapter | undefined {
return this.adapters.get(key);
}
/**
* Get all registered adapters
*/
getAdapters(): Map<string, DatabaseAdapter> {
return this.adapters;
}
/**
* Start the MCP server
*/
async start(): Promise<void> {
if (this.started) {
logger.warn('Server already started');
return;
}
logger.info('Starting MCP server...');
try {
await this.startTransport(this.config.transport);
this.started = true;
// Enable MCP protocol logging now that transport is connected
mcpLogger.setConnected(true);
logger.info('Server started successfully');
mcpLogger.info('MySQL MCP server ready', { transport: this.config.transport });
} catch (error) {
logger.error('Failed to start server', { error: String(error) });
throw error;
}
}
/**
* Start the specified transport
*/
private async startTransport(transport: TransportType): Promise<void> {
switch (transport) {
case 'stdio':
await this.startStdioTransport();
break;
case 'http':
case 'sse': {
const { createHttpTransport } = await import('../transports/http.js');
const port = this.config.port ?? 3000;
const transport = createHttpTransport({
port,
host: 'localhost', // Default to localhost, could be configurable
corsOrigins: ['*'], // Allow all for now, or make configurable
// Pass OAuth config if enabled
...(this.config.oauth?.enabled ? {
resourceServer: this.createOAuthResourceServer(),
tokenValidator: this.createTokenValidator()
} : {})
}, (sseTransport) => {
logger.info('New SSE connection');
void this.server.connect(sseTransport);
});
await transport.start();
this.activeTransport = transport;
break;
}
default:
throw new Error(`Unknown transport: ${String(transport)}`);
}
}
/**
* Start stdio transport
*/
private async startStdioTransport(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
/**
* Stop the MCP server
*/
async stop(): Promise<void> {
if (!this.started) {
return;
}
logger.info('Stopping MCP server...');
// Disable MCP logging before disconnecting
mcpLogger.setConnected(false);
// Disconnect all adapters
for (const [key, adapter] of this.adapters) {
try {
await adapter.disconnect();
logger.info(`Disconnected adapter: ${key}`);
} catch (error) {
logger.error(`Error disconnecting adapter ${key}`, { error: String(error) });
}
}
if (this.activeTransport) {
try {
await this.activeTransport.stop();
} catch (error) {
logger.error('Error stopping transport', { error: String(error) });
}
this.activeTransport = null;
}
await this.server.close();
this.started = false;
logger.info('Server stopped');
}
/**
* Get server configuration
*/
getConfig(): McpServerConfig {
return { ...this.config };
}
/**
* Get tool filter configuration
*/
getToolFilter(): ToolFilterConfig {
return this.toolFilter;
}
/**
* Check if server is running
*/
isRunning(): boolean {
return this.started;
}
/**
* Get the underlying MCP SDK server instance
*/
getSdkServer(): SdkMcpServer {
return this.server;
}
/**
* Create OAuth resource server from config
*/
private createOAuthResourceServer(): OAuthResourceServer {
if (!this.config.oauth?.enabled) {
throw new Error('OAuth is not enabled');
}
// Use audience as resource ID if not explicitly configured in future
const resourceId = this.config.oauth.audience ?? 'mysql-mcp';
const issuer = this.config.oauth.issuer;
if (!issuer) {
throw new Error('OAuth issuer is required');
}
return new OAuthResourceServer({
resource: resourceId,
authorizationServers: [issuer],
scopesSupported: ['read', 'write', 'admin'],
bearerMethodsSupported: ['header']
});
}
/**
* Create token validator from config
*/
private createTokenValidator(): TokenValidator {
if (!this.config.oauth?.enabled) {
throw new Error('OAuth is not enabled');
}
if (!this.config.oauth.jwksUri) {
throw new Error('OAuth JWKS URI is required for validation');
}
const issuer = this.config.oauth.issuer;
const audience = this.config.oauth.audience;
if (!issuer || !audience) {
throw new Error('OAuth issuer and audience are required');
}
return new TokenValidator({
issuer,
audience,
jwksUri: this.config.oauth.jwksUri,
clockTolerance: this.config.oauth.clockTolerance
});
}
}
/**
* Create a new MCP server instance
*/
export function createServer(config: Partial<McpServerConfig> = {}): McpServer {
return new McpServer(config);
}
/**
* Parse database configuration from connection string
*/
export function parseMySQLConnectionString(connectionString: string): DatabaseConfig {
// Parse mysql://user:password@host:port/database
const url = new URL(connectionString);
return {
type: 'mysql',
host: url.hostname,
port: parseInt(url.port, 10) || 3306,
username: decodeURIComponent(url.username),
password: decodeURIComponent(url.password),
database: url.pathname.slice(1), // Remove leading /
options: Object.fromEntries(url.searchParams)
};
}