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 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);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
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