diff --git a/README.md b/README.md index 7a4f3aa..ae6a4ab 100644 --- a/README.md +++ b/README.md @@ -24,27 +24,67 @@ Este fork agrega **registro dinámico de herramientas desde el agente**, algo qu **3 archivos modificados: `src/server.js`, `src/webmcp.js`, `src/websocket-server.js`** -#### 1. `agregar-tool` — Registro dinámico de herramientas (server.js, websocket-server.js, webmcp.js) -- Tool built-in `_webmcp_agregar-tool` que permite al agente registrar herramientas nuevas en el navegador en tiempo real -- El agente envía nombre, descripción, parámetros (JSON schema) y código JavaScript -- El código se compila con `new Function('args', codigo)` en el navegador y se registra como tool -- El flujo es: Agente -> MCP Server -> WebSocket Server -> Navegador (createTool) -> respuesta de vuelta -- Después de registrar, se emite `sendToolListChanged()` para que el MCP client refresque su lista +#### 1. `agregar-tool` — Registro dinámico de herramientas (solo modo dev) -#### 2. `quitar-tool` — Eliminación de herramientas (server.js, websocket-server.js, webmcp.js) -- Tool built-in `_webmcp_quitar-tool` con tres modos: - - `listar: true` — lista las herramientas activas registradas en el navegador - - `nombre: "x"` — elimina una herramienta específica por nombre - - `todas: true` — elimina todas las herramientas de una vez -- Notifica al navegador para que sincronice su estado local +Tool built-in `_webmcp_agregar-tool` que permite al agente registrar herramientas nuevas en el navegador en tiempo real. Solo disponible en modo desarrollo (`--dev`). -#### 3. Manejo de mensajes nuevos en el cliente web (webmcp.js) +**Parámetros:** + +| Param | Requerido | Tipo | Descripción | +|---|---|---|---| +| `nombre` | Sí | string | Nombre de la herramienta que Claude verá | +| `descripcion` | Sí | string | Qué hace la herramienta (Claude lee esto para decidir cuándo usarla) | +| `codigo` | Sí | string | Body de una función JS. Recibe `args` como parámetro. Debe retornar un string. | +| `parametros` | No | string | JSON string con las properties del inputSchema | + +**Flujo de ejecución:** + +``` +Agente envía nombre + descripcion + codigo + parametros + → server.js valida y reenvía al WebSocket Server + → websocket-server.js busca el primer navegador conectado + → navegador recibe el mensaje 'createTool' + → webmcp.js ejecuta new Function('args', codigo) para compilar el código + → webmcp.js llama registerTool() para registrar la herramienta + → respuesta 'toolResponse' viaja de vuelta + → websocket-server.js enruta la respuesta al MCP server + → server.js llama sendToolListChanged() +→ Claude ahora puede usar la nueva herramienta +``` + +El código se ejecuta en el contexto del navegador, con acceso a `document`, `window`, `fetch()`, `localStorage`, `navigator`, y todas las APIs web. Solo tiene acceso al scope global (no a closures/variables locales de la página, salvo que estén en `window`). + +#### 2. `quitar-tool` — Eliminación de herramientas (solo modo dev) + +Tool built-in `_webmcp_quitar-tool` con tres modos de operación. Solo disponible en modo desarrollo (`--dev`). + +**Parámetros (todos opcionales, pero debe usarse al menos uno):** + +| Param | Tipo | Efecto | +|---|---|---| +| `listar` | boolean | Si `true`, lista las herramientas activas sin borrar nada | +| `nombre` | string | Nombre de la herramienta específica a eliminar | +| `todas` | boolean | Si `true`, elimina todas las herramientas de una vez | + +**Comportamiento por modo:** + +- **`listar: true`** — Se resuelve en websocket-server.js leyendo el `toolsRegistry`. No toca el navegador. Retorna `"Herramientas activas: tool1, tool2"`. +- **`nombre: "x"`** — Busca en `toolsRegistry` por nombre, la borra, y envía `removeTool` al navegador correspondiente para que sincronice su estado local. +- **`todas: true`** — Borra todo el `toolsRegistry`, envía `removeAllTools` a todos los navegadores conectados, y retorna `"N herramientas desregistradas"`. + +En los tres casos, después de resolver se llama `sendToolListChanged()` para que Claude actualice su lista. + +#### 3. 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) - `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) -#### 4. Ruteo en WebSocket Server (websocket-server.js) +#### 5. 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 @@ -94,6 +134,34 @@ En `.claude/settings.json` o settings del proyecto: } ``` +### Modo desarrollo + +Las tools `agregar-tool` y `quitar-tool` solo están disponibles en modo desarrollo. Para activarlo: + +**Opción 1: Flag de línea de comandos** +```json +{ + "mcpServers": { + "webmcp": { + "command": "node", + "args": ["node_modules/@nucleoriofrio/webmcp/src/websocket-server.js", "--mcp", "--dev"] + } + } +} +``` + +**Opción 2: Variable de entorno** +```bash +WEBMCP_DEV=true node src/websocket-server.js --mcp +``` + +Cuando el modo dev está activo, el servidor muestra una advertencia al iniciar: +``` +[DEV] Modo desarrollo activo — agregar-tool y quitar-tool habilitados +``` + +En producción (sin `--dev`), estas tools no aparecen en el listado de herramientas y si se intentan llamar directamente retornan un error. + ### Flujo de conexión 1. El agente ejecuta `get-token` para generar un token de conexión @@ -101,7 +169,7 @@ En `.claude/settings.json` o settings del proyecto: 3. La página se conecta al WebSocket server y registra sus herramientas 4. El agente puede usar las herramientas de la página y crear nuevas dinámicamente con `agregar-tool` -### Crear herramientas dinámicas desde el agente +### Crear herramientas dinámicas desde el agente (requiere --dev) Con este fork, el agente puede crear herramientas sin modificar el HTML: diff --git a/src/server.js b/src/server.js index 1e7da79..9cfd89c 100644 --- a/src/server.js +++ b/src/server.js @@ -307,6 +307,9 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { } 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 }; } @@ -344,6 +347,9 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { } 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 }; } @@ -376,15 +382,6 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { } } - if (request.params.name === "_webmcp_define-mcp-tool") { - return { - content: [{ - type: "text", - text: "Instruct the user to view the result from the tool call. Do not say anything else.", - }] - } - } - if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { return { content: [{ @@ -445,32 +442,6 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { properties: {}, }, }, - { - name: "_webmcp_agregar-tool", - description: "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: "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_clear-cache", description: "Clears all registered tools, prompts and resources from the WebMCP server cache. Use this to force a clean state.", @@ -479,71 +450,39 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { properties: {}, }, }, - { - name: "_webmcp_define-mcp-tool", - description: "Used to define an 'mcp tool'. Only use this if the user specifically asks for an mcp tool. " + - "A webmcp token is not required for this.", - inputSchema: { - type: "object", - description: "The schema which describes the tool.", - properties: { - name: { - type: "string", - description: "The name of the tool" + ]; + + // 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." } }, - description: { - type: "string", - description: "Provides a clear and concise description of the tool and what it is used for." - }, - inputSchema: { - type: "object", - description: "The inputSchema required or optional for the tool.", - properties: { - type: { - type: "string", - enum: ["object", "array", "string", "number", "boolean", "enum"], - description: "The type of the parameter being defined." - }, - properties: { - type: "object", - description: "The properties of the parameter if it's an object type.", - additionalProperties: { - type: "object", - properties: { - type: { - type: "string", - description: "The data type of the property.", - enum: ["object", "array", "string", "number", "boolean", "enum"] - }, - description: { - type: "string", - description: "A brief description of the property." - }, - enum: { - type: "array", - description: "A list of allowed values for the property.", - items: { - type: "string" - } - } - }, - required: ["type", "description"] - } - }, - required: { - type: "array", - items: { - type: "string" - } - } - }, - required: ["type", "description", "properties"] + 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" } } }, - required: ["name", "description", "inputSchema"] - }, - } - ]; + } + ); + } if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { return {tools: builtInTools}; @@ -843,6 +782,9 @@ async function runMcpServer(serverToken) { const transport = new StdioServerTransport(); await mcpServer.connect(transport); console.error("MCP server running with stdio transport"); + if (CONFIG.dev) { + console.error("[DEV] Modo desarrollo activo — agregar-tool y quitar-tool habilitados"); + } } export { runMcpServer }; diff --git a/src/websocket-server.js b/src/websocket-server.js index 904e900..3f1a9aa 100644 --- a/src/websocket-server.js +++ b/src/websocket-server.js @@ -1585,6 +1585,7 @@ const parseArgs = async () => { let cleanTokens = false; let encodedPair = null; let daemon = true; // Default to daemonize + let dev = process.env.WEBMCP_DEV === 'true' || process.env.WEBMCP_DEV === '1'; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -1629,6 +1630,8 @@ const parseArgs = async () => { cleanTokens = true; } else if (arg === '-f' || arg === '--foreground') { daemon = false; + } else if (arg === '--dev') { + dev = true; } else if (arg === '--forked') { // This is an internal flag to indicate we're the forked child │ │ // No need to do anything with it here, just don't error on it @@ -1639,7 +1642,7 @@ const parseArgs = async () => { } } - return {port, quit, newToken, cleanTokens, encodedPair, daemon, startMCP}; + return {port, quit, newToken, cleanTokens, encodedPair, daemon, startMCP, dev}; }; const showHelp = () => { @@ -1656,7 +1659,8 @@ Options: -f, --foreground Run in foreground (don't daemonize) -m, --mcp Internal WebMCP Server codepath, likely only used in MCP client config -d, --docker Tell the MCP client that WebMCP is running in docker - + --dev Enable development mode (agregar-tool, quitar-tool) + Use --new to generate a token which clients can use to register on the /register endpoint. Use --clean to remove all authorized tokens when you want to start fresh. `); @@ -1756,10 +1760,12 @@ const main = async () => { // Start the server const PORT = CONFIG.port; httpServer.listen(PORT, () => { - console.error(`WebSocket server running at http://${HOST}:${PORT}`); console.error(`WebSocket server running at http://${HOST}:${PORT}`); console.error(`WebMCP client token (for MCP path): ${serverToken}`); console.error(`WebMCP client URL: ws://${HOST}:${PORT}${MCP_PATH}?token=${serverToken}`); + if (CONFIG.dev) { + console.error(`[DEV] Modo desarrollo activo — agregar-tool y quitar-tool habilitados`); + } console.error(`Use 'node websocket-server.js --new ' to authorize a channel-token pair`); });