WebMCP Contract Portfolio
WebMCP Contract Portfolio
Una aplicación comercial de seguros de líneas financieras que Claude opera directamente a través de navigator.modelContext — la API WebMCP (Web Model Context Protocol).
Pregunta "¿qué contratos vencen en los próximos 60 días?" y la tabla se filtra delante de ti. Pide una renovación y el plazo avanza en Postgres y en pantalla. El asistente descubre qué puede hacer la página en tiempo de ejecución leyendo los esquemas de herramientas que la página publica — sin scraping del DOM, sin selectores, sin capturas de pantalla.
Cómo ejecutarlo
Tres procesos. Necesitas una clave de API de Anthropic para el asistente real; sin ella, todo excepto el modelo sigue funcionando (ver Sin clave más abajo).
# 1. Postgres (port 5434 — 5432 and 5433 are already taken on this machine)
docker compose up -d
# 2. Backend
cd backend
uv venv .venv && uv pip install --python .venv/bin/python -r requirements.txt
cp .env.example .env # then put your ANTHROPIC_API_KEY in it
.venv/bin/python seed.py # 50 contracts
.venv/bin/uvicorn app.main:app --reload --port 8000
# 3. Frontend
npm install
PORT=3002 npm start # http://localhost:3002El backend siembra la base de datos por sí mismo en el primer arranque, así que seed.py solo se necesita si quieres volver a sembrar o cambiar el tamaño (--force, --total 200).
Sin clave
MOCK_LLM=1enbackend/.envsustituye a Claude por un stub con script que habla el protocolo idéntico. Las respuestas están prefabricadas; las llamadas a herramientas son reales, así que todas las rutas de actuación siguen funcionando. Útil para demostrar sin gastar tokens.Sin backend en absoluto, la aplicación sigue cargando y el panel Direct tool calls de la barra lateral invoca las herramientas WebMCP sin ningún modelo en el bucle.
Related MCP server: Salesforce MCP Server
Documentación
WebMCP in Practice — qué problema tiene realmente un asistente dentro de la aplicación, qué es WebMCP y cómo se comunican el navegador, el backend y el modelo, con diagramas de la secuencia de llamadas a herramientas y el traspaso servidor-a-página. Abre el archivo en un navegador.
CLAUDE.md — orientación para trabajar en este repositorio: comandos, las reglas de capas y las trampas que ya se han encontrado aquí.
Prueba estos
Pregunta | Qué deberías ver |
"¿Qué contratos vencen en los próximos 60 días?" | La tabla se reduce, la barra de filtros se vuelve morada |
"Muéstrame todo lo de Allianz." | Filtra por aseguradora |
"Busca el contrato D&O de Novaris y ábrelo." | Busca y luego navega a la vista de detalle |
"Renueva la póliza de cyber de Lumen Digital Health por 12 meses." | El plazo avanza 12 meses, la marca de renovación se limpia, la fila parpadea |
"Crea un nuevo contrato Cyber para Cortex Robotics con Markel, límite de 3m." | El formulario de nuevo contrato se abre prellenado pero no enviado |
"Sube la prima de FL-0146 a 95,000." | El contrato se actualiza en su lugar |
"¿Cuál es la prima total por aseguradora?" | Agregado en SQL, mostrado como desglose — no se traen contratos al contexto |
"¿Qué dos contratos tienen los mayores límites?" |
|
"Renueva todo lo que vence en los próximos 30 días." | Una herramienta servidor previsualiza el lote. Confirma, y se confirma en una sola transacción, y WebMCP te navega al resultado |
"Genérame un informe de renovación para los próximos 90 días." | Generado en el servidor, luego |
"¿Está FL-0142 con precio acorde al mercado?" | Datos de benchmark de fuera de la aplicación — la página no tiene ruta hacia ellos |
El borde morado alrededor del panel izquierdo significa que el asistente está conduciendo. El panel WebMCP en la parte inferior derecha lista todas las herramientas registradas — haz clic en una para ver el JSON Schema que Claude realmente recibe — y registra cada llamada cuando cruza el límite.
Todo funciona también a mano: haz clic en una fila, pulsa Editar, pulsa Renovar. El humano y el agente comparten la misma API y el mismo estado de React, así que no hay un "modo agente" separado ni forma de que los dos se contradigan.
Arquitectura
La parte interesante es que el agente vive genuinamente fuera de la página, que es como funciona realmente WebMCP: el navegador entrega al agente una lista de herramientas y devuelve sus llamadas de herramientas.
browser (React) backend (FastAPI) Claude
│ user_message + tool list │ │
│─────────────────────────────────>│ messages.stream(tools=…) │
│ │──────────────────────────> │
│ text_delta │ streamed text │
│<─────────────────────────────────│<─────────────────────────── │
│ tool_use │ stop_reason=tool_use │
│<─────────────────────────────────│<─────────────────────────── │
│ │
│ executeTool() → REST → Postgres → React state → repaint │
│ │
│ tool_result │ │
│─────────────────────────────────>│ append, continue loop │
│ │──────────────────────────> │
│ turn_end │ stop_reason=end_turn │
│<─────────────────────────────────│<─────────────────────────── │Claude nunca ve el DOM. El backend no contiene ninguna implementación de herramientas — solo informa de lo que Claude quiere llamar. Cada herramienta se ejecuta en el navegador contra el estado de React en vivo.
docker-compose.yml Postgres 17 on :5434
backend/
├── seed.py seeding CLI
└── app/
├── main.py FastAPI: REST + /ws/agent
├── db.py engine, session dependency, readiness wait
├── models.py SQLModel table + validated API schemas
├── repository.py all SQL lives here
├── seed_data.py 12 curated contracts (terms relative to today)
├── seed_gen.py deterministic generator for the rest
├── queries.py filtering, sorting and aggregation in SQL
├── server_tools.py tools that run here, not in the page
├── artifacts.py batch records and reports
├── llm.py Claude client + the mock provider
└── agent_ws.py the bridge: routes each tool call to the right side
src/
├── webmcp-polyfill.js polyfill + agent-side bridge
├── useWebMcpTools.js registration lifecycle hook
├── api.js REST client
├── App.js owns state; registers the seven tools
├── agent/agentClient.js WebSocket client; executes tool calls
└── components/ ContractList · ContractDetail · NewContractForm ·
PortfolioSummary · BatchResult · ReportView ·
AssistantChat · ToolInspector¿Por qué un bucle agéntico manual?
El ejecutor de herramientas del SDK de Anthropic ejecuta las herramientas en el proceso. Aquí las herramientas viven en el navegador del usuario, así que agent_ws.py maneja el bucle stop_reason == "tool_use" a mano y espera cada resultado a través del WebSocket. Las llamadas a herramientas paralelas se ejecutan de forma concurrente y se devuelven en un único mensaje user, como espera la API.
Dos superficies de herramientas, una lista de herramientas
Claude recibe una lista plana. No sabe ni le importa que algunas de esas herramientas se ejecuten en el navegador y otras en el backend — pero la división es la decisión de diseño más importante aquí.
Las herramientas de página (WebMCP, navigator.modelContext) son las capacidades de la página. Úsalas cuando el usuario deba ver el cambio ocurrir, y para trabajo de un solo registro. Se ejecutan contra el estado de React en vivo.
Las herramientas del servidor se ejecutan en el proceso FastAPI y nunca tocan el navegador. Úsalas cuando manejar una interfaz de usuario sería la forma completamente equivocada:
Herramienta del servidor | Por qué no pertenece a la interfaz |
| Renovar 14 contratos a través de la página son 14 idas y vueltas a través del modelo, cualquiera de las cuales puede detenerse a mitad. Una llamada, una transacción, todo o nada. |
| Ensamblar un documento es computación, no clics. |
| Los datos de tasas de mercado viven fuera de la aplicación. Ninguna cantidad de automatización de interfaz los encontraría. |
El patrón que los une es el traspaso. El trabajo del servidor es invisible — así que una herramienta del servidor devuelve un id de artefacto, y el asistente luego llama a una herramienta de página para mostrarlo en pantalla:
run_renewal_batch(expiring_within_days=30) ← server: previews, changes nothing
→ "4 contracts, €413,400. Shall I commit?"
run_renewal_batch(..., commit=true) ← server: one transaction
→ batch_id: BATCH-0002
show_batch_result(batch_id="BATCH-0002") ← page: navigates the user thereEl trabajo ocurre fuera de la página; el resultado sigue aterrizando en la página. El chat colorea los dos de forma diferente (morado = la interfaz se movió, ámbar = el trabajo ocurrió en otro lugar) y el inspector los lista bajo encabezados separados, así que qué lado hizo qué nunca es una suposición.
Los cambios masivos se previsualizan por defecto. run_renewal_batch es una ejecución en seco a menos que commit=true. Una mutación masiva no debería ocurrir porque un modelo estaba 80% seguro de que se quería — el asistente muestra el plan y espera.
Las herramientas de página
Herramienta | Efecto en pantalla |
| Filtra, ordena y limita la tabla visible (por eso una búsqueda de agente es visible) |
| Agrega en SQL y abre la vista de desglose |
| Ninguno — devuelve el registro completo |
| Cambia de vista |
| Rellena el formulario y se detiene. El humano envía. |
| Escribe en Postgres, abre el nuevo contrato |
| Actualiza la fila en su lugar |
| Avanza un plazo, limpia la marca de renovación |
| Muestra un registro de lote producido por el servidor |
| Muestra un informe producido por el servidor |
La superficie de herramientas es una decisión de coste
search_contracts ganó sort_by / sort_dir / limit, y se añadió summarise_portfolio, por una razón específica. Preguntado "¿qué dos contratos tienen la mayor suma asegurada?", el asistente originalmente llamaba a search_contracts({}), traía las 50 filas al contexto y las ordenaba él mismo — 6,809 tokens de entrada y dos llamadas a herramientas. Con el orden y el límite empujados en SQL, la misma pregunta cuesta 518 tokens y una llamada, y la aritmética es de la base de datos en lugar del modelo.
Si tu agente está leyendo mucho para responder poco, eso es una herramienta que falta, no un problema de prompting.
Los nombres de los argumentos de las herramientas coinciden exactamente con la API y las columnas de la base de datos (snake_case en todo), así que no hay ninguna capa de mapeo en ningún sitio para que un error se esconda.
prefill_new_contract_form es el caso de humano-en-el-bucle que vale la pena notar: el agente hace la escritura, la persona mantiene la decisión. El prompt del sistema le dice a Claude que lo prefiera sobre create_contract siempre que se haya inferido un detalle.
Los datos
50 contratos: 12 curados con una historia en sus notas, más 38 generados.
El generador (seed_gen.py) es determinista y se preocupa de dos cosas que un script de datos aleatorios suele fallar:
Cifras correlacionadas. La prima es una tasa sobre el límite, con una banda de tasas por producto (D&O 0.35–0.75%, Cyber 0.8–1.6%, …), y los deducibles escalan con el límite. De lo contrario, nada de lo que el asistente diga sobre la cartera sonaría creíble.
Un pipeline de vencimiento realista. Los plazos se colocan relativos a hoy contra una mezcla de estados objetivo — aproximadamente 10% vencidos, 25% vencen en 90 días, el resto activos, más dos borradores. Así que "¿qué necesita renovación?" es siempre una pregunta real, y volver a sembrar en seis meses sigue produciendo una cartera con aspecto de viva en lugar de una que ha caducado por completo.
El estado (active / expiring / expired / draft) se calcula a partir del plazo, nunca se almacena, así que no puede desviarse. renewal_pending es una marca separada que un broker establece.
Todas las empresas aseguradas son ficticias. Los nombres de las aseguradoras son participantes reales del mercado, usados como cualquier demo de broker los usa; nada aquí representa una póliza real.
El polyfill
src/webmcp-polyfill.js hace dos trabajos separados, y la distinción importa:
Lado de página (el polyfill real). navigator.modelContext nativo no se distribuye en todos los sitios todavía. Si falta, el archivo instala un stub que implementa la superficie propuesta — registerTool, unregisterTool, provideContext — que registra cada registro e invocación en la consola de DevTools. La aplicación nunca se bloquea, y la insignia del encabezado te dice cuál tienes.
Lado del agente (un puente). No existe una API orientada a página para «ser el agente», por lo que el módulo también refleja todas las herramientas registradas y expone listTools() / executeTool() además. agentClient.js usa ese puente y nada más. El espejo se mantiene tanto en navegadores nativos como con polyfill, de modo que el comportamiento es idéntico en ambos casos.
Desde la consola de DevTools:
await webmcp.listTools()
await webmcp.executeTool('search_contracts', { product: 'Cyber', status: 'expiring' })
await webmcp.executeTool('renew_contract', { contract_id: 'FL-0142', months: 24 })La trampa de React que conviene conocer
La forma obvia de registrar una herramienta es incorrecta:
useEffect(() => {
const h = registerTool({ name: 'x', execute: () => doThingWith(contracts) });
return () => h.unregister();
}, []); // `contracts` is frozen at mount foreverVolver a registrar en cada cambio de estado también es incorrecto: el navegador vería todo el conjunto de herramientas cambiando constantemente, y una llamada en curso podría arrancarse de debajo del agente.
useWebMcpTools.js registra una sola vez con una indirección estable: el execute registrado resuelve el manejador real a partir de una ref que cada render refresca. El registro es estable; los manejadores siempre ven el estado actual. Bajo el doble montaje del StrictMode de React puedes confirmar que se registran exactamente siete herramientas, ni catorce ni cero.
Notas y límites
SEED_TOTAL/seed.py --totalcambian el tamaño del libro. El filtrado, la ordenación y el límite ya se ejecutan en SQL (queries.py), así que lo único que necesita un libro mucho más grande es paginación en la vista de lista.Los registros y los informes por lotes viven en memoria (
artifacts.py, con tope de 50). Son salida de trabajo más que datos de dominio; un despliegue real los persistiría, ya que un registro de cambio masivo es un rastro de auditoría.benchmark_ratesdevuelve números inventados. Hace las veces de una suscripción a datos de mercado; el punto es que se trata de datos a los que el navegador no tiene ninguna ruta.Los nuevos ids de contrato provienen de
max(id) + 1. Dos creaciones simultáneas podrían colisionar; una secuencia de base de datos es la solución de una línea.La conversación vive en memoria por conexión WebSocket, así que una recarga inicia un chat nuevo. La cartera en sí está en Postgres y persiste.
output_config: {effort: "medium"}con pensamiento adaptativo está configurado enllm.py; súbelo ahighsi quieres que el asistente planifique el trabajo multi-paso con más cuidado.Los respaldos de rechazo del lado del servidor están habilitados. Si tu cuenta o la versión del SDK rechaza el parámetro,
llm.pyregistra una advertencia y reintenta una vez por la ruta sencilla en lugar de fallar el turno.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseAqualityAmaintenanceAn MCP server implementation that integrates Claude with Salesforce, enabling natural language interactions with Salesforce data and metadata for querying, modifying, and managing objects and records.6153,172166MIT
- AlicenseAqualityDmaintenanceAn MCP server implementation that integrates Claude with Salesforce, enabling natural language interactions with Salesforce data and metadata.850MIT
- FlicenseBqualityCmaintenanceA customer and product management MCP server using SQLite. It enables Claude Desktop users to manage client and product data through natural language interactions.91
- Flicense-qualityBmaintenanceAn MCP server that enables Claude to deploy full-stack web apps to Cloudflare, including databases, authentication, and file storage, directly through natural language.
Related MCP Connectors
MCP server giving Claude AI access to 22+ NYC public-record databases for real estate due diligence
MCP server for AI agents to plan, verify, and deploy Cloudflare-native apps.
Analytical memory for AI agents: a real Postgres queried in plain English over MCP. One command.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/hossein-finlex/web-mcp-hello'
If you have feedback or need assistance with the MCP directory API, please join our Discord server