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:
@@ -4,9 +4,24 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
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.LOG_LEVEL = 'silent';
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
// Ensure logs directory exists
|
||||
const logsDir = path.join(process.cwd(), 'logs');
|
||||
@@ -22,15 +37,80 @@ if (!fs.existsSync(envPath) && fs.existsSync(envExamplePath)) {
|
||||
fs.copyFileSync(envExamplePath, envPath);
|
||||
}
|
||||
|
||||
// Start the MCP server with redirected stderr
|
||||
try {
|
||||
// Use our silent-mcp.sh script if it exists, otherwise use mcp-stdio.cjs
|
||||
const silentScriptPath = path.join(process.cwd(), 'silent-mcp.sh');
|
||||
// Define a function to ensure the child process is properly cleaned up on exit
|
||||
function setupCleanExit(childProcess) {
|
||||
const exitHandler = () => {
|
||||
if (childProcess && !childProcess.killed) {
|
||||
childProcess.kill();
|
||||
}
|
||||
process.exit();
|
||||
};
|
||||
|
||||
if (fs.existsSync(silentScriptPath) && fs.statSync(silentScriptPath).isFile()) {
|
||||
// Execute the silent-mcp.sh script instead
|
||||
const childProcess = spawn('/bin/bash', [silentScriptPath], {
|
||||
// Handle various termination signals
|
||||
process.on('SIGINT', exitHandler);
|
||||
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
|
||||
env: {
|
||||
...process.env,
|
||||
USE_STDIO_TRANSPORT: 'true',
|
||||
LOG_LEVEL: 'silent'
|
||||
}
|
||||
});
|
||||
|
||||
childProcess.on('error', (err) => {
|
||||
@@ -38,25 +118,18 @@ try {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Properly handle process termination
|
||||
process.on('SIGINT', () => {
|
||||
childProcess.kill('SIGINT');
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
childProcess.kill('SIGTERM');
|
||||
});
|
||||
} else {
|
||||
// Fall back to original method if silent-mcp.sh doesn't exist
|
||||
// Ensure child process is properly cleaned up
|
||||
setupCleanExit(childProcess);
|
||||
}
|
||||
// Otherwise run normally (direct non-Cursor)
|
||||
else {
|
||||
const scriptPath = path.join(__dirname, 'mcp-stdio.cjs');
|
||||
|
||||
// Use 'pipe' for stdout and ignore (null) for stderr
|
||||
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: {
|
||||
...process.env,
|
||||
USE_STDIO_TRANSPORT: 'true',
|
||||
LOG_LEVEL: 'silent'
|
||||
USE_STDIO_TRANSPORT: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,14 +141,8 @@ try {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Properly handle process termination
|
||||
process.on('SIGINT', () => {
|
||||
childProcess.kill('SIGINT');
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
childProcess.kill('SIGTERM');
|
||||
});
|
||||
// Ensure child process is properly cleaned up
|
||||
setupCleanExit(childProcess);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting server:', error.message);
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Ensure we're running in a clean environment for MCP
|
||||
# Set silent environment variables
|
||||
export LOG_LEVEL=silent
|
||||
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
|
||||
# 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
|
||||
else
|
||||
# Run using npx
|
||||
npx homeassistant-mcp 2>/dev/null
|
||||
# If no local file, run directly through node using the globally installed package
|
||||
# This avoids calling npx again which would create a loop
|
||||
node $(npm root -g)/homeassistant-mcp/dist/stdio-server.js 2>/dev/null
|
||||
fi
|
||||
@@ -5,8 +5,10 @@
|
||||
* over standard input/output using JSON-RPC 2.0 protocol.
|
||||
*/
|
||||
|
||||
// Force silent logging
|
||||
process.env.LOG_LEVEL = 'silent';
|
||||
// Only force silent logging if not in Cursor compatibility mode
|
||||
if (!process.env.CURSOR_COMPATIBLE) {
|
||||
process.env.LOG_LEVEL = 'silent';
|
||||
}
|
||||
|
||||
import { createStdioServer, BaseTool } from "./mcp/index.js";
|
||||
import { z } from "zod";
|
||||
@@ -17,18 +19,36 @@ import { MCPContext } from "./mcp/types.js";
|
||||
import { LightsControlTool } from './tools/homeassistant/lights.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
|
||||
const silentStartup = true;
|
||||
// Check for Cursor compatibility mode
|
||||
const isCursorMode = process.env.CURSOR_COMPATIBLE === 'true';
|
||||
// Use silent startup except in Cursor mode
|
||||
const silentStartup = !isCursorMode;
|
||||
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 {
|
||||
const notification = {
|
||||
jsonrpc: '2.0',
|
||||
method,
|
||||
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
|
||||
@@ -79,19 +99,7 @@ async function main() {
|
||||
// Combine all tools
|
||||
const allTools = [...systemTools, ...haTools];
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Send initial notifications directly to stdout for Cursor compatibility
|
||||
// Send initial notifications BEFORE server initialization for Cursor compatibility
|
||||
// Send system info
|
||||
sendNotification('system.info', {
|
||||
name: 'Home Assistant Model Context Protocol Server',
|
||||
@@ -118,9 +126,42 @@ async function main() {
|
||||
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();
|
||||
|
||||
// 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
|
||||
process.on('SIGINT', async () => {
|
||||
await server.shutdown();
|
||||
|
||||
13
test-cursor.sh
Executable file
13
test-cursor.sh
Executable 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
|
||||
Reference in New Issue
Block a user