WebMCP Contract Portfolio
WebMCP Contract Portfolio
Коммерческое приложение для страхования финансовых линий, которым Claude управляет напрямую
через navigator.modelContext — API WebMCP (Web Model Context Protocol).
Спросите «какие договоры истекают в ближайшие 60 дней?» — и таблица отфильтруется прямо перед вами. Попросите продлить — и срок сдвинется в Postgres и на экране. Ассистент узнаёт, что умеет страница, в рантайме, читая схемы инструментов, которые страница публикует, — никакого скрейпинга DOM, никаких селекторов, никаких скриншотов.
Запуск
Три процесса. Для настоящего ассистента нужен ключ Anthropic API; без него работает всё, кроме модели (см. Без ключа ниже).
# 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:3002Бэкенд сам наполняет базу данных при первом запуске, так что seed.py нужен
только если вы хотите пересоздать данные или изменить размер (--force, --total 200).
Без ключа
MOCK_LLM=1вbackend/.envзаменяет Claude на скриптованный заглушечный модуль, который говорит на том же самом протоколе. Ответы заранее заготовлены; вызовы инструментов настоящие, так что каждый путь активации по-прежнему работает. Удобно для демонстрации без траты токенов.Без бэкенда вообще приложение всё равно загружается, и панель Прямые вызовы инструментов в сайдбаре вызывает инструменты WebMCP без модели в цикле.
Related MCP server: Salesforce MCP Server
Документация
WebMCP на практике — с какой проблемой на самом деле сталкивается встроенный ассистент, что такое WebMCP и как общаются браузер, бэкенд и модель, с диаграммами последовательности вызовов инструментов и передачи управления от сервера странице. Откройте файл в браузере.
CLAUDE.md — ориентир для работы в этом репозитории: команды, правила слоёв и уже пройденные здесь грабли.
Попробуйте это
Запрос | Что вы должны увидеть |
«Какие договоры истекают в ближайшие 60 дней?» | Таблица сужается, панель фильтров становится фиолетовой |
«Покажи всё, что связано с Allianz.» | Фильтрация по страховщику |
«Найди договор Novaris D&O и открой его.» | Поиск, затем переход к детальному виду |
«Продли киберполис Lumen Digital Health на 12 месяцев.» | Срок сдвигается на 12 месяцев, флаг продления сбрасывается, строка мигает |
«Создай новый кибердоговор для Cortex Robotics со страховщиком Markel, лимит 3 млн.» | Форма нового договора открывается предзаполненной, но не отправленной |
«Подними премию по FL-0146 до 95 000.» | Договор обновляется на месте |
«Какова суммарная премия по страховщикам?» | Агрегируется в SQL, показывается разбивкой — договоры в контекст не подтягиваются |
«Какие два договора имеют наибольшие лимиты?» |
|
«Продли всё, что истекает в ближайшие 30 дней.» | Серверный инструмент показывает предпросмотр пакета. Подтвердите — и он зафиксируется одной транзакцией, затем WebMCP переведёт вас к результату |
«Собери мне отчёт о продлениях на ближайшие 90 дней.» | Генерируется на сервере, затем |
«Соответствует ли цена по FL-0142 рынку?» | Бенчмарк-данные извне приложения — у страницы нет к ним доступа |
Фиолетовая рамка вокруг левой панели означает, что управляет ассистент. Панель WebMCP в правом нижнем углу перечисляет все зарегистрированные инструменты — щёлкните по одному, чтобы увидеть JSON Schema, которую на самом деле получает Claude, — и логирует каждый вызов при пересечении границы.
Всё работает и вручную: щёлкните строку, нажмите Изменить, нажмите Продлить. Человек и агент используют один и тот же API и одно и то же состояние React, так что нет отдельного «режима агента» и нет способа, которым они могли бы разойтись во мнениях.
Архитектура
Интересная часть в том, что агент на самом деле живёт вне страницы — именно так и работает WebMCP: браузер передаёт агенту список инструментов и маршалит его вызовы инструментов обратно.
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 никогда не видит DOM. Бэкенд не содержит никаких реализаций инструментов — он лишь сообщает, что Claude хочет вызвать. Каждый инструмент выполняется в браузере против живого состояния React.
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Почему ручной агентский цикл
Исполнитель инструментов Anthropic SDK выполняет инструменты в том же процессе. Здесь инструменты
живут в браузере пользователя, поэтому agent_ws.py вручную ведёт цикл
stop_reason == "tool_use" и ожидает каждый результат через WebSocket.
Параллельные вызовы инструментов выполняются конкурентно и возвращаются одним
сообщением user, как и ожидает API.
Две поверхности инструментов, один список инструментов
Claude получает один плоский список. Он не знает и ему всё равно, что часть этих инструментов выполняется в браузере, а часть — в бэкенде, но это разделение — самое важное проектное решение здесь.
Инструменты страницы (WebMCP, navigator.modelContext) — это возможности страницы.
Используйте их, когда пользователь должен видеть, как происходит изменение, и для работы
с одной записью. Они выполняются против живого состояния React.
Серверные инструменты выполняются в процессе FastAPI и никогда не касаются браузера. Используйте их, когда управление интерфейсом было бы в корне неверной формой:
Серверный инструмент | Почему ему не место в интерфейсе |
| Продление 14 договоров через страницу — это 14 кругов через модель, любой из которых может остановиться на полпути. Один вызов, одна транзакция, всё или ничего. |
| Сборка документа — это вычисление, а не клики. |
| Данные о рыночных ставках живут вне приложения. Никакая автоматизация интерфейса их бы не нашла. |
Связывающий их паттерн — передача управления. Серверная работа невидима — поэтому серверный инструмент возвращает идентификатор артефакта, и ассистент затем вызывает инструмент страницы, чтобы вывести его на экран:
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 thereРабота происходит вне страницы; результат всё равно попадает на страницу. Чат окрашивает их по-разному (фиолетовый = интерфейс сдвинулся, янтарный = работа произошла в другом месте), а инспектор перечисляет их под отдельными заголовками, так что то, какая сторона что сделала, никогда не приходится угадывать.
Массовые изменения по умолчанию показываются предпросмотром. run_renewal_batch — это
пробный прогон, если не указано commit=true. Массовое изменение не должно происходить потому,
что модель была на 80% уверена, что оно нужно, — ассистент показывает план и ждёт.
Инструменты страницы
Инструмент | Эффект на экране |
| Фильтрует, сортирует и ограничивает видимую таблицу (поэтому поиск агента виден) |
| Агрегирует в SQL и открывает вид разбивки |
| Никакого — возвращает полную запись |
| Переключает вид |
| Заполняет форму и останавливается. Отправляет человек. |
| Пишет в Postgres, открывает новый договор |
| Обновляет строку на месте |
| Сдвигает один срок вперёд, сбрасывает флаг продления |
| Показывает созданную сервером запись пакета |
| Показывает созданный сервером отчёт |
Поверхность инструментов — это решение о стоимости
search_contracts получил sort_by / sort_dir / limit, а
summarise_portfolio был добавлен по конкретной причине. На вопрос «какие два
договора имеют наибольшую страховую сумму?» ассистент изначально вызывал
search_contracts({}), подтягивал все 50 строк в контекст и сортировал их сам —
6 809 входных токенов и два вызова инструмента. С сортировкой и лимитом, вынесенными в SQL,
тот же вопрос стоит 518 токенов и один вызов, а арифметику выполняет база данных,
а не модель.
Если ваш агент много читает, чтобы ответить на малое, — это не хватает инструмента, а не проблема промптинга.
Имена аргументов инструментов точно совпадают с API и колонками базы данных (везде snake_case), так что нигде нет слоя маппинга, в котором мог бы спрятаться баг.
prefill_new_contract_form — это случай human-in-the-loop, на который стоит обратить внимание:
печатает агент, решение остаётся за человеком. Системный промпт говорит
Claude предпочитать его create_contract всякий раз, когда какая-то деталь была выведена.
Данные
50 договоров: 12 курируемых с историей в примечаниях, плюс 38 сгенерированных.
Генератор (seed_gen.py) детерминирован и заботится о двух вещах, которые
скрипт случайных данных обычно делает неправильно:
Скоррелированные цифры. Премия — это ставка от лимита, с диапазоном ставок по продукту (D&O 0,35–0,75%, Cyber 0,8–1,6%, …), а франшизы масштабируются вместе с лимитом. Иначе всё, что ассистент говорит о портфеле, не звучит правдоподобно.
Реалистичный конвейер истечений. Сроки размещаются относительно сегодняшнего дня под целевое распределение статусов — примерно 10% истекших, 25% истекающих в ближайшие 90 дней, остальные активные, плюс два черновика. Так что «что нужно продлить?» — всегда настоящий вопрос, и повторное наполнение через полгода всё ещё даёт живой на вид портфель, а не полностью истёкший.
Статус (active / expiring / expired / draft) вычисляется из срока,
а не хранится, поэтому не может разойтись. renewal_pending — отдельный флаг,
который ставит брокер.
Все застрахованные компании вымышлены. Названия страховщиков — реальные участники рынка, используемые так, как их использует любая брокерская демонстрация; ничто здесь не представляет реальный полис.
Полифилл
src/webmcp-polyfill.js выполняет две отдельные задачи, и различие важно:
Сторона страницы (собственно полифилл). Нативный navigator.modelContext пока
доступен не везде. Если его нет, файл устанавливает заглушку, реализующую предлагаемую
поверхность — registerTool, unregisterTool, provideContext — которая
логирует каждую регистрацию и каждый вызов в консоль DevTools. Приложение
никогда не падает, а бейдж в шапке сообщает, какая версия у вас.
Сторона агента (мост). Нет API, ориентированного на страницу, для «быть агентом», поэтому модуль также зеркалирует каждый зарегистрированный инструмент и предоставляет listTools() / executeTool() поверх. agentClient.js использует этот мост и ничего больше. Зеркало поддерживается как в нативных, так и в браузерах с полифиллами, поэтому поведение идентично в обоих случаях.
Из консоли DevTools:
await webmcp.listTools()
await webmcp.executeTool('search_contracts', { product: 'Cyber', status: 'expiring' })
await webmcp.executeTool('renew_contract', { contract_id: 'FL-0142', months: 24 })Ловушка React, о которой стоит знать
Очевидный способ зарегистрировать инструмент — неверен:
useEffect(() => {
const h = registerTool({ name: 'x', execute: () => doThingWith(contracts) });
return () => h.unregister();
}, []); // `contracts` is frozen at mount foreverПовторная регистрация при каждом изменении состояния также неверна — браузер будет видеть постоянное изменение всего набора инструментов, и выполняющийся вызов может быть вырван из-под агента.
useWebMcpTools.js регистрирует один раз с устойчивой косвенностью: зарегистрированный execute разрешает реальный обработчик из ref, который обновляется при каждом рендере. Регистрация стабильна; обработчики всегда видят текущее состояние. При двойном монтировании React StrictMode можно убедиться, что зарегистрировано ровно семь инструментов, а не четырнадцать и не ноль.
Примечания и ограничения
SEED_TOTAL/seed.py --totalизменяют размер книги. Фильтрация, сортировка и ограничение уже выполняются в SQL (queries.py), поэтому для гораздо большей книги нужна только пагинация в представлении списка.Пакетные записи и отчёты хранятся в памяти (
artifacts.py, максимум 50). Это вывод задания, а не доменные данные; в реальном развёртывании их следует сохранять, поскольку запись массового изменения является журналом аудита.benchmark_ratesвозвращает выдуманные числа. Он заменяет подписку на рыночные данные — суть в том, что это данные, к которым у браузера нет доступа.Новые идентификаторы контрактов берутся из
max(id) + 1. Два одновременных создания могут конфликтовать; исправление в одну строку — последовательность базы данных.Разговор хранится в памяти для каждого WebSocket-соединения, поэтому перезагрузка начинает новый чат. Сам портфель находится в Postgres и сохраняется.
output_config: {effort: "medium"}с адаптивным мышлением установлен вllm.py; повысьте его доhigh, если хотите, чтобы ассистент более тщательно планировал многошаговую работу.Включены запасные варианты отказа на стороне сервера. Если ваша учётная запись или версия SDK отклоняет параметр,
llm.pyзаписывает предупреждение и повторяет попытку один раз по обычному пути, а не завершает ход с ошибкой.
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