mcp-hub-004: Sample MCP backend with echo tool
This commit is contained in:
parent
9a61026bca
commit
67bbb40830
4 changed files with 202 additions and 1 deletions
|
|
@ -4,7 +4,8 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
|
|
||||||
152
sample-mcp/index.js
Normal file
152
sample-mcp/index.js
Normal file
|
|
@ -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();
|
||||||
36
sample-mcp/package-lock.json
generated
Normal file
36
sample-mcp/package-lock.json
generated
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
sample-mcp/package.json
Normal file
12
sample-mcp/package.json
Normal file
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue