chore: Enhance MCP server execution and compatibility with Cursor mode

- Introduce environment variables for Cursor compatibility in silent-mcp.sh and npx-entry.cjs
- Implement process cleanup for existing MCP instances to prevent conflicts
- Adjust logging behavior based on execution context to ensure proper message handling
- Add test-cursor.sh script to simulate Cursor environment for testing purposes
- Refactor stdio-server.ts to manage logging and message flushing based on compatibility mode
This commit is contained in:
jango-blockchained
2025-03-17 18:30:33 +01:00
parent 1bc11de465
commit 2d5ae034c9
4 changed files with 187 additions and 55 deletions

View File

@@ -4,9 +4,24 @@ const fs = require('fs');
const path = require('path'); const path = require('path');
const { spawn } = require('child_process'); const { spawn } = require('child_process');
// Set environment variable - enable stdio transport and silence output // Set environment variable - enable stdio transport
process.env.USE_STDIO_TRANSPORT = 'true'; process.env.USE_STDIO_TRANSPORT = 'true';
// Check if we're being called from Cursor (check for Cursor specific env vars)
const isCursor = process.env.CURSOR_SESSION || process.env.CURSOR_CHANNEL;
// For Cursor, we need to ensure consistent stdio handling
if (isCursor) {
// Essential for Cursor compatibility
process.env.LOG_LEVEL = 'info';
process.env.CURSOR_COMPATIBLE = 'true';
// Ensure we have a clean environment for Cursor
delete process.env.SILENT_MCP_RUNNING;
} else {
// For normal operation, silence logs
process.env.LOG_LEVEL = 'silent'; process.env.LOG_LEVEL = 'silent';
}
// Ensure logs directory exists // Ensure logs directory exists
const logsDir = path.join(process.cwd(), 'logs'); const logsDir = path.join(process.cwd(), 'logs');
@@ -22,15 +37,80 @@ if (!fs.existsSync(envPath) && fs.existsSync(envExamplePath)) {
fs.copyFileSync(envExamplePath, envPath); fs.copyFileSync(envExamplePath, envPath);
} }
// Start the MCP server with redirected stderr // Define a function to ensure the child process is properly cleaned up on exit
try { function setupCleanExit(childProcess) {
// Use our silent-mcp.sh script if it exists, otherwise use mcp-stdio.cjs const exitHandler = () => {
const silentScriptPath = path.join(process.cwd(), 'silent-mcp.sh'); if (childProcess && !childProcess.killed) {
childProcess.kill();
}
process.exit();
};
if (fs.existsSync(silentScriptPath) && fs.statSync(silentScriptPath).isFile()) { // Handle various termination signals
// Execute the silent-mcp.sh script instead process.on('SIGINT', exitHandler);
const childProcess = spawn('/bin/bash', [silentScriptPath], { process.on('SIGTERM', exitHandler);
process.on('exit', exitHandler);
}
// Start the MCP server
try {
// Critical: For Cursor, we need a very specific execution environment
if (isCursor) {
// Careful process cleanup for Cursor (optional but can help)
try {
const { execSync } = require('child_process');
execSync('pkill -f "node.*stdio-server" || true', { stdio: 'ignore' });
} catch (e) {
// Ignore errors from process cleanup
}
// Allow some time for process cleanup
setTimeout(() => {
const scriptPath = path.join(__dirname, 'mcp-stdio.cjs');
// For Cursor, we need very specific stdio handling
// Using pipe for both stdin and stdout is critical
const childProcess = spawn('node', [scriptPath], {
stdio: ['pipe', 'pipe', 'pipe'], // All piped for maximum control
env: {
...process.env,
USE_STDIO_TRANSPORT: 'true',
CURSOR_COMPATIBLE: 'true',
// Make sure stdin/stdout are treated as binary
NODE_OPTIONS: '--no-force-async-hooks-checks'
}
});
// Ensure no buffering to prevent missed messages
childProcess.stdin.setDefaultEncoding('utf8');
// Create bidirectional pipes
process.stdin.pipe(childProcess.stdin);
childProcess.stdout.pipe(process.stdout);
childProcess.stderr.pipe(process.stderr);
// Setup error handling
childProcess.on('error', (err) => {
console.error('Failed to start server:', err.message);
process.exit(1);
});
// Ensure child process is properly cleaned up
setupCleanExit(childProcess);
}, 500); // Short delay to ensure clean start
}
// For regular use, if silent-mcp.sh exists, use it
else if (!isCursor && fs.existsSync(path.join(process.cwd(), 'silent-mcp.sh')) &&
fs.statSync(path.join(process.cwd(), 'silent-mcp.sh')).isFile()) {
// Execute the silent-mcp.sh script
const childProcess = spawn('/bin/bash', [path.join(process.cwd(), 'silent-mcp.sh')], {
stdio: ['inherit', 'inherit', 'ignore'], // Redirect stderr to /dev/null stdio: ['inherit', 'inherit', 'ignore'], // Redirect stderr to /dev/null
env: {
...process.env,
USE_STDIO_TRANSPORT: 'true',
LOG_LEVEL: 'silent'
}
}); });
childProcess.on('error', (err) => { childProcess.on('error', (err) => {
@@ -38,25 +118,18 @@ try {
process.exit(1); process.exit(1);
}); });
// Properly handle process termination // Ensure child process is properly cleaned up
process.on('SIGINT', () => { setupCleanExit(childProcess);
childProcess.kill('SIGINT'); }
}); // Otherwise run normally (direct non-Cursor)
else {
process.on('SIGTERM', () => {
childProcess.kill('SIGTERM');
});
} else {
// Fall back to original method if silent-mcp.sh doesn't exist
const scriptPath = path.join(__dirname, 'mcp-stdio.cjs'); const scriptPath = path.join(__dirname, 'mcp-stdio.cjs');
// Use 'pipe' for stdout and ignore (null) for stderr
const childProcess = spawn('node', [scriptPath], { const childProcess = spawn('node', [scriptPath], {
stdio: ['inherit', 'pipe', 'ignore'], // Redirect stderr to /dev/null stdio: ['inherit', 'pipe', 'ignore'], // Redirect stderr to /dev/null for normal use
env: { env: {
...process.env, ...process.env,
USE_STDIO_TRANSPORT: 'true', USE_STDIO_TRANSPORT: 'true'
LOG_LEVEL: 'silent'
} }
}); });
@@ -68,14 +141,8 @@ try {
process.exit(1); process.exit(1);
}); });
// Properly handle process termination // Ensure child process is properly cleaned up
process.on('SIGINT', () => { setupCleanExit(childProcess);
childProcess.kill('SIGINT');
});
process.on('SIGTERM', () => {
childProcess.kill('SIGTERM');
});
} }
} catch (error) { } catch (error) {
console.error('Error starting server:', error.message); console.error('Error starting server:', error.message);

View File

@@ -1,14 +1,25 @@
#!/bin/bash #!/bin/bash
# Ensure we're running in a clean environment for MCP
# Set silent environment variables # Set silent environment variables
export LOG_LEVEL=silent export LOG_LEVEL=silent
export USE_STDIO_TRANSPORT=true export USE_STDIO_TRANSPORT=true
# Check if we're running from npx or directly # Explicitly mark that we are NOT in Cursor mode
export CURSOR_COMPATIBLE=false
# Flag to prevent recursive execution
export SILENT_MCP_RUNNING=true
# Clean up any existing processes - optional but can help with "already" errors
# pkill -f "node.*stdio-server" >/dev/null 2>&1 || true
# Direct execution - always use local file
if [ -f "./dist/stdio-server.js" ]; then if [ -f "./dist/stdio-server.js" ]; then
# Direct run from project directory - use local file # Keep stdout intact (for JSON-RPC messages) but redirect stderr to /dev/null
node ./dist/stdio-server.js 2>/dev/null node ./dist/stdio-server.js 2>/dev/null
else else
# Run using npx # If no local file, run directly through node using the globally installed package
npx homeassistant-mcp 2>/dev/null # This avoids calling npx again which would create a loop
node $(npm root -g)/homeassistant-mcp/dist/stdio-server.js 2>/dev/null
fi fi

View File

@@ -5,8 +5,10 @@
* over standard input/output using JSON-RPC 2.0 protocol. * over standard input/output using JSON-RPC 2.0 protocol.
*/ */
// Force silent logging // Only force silent logging if not in Cursor compatibility mode
if (!process.env.CURSOR_COMPATIBLE) {
process.env.LOG_LEVEL = 'silent'; process.env.LOG_LEVEL = 'silent';
}
import { createStdioServer, BaseTool } from "./mcp/index.js"; import { createStdioServer, BaseTool } from "./mcp/index.js";
import { z } from "zod"; import { z } from "zod";
@@ -17,18 +19,36 @@ import { MCPContext } from "./mcp/types.js";
import { LightsControlTool } from './tools/homeassistant/lights.tool.js'; import { LightsControlTool } from './tools/homeassistant/lights.tool.js';
import { ClimateControlTool } from './tools/homeassistant/climate.tool.js'; import { ClimateControlTool } from './tools/homeassistant/climate.tool.js';
// Check for silent startup mode - never silent in npx mode to ensure the JSON-RPC messages are sent // Check for Cursor compatibility mode
const silentStartup = true; const isCursorMode = process.env.CURSOR_COMPATIBLE === 'true';
// Use silent startup except in Cursor mode
const silentStartup = !isCursorMode;
const debugMode = process.env.DEBUG_STDIO === 'true'; const debugMode = process.env.DEBUG_STDIO === 'true';
// Send a notification directly to stdout for Cursor compatibility // Configure raw I/O handling if necessary
if (isCursorMode) {
// Ensure stdout doesn't buffer for Cursor
process.stdout.setDefaultEncoding('utf8');
// Only try to set raw mode if it's a TTY and the method exists
if (process.stdout.isTTY && typeof (process.stdout as any).setRawMode === 'function') {
(process.stdout as any).setRawMode(true);
}
}
// Send a notification directly to stdout for compatibility
function sendNotification(method: string, params: any): void { function sendNotification(method: string, params: any): void {
const notification = { const notification = {
jsonrpc: '2.0', jsonrpc: '2.0',
method, method,
params params
}; };
process.stdout.write(JSON.stringify(notification) + '\n'); const message = JSON.stringify(notification) + '\n';
process.stdout.write(message);
// For Cursor mode, ensure messages are flushed if method exists
if (isCursorMode && typeof (process.stdout as any).flush === 'function') {
(process.stdout as any).flush();
}
} }
// Create system tools // Create system tools
@@ -79,19 +99,7 @@ async function main() {
// Combine all tools // Combine all tools
const allTools = [...systemTools, ...haTools]; const allTools = [...systemTools, ...haTools];
// Create server with stdio transport // Send initial notifications BEFORE server initialization for Cursor compatibility
const { server, transport } = createStdioServer({
silent: silentStartup,
debug: debugMode,
tools: allTools
});
// Explicitly set the server reference to ensure access to tools
if ('setServer' in transport && typeof transport.setServer === 'function') {
transport.setServer(server);
}
// Send initial notifications directly to stdout for Cursor compatibility
// Send system info // Send system info
sendNotification('system.info', { sendNotification('system.info', {
name: 'Home Assistant Model Context Protocol Server', name: 'Home Assistant Model Context Protocol Server',
@@ -118,9 +126,42 @@ async function main() {
tools: toolDefinitions tools: toolDefinitions
}); });
// Start the server // Create server with stdio transport
const { server, transport } = createStdioServer({
silent: silentStartup,
debug: debugMode,
tools: allTools
});
// Explicitly set the server reference to ensure access to tools
if ('setServer' in transport && typeof transport.setServer === 'function') {
transport.setServer(server);
}
// Start the server after initial notifications
await server.start(); await server.start();
// In Cursor mode, send notifications again after startup
if (isCursorMode) {
// Small delay to ensure all messages are processed
setTimeout(() => {
// Send system info again
sendNotification('system.info', {
name: 'Home Assistant Model Context Protocol Server',
version: '1.0.0',
transport: 'stdio',
protocol: 'json-rpc-2.0',
features: ['streaming'],
timestamp: new Date().toISOString()
});
// Send available tools again
sendNotification('tools.available', {
tools: toolDefinitions
});
}, 100);
}
// Handle process exit // Handle process exit
process.on('SIGINT', async () => { process.on('SIGINT', async () => {
await server.shutdown(); await server.shutdown();

13
test-cursor.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/bash
# Clean up any existing processes first
pkill -f "node.*stdio-server" >/dev/null 2>&1 || true
# Simulate Cursor environment by setting env variables
export CURSOR_SESSION=test-session
export CURSOR_COMPATIBLE=true
export USE_STDIO_TRANSPORT=true
export LOG_LEVEL=info
# Run npx with the simulated environment
npx homeassistant-mcp