Skip to content

Latest commit

 

History

83 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WebMCP

The WebMCP spec proposes navigator.modelContext — a browser API letting a page expose its own UI as typed tools a model can call. The spec covers registration. This repo is the runtime it leaves out: the agent loop, a tool surface that changes with app state, and a trust policy driven by tool annotations. Client-side, no backend, no build step.

Live demos →

A page declares a tool

{
  name: 'plot_development_scatter',
  description: 'GDP per capita against life expectancy, log x, bubbles by population.',
  readOnlyHint: true, idempotentHint: true, destructiveHint: false,
  schema: {
    type: 'object',
    properties: {
      region: { type: 'string', enum: REGIONS },        // ← live values, not a guess
      income: { type: 'string', enum: INCOME_LEVELS },
    },
  },
  exec: async ({ region, income }) => {
    renderScatter(filterData({ region, income }));       // ← mutates the page you're looking at
    return { displayed: true, plotted: 214 };            // ← and answers the model
  },
}

Hand a list of those to mount() and the page is agent-operable:

import { mount } from '../runtime/index.js';
mount({ tools: TOOL_DEFS, getSystemPrompt, quickActions });

Three ideas in the runtime

1. The loop runs in the browser — SSE stream → parse tool_use → execute against local handlers → inject tool_result → continue, until the model stops calling tools. No server in the path. (runtime/loop.js)

2. The tool surface is a function of state — tools appear and vanish as the app changes, and schema enums are rewritten from live data. A model cannot select a region that isn't in the dataset, because the enum is the dataset. (runtime/tools.js, plus each demo's adjustTool / getDynamicTools)

3. Annotations are execution policy, not metadatareadOnlyHint runs immediately; destructiveHint stops for human confirmation. The annotation is the gate between model capability and human authority.

Demos

Demo Data What it exercises
World Development World Bank · 217 countries Ranking, log-scale scatter, and time series fetched beyond the loaded snapshot — back to 1960
Earthquake Monitor USGS live feed 30 days of global seismic activity against data that changes under you
US Hospital Capacity HealthData.gov · 1,265 facilities Suppressed values and partial reporting handled in the open; flagging, CSV export, destructiveHint gating

Each demo's index.html is data plus tool definitions. The runtime and both stylesheets are shared.

Run it

npx serve docs

The model runs on your machine. These demos drive it through termd — a daemon that runs a coding agent locally and lends it out over HTTP, so the agent uses your own machine and your own subscription. Install it and its bridge extension — jonasneves.com/termd — then reload; the demos detect it and the model picker offers whatever it can run.

There is no API key anywhere in this repo. No key is typed into a page, none is stored, and no page holds a credential — the daemon owns whatever auth the model needs, on your machine. Hosted providers were dropped for that reason: what is worth showing is not that a browser can call a model API, it's that an agent running locally can drive a page it does not own.

Layout

docs/
  index.html          landing page
  app.css             page shell — tokens, header, controls, data table
  chat.css            chat surface + the two-column grid
  runtime/            ← the product; every demo mounts this
    index.js            mount() entry
    loop.js             agent loop (SSE, tool_use → tool_result)
    tools.js            modelContext polyfill + registry + tool panel
    providers.js        termd transport — bridge extension or same-origin
    auth.js chat.js ui.js theme.js
  countries/ earthquakes/ hospital-risk-explorer/

Constraints worth knowing

Every demo reads live public data — nothing here is invented. Demos may only use APIs that send access-control-allow-origin — there is no backend to proxy through. That rules out otherwise-obvious sources: OpenSky echoes its own origin, and REST Countries now redirects to a CDN with no CORS headers, which is why the countries demo moved to the World Bank.

The loop is inverted from the usual shape. Rather than the browser driving a model and calling out for tools, termd runs the agent loop on your machine and calls back into the page for every tool — so the page is the tool provider, not the client.

termd ships no CORS headers on purpose: it has no authentication, and anything that reaches its port gets a shell, so the same-origin policy is the only gate in front of it. A page therefore cannot call it directly. Two ways through: serve these pages from the daemon itself, or install the two-file bridge extension, which runs on its own origin — where host_permissions exempt it from CORS and mixed content — and relays the stream. Its externally_connectable allowlist is the access control. Without either, the probe fails and the page says so instead of offering a dead Send button.

Nothing here is load-bearing on a remote module. marked, dompurify and echarts come from a CDN; the runtime itself is dependency-free. Deploys are gated on every one of those URLs still resolving, because a moved dependency is otherwise invisible until a user opens the page.

About

Browser-side runtime for the WebMCP spec (navigator.modelContext) — a page exposes its own UI as typed tools, and the agent loop runs client-side with no backend

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors