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:
31
README.md
31
README.md
@@ -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
27
build.js
Normal 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');
|
||||
26825
build/index.js
26825
build/index.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1763
build/webmcp.js
1763
build/webmcp.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user