fix: resolve Docker stdio initialization timeout issue
- Add InitializeRequestSchema handler to MCP server - Implement stdout flushing for Docker environments - Create stdio-wrapper for clean JSON-RPC communication - Update docker-entrypoint.sh to prevent stdout pollution - Fix logger to check MCP_MODE before level check These changes ensure the MCP server responds to initialization requests within Claude Desktop's 60-second timeout when running in Docker.
This commit is contained in:
@@ -46,33 +46,53 @@ export interface ColumnDefinition {
|
||||
*/
|
||||
export async function createDatabaseAdapter(dbPath: string): Promise<DatabaseAdapter> {
|
||||
// Log Node.js version information
|
||||
logger.info(`Node.js version: ${process.version}`);
|
||||
logger.info(`Platform: ${process.platform} ${process.arch}`);
|
||||
// Only log in non-stdio mode
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.info(`Node.js version: ${process.version}`);
|
||||
}
|
||||
// Only log in non-stdio mode
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.info(`Platform: ${process.platform} ${process.arch}`);
|
||||
}
|
||||
|
||||
// First, try to use better-sqlite3
|
||||
try {
|
||||
logger.info('Attempting to use better-sqlite3...');
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.info('Attempting to use better-sqlite3...');
|
||||
}
|
||||
const adapter = await createBetterSQLiteAdapter(dbPath);
|
||||
logger.info('Successfully initialized better-sqlite3 adapter');
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.info('Successfully initialized better-sqlite3 adapter');
|
||||
}
|
||||
return adapter;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// Check if it's a version mismatch error
|
||||
if (errorMessage.includes('NODE_MODULE_VERSION') || errorMessage.includes('was compiled against a different Node.js version')) {
|
||||
logger.warn(`Node.js version mismatch detected. Better-sqlite3 was compiled for a different Node.js version.`);
|
||||
logger.warn(`Current Node.js version: ${process.version}`);
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.warn(`Node.js version mismatch detected. Better-sqlite3 was compiled for a different Node.js version.`);
|
||||
}
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.warn(`Current Node.js version: ${process.version}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.warn('Failed to initialize better-sqlite3, falling back to sql.js', error);
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.warn('Failed to initialize better-sqlite3, falling back to sql.js', error);
|
||||
}
|
||||
|
||||
// Fall back to sql.js
|
||||
try {
|
||||
const adapter = await createSQLJSAdapter(dbPath);
|
||||
logger.info('Successfully initialized sql.js adapter (pure JavaScript, no native dependencies)');
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.info('Successfully initialized sql.js adapter (pure JavaScript, no native dependencies)');
|
||||
}
|
||||
return adapter;
|
||||
} catch (sqlJsError) {
|
||||
logger.error('Failed to initialize sql.js adapter', sqlJsError);
|
||||
if (process.env.MCP_MODE !== 'stdio') {
|
||||
logger.error('Failed to initialize sql.js adapter', sqlJsError);
|
||||
}
|
||||
throw new Error('Failed to initialize any database adapter');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
InitializeRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
@@ -102,6 +103,27 @@ export class N8NDocumentationMCPServer {
|
||||
}
|
||||
|
||||
private setupHandlers(): void {
|
||||
// Handle initialization
|
||||
this.server.setRequestHandler(InitializeRequestSchema, async () => {
|
||||
const response = {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
serverInfo: {
|
||||
name: 'n8n-documentation-mcp',
|
||||
version: '2.4.1',
|
||||
},
|
||||
};
|
||||
|
||||
// Debug: Log to stderr to see if handler is called
|
||||
if (process.env.DEBUG_MCP === 'true') {
|
||||
console.error('Initialize handler called, returning:', JSON.stringify(response));
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
|
||||
// Handle tool listing
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools: n8nDocumentationToolsFinal,
|
||||
@@ -915,9 +937,15 @@ Full documentation is being prepared. For now, use get_node_essentials for confi
|
||||
|
||||
// Force flush stdout for Docker environments
|
||||
// Docker uses block buffering which can delay MCP responses
|
||||
if (!process.stdout.isTTY) {
|
||||
// Write empty string to force flush
|
||||
process.stdout.write('', () => {});
|
||||
if (!process.stdout.isTTY || process.env.IS_DOCKER) {
|
||||
// Override write to auto-flush
|
||||
const originalWrite = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = function(chunk: any, encoding?: any, callback?: any) {
|
||||
const result = originalWrite(chunk, encoding, callback);
|
||||
// Force immediate flush
|
||||
process.stdout.emit('drain');
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
logger.info('n8n Documentation MCP Server running on stdio transport');
|
||||
|
||||
52
src/mcp/stdio-wrapper.ts
Normal file
52
src/mcp/stdio-wrapper.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Stdio wrapper for MCP server
|
||||
* Ensures clean JSON-RPC communication by suppressing all non-JSON output
|
||||
*/
|
||||
|
||||
// Suppress all console output before anything else
|
||||
const originalConsoleLog = console.log;
|
||||
const originalConsoleError = console.error;
|
||||
const originalConsoleWarn = console.warn;
|
||||
const originalConsoleInfo = console.info;
|
||||
const originalConsoleDebug = console.debug;
|
||||
|
||||
// Override all console methods
|
||||
console.log = () => {};
|
||||
console.error = () => {};
|
||||
console.warn = () => {};
|
||||
console.info = () => {};
|
||||
console.debug = () => {};
|
||||
|
||||
// Set environment to ensure logger suppression
|
||||
process.env.MCP_MODE = 'stdio';
|
||||
process.env.DISABLE_CONSOLE_OUTPUT = 'true';
|
||||
process.env.LOG_LEVEL = 'error';
|
||||
|
||||
// Import and run the server
|
||||
import { N8NDocumentationMCPServer } from './server-update';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const server = new N8NDocumentationMCPServer();
|
||||
await server.run();
|
||||
} catch (error) {
|
||||
// In case of fatal error, output to stderr only
|
||||
originalConsoleError('Fatal error:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle uncaught errors silently
|
||||
process.on('uncaughtException', (error) => {
|
||||
originalConsoleError('Uncaught exception:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
originalConsoleError('Unhandled rejection:', reason);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
main();
|
||||
@@ -51,15 +51,19 @@ export class Logger {
|
||||
}
|
||||
|
||||
private log(level: LogLevel, levelName: string, message: string, ...args: any[]): void {
|
||||
// Check environment variables FIRST, before level check
|
||||
// In stdio mode, suppress ALL console output to avoid corrupting JSON-RPC
|
||||
const isStdio = process.env.MCP_MODE === 'stdio';
|
||||
const isDisabled = process.env.DISABLE_CONSOLE_OUTPUT === 'true';
|
||||
|
||||
if (isStdio || isDisabled) {
|
||||
// Silently drop all logs in stdio mode
|
||||
return;
|
||||
}
|
||||
|
||||
if (level <= this.config.level) {
|
||||
const formattedMessage = this.formatMessage(levelName, message);
|
||||
|
||||
// In stdio mode, suppress ALL console output to avoid corrupting JSON-RPC
|
||||
if (process.env.MCP_MODE === 'stdio' || process.env.DISABLE_CONSOLE_OUTPUT === 'true') {
|
||||
// Silently drop all logs in stdio mode
|
||||
return;
|
||||
}
|
||||
|
||||
// In HTTP mode during request handling, suppress console output
|
||||
// The ConsoleManager will handle this, but we add a safety check
|
||||
if (process.env.MCP_MODE === 'http' && process.env.MCP_REQUEST_ACTIVE === 'true') {
|
||||
|
||||
Reference in New Issue
Block a user