WebMCP Contract Portfolio
WebMCP Contract Portfolio
一款由 Claude 通过 navigator.modelContext —— WebMCP(Web Model Context Protocol)API 直接操作的商业金融险应用。
问 “未来 60 天有哪些合同到期?”,表格会在你眼前筛选。要求续保,期限便会在 Postgres 和屏幕上向前推进。助手通过阅读页面发布的工具 schema,在运行时发现页面能做什么——不做 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)。
无密钥
backend/.env中的MOCK_LLM=1会把 Claude 换成一个使用相同协议的脚本化桩。回复是预设好的;工具调用是真实的,因此每条执行路径仍然有效。适合在不消耗 token 的情况下演示。即使完全没有后端,应用依然可以加载,侧边栏中的 直接工具调用面板会在没有模型参与的情况下调用 WebMCP 工具。
Related MCP server: Salesforce MCP Server
文档
WebMCP 实践 —— 应用内助手实际面临什么问题、WebMCP 是什么,以及浏览器、后端和模型之间如何通信,配有工具调用序列和服务端到页面交接的示意图。请在浏览器中打开该文件。
CLAUDE.md —— 在本仓库中工作的指引:命令、分层规则,以及这里已经踩过的坑。
试试这些
提问 | 预期效果 |
"未来 60 天有哪些合同到期?" | 表格收窄,筛选栏变成紫色 |
"显示所有 Allianz 的合同。" | 按保险公司筛选 |
"找到 Novaris 的 D&O 合同并打开它。" | 搜索,然后导航到详情视图 |
"将 Lumen Digital Health 的网络保险续保 12 个月。" | 期限滚动 12 个月,续保标记清除,行闪烁 |
"为 Cortex Robotics 与 Markel 新建一份 Cyber 合同,限额 300 万。" | 新合同表单打开,已预填但未提交 |
"将 FL-0146 的保费提高到 95,000。" | 合同就地更新 |
"按保险公司统计的保费总额是多少?" | 在 SQL 中聚合,显示为明细——不把任何合同拉入上下文 |
"哪两份合同的限额最大?" | 在 SQL 中使用 |
"续保未来 30 天内到期的所有合同。" | 一个服务器工具预览批次。确认后,它会在一个事务中提交,然后 WebMCP 将你导航到结果 |
"为我生成一份未来 90 天的续保报告。" | 在服务器上生成,然后 |
"FL-0142 的定价与市场一致吗?" | 基准数据来自应用之外——页面没有获取它的路径 |
左侧窗格周围的紫色边框表示助手正在驱动。右下角的 WebMCP 面板列出每个已注册的工具——点击一个即可查看 Claude 实际收到的 JSON Schema——并记录每次跨越边界的调用。
一切也都可以手动完成:点击一行,点击“编辑”,点击“续保”。人类和代理共享相同的 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 等待每个结果。并行工具调用会并发执行,并按照 API 的预期在单条 user 消息中返回。
两个工具面,一份工具列表
Claude 收到一份扁平列表。它既不知道也不关心其中一些工具在浏览器中运行、另一些在后端运行——但这个拆分是这里最重要的设计决策。
页面工具(WebMCP、navigator.modelContext)是页面的能力。当用户应当看到变化发生,以及处理单条记录的工作时,使用它们。它们针对实时 React 状态执行。
服务器工具在 FastAPI 进程中运行,从不接触浏览器。当驱动 UI 完全不是正确的形态时,使用它们:
服务器工具 | 为什么它不属于 UI |
| 通过页面续保 14 份合同需要经过模型的 14 次往返,其中任何一次都可能中途停止。一次调用、一个事务、全有或全无。 |
| 组装文档是计算,而不是点击。 |
| 市场费率数据存在于应用之外。再多的 UI 自动化也找不到它。 |
把它们联系在一起的模式是交接。服务器工作是不可见的——因此服务器工具会返回一个产物 id,然后助手调用页面工具把它放到屏幕上:
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工作在页面之外进行;但结果仍然落在页面上。聊天界面用不同颜色区分两者(紫色 = UI 发生了变化,琥珀色 = 工作发生在别处),检查器将它们列在不同的标题下,因此哪一侧做了什么永远无需猜测。
批量变更默认先预览。 run_renewal_batch 默认是试运行,除非 commit=true。批量变更不应仅仅因为模型有 80% 的把握认为用户想要它,就贸然执行——助手会先展示计划并等待。
页面工具
工具 | 屏幕上的效果 |
| 筛选、排序并限制可见表格(这就是为什么代理搜索是可见的) |
| 在 SQL 中聚合,并打开明细视图 |
| 无——返回完整记录 |
| 切换视图 |
| 填写表单并停下。由人工提交。 |
| 写入 Postgres,打开新合同 |
| 就地更新行 |
| 向前滚动一个期限,清除续保标记 |
| 显示服务器生成的批次记录 |
| 显示服务器生成的报告 |
工具面是一项成本决策
search_contracts 增加了 sort_by / sort_dir / limit,并新增了 summarise_portfolio,这是出于一个特定的原因。当被问到 “哪两份合同的保额最大?” 时,助手最初调用 search_contracts({}),将所有 50 行拉入上下文并自行排序——6,809 个输入 token 和两次工具调用。把排序和限制下推到 SQL 后,同一个问题只需 518 个 token 和一次调用,而且算术是由数据库完成的,而不是模型。
如果你的代理读了很多却只回答很少,那是缺少工具,而不是提示词的问题。
工具参数名与 API 和数据库列完全一致(全部使用 snake_case),因此任何地方都没有映射层可以让 bug 藏身。
prefill_new_contract_form 是一个值得注意的人机协作案例:代理负责输入,人保留决定权。系统提示词告诉 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 是经纪人设置的一个独立标志。
所有被保险的公司都是虚构的。保险公司名称是真实的市场参与者,其使用方式与任何经纪人演示相同;这里没有任何内容代表真实保单。
Polyfill
src/webmcp-polyfill.js 承担两项不同的工作,区分它们很重要:
页面侧(真正的 polyfill)。 原生 navigator.modelContext 尚未在所有环境中提供。如果缺失,该文件会安装一个桩,实现提议的接口——registerTool、unregisterTool、provideContext——它会把每次注册和调用记录到 DevTools 控制台。应用永远不会崩溃,顶部徽章会告诉你你用的是哪一个。
Agent 侧(一座桥)。 没有面向页面的 API 可以"充当 agent",因此
该模块还镜像了每个已注册的工具,并在其上暴露 listTools() /
executeTool()。agentClient.js 只使用这座桥,不依赖其他任何东西。
镜像在本机浏览器和 polyfill 浏览器中均保持维护,因此两种方式下行为完全一致。
从 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在每次状态变更时重新注册同样是错误的——浏览器会看到整个工具集不断翻腾,而一个进行中的调用可能会被从 agent 脚下抽走。
useWebMcpTools.js 通过一个稳定的间接层只注册一次:注册的
execute 从一个每次渲染都会刷新的 ref 中解析真正的处理器。注册是稳定的;处理器始终能看到当前状态。在
React StrictMode 的双重挂载下,你可以确认恰好注册了七个工具,
既不是十四个,也不是零个。
备注与限制
SEED_TOTAL/seed.py --total改变书籍规模。过滤、排序和 限制已经在 SQL 中执行(queries.py),因此更大的书籍唯一需要的就是列表视图中的分页。批量记录和报告存放在内存中(
artifacts.py,上限为 50)。它们 是任务输出而非领域数据;真实部署会持久化它们, 因为批量变更记录就是一条审计轨迹。benchmark_rates返回虚构的数字。它代替市场数据订阅—— 关键在于这些是浏览器没有途径获取的数据。新的合约 id 来自
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