mcp-hub/sample-mcp/index.js

152 lines
4.4 KiB
JavaScript

'use strict';
const WebSocket = require('ws');
const HUB_URL = 'wss://mcp.arik.work/ws/register';
const HUB_URL_FALLBACK = 'ws://mcp.arik.work/ws/register';
const SERVICE_ID = 'sample-mcp';
const SECRET = 'dev-secret';
let reconnectDelay = 1000;
let ws = null;
let useTLS = true;
function getHubUrl() {
return useTLS ? HUB_URL : HUB_URL_FALLBACK;
}
function connect() {
const url = getHubUrl();
console.log(`[sample-mcp] Connecting to hub at ${url}`);
ws = new WebSocket(url);
ws.on('open', () => {
reconnectDelay = 1000;
console.log('[sample-mcp] Connected to hub');
const registerMsg = { type: 'register', serviceId: SERVICE_ID, secret: SECRET };
ws.send(JSON.stringify(registerMsg));
console.log(`[sample-mcp] Sent register: serviceId=${SERVICE_ID}`);
});
ws.on('message', (data) => {
let envelope;
try {
envelope = JSON.parse(data);
} catch (err) {
console.error('[sample-mcp] Failed to parse message:', err.message);
return;
}
if (envelope.type !== 'mcp-request') {
console.log(`[sample-mcp] Ignoring message type: ${envelope.type}`);
return;
}
const { requestId, clientSessionId, payload } = envelope;
console.log(`[sample-mcp] Request received: requestId=${requestId} method=${payload && payload.method}`);
const method = payload && payload.method;
// Notifications have no id and need no response
if (method === 'notifications/initialized') {
console.log('[sample-mcp] Got notifications/initialized, no response needed');
return;
}
let result;
switch (method) {
case 'initialize':
result = {
protocolVersion: '2024-11-05',
capabilities: { tools: {} },
serverInfo: { name: 'sample-mcp', version: '1.0.0' },
};
break;
case 'tools/list':
result = {
tools: [
{
name: 'echo',
description: 'Echoes back the input',
inputSchema: {
type: 'object',
properties: { message: { type: 'string' } },
required: ['message'],
},
},
],
};
break;
case 'tools/call': {
const toolName = payload.params && payload.params.name;
const args = (payload.params && payload.params.arguments) || {};
if (toolName === 'echo') {
result = {
content: [{ type: 'text', text: `Echo: ${args.message}` }],
};
} else {
const errorResponse = {
jsonrpc: '2.0',
id: payload.id,
error: { code: -32601, message: `Unknown tool: ${toolName}` },
};
sendResponse(requestId, clientSessionId, errorResponse);
return;
}
break;
}
default:
console.warn(`[sample-mcp] Unknown method: ${method}`);
const errorResponse = {
jsonrpc: '2.0',
id: payload.id,
error: { code: -32601, message: `Method not found: ${method}` },
};
sendResponse(requestId, clientSessionId, errorResponse);
return;
}
const jsonrpcResponse = { jsonrpc: '2.0', id: payload.id, result };
sendResponse(requestId, clientSessionId, jsonrpcResponse);
});
ws.on('close', (code, reason) => {
console.log(`[sample-mcp] Disconnected (code=${code} reason=${reason}), reconnecting in ${reconnectDelay}ms`);
scheduleReconnect();
});
ws.on('error', (err) => {
console.error(`[sample-mcp] WebSocket error: ${err.message}`);
if (useTLS && err.message && (err.message.includes('ECONNREFUSED') || err.message.includes('certificate') || err.message.includes('connect'))) {
console.log('[sample-mcp] TLS connection failed, will try ws:// on next attempt');
useTLS = false;
}
});
}
function sendResponse(requestId, clientSessionId, jsonrpcPayload) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
console.error('[sample-mcp] Cannot send response: not connected');
return;
}
const envelope = {
type: 'mcp-response',
requestId,
clientSessionId,
payload: jsonrpcPayload,
};
ws.send(JSON.stringify(envelope));
console.log(`[sample-mcp] Response sent: requestId=${requestId} method=${jsonrpcPayload.result ? 'ok' : 'error'}`);
}
function scheduleReconnect() {
setTimeout(() => {
connect();
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
}, reconnectDelay);
}
connect();