WebMCP en la práctica: convirtiendo una tabla de tareas en herramientas para agentes de IA.
WebMCP en la práctica: una tabla de tareas transformada en herramientas para agentes de IA con herramientas declarativas y por script, una lista de herramientas variable, un puente MCP y un rechazo.
Agentes que adivinan sobre su sitio web
Hoy en día, un agente de IA que trabaja con un sitio web suele basarse en conjeturas. Toma una captura de pantalla en busca de algo que se asemeje a un botón, o analiza el DOM con la esperanza de que los nombres de las clases signifiquen lo que parecen significar. Después de hacer clic, verifica si la página ha cambiado de manera que indique éxito. Se trata de un truco impresionante, pero basado en fundamentos débiles, ya que el sitio nunca le dice nada al agente y todas las conclusiones se infieren.
WebMCP es la propuesta de Chrome para poner fin a esas conjeturas. Una página anuncia sus capacidades, el agente lee ese anuncio e invoca una función con argumentos estructurados, en lugar de apuntar a un rectángulo en la pantalla. El cambio parece menor, pero traslada la inteligencia: en lugar de reconstruir el propósito de una aplicación a partir de píxeles y marcado, el agente recibe ese propósito directamente de la aplicación misma.
Para comprobar si esto funciona en la práctica, se creó un pequeño tablero de tareas operativo al que se asignó un agente real. El resultado más revelador surgió cuando el agente rechazó categóricamente una solicitud.
Tres roles
Cada momento del experimento involucra a uno de tres actores.
- La página es el proveedor. Registra las herramientas. Cada herramienta combina un nombre con una descripción dirigida a un modelo, un JSON Schema que describe los argumentos y una función que realiza el trabajo.
- El navegador es el registro. Para cada pestaña almacena la lista de herramientas, deriva esquemas a partir del HTML siempre que es posible y decide cuándo una herramienta aparece o desaparece.
- El agente es el consumidor. Consulta al navegador qué herramientas están disponibles mediante
getTools()y ejecuta la herramienta elegida conexecuteTool(). El DOM está fuera de su alcance, y no tiene forma de compensar por capacidades que la página nunca ha publicado.
Ese último punto es el núcleo de la idea. La documentación de Chrome describe la API con suficiente claridad, pero leer una explicación no es lo mismo que comprender algo, por lo que la forma más rápida fue crear la versión más pequeña posible.
La demostración: un tablero de tareas en tres archivos
La estructura está formada por index.html, app.js y style.css. No es necesario crear ni instalar nada. Las personas pueden agregar tareas, marcarlas como completadas y filtrar la lista; se trata de un proyecto que se puede terminar en una tarde. Lo nuevo es que la página también se presenta a un agente de IA como un conjunto de herramientas llamables, a través de un objeto que proporciona el navegador.
const mc = document.modelContext; // undefined if WebMCP is off
Esa única línea es todo el punto de entrada. Cuando el objeto existe, la página puede registrar herramientas. Cuando no existe, la página funciona simplemente como un tablero de tareas, que es así como debería comportarse en caso de fallo.
Para mantener ambos componentes visibles, la página de demostración los muestra uno al lado del otro. A la izquierda se encuentra el tablero, es decir, el proveedor. A la derecha, un panel de “Agente ficticio” actúa como el consumidor y realiza llamadas a getTools() y executeTool() tal como lo haría un agente real, lo que evita la necesidad de conexiones adicionales. Verá ese panel en todas las capturas de pantalla a continuación.
WebMCP sigue teniendo el estatus “Propuesto” en Chrome Platform Status y no está activo por defecto en ningún lugar, por lo que primero se necesita activar una bandera correspondiente.
Abra chrome://flags/#enable-webmcp-testing, cámbielo a “Enabled” y reinicie el navegador. Sin ello, document.modelContext queda indefinido. Chrome también ofrece una versión de prueba de WebMCP que comienza a estar disponible desde Chrome 149. La opción de flag sirve para experimentos locales, mientras que la versión de prueba es el método que Chrome recomienda para pruebas más amplias.
Primero el bucle completado
Es más fácil entender la API después de ver qué produce. Claude Code se conectó a la placa y se le dio una oración pidiéndole que agregara cuatro tareas: buscar información e investigar, redactar esquemas de blog, crear un blog completo con ejemplos y publicarlo. Aparecieron cuatro tareas. Una segunda oración le pidió que marcara las tareas 3, 4 y 5 como completadas, y eso también funcionó.
Claude no tomó captura de pantalla del tablero ni leyó su HTML. Consultó al navegador las herramientas disponibles, encontró add-task y lo invocó cuatro veces con argumentos estructurados. Aquí se muestra el mismo momento visto desde dentro del navegador.
Las cuatro tareas nuevas ahora ocupan los puestos del #3 al #6 en el tablero, y el panel de DevTools (Aplicación, luego WebMCP) enumera las seis herramientas que la página publica actualmente. Una de ellas, clear-completed, no estaba allí un minuto antes, y la razón se explica en breve. Las tres tareas completadas se registran una por una.
Cada llamada se muestra con sus argumentos ({"id":3}, {"id":4} y {"id":5}), y la línea de resumen indica tres llamadas en total, sin fallos, cancelaciones ni llamadas en ejecución. Depurar WebMCP es más rápido aquí, ya que el panel muestra con precisión lo que envió el agente.
Así, una oración escrita en una terminal se convirtió en cuatro llamadas a funciones estructuradas dentro de una pestaña del navegador. La siguiente pregunta es qué tuvo que hacer la página para lograrlo.
Dos formas de publicar una herramienta
La primera no requiere JavaScript en absoluto. Un formulario existente recibe cuatro atributos adicionales.
<form id="add-form"
toolname="add-task"
tooldescription="Add a new task to the user's task board."
toolautosubmit>
<input name="title" required maxlength="80"
toolparamdescription="Short description of the task to add.">
<select name="priority"
toolparamdescription="How urgent the task is.">
<option value="low">low</option>
<option value="normal" selected>normal</option>
<option value="high">high</option>
</select>
<button type="submit">Add</button>
</form>
Ese formulario es la herramienta add-task que Claude invocó cuatro veces. Los nombres de los campos, los atributos required y las opciones <select> son leídos por el navegador, el cual elabora el JSON Schema por sí mismo. El agente ve una cadena obligatoria y un enum de tres valores, y nadie escribió el esquema. En una aplicación existente, es probable que sea el experimento más sencillo con WebMCP, ya que un formulario HTML ordinario ya proporciona una ventaja inicial. Lo único que se añade es una descripción de lo que el formulario significa para un agente.
Hay un detalle que lleva un momento comprender: el mismo manejador submit sirve tanto a una persona que hace clic en “Añadir” como a un agente que llama a la herramienta, y el evento indica cuál de los dos fue.
$('#add-form').addEventListener('submit', (e) => {
e.preventDefault();
const task = addTask(new FormData(e.target).get('title'), /* ... */);
if (!e.agentInvoked) {
e.target.reset(); // human — clear the box
} else {
e.respondWith?.(Promise.resolve(
text(`Added task #${task.id}: "${task.title}".`)
));
}
});
La bandera agentInvoked indica quién presionó el botón, y respondWith() devuelve un resultado al agente sin salir de la página. Existe un único camino de código con dos llamantes, lo cual es una buena elección de diseño, ya que nadie quiere una “versión de IA” para cada acción en una aplicación. La misma lógica debe servir tanto a las personas como a los agentes siempre que sea apropiado.
La ruta declarativa es, sin duda, la menos interesante, aunque es la que probablemente atraiga atención, ya que se muestra de manera clara y no requiere mucho esfuerzo. Lo que un formulario no puede expresar es donde se vuelve interesante. Ese es el segundo camino, registerTool, que se parece mucho a lo que se escribiría para un servidor MCP de backend.
await mc.registerTool({
name: 'list-tasks',
description: 'List the tasks on the board. Use this before acting so you know the task IDs.',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', enum: ['all', 'open', 'done'] },
},
},
annotations: { readOnlyHint: true },
async execute({ status = 'all' }) {
const rows = visible(tasks, status);
return text(rows.map((t) => `#${t.id} [${t.done ? 'x' : ' '}] ${t.title}`).join('\n'));
},
});
Observe con atención esa description. Está escrita para un modelo y no para un desarrollador, y una frase como “utilice esto antes de actuar para conocer los IDs de las tareas” indica cuándo debe llamarse. Las descripciones forman ahora parte de la interfaz de la API, y las vagas generan agentes que tienen que adivinar.
La bandera readOnlyHint indica que es seguro llamar a una herramienta sin preguntar primero al usuario. list-tasks y estimate-task la incluyen. complete-task y clear-completed no, ya que modifican algo.
Las herramientas también pueden ser lentas, y un agente podría cambiar de opinión a mitad de proceso. La herramienta estimate-task espera intencionadamente tres segundos, y recibe un AbortSignal como segundo argumento.
async execute({ id }, { signal }) {
const t = findTask(tasks, id);
if (!t) {
return text(`No task #${id}.`);
}
await sleep(3000, signal); // throws if the agent aborts
return text(
`Task #${t.id} is about ${
t.priority === 'high' ? '2 hours' : '30 minutes'
}.`
);
}
Si se respeta esa señal, la cancelación funciona. Si se ignora, la herramienta sigue ejecutándose después de que el agente se haya ido.
Una lista de herramientas que cambia mientras la página está abierta
La razón más importante para registrar las herramientas en el script, en lugar de enviar un manifiesto estático, es que se permite que la lista cambie en tiempo de ejecución. La plataforma cuenta con una herramienta clear-completed que elimina todas las tareas completadas. Eso solo tiene sentido cuando algo se termina, lo cual explica por qué apareció en la captura de pantalla anterior justo después de que tres tareas fueran marcadas como completadas. Por lo tanto, solo existe en esos momentos.
let clearCtl = null;
async function syncClearTool() {
const has = tasks.some((t) => t.done);
if (has && !clearCtl) {
const ctl = new AbortController();
clearCtl = ctl;
await mc.registerTool(
{
name: 'clear-completed',
/* ... */
},
{
signal: ctl.signal
}
);
} else if (!has && clearCtl) {
const ctl = clearCtl;
clearCtl = null;
setTimeout(() => ctl.abort(), 0); // abort() IS unregister
}
}
No existe unregisterTool(). Al registrarse se pasa un AbortSignal, y al abortarlo se elimina la herramienta. Ahí es donde tiene sentido el diseño: la señal que cancela una llamada en ejecución también revoca la capacidad correspondiente, de modo que un mismo mecanismo cumple dos funciones.
Al marcar una tarea como completada, clear-completed aparece en la lista del agente; al desmarcarla, la herramienta desaparece. El agente se entera de esto a través de un evento toolchange y no necesita realizar consultas periódicas.
mc.addEventListener('toolchange', refreshTools);
Por lo tanto, la lista de herramientas no es un manifiesto publicado una sola vez. Describe lo que la aplicación puede hacer en este momento, en su estado actual.
En qué difiere esto de un servidor MCP de backend
A continuación, se le pidió a Claude Code que redujera el tablero a las tareas completadas.
El agente llamó a filter-tasks con {"status":"done"}; el botón “Hecho” se activó y el tablero mostró únicamente las tareas finalizadas. El agente no había obtenido una copia filtrada de los datos. Simplemente cambió la vista que el usuario estaba viendo.
Un servidor MCP de backend no puede lograr esto. Al encontrarse al otro lado de los límites de la red, opera con datos almacenados y no sabe nada sobre lo que muestra cualquier pantalla en ese momento. Las herramientas WebMCP se encuentran dentro de la pestaña, compartiendo su DOM, sesión y cookies, por lo que el agente y la persona ven la misma pantalla y cualquiera de ellos puede modificarla. Eso representa una verdadera ventaja, pero también un riesgo real, el cual tendrá su propia sección más abajo. También es por eso que llamar a WebMCP “MCP dentro del navegador” subestima sus capacidades. Es importante dónde se encuentra la herramienta, ya que está vinculada al estado exacto de la página que está viendo el usuario.
Conectar un agente externo
El panel Pretend Agent ayuda durante el desarrollo, pero resulta de poca utilidad, ya que es la propia página la que se llama a sí misma. Una prueba adecuada necesita algo fuera del navegador que solo pueda ver lo que reporta getTools(). Por eso existe agent.mjs, una pequeña herramienta de línea de comandos que utiliza parte del protocolo de Chrome DevTools y no tiene dependencias, ya que Node 22 dispone de un WebSocket global. Por su diseño, solo puede ver lo que reporta getTools() y actuar únicamente a través de executeTool(), sin posibilidad de extraer datos del DOM.
node agent.mjs list
node agent.mjs call add-task '{"title":"Ship the demo","priority":"high"}'
Esos dos comandos enumeran las herramientas y luego llaman a add-task con un título y una prioridad. La configuración que generó las capturas de pantalla va un paso más allá. Claude Desktop, Claude Code y ChatGPT utilizan MCP y no WebMCP, por lo que mcp-bridge.mjs actúa como adaptador, y es un adaptador sencillo, ya que los protocolos coinciden casi exactamente: tools/list se corresponde con getTools() y tools/call con executeTool(). Claude Code lo detecta a través de un archivo .mcp.json.
{
"mcpServers": {
"webmcp-board": {
"command": "node",
"args": ["C:\\projects\\web-mcp-demo\\mcp-bridge.mjs"],
"env": { "PAGE_URL": "https://tusharkanjariya.github.io/web-mcp-demo/" }
}
}
}
Toda la cadena consiste, por lo tanto, en Claude Code que habla mediante MCP con el puente, el puente que habla mediante CDP con Chrome, y Chrome que llama a las herramientas registradas por la página. Eso son cuatro etapas, y la página final no puede saber qué cliente se encuentra al otro lado.
Claude Code
↓
MCP
↓
mcp-bridge.mjs
↓
Chrome DevTools Protocol
↓
WebMCP
↓
my task board
Otra comodidad: el puente inicia su propio Chrome si no hay respuesta en el puerto de depuración, y abre la página cuando esa pestaña no está presente. Un cliente MCP puede iniciar todo desde cero.
La negativa
Luego se le pidió al agente que eliminara una sola tarea.
La mitad superior de la captura de pantalla muestra la solicitud de filtrado de antes. La mitad inferior es la parte interesante: Claude indicó que no existe una herramienta delete-task en esta plataforma, que la única opción de eliminación disponible es clear-completed, la cual también borraría las tareas #2, #3, #4 y #5, y que no iba a hacerlo.
Ninguna barandilla de seguridad, indicación del sistema ni capa de protección provocó esa negativa. Simplemente no existía la posibilidad de eliminar una tarea individual, y el agente se dio cuenta. Sería un error interpretar demasiado un solo intercambio en una aplicación sencilla. Aun así, eso cambia la forma en que se percibe el problema. Un agente que dirige una página mediante píxeles y coordenadas habría encontrado algo similar a una función de eliminación e intentado usarla, ya que los agentes de extracción de pantalla solo pueden recurrir a la improvisación. Este agente contó sus seis herramientas, vio que ninguna servía para eliminar una tarea, reconoció los daños colaterales de la opción más cercana y se detuvo para preguntar.
La superficie de herramientas constituía el límite. Nadie escribió una regla que prohibiera eliminar elementos. La página simplemente nunca ofreció la posibilidad de eliminar algo, y eso fue suficiente, aunque WebMCP no haga que los agentes sean seguros de forma mágica.
Donde Chrome y la especificación difieren
Al probarlo se detectaron diferencias entre la documentación y el navegador, y resolverlas me costó toda una tarde. Tres de ellas se refieren a la estructura de los datos.
- Argumentos de
executeTool(): la especificación describe un objeto, pero Chrome 151 espera una cadena JSON, y un objeto provoca el errorFailed to parse input arguments. RegisteredTool.inputSchema: en la especificación está definido como objeto, pero Chrome devuelve una cadena JSON, por lo que es necesario parsearla antes de leer lasproperties.- Lo que devuelve
executeTool(): la especificación indicaDOMString, lo cual es cierto, pero se trata de un sobre serializado{content:[…]}que debe desempaquetarse dos veces para acceder al texto.
Nada de esto resulta difícil una vez que lo sabes. Los dos problemas reales fueron errores relacionados con el ciclo de vida, y solo aparecen cuando un agente real navega por la página.
Llamar a form.reset() mientras se está ejecutando un envío iniciado por el agente cancela la llamada a la herramienta con el mensaje La ejecución de la herramienta fue cancelada por un restablecimiento del formulario. Un manejador completamente normal que vacía los campos de entrada después del envío, como lo hacen la mayoría, hizo que fallara cada llamada add-task realizada por el agente. La tarea se creaba, pero nunca llegaba la respuesta, y por eso el manejador mostrado anteriormente solo realiza el restablecimiento en los envíos hechos por usuarios.
En versiones anteriores a Chrome 153, al eliminar una herramienta también se cancelan todas sus llamadas que aún estaban en ejecución. Eso no supone problema alguno hasta que una herramienta se elimina por haber completado su tarea. clear-completed elimina todas las tareas finalizadas, de modo que no queda nada pendiente y la herramienta se retira por sí misma. En la versión 151, esa función interrumpía su propia respuesta antes de que esta pudiera ser devuelta. La solución es setTimeout(() => ctl.abort(), 0): posponer la interrupción por un instante permite que la llamada se resuelva primero. Es comprensible y funciona, pero posponer el cierre por un instante es el tipo de corrección que merece ser revisada tras varios lanzamientos de Chrome.
Entrega de la demostración
La demostración es completamente estática, por lo que GitHub Pages puede servirla sin cambios, y HTTPS proporciona el contexto seguro que exige WebMCP. De esta manera, todos obtienen una tabla de tareas funcional, pero no WebMCP funcional, ya que los visitantes que no han activado la opción siguen viendo document.modelContext como undefined. Para solucionarlo se necesita un token de prueba de origen en el <head>, registrado como de primera parte, ya que Chrome rechaza los tokens de terceros colocados en una etiqueta meta. En un perfil limpio sin opciones activadas, modelContext devolvía el valor “object” junto con las seis herramientas. La prueba durará hasta Chrome 156.
La pregunta de seguridad sin una buena respuesta
Las herramientas de WebMCP se ejecutan dentro de una pestaña con sesión iniciada. Ese es el objetivo principal, pero también el problema principal.
La especificación incluye un untrustedContentHint, que solo sirve como indicación. Aún no se ha resuelto cómo manejar la inyección de comandos, y ninguna parte de esta API funciona como límite de autorización. Si el texto controlado por el atacante puede mostrarse en una página y un agente lo lee, readOnlyHint no puede ayudar.
También se puede ofrecer el puente a través de HTTP mediante un túnel para que los conectores de ChatGPT puedan acceder a él. Funciona, pero hay que pensar cuidadosamente antes de hacerlo, ya que el túnel expone un endpoint sin autenticación que permite ejecutar herramientas dentro de la pestaña del navegador con sesión iniciada a quienquiera que adivine la dirección. Veinte minutos de experimentación están bien, pero dejarlo activo toda la noche no lo está.
Probándolo en tu propia aplicación
Los mantenedores de una aplicación web pueden evaluar la API en una tarde, ya que es pequeña. Elijan cualquier formulario existente, añadanle los atributos toolname y tooldescription, luego abran el panel WebMCP en DevTools y observarán cómo aparece la herramienta. Aproximadamente diez minutos de esto enseñan más que otra hora de lectura.
Lo importante va más allá de la sintaxis. Decir con sinceridad qué puede hacer tu aplicación se ha convertido en parte de lo que se entrega, en lugar de ser una documentación escrita posteriormente. La plataforma no ofrecía una función de eliminación para tareas individuales, por lo que el agente nunca eliminó ninguna. Lo que devuelve getTools() determina cómo el agente percibe las capacidades de tu aplicación, y esa percepción se está convirtiendo gradualmente en la propia interfaz. La demostración incluía seis herramientas distribuidas en tres archivos sin dependencias, así que piensa en qué diría tu propia página sobre sí misma. Para reproducirla, activa la opción chrome://flags/#enable-webmcp-testing y luego ejecuta node agent.mjs list. Obtener seis herramientas como respuesta significa que estás interactuando con una página web de la misma manera en que lo haría un agente.
Respuestas rápidas
¿Qué es WebMCP? Es una API de navegador propuesta a través de la cual una página publica sus propias funciones como herramientas llamables para un agente de IA, utilizando document.modelContext. Los agentes llaman directamente a esas herramientas, evitan el análisis del DOM y coordinan los clics.
¿En qué difiere de un servidor MCP regular? Los servidores backend operan más allá de los límites de la red y actúan sobre datos almacenados. Las herramientas de WebMCP se ejecutan dentro de la pestaña, por lo que pueden modificar lo que ve el usuario y reutilizan la sesión y los permisos de la página. Llamar a filter-tasks cambió visiblemente el tablero.
¿Cómo registra una página una herramienta? Puede añadir los atributos toolname y tooldescription a un formulario existente y dejar que el navegador derive el esquema a partir de los campos, o bien llamar a document.modelContext.registerTool() con un nombre, una descripción, un JSON Schema y una función execute. Los formularios son adecuados para acciones simples, mientras que registerTool abarca todo aquello que un formulario no puede expresar.
¿Se requiere una bandera? Por ahora, sí. Active la bandera y vuelva a iniciar Chrome, o inícielo con --enable-features=WebMCP. Para los visitantes que no hayan activado la bandera, funciona un token de prueba de origen propio en el <head> de la página.
¿Pueden Claude Desktop o ChatGPT llamar a estas herramientas? No por sí solos, ya que utilizan MCP en lugar de WebMCP. Un pequeño adaptador mapea tools/list a getTools() y tools/call a executeTool(). El código correspondiente tiene unas 130 líneas, no requiere dependencias y funciona a través de stdio para clientes locales.
¿Es seguro? Consideremos la pregunta abierta. untrustedContentHint y readOnlyHint son solo indicaciones; ninguno marca un límite de autorización, y la especificación aún no ha resuelto el problema de la inyección de comandos. Construya la interfaz de herramientas asumiendo que un agente podría ser convencido para llamar a cualquier cosa que exponga.
Leer más
- Cuando un agente de IA tiene 1,000 herramientas MCP: selección, contexto y habilidades — Conectar agentes a muchos servidores MCP genera una explosión de herramientas, un contexto excesivo y elecciones inadecuadas. Por qué el descubrimiento, las habilidades y las pasarelas pueden ser más importantes que el acceso directo.
- Notas prácticas: MCP para laboratorios de investigación: un contrato para herramientas, datos y — Guía paso a paso de las Notas prácticas: MCP para laboratorios de investigación: un contrato para herramientas, datos y: contratos, verificaciones y espacios para código listo para usar para los equipos que implementan este patrón.