Open source · zero dependencies · v0.2.4

See where agent time actually goes.

A drop-in performance analyser for WebMCP tools. It separates page compute, payload weight and host/model wait—so “the agent feels slow” becomes a precise, fixable diagnosis.

1 importAutomatic instrumentation—no edits to individual tools.
500Spans retained by the default in-memory ring buffer.
3 layersCompute, payload and host/model gap measured separately.
0 depsFramework-free ESM and classic-script builds.

The problem

“WebMCP is slow” hides three different problems.

A stopwatch around execute() sees only the page-owned part of a call. It misses the cost of putting a result into model context and the seconds spent between calls. The profiler records enough evidence to assign each delay to the right owner—and therefore the right fix.

A WebMCP request separated into page compute, payload cost and host plus model wait
Three causes, three remedies: optimise code, shrink results, or reduce round trips.
Page-owned

Tool execution

Wall time and Long-Task overlap reveal expensive code, layout, encoding or blocking work inside the page.

Shared cost

Payload weight

Approximate input and result sizes, content types, image size and estimated tokens reveal oversized tool contracts.

Platform time

Host + model gaps

The settled-to-next-invoke interval brackets orchestration and reasoning time that the page cannot see directly.

Quickstart

Attach before your tools register.

The standard gate keeps production overhead at zero until profiling is requested. The mode persists in localStorage, which matters when an app rewrites its URL or an agent can only be steered to a page once.

Install, then gate at boot
// npm install webmcp-profiler
import { maybeAttachProfiler } from "webmcp-profiler/attach"
maybeAttachProfiler()

?perf=1

Enable profiling and remember the mode for this origin.

?perf=overlay

Enable profiling and open the floating live panel immediately.

?perf=0

Remove the saved mode and return the page to its normal state.

Classic script—no bundler
<script src="https://cdn.jsdelivr.net/npm/webmcp-profiler@0.2.4/dist/webmcp-profiler.iife.js"></script>
Attach with explicit options
import { attachProfiler } from "webmcp-profiler"

const profiler = attachProfiler({
  buffer: 500,
  relay: true,
  overlay: false,
})
Use it from DevTools
__webmcpPerf.table()    // per-tool calls, percentiles, payload and errors
__webmcpPerf.overlay()  // toggle the live panel
__webmcpPerf.report()   // return the complete versioned document
__webmcpPerf.export()   // download that document as JSON

Architecture

Transparent interception, one shared source of truth.

The profiler watches all current WebMCP registry locations—document, navigator and window. It wraps the tool object in place, so the host, the app and any test harness all invoke the same measured function. Original inputs, return values, thrown errors and extra host arguments pass through unchanged.

Profiler architecture from WebMCP host through interceptor, tool, collector and output surfaces
The package observes a WebMCP host; it is not a host or polyfill.
Profiler interception flow for early and late attachment and complete detachment
Early attachment is automatic. Late attachment needs an exposed site tool map.
Why poll every 250 ms?

Agent hosts may inject modelContext after the app has started. The watcher stays alive so late host injection is still captured. A host that never appears is simply left untouched.

One call, one span

The smallest useful record of a tool invocation.

Each wrapper brackets the original execute(), summarises the result, records a span and returns the original result. Long Tasks are attributed later when Chromium delivers their observer entries.

Lifecycle of a profiled tool invocation from input to span and output views
The hot path is deliberately small: two clock reads, size accounting and a ring-buffer push.
Span fieldWhat it tells youSource
tool, seqTool identity and call order in this session.Registration metadata + collector counter
invokedAt, settledAtThe exact performance.now() window around the awaited tool.Wrapper clocks
wallMsEnd-to-end duration of the page’s execute().Settled minus invoked
blockingMsLong-Task overlap attributed to that call window. Chromium-only; otherwise zero.PerformanceObserver
inputBytes, resultBytesApproximate JSON size entering and leaving the tool; exact for typical ASCII-heavy payloads.JSON.stringify string length
contentTypes, imageBytesResult composition and encoded image-string length.MCP content summary
estTokensA directional model-context estimate: result bytes divided by four.Heuristic, rounded up
gapSincePrevCallMsTime between the previous result settling and this call arriving.Session ledger
isError, errorTool-reported failures and thrown exceptions.Result flag or caught error
syntheticReserved distinction between live and benchmark calls; current captured calls are false.Profiler

The differentiator

Host-gap analysis reveals the seconds around the milliseconds.

A page cannot inspect model reasoning or host internals. It can still prove what did not happen inside the page. The ledger sums the positive time between one result settling and the next invocation arriving, then reports it separately from tool work and payload size.

A sequence of millisecond tool calls separated by multi-second host and model gaps
Illustrative timeline: the page owns 16 ms; 15.2 seconds sit between calls.
What the first production case study found

Tool execution was already in the 1–15 ms range. The profiler exposed a roughly 130 KB preview image payload, which was reduced to a roughly 7 KB JPEG. It also showed that perceived delay was dominated by model round trips, not page compute.

Surfaces

Watch live, inspect in DevTools, or export evidence.

Every surface reads the same collector. The overlay is lazy-loaded into Shadow DOM, console commands work without UI, and every call emits a native performance.measure named webmcp:<tool>#<seq>.

Captured spans becoming aggregates, views and the correct optimisation decision
Evidence routes the team toward code, payload or orchestration changes—not generic optimisation.
Console APIPurpose
spans()Read the current immutable view of captured span records.
aggregates()Get per-tool calls, errors, min, p50, p95, max, blocking and payload totals.
table()Print per-tool aggregates with console.table.
report()Return a stable webmcp-perf-report/1 JSON document.
export()Download the report as a timestamped JSON file.
overlay()Lazy-create, show or hide the floating live table.
instrument(toolMap)Retrofit a site-exposed { name: tool } map after registration.
reset()Clear captured spans and call totals while retaining host and registration facts.
detach()Restore every original tool and registry method, stop observers and remove the global API.

Hidden agent browsers

The profiler can send measurements to the screen you can see.

With relay enabled—the default—each span is mirrored through a same-origin BroadcastChannel. The visible overlay currently shows a live count of spans received from another tab; merging remote spans into its per-tool table is future work. Only span metadata moves—tool inputs and result bodies do not.

Span metadata relayed from a hidden agent tab to a visible same-origin overlay, with a separate-context limitation
Important limits: the shipped overlay counts remote spans, and ChatGPT hidden and in-app browsers may be isolated contexts. A fuller WebSocket relay is planned.

Reports and CI

A portable record of what happened.

report() returns session metadata, the ledger, per-tool aggregates and every retained span under a versioned format identifier. Reports can be diffed in CI or ingested by a dashboard without scraping the overlay.

webmcp-perf-report/1
{
  "format": "webmcp-perf-report/1",
  "session": { "origin": "https://example.com", "userAgent": "…", "generatedAt": "…" },
  "ledger": { "registeredTools": ["search", "checkout"], "totals": { "calls": 12, "hostGapMs": 39120 } },
  "tools": [{ "tool": "search", "p50Ms": 4.2, "p95Ms": 8.1 }],
  "spans": [{ "tool": "search", "wallMs": 4.9, "estTokens": 194 }]
}

Live-agent evidence

Profile real calls from Chrome, ChatGPT or another host and preserve the exact sequence, payload shape and gaps the page observed.

Agentless baseline

The repository’s current npm run perf harness uses Playwright to invoke tools through modelContext. A schema-driven bench() API is planned for the package.

Truthful scope

What ships today—and what comes next.

The profiler is useful now, but it is deliberately early. The page keeps current behavior separate from the larger analyser described in the design specification.

Shipped in 0.2.4

Measure and explain

  • Automatic registry interception and in-place wrapping
  • Wall time, Long-Task overlap, bytes, token estimate and errors
  • Host injection, registration and inter-call gap ledger
  • 500-span configurable in-memory ring buffer
  • Overlay, console API, JSON export and DevTools measures
  • Same-origin BroadcastChannel relay
  • ESM, attach gate and single-file IIFE builds
  • Clean detach and late registry instrumentation
Designed, not shipped

Grade, persist and connect

  • IndexedDB persistence across reloads
  • Cross-context and cross-device WebSocket relay
  • Performance budgets, grades and anti-pattern detectors
  • Schema-driven in-package bench()
  • Beacon and OpenTelemetry exporters
  • Conversation waterfall and violation badges
  • Memory, frame-drop, queue-delay and schema-validity fields
  • Hostless recording stub and opt-in body capture
Privacy by default

The shipped profiler records sizes, timings, content-type counts and error strings—not tool input or result bodies. Data stays in memory unless the developer explicitly exports a report. Relay messages contain spans, never payload bodies.

Known limitations: a reload starts a fresh buffer; Long-Task attribution is Chromium-only; the current “byte” fields use JavaScript string length and are approximate for non-ASCII or base64 data; token count is that approximate result size divided by four; already-registered tools require an exposed map; the overlay counts but does not aggregate relayed spans; BroadcastChannel cannot cross isolated browsing contexts; and host gaps identify time outside the page without claiming whether the host or model consumed each millisecond.

The judge takeaway

WebMCP performance can be measured, not guessed.

The profiler makes agent-facing performance visible with a tiny, removable layer. It helped turn one vague latency complaint into a concrete payload fix—and proved where the remaining time really lived.