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.
|
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.
|
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
|
- `createTool` — recibe la definición de herramienta del server, la compila y registra localmente
|
||||||
- `removeTool` — elimina una herramienta específica del registro local
|
- `removeTool` — elimina una herramienta específica del registro local
|
||||||
- `removeAllTools` — limpia todas las herramientas del registro local
|
- `removeAllTools` — limpia todas las herramientas del registro local
|
||||||
- `clipboardCopy` — permite copiar texto al portapapeles del usuario (usado para tokens)
|
- `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
|
- `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
|
- `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
|
## 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');
|
||||||
26827
build/index.js
26827
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 (request.params.name === "_webmcp_agregar-tool") {
|
||||||
if (!CONFIG.dev) {
|
if (!CONFIG.dev) {
|
||||||
return { content: [{ type: "text", text: "agregar-tool solo esta disponible en modo desarrollo (--dev)" }], isError: true };
|
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: {},
|
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
|
// Tools de desarrollo: solo disponibles con --dev o WEBMCP_DEV=true
|
||||||
|
|||||||
@@ -1193,6 +1193,16 @@ class WebMCP {
|
|||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'welcome':
|
case 'welcome':
|
||||||
console.log(`Server says: ${message.message}`);
|
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;
|
break;
|
||||||
|
|
||||||
case 'toolRegistered':
|
case 'toolRegistered':
|
||||||
@@ -1273,6 +1283,20 @@ class WebMCP {
|
|||||||
console.log('All tools removed by server');
|
console.log('All tools removed by server');
|
||||||
break;
|
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':
|
case 'clipboardCopy':
|
||||||
if (message.text && navigator.clipboard) {
|
if (message.text && navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(message.text).then(() => {
|
navigator.clipboard.writeText(message.text).then(() => {
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ const wss = new WebSocketServer({
|
|||||||
// Store active WebSocket connections by channel
|
// Store active WebSocket connections by channel
|
||||||
const channels = {};
|
const channels = {};
|
||||||
|
|
||||||
|
// Store browser metadata per WebSocket connection
|
||||||
|
const clientMetadata = new Map();
|
||||||
|
|
||||||
// Special channel paths
|
// Special channel paths
|
||||||
const MCP_PATH = '/mcp';
|
const MCP_PATH = '/mcp';
|
||||||
const REGISTER_PATH = '/register';
|
const REGISTER_PATH = '/register';
|
||||||
@@ -356,6 +359,14 @@ wss.on('connection', (ws, req) => {
|
|||||||
handleClipboardCopy(data);
|
handleClipboardCopy(data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'clientInfo':
|
||||||
|
handleClientInfo(ws, data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'clientInfoResponse':
|
||||||
|
handleToolResponse(data);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'clearRegistry':
|
case 'clearRegistry':
|
||||||
handleClearRegistry(ws, data);
|
handleClearRegistry(ws, data);
|
||||||
break;
|
break;
|
||||||
@@ -391,6 +402,9 @@ wss.on('connection', (ws, req) => {
|
|||||||
ws.on('close', async () => {
|
ws.on('close', async () => {
|
||||||
console.error(`Client disconnected from path: ${clientChannel}`);
|
console.error(`Client disconnected from path: ${clientChannel}`);
|
||||||
|
|
||||||
|
// Remove browser metadata
|
||||||
|
clientMetadata.delete(ws);
|
||||||
|
|
||||||
// Remove from channel
|
// Remove from channel
|
||||||
const channel = channels[clientChannel];
|
const channel = channels[clientChannel];
|
||||||
if (channel) {
|
if (channel) {
|
||||||
@@ -468,6 +482,9 @@ wss.on('connection', (ws, req) => {
|
|||||||
ws.on('error', (error) => {
|
ws.on('error', (error) => {
|
||||||
console.error('WebSocket error:', error);
|
console.error('WebSocket error:', error);
|
||||||
|
|
||||||
|
// Remove browser metadata
|
||||||
|
clientMetadata.delete(ws);
|
||||||
|
|
||||||
// Remove from channel
|
// Remove from channel
|
||||||
const channel = channels[clientChannel];
|
const channel = channels[clientChannel];
|
||||||
if (channel) {
|
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
|
// Handle clearing all registries
|
||||||
function handleClearRegistry(ws, data) {
|
function handleClearRegistry(ws, data) {
|
||||||
const {id} = data;
|
const {id} = data;
|
||||||
|
|||||||
Reference in New Issue
Block a user