const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const { runRemoteCommand } = require('./ssh');
const { startInstance, stopInstance } = require('./gcp');
const app = express();
const PORT = process.env.PORT || 3000;
// Store for OAuth clients (in production, use a database)
const oauthClients = new Map();
// --- Middleware ---
app.use(cors());
app.use(bodyParser.json());
// Helper function to send JSON-RPC response
function sendJsonRpcResponse(res, id, result = null, error = null) {
const response = {
jsonrpc: '2.0',
id
};
if (error) {
response.error = error;
} else {
response.result = result;
}
res.json(response);
}
// Helper function to send JSON-RPC notification
function sendJsonRpcNotification(res, method, params = {}) {
const message = {
jsonrpc: '2.0',
method,
params
};
res.write(`data: ${JSON.stringify(message)}\n\n`);
}
// Define MCP tools
// --- MCP tools (improved descriptions for Cursor suggestions) ---
const tools = [
{
name: 'run_command',
description: 'Execute a SSH command on the dev server (always connects to "dev" host regardless of host specified). Useful for checking disk usage, running maintenance commands, or viewing logs on servers.',
inputSchema: {
type: 'object',
properties: {
host: { type: 'string', description: 'The hostname or IP address (ignored - always uses "dev")' },
cmdKey: { type: 'string', description: 'The command you want to run, e.g., "df -h" or "ls -la"' }
},
required: ['host', 'cmdKey']
}
},
{
name: 'start_instance',
description: 'Start a Google Cloud Compute Engine (GCE) virtual machine. Use for powering up instances in your project and zone.',
inputSchema: {
type: 'object',
properties: {
project: { type: 'string', description: 'GCP project ID containing the VM' },
zone: { type: 'string', description: 'Zone where the VM resides, e.g., us-central1-a' },
instance: { type: 'string', description: 'The name of the instance to start' }
},
required: ['project', 'zone', 'instance']
}
},
{
name: 'stop_instance',
description: 'Stop a Google Cloud Compute Engine (GCE) virtual machine. Use to safely shut down a VM to save costs or perform maintenance.',
inputSchema: {
type: 'object',
properties: {
project: { type: 'string', description: 'GCP project ID containing the VM' },
zone: { type: 'string', description: 'Zone where the VM resides, e.g., us-central1-a' },
instance: { type: 'string', description: 'The name of the instance to stop' }
},
required: ['project', 'zone', 'instance']
}
}
];
// --- MCP JSON-RPC endpoint (POST /) ---
app.post('/', async (req, res) => {
try {
const { method, params, id, jsonrpc } = req.body || {};
if (!method) {
console.error('Missing method in request');
return res.status(400).json({ error: 'Missing method' });
}
if (jsonrpc !== '2.0') {
console.error('Invalid JSON-RPC version:', jsonrpc);
return sendJsonRpcResponse(res, id, null, {
code: -32600,
message: 'Invalid Request'
});
}
// Handle notifications (no id)
if (id === undefined) {
switch (method) {
case 'initialized':
console.log('Client initialized');
return res.status(200).json({ jsonrpc: '2.0' });
default:
console.log(`Received notification: ${method}`);
return res.status(200).json({ jsonrpc: '2.0' });
}
}
// Handle requests (with id)
switch (method) {
case 'initialize':
console.log('Initializing MCP server...');
sendJsonRpcResponse(res, id, {
protocolVersion: params.protocolVersion || '2025-06-18',
capabilities: {
tools: {
listChanged: false
},
prompts: {},
resources: {}
},
serverInfo: {
name: 'mcp-remote',
version: '1.0.0'
}
});
break;
case 'tools/list':
console.log('Listing tools...');
sendJsonRpcResponse(res, id, {
tools
});
break;
case 'tools/call':
try {
if (!params || !params.name) {
throw new Error('Missing tool name in params');
}
const { name, arguments: args } = params;
console.log(`Calling tool: ${name} with args:`, args);
let result;
switch (name) {
case 'run_command':
try {
// IGNORE the host parameter from user - always use "dev" from ~/.ssh/config
// The host alias "dev" must be defined in ~/.ssh/config
const hostAlias = 'dev';
const command = args.cmdKey || args.command || '';
if (!command) {
throw new Error('Command is required');
}
// Log that we're ignoring the user's host and using dev instead
const userSpecifiedHost = args.host || 'unspecified';
console.log(`Ignoring host "${userSpecifiedHost}" - always using "dev" from ~/.ssh/config`);
console.log(`Executing command on ${hostAlias} (from ~/.ssh/config): ${command}`);
// Connection details are read from ~/.ssh/config for the "dev" host
const sshResult = await runRemoteCommand({
host: hostAlias, // Always "dev" regardless of user input
command
});
// Build output message
let output = `Command executed on "dev" (from ~/.ssh/config):\n\n`;
output += `Command: ${command}\n\n`;
if (sshResult.stdout) {
output += `STDOUT:\n${sshResult.stdout}\n`;
}
if (sshResult.stderr) {
output += `STDERR:\n${sshResult.stderr}\n`;
}
output += `\nExit code: ${sshResult.code}`;
result = {
content: [
{
type: 'text',
text: output
}
]
};
} catch (error) {
console.error('SSH command execution error:', error);
throw new Error(`Failed to execute SSH command: ${error.message}`);
}
break;
case 'start_instance':
try {
await startInstance({
project: args.project,
zone: args.zone,
instance: args.instance
});
result = {
content: [
{
type: 'text',
text: `Instance '${args.instance}' started successfully`
}
]
};
} catch (error) {
throw new Error(`Failed to start instance: ${error.message}`);
}
break;
case 'stop_instance':
try {
await stopInstance({
project: args.project,
zone: args.zone,
instance: args.instance
});
result = {
content: [
{
type: 'text',
text: `Instance '${args.instance}' stopped successfully`
}
]
};
} catch (error) {
throw new Error(`Failed to stop instance: ${error.message}`);
}
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
sendJsonRpcResponse(res, id, result);
} catch (error) {
sendJsonRpcResponse(res, id, null, {
code: -32603,
message: error.message || 'Internal error'
});
}
break;
case 'ping':
sendJsonRpcResponse(res, id, {});
break;
default:
console.log(`Unknown method: ${method}`);
sendJsonRpcResponse(res, id, null, {
code: -32601,
message: `Method not found: ${method}`
});
}
} catch (error) {
console.error('Error processing request:', error);
if (id !== undefined) {
sendJsonRpcResponse(res, id, null, {
code: -32603,
message: error.message || 'Internal error'
});
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
// --- SSE endpoint for MCP (GET / with text/event-stream) ---
app.get('/', (req, res) => {
if (req.headers.accept && req.headers.accept.includes('text/event-stream')) {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Headers', 'Cache-Control');
// Send initial connection message
res.write(': connected\n\n');
// Keep connection alive
const keepAlive = setInterval(() => {
res.write(': keepalive\n\n');
}, 30000);
req.on('close', () => {
clearInterval(keepAlive);
res.end();
});
} else {
res.status(404).json({ error: 'Not found' });
}
});
// --- OAuth Well-known endpoints ---
app.get('/.well-known/oauth-protected-resource', (req, res) => {
res.json({
resource: 'mcp-remote',
scopes_supported: ['mcp'],
bearer_methods_supported: ['none']
});
});
app.get('/.well-known/oauth-authorization-server', (req, res) => {
res.json({
issuer: `http://localhost:${PORT}`,
authorization_endpoint: `http://localhost:${PORT}/authorize`,
token_endpoint: `http://localhost:${PORT}/token`,
registration_endpoint: `http://localhost:${PORT}/register`,
scopes_supported: ['mcp'],
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
token_endpoint_auth_methods_supported: ['none']
});
});
app.get('/.well-known/openid-configuration', (req, res) => {
res.json({
issuer: `http://localhost:${PORT}`,
authorization_endpoint: `http://localhost:${PORT}/authorize`,
token_endpoint: `http://localhost:${PORT}/token`,
registration_endpoint: `http://localhost:${PORT}/register`,
scopes_supported: ['mcp', 'openid'],
response_types_supported: ['code'],
grant_types_supported: ['authorization_code', 'refresh_token'],
token_endpoint_auth_methods_supported: ['none']
});
});
// --- OAuth registration endpoint ---
app.post('/register', (req, res) => {
const { redirect_uris, client_name } = req.body;
// Generate a client ID and secret
const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const clientSecret = `secret_${Math.random().toString(36).substr(2, 32)}`;
const client = {
client_id: clientId,
client_secret: clientSecret,
redirect_uris: redirect_uris || [],
client_name: client_name || 'MCP Client',
token_endpoint_auth_method: 'none',
grant_types: ['authorization_code', 'refresh_token'],
response_types: ['code']
};
oauthClients.set(clientId, client);
console.log('Registered OAuth client:', clientId);
res.json({
client_id: clientId,
client_secret: clientSecret,
client_id_issued_at: Math.floor(Date.now() / 1000),
client_secret_expires_at: 0, // Never expires
redirect_uris: client.redirect_uris,
grant_types: client.grant_types,
response_types: client.response_types,
token_endpoint_auth_method: client.token_endpoint_auth_method
});
});
// --- OAuth authorize endpoint (placeholder) ---
app.get('/authorize', (req, res) => {
// For MCP, we might not need full OAuth flow
// This is a placeholder
res.json({
message: 'OAuth authorization endpoint',
note: 'MCP may handle authentication differently'
});
});
// --- OAuth token endpoint (placeholder) ---
app.post('/token', (req, res) => {
// For MCP, we might not need full OAuth flow
// This is a placeholder
res.json({
access_token: 'placeholder_token',
token_type: 'Bearer',
expires_in: 3600
});
});
// --- Legacy endpoints (for backward compatibility) ---
app.get('/manifest', (req, res) => {
res.json({
name: 'mcp-remote',
version: '1.0.0',
description: 'Local MCP for SSH and GCE',
tools
});
});
// --- Start server ---
app.listen(PORT, () => {
console.log(`MCP server running on http://localhost:${PORT}`);
console.log('Ready to accept MCP connections from Cursor');
});