diff --git a/package.json b/package.json index e3f2d8e..5260d78 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "start": "node src/index.js" + "start": "node src/index.js", + "sample-mcp": "node sample-mcp/index.js" }, "keywords": [], "author": "", diff --git a/sample-mcp/index.js b/sample-mcp/index.js new file mode 100644 index 0000000..dd3d59b --- /dev/null +++ b/sample-mcp/index.js @@ -0,0 +1,152 @@ +'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(); diff --git a/sample-mcp/package-lock.json b/sample-mcp/package-lock.json new file mode 100644 index 0000000..a0a21d2 --- /dev/null +++ b/sample-mcp/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "sample-mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sample-mcp", + "version": "1.0.0", + "dependencies": { + "ws": "^8.19.0" + } + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/sample-mcp/package.json b/sample-mcp/package.json new file mode 100644 index 0000000..5493ef0 --- /dev/null +++ b/sample-mcp/package.json @@ -0,0 +1,12 @@ +{ + "name": "sample-mcp", + "version": "1.0.0", + "description": "Sample MCP backend for testing hub relay", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "ws": "^8.19.0" + } +}