Built-in tool _webmcp_browser-info para ver navegadores conectados

Agrega metadata tracking de browsers (userAgent, URL, hostname,
idioma, resolucion, timestamp) y tool browser-info para consultarla.
This commit is contained in:
2026-02-13 00:36:51 -06:00
parent 2a07e89a17
commit 603c547bfe
9 changed files with 28745 additions and 31 deletions

View File

@@ -74,19 +74,44 @@ Tool built-in `_webmcp_quitar-tool` con tres modos de operación. Solo disponibl
En los tres casos, después de resolver se llama `sendToolListChanged()` para que Claude actualice su lista.
#### 3. Eliminación de `define-mcp-tool`
#### 3. `browser-info` — Información de navegadores conectados
Tool built-in `_webmcp_browser-info` que muestra metadata de todos los navegadores conectados al servidor. Siempre disponible (no requiere `--dev`).
**No recibe parámetros.** Retorna un JSON array con la info de cada navegador:
| Campo | Descripción |
|---|---|
| `channel` | Canal WebSocket al que está conectado |
| `userAgent` | User agent del navegador |
| `url` | URL completa de la página |
| `hostname` | Hostname de la página |
| `language` | Idioma del navegador |
| `screenWidth` | Ancho de la ventana (px) |
| `screenHeight` | Alto de la ventana (px) |
| `connectedSince` | Timestamp de cuando se conectó |
**Flujo:** El navegador envía su `clientInfo` automáticamente al conectarse (en el `welcome`). El servidor almacena esta metadata en un `Map`. Cuando el agente llama `browser-info`, el server recopila la info de todos los clientes conectados y la retorna.
Si no hay navegadores conectados, retorna `"No hay navegadores conectados"`.
#### 4. Eliminación de `define-mcp-tool`
Se eliminó la built-in `_webmcp_define-mcp-tool` del upstream por no tener utilidad real en este fork.
#### 4. Manejo de mensajes nuevos en el cliente web (webmcp.js)
#### 5. Manejo de mensajes nuevos en el cliente web (webmcp.js)
- `createTool` — recibe la definición de herramienta del server, la compila y registra localmente
- `removeTool` — elimina una herramienta específica del registro local
- `removeAllTools` — limpia todas las herramientas del registro local
- `clipboardCopy` — permite copiar texto al portapapeles del usuario (usado para tokens)
- `clientInfo` — envía metadata del navegador al servidor al conectarse (automático en welcome)
- `getClientInfo` — responde con metadata actualizada del navegador cuando el server la solicita
#### 5. Ruteo en WebSocket Server (websocket-server.js)
#### 6. Ruteo en WebSocket Server (websocket-server.js)
- `handleCreateTool()` — recibe la petición del MCP server, encuentra un navegador conectado y le reenvía la instrucción de crear la herramienta
- `handleRemoveTool()` — maneja listar/eliminar herramientas, sincroniza entre el registry del server y los navegadores conectados
- `handleClientInfo()` — almacena la metadata del navegador en `clientMetadata` al conectarse
- `handleGetClientInfo()` — recopila metadata de todos los navegadores conectados y responde al MCP server
## Instalación

27
build.js Normal file
View File

@@ -0,0 +1,27 @@
import esbuild from 'esbuild';
// Bundle server (Node.js entry point)
await esbuild.build({
entryPoints: ['src/websocket-server.js'],
bundle: true,
platform: 'node',
format: 'esm',
target: 'node18',
outfile: 'build/index.js',
sourcemap: true,
banner: { js: '#!/usr/bin/env node\nimport { createRequire } from "module"; const require = createRequire(import.meta.url);' },
external: [],
});
// Bundle client (browser IIFE)
await esbuild.build({
entryPoints: ['src/webmcp.js'],
bundle: true,
platform: 'browser',
format: 'iife',
target: 'es2020',
outfile: 'build/webmcp.js',
sourcemap: true,
});
console.log('Build complete');

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -306,6 +306,31 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
}
}
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 };
@@ -450,6 +475,14 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => {
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

View File

@@ -1193,6 +1193,16 @@ class WebMCP {
switch (message.type) {
case 'welcome':
console.log(`Server says: ${message.message}`);
this._sendMessage({
type: 'clientInfo',
userAgent: navigator.userAgent,
url: window.location.href,
hostname: window.location.hostname,
language: navigator.language,
screenWidth: window.innerWidth,
screenHeight: window.innerHeight,
timestamp: Date.now()
});
break;
case 'toolRegistered':
@@ -1273,6 +1283,20 @@ class WebMCP {
console.log('All tools removed by server');
break;
case 'getClientInfo':
this._sendMessage({
id: message.id,
type: 'clientInfoResponse',
userAgent: navigator.userAgent,
url: window.location.href,
hostname: window.location.hostname,
language: navigator.language,
screenWidth: window.innerWidth,
screenHeight: window.innerHeight,
timestamp: Date.now()
});
break;
case 'clipboardCopy':
if (message.text && navigator.clipboard) {
navigator.clipboard.writeText(message.text).then(() => {

View File

@@ -54,6 +54,9 @@ const wss = new WebSocketServer({
// Store active WebSocket connections by channel
const channels = {};
// Store browser metadata per WebSocket connection
const clientMetadata = new Map();
// Special channel paths
const MCP_PATH = '/mcp';
const REGISTER_PATH = '/register';
@@ -356,6 +359,14 @@ wss.on('connection', (ws, req) => {
handleClipboardCopy(data);
break;
case 'clientInfo':
handleClientInfo(ws, data);
break;
case 'clientInfoResponse':
handleToolResponse(data);
break;
case 'clearRegistry':
handleClearRegistry(ws, data);
break;
@@ -391,6 +402,9 @@ wss.on('connection', (ws, req) => {
ws.on('close', async () => {
console.error(`Client disconnected from path: ${clientChannel}`);
// Remove browser metadata
clientMetadata.delete(ws);
// Remove from channel
const channel = channels[clientChannel];
if (channel) {
@@ -468,6 +482,9 @@ wss.on('connection', (ws, req) => {
ws.on('error', (error) => {
console.error('WebSocket error:', error);
// Remove browser metadata
clientMetadata.delete(ws);
// Remove from channel
const channel = channels[clientChannel];
if (channel) {
@@ -814,6 +831,46 @@ function handleRemoveTool(ws, data) {
}
}
// Handle client info sent by browser on connect
function handleClientInfo(ws, data) {
const { userAgent, url, hostname, language, screenWidth, screenHeight, timestamp } = data;
clientMetadata.set(ws, { userAgent, url, hostname, language, screenWidth, screenHeight, timestamp });
console.error(`Client info stored for ${hostname}: ${url}`);
}
// Handle getClientInfo request - collect info from all connected browsers
function handleGetClientInfo(ws, callerChannel, data) {
const { id } = data;
const clients = [];
for (const [channelPath, channelClients] of Object.entries(channels)) {
if (channelPath === MCP_PATH || channelPath === REGISTER_PATH) continue;
for (const clientWs of channelClients) {
const meta = clientMetadata.get(clientWs) || {};
clients.push({
channel: channelPath,
userAgent: meta.userAgent || 'unknown',
url: meta.url || 'unknown',
hostname: meta.hostname || 'unknown',
language: meta.language || 'unknown',
screenWidth: meta.screenWidth || 0,
screenHeight: meta.screenHeight || 0,
connectedSince: meta.timestamp || 0
});
}
}
const text = clients.length > 0
? JSON.stringify(clients, null, 2)
: 'No hay navegadores conectados';
ws.send(JSON.stringify({
id,
type: 'toolResponse',
result: { content: [{ type: 'text', text }] }
}));
}
// Handle clearing all registries
function handleClearRegistry(ws, data) {
const {id} = data;