Subir archivos a "/"

This commit is contained in:
2026-03-06 02:41:54 -06:00
parent 59284c2876
commit 0244f0f5a4
5 changed files with 3529 additions and 0 deletions

118
config.js Normal file
View File

@@ -0,0 +1,118 @@
import * as path from 'path';
import * as dotenv from 'dotenv';
import * as os from 'os';
import * as fs from 'fs/promises';
import envPaths from 'env-paths';
// Create config directory in user's home folder
const HOME_DIR = os.homedir();
const CONFIG_DIR = path.join(HOME_DIR, '.webmcp');
// Ensure config directory exists
const ensureConfigDir = async () => {
try {
await fs.mkdir(CONFIG_DIR, {recursive: true});
} catch (error) {
console.error(`Error creating config directory at ${CONFIG_DIR}:`, error);
}
};
// Process ID file path
const PID_FILE = path.join(CONFIG_DIR, '.webmcp-server.pid');
// Environment file path
const ENV_FILE = path.join(CONFIG_DIR, '.env');
// Tokens file path
const TOKENS_FILE = path.join(CONFIG_DIR, '.webmcp-tokens.json');
// Load environment variables
dotenv.config({path: ENV_FILE});
// Server token for MCP authentication
const SERVER_TOKEN = process.env.WEBMCP_SERVER_TOKEN || '';
const HOST = "localhost";
const CONFIG = {};
function setConfig(args) {
Object.entries(args).forEach(([key, value]) => {
CONFIG[key] = value;
});
}
function formatChannel(channel) {
return `/${channel.replace(/[.:]/g, '_')}`
}
async function exists(somePath) {
try {
await fs.access(somePath);
return true;
} catch (e) {
return false;
}
}
async function configureMcpClientWithPath(clientConfigPath) {
const directory = path.dirname(clientConfigPath);
if (!await exists(directory)) {
await fs.mkdir(directory, { recursive: true });
}
const webmcpConfig = {
"webmcp": {
"command": "npx",
"args": [
"-y",
"@nucleoriofrio/webmcp@latest",
"--mcp"
]
}
};
let json = { mcpServers: {} };
// If one already exists, we'll want to update it
if (await exists(clientConfigPath)) {
const rawJSON = await fs.readFile(clientConfigPath);
try {
json = JSON.parse(rawJSON);
} catch (e) {
throw new Error(`Failed to update MCP client configuration: ${e}`);
}
}
json.mcpServers = { ...json.mcpServers, ...webmcpConfig};
await fs.writeFile(clientConfigPath, JSON.stringify(json, null, 2));
}
const availableClientConfigs = {
"claude": [envPaths("Claude", { suffix: "" }).data, "claude_desktop_config.json"],
"cline": [envPaths("Code", { suffix: "" }).data, "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"],
"cursor": [HOME_DIR, ".cursor", "mcp.json"],
"windsurf": [HOME_DIR, ".codeium", "windsurf", "mcp_config.json"]
};
async function configureMcpClient(clientType) {
let clientConfigPath = availableClientConfigs[clientType];
if (clientConfigPath) {
await configureMcpClientWithPath(clientConfigPath);
} else {
console.error("Unsupported client - treating it like a path...")
await configureMcpClientWithPath(clientType);
}
}
export {
CONFIG,
HOST,
PID_FILE,
ENV_FILE,
TOKENS_FILE,
SERVER_TOKEN,
ensureConfigDir,
formatChannel,
setConfig,
configureMcpClientWithPath,
configureMcpClient,
};

889
server.js Normal file
View File

@@ -0,0 +1,889 @@
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 };

118
tokens.js Normal file
View File

@@ -0,0 +1,118 @@
import * as fs from 'fs/promises';
import * as crypto from 'crypto';
import {ENV_FILE, formatChannel, HOST, TOKENS_FILE,CONFIG} from "./config.js";
// Function to generate a secure random token
function generateToken() {
return crypto.randomBytes(16).toString('hex');
}
// Authorized channel-token pairs - Only channels with valid tokens can connect
// Format: { "/channel1": "token123" }
let authorizedTokens = {};
function getToken(channel) {
return authorizedTokens[channel];
}
function setToken(channel, value) {
authorizedTokens[channel] = value;
}
function deleteToken(channel) {
delete authorizedTokens[channel];
}
function clearTokens(channel) {
authorizedTokens = {};
}
// Load authorized tokens from disk
async function loadAuthorizedTokens() {
try {
const data = await fs.readFile(TOKENS_FILE, 'utf8');
authorizedTokens = JSON.parse(data || "{}");
// console.error(`Loaded ${Object.keys(authorizedTokens).length} authorized channel-token pairs from ${TOKENS_FILE}`);
return true;
} catch (error) {
// If file doesn't exist, start with empty tokens
if (error.code === 'ENOENT') {
authorizedTokens = {};
return true;
}
console.error('Error loading authorized tokens:', error);
return false;
}
}
// Save authorized tokens to disk
async function saveAuthorizedTokens() {
try {
// Convert Map to object for JSON serialization
const stringified = JSON.stringify(authorizedTokens, null, 2);
await fs.writeFile(TOKENS_FILE, stringified, 'utf8');
// console.error(`Saved ${stringified} authorized channel-token pairs to ${TOKENS_FILE}`);
return true;
} catch (error) {
console.error('Error saving authorized tokens:', error);
return false;
}
}
// Function to save server token to .env file
async function saveServerTokenToEnv(token) {
try {
let envContent = '';
try {
// Try to read existing .env file
envContent = await fs.readFile(ENV_FILE, 'utf8');
// Check if WEBMCP_SERVER_TOKEN is already defined
if (envContent.includes('WEBMCP_SERVER_TOKEN=')) {
// Replace the existing token
envContent = envContent.replace(/WEBMCP_SERVER_TOKEN=.*(\r?\n|$)/g, `WEBMCP_SERVER_TOKEN=${token}$1`);
} else {
// Add the token to the end
envContent += `\nWEBMCP_SERVER_TOKEN=${token}\n`;
}
} catch (err) {
// File doesn't exist, create new content
envContent = `WEBMCP_SERVER_TOKEN=${token}\n`;
}
// Write the content to the .env file
await fs.writeFile(ENV_FILE, envContent, 'utf8');
console.error(`Server token saved to ${ENV_FILE}`);
return true;
} catch (error) {
console.error('Error saving server token to .env file:', error);
return false;
}
}
async function generateNewRegistrationToken() {
// Generate a random token for registration
const token = generateToken();
// Create a connection object with server address and token
const address = `${HOST}:${CONFIG.port}`;
const serverAddress = `ws://${address}`;
const connectionData = {
server: serverAddress,
token: token
};
// Convert to JSON and base64 encode
const jsonStr = JSON.stringify(connectionData);
const encodedData = Buffer.from(jsonStr).toString('base64');
setToken(formatChannel(address), token);
await saveAuthorizedTokens();
return encodedData;
}
export {generateToken, getToken, setToken, loadAuthorizedTokens, saveAuthorizedTokens, clearTokens, deleteToken, saveServerTokenToEnv, generateNewRegistrationToken};

78
webmcp.d.ts vendored Normal file
View File

@@ -0,0 +1,78 @@
export interface WebMCPOptions {
color?: string;
position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
size?: string;
padding?: string;
inactivityTimeout?: number;
headless?: boolean;
}
export interface ToolInputSchema {
type: string;
properties: Record<string, any>;
required?: string[];
}
export interface ResourceOptions {
uri?: string;
uriTemplate?: string;
mimeType?: string;
}
export interface PromptArgument {
name: string;
description?: string;
required?: boolean;
}
export type ToolHandler = (args: any) => string | object | Promise<string | object>;
export type PromptHandler = (args: any) => object | Promise<object>;
export type ResourceHandler = (uri: string) => object | Promise<object>;
export interface ConnectionInfo {
isConnected: boolean;
channel: string;
server: string;
status: string;
tools: string[];
prompts: string[];
resources: string[];
}
export interface WebMCPEventMap {
connected: { channel: string; server: string };
disconnected: { code: number; reason: string };
reconnecting: { attempt: number; maxAttempts: number; delay: number };
statusChange: { status: string; message: string };
toolRegistered: { name: string };
toolCreated: { name: string };
toolRemoved: { name: string };
error: { message: string };
}
declare class WebMCP {
readonly isConnected: boolean;
readonly isExpanded: boolean;
constructor(options?: WebMCPOptions);
connect(connectionToken: string): Promise<void>;
disconnect(): void;
on<K extends keyof WebMCPEventMap>(event: K, callback: (data: WebMCPEventMap[K]) => void): () => void;
off<K extends keyof WebMCPEventMap>(event: K, callback: (data: WebMCPEventMap[K]) => void): void;
getConnectionInfo(): ConnectionInfo;
registerTool(name: string, description: string, schema: ToolInputSchema, executeFn: ToolHandler): void;
unregisterTool(name: string): void;
unregisterAllTools(): void;
registerPrompt(name: string, description: string, args: PromptArgument[], executeFn: PromptHandler): void;
unregisterPrompt(name: string): void;
registerResource(name: string, description: string, options: ResourceOptions, provideFn: ResourceHandler): void;
unregisterResource(name: string): void;
}
export default WebMCP;
export { WebMCP };

2326
webmcp.js Normal file

File diff suppressed because it is too large Load Diff