Agregar endpoints HTTP /token y /status al servidor WebMCP

POST /token genera un token de registro autenticado con Bearer token.
GET /status retorna estado del servidor, clientes conectados y version.
This commit is contained in:
2026-02-14 15:43:13 -06:00
parent cec5be355d
commit 64da2eacc5
5 changed files with 165 additions and 14 deletions

View File

@@ -25570,15 +25570,48 @@ async function runMcpServer(serverToken2) {
// src/websocket-server.js // src/websocket-server.js
var serverToken = SERVER_TOKEN; var serverToken = SERVER_TOKEN;
var httpServer = createServer((req, res) => { var httpServer = createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*"); res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
if (req.method === "OPTIONS") { if (req.method === "OPTIONS") {
res.writeHead(204); res.writeHead(204);
res.end(); res.end();
return; return;
} }
const url2 = new URL(`http://${HOST}${req.url}`);
if (url2.pathname === "/token" && req.method === "POST") {
try {
if (serverToken) {
const authHeader = req.headers.authorization;
if (authHeader !== `Bearer ${serverToken}`) {
res.writeHead(401, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Unauthorized" }));
return;
}
}
const token = await generateNewRegistrationToken();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
success: true,
token
}));
} catch (error2) {
console.error("Error generating token:", error2);
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Failed to generate token" }));
}
return;
}
if (url2.pathname === "/status" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({
status: "running",
clients: wss.clients.size,
version: "0.2.0"
}));
return;
}
res.writeHead(200, { "Content-Type": "text/plain" }); res.writeHead(200, { "Content-Type": "text/plain" });
res.end("MCP WebSocket server is running"); res.end("MCP WebSocket server is running");
}); });

File diff suppressed because one or more lines are too long

View File

@@ -9,8 +9,11 @@
padding: "20px", padding: "20px",
inactivityTimeout: 5 * 60 * 1e3, inactivityTimeout: 5 * 60 * 1e3,
// 5 minutes in milliseconds // 5 minutes in milliseconds
headless: false,
...options ...options
}; };
this._listeners = {};
this._currentStatus = "disconnected";
this.isConnected = false; this.isConnected = false;
this.isExpanded = false; this.isExpanded = false;
this.socket = null; this.socket = null;
@@ -37,6 +40,62 @@
this.REGISTER_PATH = "/register"; this.REGISTER_PATH = "/register";
this._init(); this._init();
} }
/**
* Register an event listener
* @public
* @param {string} event - Event name
* @param {Function} callback - Callback function
* @returns {Function} Unsubscribe function
*/
on(event, callback) {
if (!this._listeners[event]) {
this._listeners[event] = [];
}
this._listeners[event].push(callback);
return () => this.off(event, callback);
}
/**
* Remove an event listener
* @public
* @param {string} event - Event name
* @param {Function} callback - Callback function to remove
*/
off(event, callback) {
if (!this._listeners[event]) return;
this._listeners[event] = this._listeners[event].filter((cb) => cb !== callback);
}
/**
* Emit an event to all registered listeners
* @private
* @param {string} event - Event name
* @param {Object} data - Event data
*/
_emit(event, data) {
if (!this._listeners[event]) return;
this._listeners[event].forEach((cb) => {
try {
cb(data);
} catch (e) {
console.error(`Error in ${event} listener:`, e);
}
});
}
/**
* Get current connection info snapshot
* @public
* @returns {Object} Connection state snapshot
*/
getConnectionInfo() {
return {
isConnected: this.isConnected,
channel: this.currentChannel,
server: this.currentServer,
status: this._currentStatus,
tools: Array.from(this.availableTools.keys()),
prompts: Array.from(this.availablePrompts.keys()),
resources: Array.from(this.availableResources.keys())
};
}
_format(s) { _format(s) {
return s.replace(/[.:]/g, "_"); return s.replace(/[.:]/g, "_");
} }
@@ -45,12 +104,14 @@
* @private * @private
*/ */
_init() { _init() {
if (document.querySelector("[data-webmcp-widget]")) { if (!this.options.headless) {
console.warn("WebMCP widget already initialized on this page"); if (document.querySelector("[data-webmcp-widget]")) {
return; console.warn("WebMCP widget already initialized on this page");
return;
}
this._createWidget();
this._setupEventListeners();
} }
this._createWidget();
this._setupEventListeners();
this._resetInactivityTimer(); this._resetInactivityTimer();
this._checkStoredToken(); this._checkStoredToken();
} }
@@ -542,6 +603,8 @@
* @private * @private
*/ */
_updateStatus(status, message) { _updateStatus(status, message) {
this._currentStatus = status;
this._emit("statusChange", { status, message: message || status });
const container = document.getElementById(this.elementId); const container = document.getElementById(this.elementId);
if (!container) return; if (!container) return;
const statusIndicator = container.querySelector(".webmcp-status"); const statusIndicator = container.querySelector(".webmcp-status");
@@ -892,6 +955,7 @@
this._reconnectAttempts = 0; this._reconnectAttempts = 0;
this._updateStatus("connected", `Connected to ${this.currentChannel}`); this._updateStatus("connected", `Connected to ${this.currentChannel}`);
this._updateConnectionUI(true); this._updateConnectionUI(true);
this._emit("connected", { channel: this.currentChannel, server: this.currentServer });
console.log("WebMCP connection established"); console.log("WebMCP connection established");
this._registerItemsWithServer(); this._registerItemsWithServer();
}); });
@@ -901,6 +965,7 @@
if (event.code === 1e3) { if (event.code === 1e3) {
this._updateStatus("disconnected", "Disconnected"); this._updateStatus("disconnected", "Disconnected");
this._updateConnectionUI(false); this._updateConnectionUI(false);
this._emit("disconnected", { code: event.code, reason: event.reason });
return; return;
} }
if (this._reconnectAttempts < this._maxReconnectAttempts && this._lastConnectionToken) { if (this._reconnectAttempts < this._maxReconnectAttempts && this._lastConnectionToken) {
@@ -908,6 +973,11 @@
const delay = this._reconnectDelay * this._reconnectAttempts; const delay = this._reconnectDelay * this._reconnectAttempts;
console.log(`Reconnecting (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts}) in ${delay}ms...`); console.log(`Reconnecting (attempt ${this._reconnectAttempts}/${this._maxReconnectAttempts}) in ${delay}ms...`);
this._updateStatus("connecting", `Reconectando (${this._reconnectAttempts}/${this._maxReconnectAttempts})...`); this._updateStatus("connecting", `Reconectando (${this._reconnectAttempts}/${this._maxReconnectAttempts})...`);
this._emit("reconnecting", {
attempt: this._reconnectAttempts,
maxAttempts: this._maxReconnectAttempts,
delay
});
setTimeout(() => { setTimeout(() => {
this.connect(this._lastConnectionToken); this.connect(this._lastConnectionToken);
}, delay); }, delay);
@@ -915,6 +985,7 @@
} }
this._updateStatus("disconnected", "Disconnected"); this._updateStatus("disconnected", "Disconnected");
this._updateConnectionUI(false); this._updateConnectionUI(false);
this._emit("disconnected", { code: event.code, reason: event.reason });
this.currentToken = ""; this.currentToken = "";
this.currentServer = ""; this.currentServer = "";
this.currentChannel = ""; this.currentChannel = "";
@@ -961,6 +1032,7 @@
break; break;
case "toolRegistered": case "toolRegistered":
console.log(`Tool registered with server: ${message.name}`); console.log(`Tool registered with server: ${message.name}`);
this._emit("toolRegistered", { name: message.name });
break; break;
case "promptRegistered": case "promptRegistered":
console.log(`Prompt registered with server: ${message.name}`); console.log(`Prompt registered with server: ${message.name}`);
@@ -1005,6 +1077,7 @@
this.registeredTools.delete(message.name); this.registeredTools.delete(message.name);
this._saveItemsToStorage(); this._saveItemsToStorage();
this._updateToolsList(); this._updateToolsList();
this._emit("toolRemoved", { name: message.name });
console.log(`Tool removed by server: ${message.name}`); console.log(`Tool removed by server: ${message.name}`);
} }
break; break;
@@ -1013,6 +1086,7 @@
this.registeredTools.clear(); this.registeredTools.clear();
this._saveItemsToStorage(); this._saveItemsToStorage();
this._updateToolsList(); this._updateToolsList();
this._emit("toolRemoved", { name: "*" });
console.log("All tools removed by server"); console.log("All tools removed by server");
break; break;
case "getClientInfo": case "getClientInfo":
@@ -1039,6 +1113,7 @@
break; break;
case "error": case "error":
console.error(`Server error: ${message.message}`); console.error(`Server error: ${message.message}`);
this._emit("error", { message: message.message });
break; break;
default: default:
console.warn(`Unknown message type: ${message.type}`); console.warn(`Unknown message type: ${message.type}`);
@@ -1062,6 +1137,7 @@
type: "toolResponse", type: "toolResponse",
result: { content: [{ type: "text", text: `Herramienta "${name}" registrada exitosamente` }] } result: { content: [{ type: "text", text: `Herramienta "${name}" registrada exitosamente` }] }
}); });
this._emit("toolCreated", { name });
console.log(`Tool created by agent: ${name}`); console.log(`Tool created by agent: ${name}`);
} catch (e) { } catch (e) {
this._sendMessage({ this._sendMessage({

File diff suppressed because one or more lines are too long

View File

@@ -27,11 +27,11 @@ import {
let serverToken = SERVER_TOKEN; let serverToken = SERVER_TOKEN;
// Create HTTP server with CORS headers // Create HTTP server with CORS headers
const httpServer = createServer((req, res) => { const httpServer = createServer(async (req, res) => {
// Set CORS headers // Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
// Handle preflight requests // Handle preflight requests
@@ -40,6 +40,48 @@ const httpServer = createServer((req, res) => {
return; return;
} }
// Parse URL
const url = new URL(`http://${HOST}${req.url}`);
// Endpoint: POST /token - Genera un nuevo token de registro
if (url.pathname === '/token' && req.method === 'POST') {
try {
// Autenticacion con SERVER_TOKEN
if (serverToken) {
const authHeader = req.headers.authorization;
if (authHeader !== `Bearer ${serverToken}`) {
res.writeHead(401, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Unauthorized' }));
return;
}
}
const token = await generateNewRegistrationToken();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
token: token
}));
} catch (error) {
console.error('Error generating token:', error);
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Failed to generate token' }));
}
return;
}
// Endpoint: GET /status - Estado del servidor
if (url.pathname === '/status' && req.method === 'GET') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'running',
clients: wss.clients.size,
version: '0.2.0'
}));
return;
}
// Default response
res.writeHead(200, {'Content-Type': 'text/plain'}); res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('MCP WebSocket server is running'); res.end('MCP WebSocket server is running');
}); });