890 lines
33 KiB
JavaScript
890 lines
33 KiB
JavaScript
import {Server} from "@modelcontextprotocol/sdk/server/index.js";
|
|
import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
import WebSocket from 'ws';
|
|
import {
|
|
CallToolRequestSchema,
|
|
CreateMessageRequestSchema,
|
|
GetPromptRequestSchema,
|
|
ListPromptsRequestSchema,
|
|
ListResourcesRequestSchema,
|
|
ListResourceTemplatesRequestSchema,
|
|
ListToolsRequestSchema,
|
|
ReadResourceRequestSchema
|
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
import {generateNewRegistrationToken} from "./tokens.js";
|
|
import {CONFIG} from "./config.js";
|
|
import {execSync} from "child_process";
|
|
|
|
// Create a central MCP server that communicates over stdio
|
|
const mcpServer = new Server(
|
|
{
|
|
name: "@nucleoriofrio/webmcp",
|
|
version: "0.2.0",
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {
|
|
listChanged: true
|
|
},
|
|
prompts: {
|
|
listChanged: true
|
|
},
|
|
resources: {
|
|
listChanged: true,
|
|
subscribe: true
|
|
},
|
|
sampling: {}
|
|
}
|
|
}
|
|
);
|
|
|
|
// WebSocket client connection
|
|
let wsClient = null;
|
|
|
|
// MCP specific channel path
|
|
const MCP_PATH = '/mcp';
|
|
|
|
// Map to store pending requests from WebSocket to MCP
|
|
const pendingRequests = new Map();
|
|
let requestIdCounter = 1;
|
|
|
|
// Debounce timers for list changed notifications
|
|
let toolListChangedTimer = null;
|
|
let promptListChangedTimer = null;
|
|
let resourceListChangedTimer = null;
|
|
const LIST_CHANGED_DEBOUNCE_MS = 500;
|
|
|
|
function debouncedToolListChanged() {
|
|
if (toolListChangedTimer) clearTimeout(toolListChangedTimer);
|
|
toolListChangedTimer = setTimeout(async () => {
|
|
toolListChangedTimer = null;
|
|
try { await mcpServer.sendToolListChanged(); } catch (e) { console.error('Error sending tool list changed:', e); }
|
|
}, LIST_CHANGED_DEBOUNCE_MS);
|
|
}
|
|
|
|
function debouncedPromptListChanged() {
|
|
if (promptListChangedTimer) clearTimeout(promptListChangedTimer);
|
|
promptListChangedTimer = setTimeout(async () => {
|
|
promptListChangedTimer = null;
|
|
try { await mcpServer.sendPromptListChanged(); } catch (e) { console.error('Error sending prompt list changed:', e); }
|
|
}, LIST_CHANGED_DEBOUNCE_MS);
|
|
}
|
|
|
|
function debouncedResourceListChanged() {
|
|
if (resourceListChangedTimer) clearTimeout(resourceListChangedTimer);
|
|
resourceListChangedTimer = setTimeout(async () => {
|
|
resourceListChangedTimer = null;
|
|
try { await mcpServer.sendResourceListChanged(); } catch (e) { console.error('Error sending resource list changed:', e); }
|
|
}, LIST_CHANGED_DEBOUNCE_MS);
|
|
}
|
|
|
|
// Function to handle WebSocket messages
|
|
async function handleWebSocketMessage(message) {
|
|
try {
|
|
const data = JSON.parse(message);
|
|
console.error(`Received message: ${data.type}`);
|
|
|
|
if (data.type === 'toolResponse') {
|
|
// Handle tool response from WebSocket server
|
|
const {id, result, error} = data;
|
|
|
|
// Check if this is a response to a pending request
|
|
if (pendingRequests.has(id)) {
|
|
const {resolve, reject} = pendingRequests.get(id);
|
|
pendingRequests.delete(id);
|
|
|
|
if (error) {
|
|
reject(new Error(error));
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
} else {
|
|
console.error(`No pending request found for ID: ${id}`);
|
|
}
|
|
} else if (data.type === 'promptResponse') {
|
|
// Handle prompt response from WebSocket server
|
|
const {id, result, error} = data;
|
|
|
|
// Check if this is a response to a pending request
|
|
if (pendingRequests.has(id)) {
|
|
const {resolve, reject} = pendingRequests.get(id);
|
|
pendingRequests.delete(id);
|
|
|
|
if (error) {
|
|
reject(new Error(error));
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
} else {
|
|
console.error(`No pending request found for ID: ${id}`);
|
|
}
|
|
} else if (data.type === 'resourceResponse') {
|
|
// Handle resource response from WebSocket server
|
|
const {id, result, error} = data;
|
|
|
|
// Check if this is a response to a pending request
|
|
if (pendingRequests.has(id)) {
|
|
const {resolve, reject} = pendingRequests.get(id);
|
|
pendingRequests.delete(id);
|
|
|
|
if (error) {
|
|
reject(new Error(error));
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
} else {
|
|
console.error(`No pending request found for ID: ${id}`);
|
|
}
|
|
} else if (data.type === 'samplingResponse') {
|
|
// Handle sampling response from WebSocket server
|
|
const {id, result, error} = data;
|
|
|
|
// Check if this is a response to a pending request
|
|
if (pendingRequests.has(id)) {
|
|
const {resolve, reject} = pendingRequests.get(id);
|
|
pendingRequests.delete(id);
|
|
|
|
if (error) {
|
|
reject(new Error(error));
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
} else {
|
|
console.error(`No pending request found for ID: ${id}`);
|
|
}
|
|
} else if (data.type === 'listToolsResponse') {
|
|
// Handle list tools response from WebSocket server
|
|
const {id, tools, error} = data;
|
|
|
|
// Check if this is a response to a pending request
|
|
if (pendingRequests.has(id)) {
|
|
const {resolve, reject} = pendingRequests.get(id);
|
|
pendingRequests.delete(id);
|
|
|
|
if (error) {
|
|
reject(new Error(error));
|
|
} else {
|
|
resolve(tools);
|
|
}
|
|
} else {
|
|
console.error(`No pending request found for ID: ${id}`);
|
|
}
|
|
} else if (data.type === 'listPromptsResponse') {
|
|
// Handle list prompts response from WebSocket server
|
|
const {id, prompts, error} = data;
|
|
|
|
// Check if this is a response to a pending request
|
|
if (pendingRequests.has(id)) {
|
|
const {resolve, reject} = pendingRequests.get(id);
|
|
pendingRequests.delete(id);
|
|
|
|
if (error) {
|
|
reject(new Error(error));
|
|
} else {
|
|
resolve(prompts);
|
|
}
|
|
} else {
|
|
console.error(`No pending request found for ID: ${id}`);
|
|
}
|
|
} else if (data.type === 'listResourcesResponse') {
|
|
// Handle list resources response from WebSocket server
|
|
const {id, resources, resourceTemplates, error} = data;
|
|
|
|
// Check if this is a response to a pending request
|
|
if (pendingRequests.has(id)) {
|
|
const {resolve, reject} = pendingRequests.get(id);
|
|
pendingRequests.delete(id);
|
|
|
|
if (error) {
|
|
reject(new Error(error));
|
|
} else {
|
|
resolve({resources, resourceTemplates});
|
|
}
|
|
} else {
|
|
console.error(`No pending request found for ID: ${id}`);
|
|
}
|
|
} else if (data.type === 'toolRegistered') {
|
|
debouncedToolListChanged();
|
|
} else if (data.type === 'resourceRegistered') {
|
|
debouncedResourceListChanged();
|
|
} else if (data.type === 'promptRegistered') {
|
|
debouncedPromptListChanged();
|
|
} else if (data.type === 'welcome') {
|
|
// Welcome message from the server, we're already connected to the MCP path
|
|
console.error(`Connected to path: ${data.channel}`);
|
|
} else if (data.type === 'pong') {
|
|
// Pong response
|
|
console.error(`Received pong with timestamp: ${data.timestamp}`);
|
|
} else if (data.type === 'error') {
|
|
// Error message
|
|
console.error(`Received error: ${data.message}`);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error processing WebSocket message:', error);
|
|
}
|
|
}
|
|
|
|
// Function to connect to the WebSocket server
|
|
function connectToWebSocketServer(serverToken) {
|
|
// Connect to the MCP path directly with server token
|
|
const serverUrl = `ws://localhost:${CONFIG.port}${MCP_PATH}?token=${serverToken}`;
|
|
|
|
console.error(`Connecting to WebSocket server at ${MCP_PATH} with authentication...`);
|
|
|
|
wsClient = new WebSocket(serverUrl);
|
|
|
|
// Handle connection opening
|
|
wsClient.on('open', () => {
|
|
console.error(`Connected to WebSocket server on path: ${MCP_PATH}`);
|
|
});
|
|
|
|
// Handle incoming messages
|
|
wsClient.on('message', (message) => {
|
|
handleWebSocketMessage(message);
|
|
});
|
|
|
|
// Handle connection closing
|
|
wsClient.on('close', (code, reason) => {
|
|
console.error(`WebSocket connection closed: ${code} ${reason}`);
|
|
wsClient = null;
|
|
|
|
// Try to reconnect after a delay
|
|
setTimeout(connectToWebSocketServer, 5000);
|
|
});
|
|
|
|
// Handle connection errors
|
|
wsClient.on('error', (error) => {
|
|
console.error('WebSocket connection error:', error);
|
|
});
|
|
}
|
|
|
|
// Function to send a message to the WebSocket server
|
|
function sendMessage(message) {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
console.error('Cannot send message: WebSocket not connected');
|
|
return Promise.reject(new Error('WebSocket not connected'));
|
|
}
|
|
|
|
try {
|
|
wsClient.send(JSON.stringify(message));
|
|
return Promise.resolve();
|
|
} catch (error) {
|
|
console.error('Error sending message:', error);
|
|
return Promise.reject(error);
|
|
}
|
|
}
|
|
|
|
// Set up the MCP server to handle tool calls by sending them to the WebSocket server
|
|
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
if (request.params.name === "_webmcp_get-token") {
|
|
const token = await generateNewRegistrationToken();
|
|
|
|
// Copiar token al portapapeles del sistema
|
|
try {
|
|
execSync('clip.exe', { input: token, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
} catch (e) {
|
|
try {
|
|
execSync('xclip -selection clipboard', { input: token, stdio: ['pipe', 'ignore', 'ignore'] });
|
|
} catch (e2) {
|
|
// No hay clipboard disponible
|
|
}
|
|
}
|
|
|
|
// Enviar token a los navegadores conectados para copiar al portapapeles
|
|
try {
|
|
await sendMessage({
|
|
type: 'clipboardCopy',
|
|
text: token
|
|
});
|
|
} catch (e) {
|
|
// Ignorar si no hay clientes conectados
|
|
}
|
|
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Token copiado al portapapeles.\n${token}`,
|
|
}]
|
|
};
|
|
}
|
|
|
|
if (request.params.name === "_webmcp_clear-cache") {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return { content: [{ type: "text", text: "No hay conexion al servidor WebSocket" }], isError: true };
|
|
}
|
|
|
|
const requestId = (requestIdCounter++).toString();
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Clear cache timeout'));
|
|
}
|
|
}, 10000);
|
|
});
|
|
|
|
try {
|
|
await sendMessage({ id: requestId, type: 'clearRegistry' });
|
|
const result = await responsePromise;
|
|
await mcpServer.sendToolListChanged();
|
|
await mcpServer.sendPromptListChanged();
|
|
await mcpServer.sendResourceListChanged();
|
|
return { content: [{ type: "text", text: result }] };
|
|
} catch (e) {
|
|
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
if (request.params.name === "_webmcp_browser-info") {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return { content: [{ type: "text", text: "No hay conexion al servidor WebSocket" }], isError: true };
|
|
}
|
|
|
|
const requestId = (requestIdCounter++).toString();
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Timeout obteniendo info de navegadores'));
|
|
}
|
|
}, 10000);
|
|
});
|
|
|
|
try {
|
|
await sendMessage({ id: requestId, type: 'getClientInfo' });
|
|
const result = await responsePromise;
|
|
return result;
|
|
} catch (e) {
|
|
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
if (request.params.name === "_webmcp_agregar-tool") {
|
|
if (!CONFIG.dev) {
|
|
return { content: [{ type: "text", text: "agregar-tool solo esta disponible en modo desarrollo (--dev)" }], isError: true };
|
|
}
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return { content: [{ type: "text", text: "No hay conexion al servidor WebSocket" }], isError: true };
|
|
}
|
|
const { nombre, descripcion, codigo, parametros } = request.params.arguments;
|
|
if (!nombre || !descripcion || !codigo) {
|
|
return { content: [{ type: "text", text: "Se requieren: nombre, descripcion y codigo" }], isError: true };
|
|
}
|
|
|
|
const requestId = (requestIdCounter++).toString();
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Timeout creando herramienta'));
|
|
}
|
|
}, 15000);
|
|
});
|
|
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'createTool',
|
|
name: nombre,
|
|
description: descripcion,
|
|
code: codigo,
|
|
parametros: parametros
|
|
});
|
|
const result = await responsePromise;
|
|
await mcpServer.sendToolListChanged();
|
|
return result;
|
|
} catch (e) {
|
|
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
if (request.params.name === "_webmcp_quitar-tool") {
|
|
if (!CONFIG.dev) {
|
|
return { content: [{ type: "text", text: "quitar-tool solo esta disponible en modo desarrollo (--dev)" }], isError: true };
|
|
}
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return { content: [{ type: "text", text: "No hay conexion al servidor WebSocket" }], isError: true };
|
|
}
|
|
const { nombre, listar, todas } = request.params.arguments || {};
|
|
|
|
const requestId = (requestIdCounter++).toString();
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Timeout'));
|
|
}
|
|
}, 10000);
|
|
});
|
|
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'removeTool',
|
|
name: nombre,
|
|
listar: !!listar,
|
|
todas: !!todas
|
|
});
|
|
const result = await responsePromise;
|
|
await mcpServer.sendToolListChanged();
|
|
return result;
|
|
} catch (e) {
|
|
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
if (request.params.name === "_webmcp_server-info") {
|
|
if (!CONFIG.dev) {
|
|
return { content: [{ type: "text", text: "server-info solo esta disponible en modo desarrollo (--dev)" }], isError: true };
|
|
}
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return { content: [{ type: "text", text: "No hay conexion al servidor WebSocket" }], isError: true };
|
|
}
|
|
|
|
const requestId = (requestIdCounter++).toString();
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
pendingRequests.set(requestId, { resolve, reject });
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('Timeout obteniendo info del servidor'));
|
|
}
|
|
}, 10000);
|
|
});
|
|
|
|
try {
|
|
await sendMessage({ id: requestId, type: 'getServerInfo' });
|
|
const result = await responsePromise;
|
|
return result;
|
|
} catch (e) {
|
|
return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
|
|
}
|
|
}
|
|
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: "Not connected to WebSocket server"
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
|
|
// Create a unique request ID
|
|
const requestId = (requestIdCounter++).toString();
|
|
|
|
// Create a promise that will be resolved when we get a response
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
// Store the resolver functions
|
|
pendingRequests.set(requestId, {resolve, reject});
|
|
|
|
// Set a timeout to prevent hanging requests
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error(`Tool call timed out: ${request.params.name}`));
|
|
}
|
|
}, 30000); // 30 second timeout
|
|
});
|
|
|
|
// Send the request to the WebSocket server
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'callTool',
|
|
tool: request.params.name,
|
|
arguments: request.params.arguments
|
|
});
|
|
|
|
// Wait for the response
|
|
return await responsePromise;
|
|
} catch (error) {
|
|
return {
|
|
content: [{
|
|
type: "text",
|
|
text: `Error: ${error.message}`
|
|
}],
|
|
isError: true
|
|
};
|
|
}
|
|
});
|
|
|
|
// Set up the MCP server to handle list tools by querying the WebSocket server
|
|
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
const builtInTools = [
|
|
{
|
|
name: "_webmcp_get-token",
|
|
description: "Genera un token de conexion para vincular un navegador con @nucleoriofrio/webmcp. El usuario puede decir 'registrar token' o 'conectar webmcp'.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {},
|
|
},
|
|
},
|
|
{
|
|
name: "_webmcp_clear-cache",
|
|
description: "Limpia todas las herramientas, prompts y recursos registrados en el cache del servidor @nucleoriofrio/webmcp. Usar para forzar un estado limpio.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {},
|
|
},
|
|
},
|
|
{
|
|
name: "_webmcp_browser-info",
|
|
description: "Muestra informacion de los navegadores conectados: user agent, URL, hostname, idioma, resolucion y tiempo de conexion.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {},
|
|
},
|
|
},
|
|
];
|
|
|
|
// Tools de desarrollo: solo disponibles con --dev o WEBMCP_DEV=true
|
|
if (CONFIG.dev) {
|
|
builtInTools.push(
|
|
{
|
|
name: "_webmcp_agregar-tool",
|
|
description: "[DEV] Registra una nueva herramienta dinamicamente. El agente puede usar esto para crear herramientas nuevas en tiempo real.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
nombre: { type: "string", description: "Nombre de la herramienta" },
|
|
descripcion: { type: "string", description: "Descripcion de lo que hace" },
|
|
parametros: { type: "string", description: "JSON string con las properties del schema, ej: {\"msg\":{\"type\":\"string\",\"description\":\"mensaje\"}}" },
|
|
codigo: { type: "string", description: "Codigo JavaScript del body de la funcion. Recibe \"args\" como parametro. Debe retornar un string." }
|
|
},
|
|
required: ["nombre", "descripcion", "codigo"]
|
|
},
|
|
},
|
|
{
|
|
name: "_webmcp_quitar-tool",
|
|
description: "[DEV] Desregistra herramientas. Usa listar=true para ver las disponibles, todas=true para quitar todas, o nombre para quitar una especifica.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
nombre: { type: "string", description: "Nombre de la herramienta a quitar" },
|
|
listar: { type: "boolean", description: "Si es true, lista las herramientas en vez de quitar" },
|
|
todas: { type: "boolean", description: "Si es true, quita todas las herramientas" }
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "_webmcp_server-info",
|
|
description: "[DEV] Muestra estado del servidor @nucleoriofrio/webmcp: version, commit, host, puerto, canales activos, clientes, registry de tools/prompts/resources, uptime y requests pendientes.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {},
|
|
},
|
|
}
|
|
);
|
|
}
|
|
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return {tools: builtInTools};
|
|
}
|
|
|
|
// Create a unique request ID
|
|
const requestId = (requestIdCounter++).toString();
|
|
|
|
// Create a promise that will be resolved when we get a response
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
// Store the resolver functions
|
|
pendingRequests.set(requestId, {resolve, reject});
|
|
|
|
// Set a timeout to prevent hanging requests
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('List tools request timed out'));
|
|
}
|
|
}, 10000); // 10 second timeout
|
|
});
|
|
|
|
// Send the request to the WebSocket server
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'listTools'
|
|
});
|
|
|
|
const tools = await responsePromise;
|
|
|
|
// Wait for the response
|
|
return { tools: [...tools, ...builtInTools] };
|
|
} catch (error) {
|
|
console.error('Error listing tools:', error);
|
|
return {tools: []}; // Return empty list on error
|
|
}
|
|
});
|
|
|
|
// Set up the MCP server to handle list prompts by querying the WebSocket server
|
|
mcpServer.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
const builtInPrompts = [];
|
|
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return {
|
|
prompts: builtInPrompts
|
|
};
|
|
}
|
|
|
|
// Create a unique request ID
|
|
const requestId = (requestIdCounter++).toString();
|
|
|
|
// Create a promise that will be resolved when we get a response
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
// Store the resolver functions
|
|
pendingRequests.set(requestId, {resolve, reject});
|
|
|
|
// Set a timeout to prevent hanging requests
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('List prompts request timed out'));
|
|
}
|
|
}, 10000); // 10 second timeout
|
|
});
|
|
|
|
// Send the request to the WebSocket server
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'listPrompts'
|
|
});
|
|
|
|
const prompts = await responsePromise;
|
|
|
|
// Wait for the response
|
|
return {
|
|
prompts: [
|
|
...prompts,
|
|
...builtInPrompts
|
|
],
|
|
};
|
|
} catch (error) {
|
|
console.error('Error listing prompts:', error);
|
|
return {prompts: []}; // Return empty list on error
|
|
}
|
|
});
|
|
|
|
// Set up the MCP server to handle get prompt by querying the WebSocket server
|
|
mcpServer.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
throw new Error("Not connected to WebSocket server");
|
|
}
|
|
|
|
// Create a unique request ID
|
|
const requestId = (requestIdCounter++).toString();
|
|
|
|
// Create a promise that will be resolved when we get a response
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
// Store the resolver functions
|
|
pendingRequests.set(requestId, {resolve, reject});
|
|
|
|
// Set a timeout to prevent hanging requests
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error(`Get prompt request timed out: ${request.params.name}`));
|
|
}
|
|
}, 30000); // 30 second timeout
|
|
});
|
|
|
|
// Send the request to the WebSocket server
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'getPrompt',
|
|
name: request.params.name,
|
|
arguments: request.params.arguments
|
|
});
|
|
|
|
// Wait for the response
|
|
return await responsePromise;
|
|
} catch (error) {
|
|
console.error('Error getting prompt:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Set up the MCP server to handle list resources by querying the WebSocket server
|
|
mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return {resources: []};
|
|
}
|
|
|
|
// Create a unique request ID
|
|
const requestId = (requestIdCounter++).toString();
|
|
|
|
// Create a promise that will be resolved when we get a response
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
// Store the resolver functions
|
|
pendingRequests.set(requestId, {resolve, reject});
|
|
|
|
// Set a timeout to prevent hanging requests
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('List resources request timed out'));
|
|
}
|
|
}, 10000); // 10 second timeout
|
|
});
|
|
|
|
// Send the request to the WebSocket server
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'listResources'
|
|
});
|
|
|
|
const {resources} = await responsePromise;
|
|
|
|
// Wait for the response
|
|
return {resources};
|
|
} catch (error) {
|
|
console.error('Error listing resources:', error);
|
|
return {resources: []}; // Return empty list on error
|
|
}
|
|
});
|
|
|
|
// Set up the MCP server to handle list resource templates by querying the WebSocket server
|
|
mcpServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
return {resourceTemplates: []};
|
|
}
|
|
|
|
// Create a unique request ID
|
|
const requestId = (requestIdCounter++).toString();
|
|
|
|
// Create a promise that will be resolved when we get a response
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
// Store the resolver functions
|
|
pendingRequests.set(requestId, {resolve, reject});
|
|
|
|
// Set a timeout to prevent hanging requests
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error('List resource templates request timed out'));
|
|
}
|
|
}, 10000); // 10 second timeout
|
|
});
|
|
|
|
// Send the request to the WebSocket server
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'listResources'
|
|
});
|
|
|
|
const {resourceTemplates} = await responsePromise;
|
|
|
|
// Wait for the response
|
|
return {resourceTemplates};
|
|
} catch (error) {
|
|
console.error('Error listing resource templates:', error);
|
|
return {resourceTemplates: []}; // Return empty list on error
|
|
}
|
|
});
|
|
|
|
// Set up the MCP server to handle read resource by querying the WebSocket server
|
|
mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
throw new Error("Not connected to WebSocket server");
|
|
}
|
|
|
|
// Create a unique request ID
|
|
const requestId = (requestIdCounter++).toString();
|
|
|
|
// Create a promise that will be resolved when we get a response
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
// Store the resolver functions
|
|
pendingRequests.set(requestId, {resolve, reject});
|
|
|
|
// Set a timeout to prevent hanging requests
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error(`Read resource request timed out: ${request.params.uri}`));
|
|
}
|
|
}, 30000); // 30 second timeout
|
|
});
|
|
|
|
// Send the request to the WebSocket server
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'readResource',
|
|
uri: request.params.uri
|
|
});
|
|
|
|
// Wait for the response
|
|
return await responsePromise;
|
|
} catch (error) {
|
|
console.error('Error reading resource:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Set up the MCP server to handle sampling by querying the WebSocket server
|
|
mcpServer.setRequestHandler(CreateMessageRequestSchema, async (request) => {
|
|
if (!wsClient || wsClient.readyState !== WebSocket.OPEN) {
|
|
throw new Error("Not connected to WebSocket server");
|
|
}
|
|
|
|
// Create a unique request ID
|
|
const requestId = (requestIdCounter++).toString();
|
|
|
|
// Create a promise that will be resolved when we get a response
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
// Store the resolver functions
|
|
pendingRequests.set(requestId, {resolve, reject});
|
|
|
|
// Set a timeout to prevent hanging requests
|
|
setTimeout(() => {
|
|
if (pendingRequests.has(requestId)) {
|
|
pendingRequests.delete(requestId);
|
|
reject(new Error(`Sampling request timed out`));
|
|
}
|
|
}, 120000); // 120 second timeout (sampling can take longer)
|
|
});
|
|
|
|
// Send the request to the WebSocket server with all parameters from the request
|
|
try {
|
|
await sendMessage({
|
|
id: requestId,
|
|
type: 'createSamplingMessage',
|
|
messages: request.params.messages,
|
|
systemPrompt: request.params.systemPrompt,
|
|
includeContext: request.params.includeContext,
|
|
temperature: request.params.temperature,
|
|
maxTokens: request.params.maxTokens,
|
|
stopSequences: request.params.stopSequences,
|
|
metadata: request.params.metadata,
|
|
modelPreferences: request.params.modelPreferences
|
|
});
|
|
|
|
// Wait for the response
|
|
return await responsePromise;
|
|
} catch (error) {
|
|
console.error('Error creating sampling message:', error);
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
async function runMcpServer(serverToken) {
|
|
// Connect to the WebSocket server
|
|
connectToWebSocketServer(serverToken);
|
|
const transport = new StdioServerTransport();
|
|
await mcpServer.connect(transport);
|
|
console.error("@nucleoriofrio/webmcp v0.2.0 — MCP server running with stdio transport");
|
|
if (CONFIG.dev) {
|
|
console.error("[DEV] Modo desarrollo activo — agregar-tool y quitar-tool habilitados");
|
|
}
|
|
}
|
|
|
|
export { runMcpServer };
|