MCP

WebMCP: la API que deja a tu web ofrecer herramientas a los agentes

Autorangel cruz
Actualizado
Publicado
Lectura10 min de lectura
WebMCP: la API que deja a tu web ofrecer herramientas a los agentes

Hoy, cuando un agente de IA usa tu web, adivina. Mira el DOM, decide que ese <button> probablemente envía el formulario, hace clic y espera que salga bien. WebMCP propone lo contrario: que la propia página registre herramientas con nombre, descripción y esquema de entrada, y que el agente llame a esas funciones en vez de simular clics.

Es una propuesta de estándar web que Google y Microsoft están incubando en el Web Machine Learning Community Group del W3C, y desde Chrome 149 se puede probar en un origin trial. Aquí va lo que dicen las fuentes originales: la especificación, el explainer y la documentación de Chrome.

Qué es WebMCP exactamente

Una sola cosa nueva en el DOM: document.modelContext. Desde ahí registras tools, que son funciones JavaScript con un JSON Schema de entrada y una descripción en lenguaje natural. Un agente que esté operando en esa pestaña las descubre y las invoca.

El vocabulario viene de MCP: tools, schemas, parámetros. Pero el explainer es explícito en que WebMCP no reemplaza a MCP. MCP conecta un modelo con servidores; WebMCP vive dentro de una página, con las reglas del navegador (orígenes, permissions policy, DOM) y reutilizando la lógica de cliente que ya escribiste.

Los objetivos declarados en el explainer son estos, y el tercero es el más interesante para quien tiene un producto web:

  • Flujos con humano en el bucle, donde el usuario ve la página y mantiene el control.
  • Integración de agentes sin automatización frágil de UI.
  • Evitar la desintermediación: que tu frontend se adapte a los agentes en lugar de que lo salteen con una integración de backend.
  • Reutilizar la lógica de cliente que ya tienes.
  • Accesibilidad: un agente como intermediario capaz para quien usa tecnología asistiva.

Y los no objetivos importan igual: no está pensado para flujos totalmente autónomos sin una interfaz de navegador, ni para reemplazar las integraciones de backend, ni la interfaz humana. La navegación headless, en cambio, el explainer ya la cuenta entre los objetivos: las mismas tools sirven cuando una tarea pasa de hacerse con el humano mirando a completarse en headless.

En qué estado está de verdad

Aquí es donde conviene ser preciso, porque circula mucho artículo que lo presenta como si ya fuera estándar.

  • La especificación es un Draft Community Group Report que se publica de nuevo con cada cambio. No es un estándar del W3C. Los editores son Brandon Walderman (Microsoft), Khushal Sagar y Dominic Farolino (Google).
  • En Chrome hubo developer trial desde la 146 y el origin trial arrancó en Chrome 149 con cierre previsto en la 156. Chrome Platform Status registra una solicitud para extenderlo hasta Chrome 162. El estado de la feature en Chrome Platform Status sigue siendo "Proposed", y la review del TAG está pendiente: la piden antes de shippear.
  • Firefox y Safari: sin señal. Ninguno de los dos ha publicado una posición.

O sea: ya puedes escribirlo hoy y es probable que siga adelante, pero no está garantizado. Si lo adoptas ahora, lo haces como progressive enhancement detrás de un if ('modelContext' in document), no como camino crítico.

La API imperativa

El caso normal. Registras una tool con registerTool:

await document.modelContext.registerTool({
  name: 'toggle_layer',
  description: 'Control pizza layers (sauce, cheese). Use "add", "remove", or "toggle".',
  inputSchema: {
    type: 'object',
    properties: {
      layer: { type: 'string', enum: ['sauce-layer', 'cheese-layer'] },
      action: { type: 'string', enum: ['add', 'remove', 'toggle'] },
    },
    required: ['layer'],
  },
  execute: async ({ layer, action }) => {
    await toggleLayer(layer, action);
    return `Performed ${action || 'toggle'} on layer: ${layer}`;
  },
});

Cuatro piezas: name, description, inputSchema en JSON Schema y execute. La especificación limita el nombre a 128 caracteres alfanuméricos ASCII con guiones, guiones bajos y puntos, y exige que la descripción no esté vacía.

Fíjate en el return: la tool devuelve una frase, no un objeto opaco. Ese texto es lo que el modelo lee para saber si funcionó, así que escríbelo pensando en que lo va a leer un modelo.

execute recibe además un segundo argumento con el que cancelar trabajo largo cuando el agente abandona. Ojo con la forma exacta, porque la documentación de Chrome la describe de pasada como "un AbortSignal" y no lo es: la especificación define ese parámetro como un diccionario ToolExecuteCallbackOptions cuyo miembro signal sí es el AbortSignal. Por eso los ejemplos lo desestructuran con llaves. Si escribes async (input, signal) => … recibes el objeto entero y tu fetch se queda sin señal.

execute: async ({ url, priority }, { signal }) => {
  const response = await fetch(url, { priority, signal });
  const stream = response.body.pipeThrough(new TextDecoderStream());
  for await (const chunk of stream) {
    document.querySelector('pre').textContent += chunk;
  }
  return 'Success';
}

Para desregistrar una tool, otro AbortController, esta vez en las opciones del registro:

const controller = new AbortController();
await document.modelContext.registerTool(addTodoTool, { signal: controller.signal });
controller.abort(); // adiós tool

Un detalle que importa si pruebas hoy: hasta Chrome 152, desregistrar cancela también las ejecuciones en vuelo. Desde la 153 puedes retirar una tool sin romper lo que estuviera corriendo, que es lo que necesitas cuando el registro cuelga del ciclo de vida de un componente. Como el origin trial arranca en la 149, el comportamiento viejo entra dentro de la ventana.

Y el resto de la superficie: getTools() lista lo registrado en el documento y sus descendientes, executeTool() ejecuta una tool en su documento de origen, y el evento toolchange avisa cuando algo se registra o se va.

document.modelContext.addEventListener('toolchange', () => {
  // la lista de tools cambió
});

Si trabajas con tipos, existe el paquete webmcp-types en npm. Para React hay hooks experimentales en usewebmcp, y Angular tiene soporte experimental propio.

La API declarativa: formularios anotados

La segunda vía no lleva JavaScript. Anotas un formulario que ya existe y se convierte en una tool:

<form toolname="supportRequestTool"
      tooldescription="Submit a request for support."
      action="/submit">
  <label for="firstName">First Name</label>
  <input type=text name=firstName>
 
  <select name="select" required
    toolparamdescription="Determines what team this request is routed to.">
    <option value="Customer happiness team">Return my purchase.</option>
    <option value="Distribution team">Check where my package is.</option>
  </select>
 
  <button type=submit>Submit</button>
</form>

toolname y tooldescription sobre el <form>, toolparamdescription sobre cada campo que necesite explicación. Los name de los inputs se convierten en las propiedades del schema.

Con toolautosubmit el formulario se envía solo cuando lo invoca un agente, y el SubmitEvent trae dos cosas nuevas para que distingas quién lo disparó y devuelvas un resultado:

<form toolautosubmit toolname="search_tool"
      tooldescription="Search the web" action="/search">
  <input type=text name=query>
</form>
 
<script>
  document.querySelector("form").addEventListener("submit", (e) => {
    e.preventDefault();
    if (!myFormIsValid()) {
      if (e.agentInvoked) { e.respondWith(myFormValidationErrorPromise); }
      return;
    }
    if (e.agentInvoked) { e.respondWith(Promise.resolve("Search is done!")); }
  });
</script>

e.agentInvoked es booleano y e.respondWith(promise) devuelve el resultado al modelo. Para un formulario de contacto o una búsqueda, esto es todo el trabajo.

Seguridad: lo que hay que leer antes de registrar nada

WebMCP amplía la superficie de ataque de tu web de una forma nueva, y la especificación lo asume por escrito. Documenta tres vectores de prompt injection: por metadatos (nombre y descripción de la tool), por la salida de la tool y por la implementación misma.

Las defensas que trae la plataforma:

  • Permissions Policy. Está detrás de la policy tools, con allowlist por defecto ['self']. Un iframe cross-origin necesita allow="tools" explícito, y si no quieres WebMCP en ninguna parte de tu sitio, la especificación propone mandar Permissions-Policy: tools=(). (Las primeras versiones del borrador además exigían aislamiento de origen; el grupo quitó ese requisito en el PR #330.)
  • exposedTo. Al registrar puedes limitar qué orígenes ven esa tool: { exposedTo: ['https://example.com'] }.
  • Anotaciones. readOnlyHint marca una tool que no muta nada y untrustedContentHint marca que lo que devuelve viene de contenido no confiable, para que el agente lo trate como datos y no como instrucciones. consequentialHint marca acciones con consecuencias reales o irreversibles (reservar, transferir dinero), para que el agente o el navegador pidan confirmación.

La regla mental es la de siempre con agentes: la tool es una API pública más. Lo que no expondrías en un endpoint sin autorización, no lo expongas como tool. Si te interesa el lado feo de esto, escribí sobre servidores MCP maliciosos y sobre OAuth para agentes.

Buenas prácticas, según Chrome

La documentación de Chrome tiene una guía de best practices y hay cuatro ideas que valen más que el resto:

Nombra distinguiendo ejecutar de iniciar. create-event para lo que crea el evento; start-event-creation-process si lo que haces es llevar al usuario a un formulario. El agente decide por el verbo.

Describe en positivo. En vez de "no uses esta tool para el clima", escribe "esta tool crea un evento de calendario para una fecha y hora concretas". Las limitaciones se deducen de una buena descripción.

Una función por tool, y registro estático por defecto. Nada de tools que se solapan. El registro dinámico existe, pero úsalo con moderación.

Valida estricto en el código, laxo en el schema. El schema no está garantizado; lo que sí funciona es devolver un error descriptivo que el modelo pueda leer y corregir. Y acepta la entrada cruda del usuario en lugar de pedirle al modelo que haga cálculos previos.

Sobre pruebas, la recomendación es eval-driven: defines un baseline, un resultado ideal y evalúas cualitativamente, en lugar de tests unitarios rígidos.

Cómo probarlo hoy

Para desarrollo local basta el flag chrome://flags/#enable-webmcp-testing. Para servirlo a usuarios reales necesitas registrarte en el origin trial desde developer.chrome.com/origintrials, que arrancó en Chrome 149.

Para inspeccionar hay una extensión, el Model Context Tool Inspector, que lista las tools registradas, te deja ejecutarlas a mano, valida el JSON Schema y permite probar con lenguaje natural. Y en GoogleChromeLabs/webmcp-tools están las demos de referencia: un Pizza Maker y un viaje en React con la API imperativa, y Le Petit Bistro con la declarativa.

Lo que todavía no está decidido

El explainer mantiene una lista abierta que conviene mirar antes de construir encima: soporte multimodal (entradas y salidas binarias), qué pasa con la respuesta de una tool si la página navega, qué ven por defecto los agentes integrados del navegador, streaming de entradas y salidas grandes, validación de schemas de entrada y de salida, agrupación de tools en "skills", cómo pedir confirmación explícita al usuario en las tools que la necesiten, reporte de progreso en tareas largas e integración con service workers para descubrir tools sin abrir el sitio.

Esa última es la que más cambiaría las cosas, porque hoy la única forma de descubrir tus tools es que alguien visite tu página. Mientras eso siga así, WebMCP mejora la sesión de un agente que ya llegó, pero no te trae agentes nuevos. Para lo otro están las vías de descubrimiento que ya existen, como content negotiation o DNS.

Preguntas frecuentes

¿WebMCP reemplaza a MCP?

No, y el explainer lo dice como no objetivo explícito. MCP conecta un modelo con servidores externos; WebMCP expone tools dentro de una página, con las reglas del navegador. Un producto puede tener los dos: un servidor MCP para integraciones de backend y tools WebMCP para la sesión del usuario en el navegador.

¿Funciona ya en producción?

Solo en Chrome y solo dentro del origin trial, que arrancó en la versión 149. Firefox y Safari no han dado señal. Regístralo detrás de una comprobación de capacidad y trata la UI humana como el camino principal.

¿Necesito escribir JavaScript para usar WebMCP?

No siempre. Si lo que quieres exponer es un formulario, la API declarativa te lo resuelve con toolname, tooldescription y toolparamdescription en el HTML. El JavaScript hace falta cuando la tool no es un envío de formulario o cuando quieres controlar el resultado que ve el modelo.

¿Cualquier página puede ver mis tools?

No. La policy tools tiene allowlist ['self'] por defecto, así que un iframe cross-origin necesita allow="tools", y al registrar puedes acotar con exposedTo.

¿WebMCP es un estándar del W3C?

Todavía no. Es un Draft Community Group Report del Web Machine Learning Community Group, que es la fase de incubación. Chrome lo tiene como "Proposed" y la review del TAG está pendiente.

¿Sirve para agentes headless o scraping?

No es su caso principal. El explainer excluye los flujos totalmente autónomos sin interfaz de navegador, aunque cuenta el headless entre sus objetivos cuando una tarea alterna entre el humano y el agente. La documentación de Chrome lo resume así: puede funcionar en headless, pero está diseñado sobre todo para un navegador local con un humano en el bucle.

WebMCP cubre lo que un agente puede hacer en tu sitio. Lo que puede leer, que es la base sobre la que se apoya, está en superficies para agentes.

Fuentes

¿Tienes un proyecto en mente?

Trabajo con Laravel, WordPress, SEO técnico y servidores MCP. El primer paso es una llamada de descubrimiento, sin costo ni compromiso, donde me cuentas qué necesitas y te digo con honestidad si puedo ayudarte.

Hablemos