Skip to main content
Glama
hossein-finlex

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, показывается разбивкой — договоры в контекст не подтягиваются

«Какие два договора имеют наибольшие лимиты?»

sort_by + limit в SQL; таблица переупорядочивается и показывает ровно два

«Продли всё, что истекает в ближайшие 30 дней.»

Серверный инструмент показывает предпросмотр пакета. Подтвердите — и он зафиксируется одной транзакцией, затем WebMCP переведёт вас к результату

«Собери мне отчёт о продлениях на ближайшие 90 дней.»

Генерируется на сервере, затем show_report выводит его на экран

«Соответствует ли цена по 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 и никогда не касаются браузера. Используйте их, когда управление интерфейсом было бы в корне неверной формой:

Серверный инструмент

Почему ему не место в интерфейсе

run_renewal_batch

Продление 14 договоров через страницу — это 14 кругов через модель, любой из которых может остановиться на полпути. Один вызов, одна транзакция, всё или ничего.

generate_renewal_report

Сборка документа — это вычисление, а не клики.

benchmark_rates

Данные о рыночных ставках живут вне приложения. Никакая автоматизация интерфейса их бы не нашла.

Связывающий их паттерн — передача управления. Серверная работа невидима — поэтому серверный инструмент возвращает идентификатор артефакта, и ассистент затем вызывает инструмент страницы, чтобы вывести его на экран:

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% уверена, что оно нужно, — ассистент показывает план и ждёт.

Инструменты страницы

Инструмент

Эффект на экране

search_contracts

Фильтрует, сортирует и ограничивает видимую таблицу (поэтому поиск агента виден)

summarise_portfolio

Агрегирует в SQL и открывает вид разбивки

get_contract

Никакого — возвращает полную запись

navigate

Переключает вид

prefill_new_contract_form

Заполняет форму и останавливается. Отправляет человек.

create_contract

Пишет в Postgres, открывает новый договор

update_contract

Обновляет строку на месте

renew_contract

Сдвигает один срок вперёд, сбрасывает флаг продления

show_batch_result

Показывает созданную сервером запись пакета

show_report

Показывает созданный сервером отчёт

Поверхность инструментов — это решение о стоимости

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 записывает предупреждение и повторяет попытку один раз по обычному пути, а не завершает ход с ошибкой.

Related MCP Connectors

Related MCP Servers