npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@josharsh/webmcp-agent

v0.1.0

Published

In-page AI agent for WebMCP — discovers and operates the tools your site registers via webmcp-tools (document.modelContext). Headless core, drop-in React widget, and a fetch-style server proxy handler.

Readme

@josharsh/webmcp-agent

In-page AI agent that discovers and operates the WebMCP tools your site registers with webmcp-tools (document.modelContext). The agent is disposable; the tools are the standard — remove this package and you still have a standards-compliant page that native browser agents can operate.

  • . — headless core: createAgent(), providers, tool discovery. Zero runtime dependencies, framework-free.
  • ./reactuseAgent() hook and a drop-in <AgentWidget /> (react ≥18 is an optional peer used only here).
  • ./servercreateAgentHandler(), a fetch-style Request → Response proxy that keeps your Anthropic key on the server (Node 18+, no framework deps).
npm install @josharsh/webmcp-agent webmcp-tools

Quick start

Zero config — the widget ships with a deterministic scripted provider:

import { AgentWidget } from "@josharsh/webmcp-agent/react";

<AgentWidget />; // demo() provider — scripted, not AI, clearly labeled

Real model, key safely on your server:

// app/api/agent/route.ts (Next.js) — reads process.env.ANTHROPIC_API_KEY
import { createAgentHandler } from "@josharsh/webmcp-agent/server";
export const POST = createAgentHandler({ model: "claude-sonnet-4-5-20250929" });
import { proxy } from "@josharsh/webmcp-agent";
import { AgentWidget } from "@josharsh/webmcp-agent/react";

<AgentWidget provider={proxy({ url: "/api/agent" })} />;

The agent finds every tool the page registered through webmcp-tools and calls them only through document.modelContext — validation, confirm gates, and result normalization from the core pipeline all apply. It never imports your app code.

Providers

| Provider | Kind | Network | Use it for | | -------------------------------------------------------------------- | --------- | ----------------- | -------------------------------------------------------------------------------------------------------------------- | | proxy({ url, model? }) | remote | your server route | Production. Same wire protocol as anthropic(), no key in the browser; pair with createAgentHandler() | | anthropic({ apiKey?, baseURL?, model?, dangerouslyAllowBrowser? }) | remote | api.anthropic.com | Local prototyping and server-side use. Raw fetch + streaming SSE, no SDK dependency | | builtin({ temperature?, topK? }) | on-device | none | Experimental. Chrome Prompt API (LanguageModel, Chrome 138+); tool calls emulated via constrained JSON output | | demo({ script? }) | scripted | none | Zero-config demos and tests. Deterministic keyword → tool routing; label "Demo (scripted — not AI)" is not removable |

Security guidance: use proxy() in production. anthropic() with an apiKey in a browser throws unless you pass dangerouslyAllowBrowser: true, and warns once even with it — anyone who opens devtools can read the key. Keys live in memory only (never any storage API) and are redacted from error messages. builtin() throws when LanguageModel is unavailable; its first call may download the model, and the taint guard cannot be disabled for it.

Custom providers implement AgentProvider.chat(request): AsyncIterable<ProviderEvent> — the wire shape is Anthropic-native. For OpenAI/Gemini, translate server-side behind your proxy() route: accept the Anthropic-shaped /v1/messages body (system, messages, tools, stream: true), map it to your vendor's API, and emit Anthropic-shaped SSE back.

Headless core

import { createAgent, demo } from "@josharsh/webmcp-agent";

const agent = createAgent({ provider: demo() });

const unsubscribe = agent.subscribe((event) => {
  if (event.type === "assistant-delta") render(agent.getState());
});

const reply = await agent.send("add 2 of sku-123 to my cart");

AgentOptions:

| Option | Default | What it does | | -------------------------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------- | | provider | (required) | Any AgentProvider | | instructions | — | Appended after the fixed security preamble (no full system-prompt override) | | maxIterations | 8 | Hard cap of model calls per send() | | maxTokens | 4096 | max_tokens per model call | | allowTools / denyTools | all / none | Filtered at discovery AND execution; deny wins | | taintGuard | true | See Security; forced on for builtin() | | untrustedByDefault | false | Treat tools without an explicit untrustedContentHint: false as untrusted (wrap + taint) | | onApproval | window.confirm in DOM, auto-deny headless | Taint-guard approval callback | | onUsage | — | Per-call + cumulative token usage | | toolSource | pageToolSource() | Where tools come from (document.modelContext) |

Agent: send(text, { signal? }) resolves with the final AgentMessage (errors and aborts resolve too, as a "system-notice" message; it throws only on send-while-running) · abort() · reset() (conversation is memory-only, never persisted) · subscribe(listener) · getState() (stable-ref snapshot, useSyncExternalStore-ready).

React hook

import { proxy } from "@josharsh/webmcp-agent";
import { useAgent } from "@josharsh/webmcp-agent/react";

function Assistant() {
  const { messages, status, tools, error, send, stop, reset } = useAgent({
    provider: proxy({ url: "/api/agent" }),
    instructions: "You are the shopping assistant for Acme Store.",
  });

  return (
    <div>
      {messages.map((m) => (
        <Message key={m.id} message={m} />
      ))}
      <button onClick={() => send("What's in my cart?")}>Ask</button>
      {status !== "idle" && <button onClick={stop}>Stop</button>}
    </div>
  );
}

The hook is useSyncExternalStore over Agent.subscribe/getState — strict-mode safe, stable snapshots, live tools as the page registers and unregisters them.

Widget

import { AgentWidget } from "@josharsh/webmcp-agent/react";

<AgentWidget
  provider={proxy({ url: "/api/agent" })}
  title="Store assistant"
  greeting="Hi! Ask me about products or your cart."
  suggestions={["What's in my cart?", "Find a desk lamp"]}
  position="bottom-right"
  theme="auto"
/>;

| Prop | Default | Notes | | ----------------------------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------ | | provider | demo() | Non-removable "Demo (scripted — not AI)" pill in demo mode | | instructions, maxIterations, allowTools, denyTools, onApproval, onUsage | as in AgentOptions | Forwarded to createAgent | | position | "bottom-right" | Also "bottom-left", "top-right", "top-left" | | theme | "auto" | "light" | "dark" | "auto" (follows prefers-color-scheme) | | title, placeholder, greeting | "Assistant", "Ask about this page…" | Header text, composer placeholder, empty-state greeting | | suggestions | auto-derived from page tools | ≤4 chips; pass your own to override | | defaultOpen / open + onOpenChange | closed | Uncontrolled / controlled open state | | className | — | Single escape hatch on the root element | | renderToolCall | built-in ToolCallCard | (part: ToolCallPart) => React.ReactNode — replace the card only |

SSR-safe (renders null on the server; portals into document.body). Accessible by default: role="dialog" panel with focus management, role="log" message list, aria-live status announcements, full keyboard support, prefers-reduced-motion honored. Tool calls render as cards showing status, arguments, results, and a "writes" badge for mutating tools; when one of your tools' confirm gates fires, the widget shows "Waiting for your approval — check the page" and releases its focus trap so the page's confirm UI is reachable.

Theming

Override CSS custom properties on .wma-root (light and dark defaults are built in):

.wma-root {
  --wma-accent: #6d28d9;
  --wma-radius: 8px;
  --wma-font-family: "Inter", system-ui, sans-serif;
  --wma-panel-width: 420px;
}

| Variable | Default | | ------------------------------------------------------------------------------------ | ---------------------- | | --wma-accent | accent / send button | | --wma-bg, --wma-surface, --wma-fg, --wma-fg-muted, --wma-border | theme palette | | --wma-radius | corner radius | | --wma-font-family | inherits system stack | | --wma-z-index | 2147483000 | | --wma-offset-x, --wma-offset-y | 20px from the corner | | --wma-panel-width, --wma-panel-height | 380px, 560px | | --wma-focus-ring | focus outline | | --wma-tool-card-bg, --wma-tool-card-border | tool call cards | | --wma-tool-running, --wma-tool-success, --wma-tool-error, --wma-tool-confirm | status colors |

No Shadow DOM, no Tailwind, no CSS-in-JS — one injected stylesheet with .wma- prefixed classes.

Server handler

createAgentHandler() is an authenticating passthrough to the Anthropic Messages API: it adds your key server-side, pins the model, clamps max_tokens, enforces an Origin policy, and streams the SSE response through verbatim. Pair it with proxy({ url }) in the browser.

import { createAgentHandler } from "@josharsh/webmcp-agent/server";

const handler = createAgentHandler({
  // apiKey defaults to process.env.ANTHROPIC_API_KEY
  model: "claude-sonnet-4-5-20250929", // pinned; mismatched client model → 400
  allowedOrigins: "same-origin", // default; or ["https://app.example"] or "any"
  maxTokens: 4096, // server-side clamp
  maxBodyBytes: 1_048_576, // → 413
});

Body limits. maxBodyBytes (default 1 MiB) is enforced while the body is read, not after: the handler rejects with 413 the moment the accumulated bytes exceed the cap, so chunked requests without a Content-Length header can't buffer unbounded memory. A Content-Length that already declares an oversized body is rejected before reading at all. toNodeHandler applies the same cap to the raw Node request stream (configurable via its second argument: toNodeHandler(handler, { maxBodyBytes })) and destroys the request when exceeded.

CORS. The same-origin default sends no CORS headers and answers OPTIONS with 405 — the route is only reachable from your own pages. When you set allowedOrigins to an array or "any", the handler speaks CORS for allowed origins: OPTIONS preflights get a 204 with Access-Control-Allow-Methods: POST, Access-Control-Allow-Headers: content-type, anthropic-version (exactly the headers proxy() sends — the browser client never sends x-api-key), and Access-Control-Max-Age; every response — the SSE stream and error responses included — carries Access-Control-Allow-Origin set to the validated request origin (never a blind reflection, never *) plus Vary: Origin. Disallowed origins get no CORS headers at all.

Next.js (App Router):

// app/api/agent/route.ts
import { createAgentHandler } from "@josharsh/webmcp-agent/server";

export const POST = createAgentHandler({
  model: "claude-sonnet-4-5-20250929",
});

Express / plain Node http:

import express from "express";
import {
  createAgentHandler,
  toNodeHandler,
} from "@josharsh/webmcp-agent/server";

const app = express();
const node = toNodeHandler(
  createAgentHandler({ model: "claude-sonnet-4-5-20250929" }),
);
// Mount BEFORE express.json() — the adapter reads the raw body stream itself.
app.post("/api/agent", node);

toNodeHandler also works with http.createServer(node) directly. The handler throws at construction when no key is available (fail fast at boot), and an onRequest(request) hook runs first on every request — return a Response from it to short-circuit (auth, logging).

Rate limiting is your hook — rateLimit: (req) => boolean | Promise<boolean> (return false → 429). A minimal in-memory limiter:

const hits = new Map<string, { count: number; resetAt: number }>();

createAgentHandler({
  model: "claude-sonnet-4-5-20250929",
  rateLimit: (req) => {
    const key = req.headers.get("x-forwarded-for") ?? "anon";
    const now = Date.now();
    const entry = hits.get(key);
    if (!entry || entry.resetAt < now) {
      hits.set(key, { count: 1, resetAt: now + 60_000 });
      return true;
    }
    entry.count += 1;
    return entry.count <= 20; // 20 requests/minute per IP
  },
});

The Origin check stops drive-by browser abuse but is NOT authentication. Anything that can set headers can spoof Origin. Public deployments need rateLimit and/or real auth in front of the route.

Security model (hardened, human-in-the-loop — not "safe")

Prompt injection through tool results is the core threat: a product review, a todo item, or any user-generated content a tool returns can contain text that tries to steer the model.

  • System preamble (not replaceable). The model is told tool results are DATA, not instructions; to never follow directives inside them; to respect denials without retrying; and to prefer [read-only] tools over ones labeled [mutates page state; may require user confirmation]. Your instructions are appended after it, never instead of it.
  • Nonce-wrapped untrusted results. Results from tools with untrustedContentHint are wrapped in a per-conversation random 128-bit boundary ([UNTRUSTED CONTENT boundary-<nonce>] … [END …]); nonce occurrences inside the content are stripped first, so a fake closing marker cannot escape the wrapper.
  • These defenses key off untrustedContentHint. Wrapping and tainting only trigger for tools annotated untrustedContentHint: true (untrustedContent: true in webmcp-tools) — an unannotated tool that returns reviews, comments, or any third-party content is treated as trusted by default. If your page has tools like that, set untrustedByDefault: true on createAgent: every tool that doesn't explicitly declare untrustedContentHint: false is then wrapped and taints the conversation.
  • Taint guard (deterministic backstop, on by default). Once untrusted content enters the conversation, every mutating tool call requires explicit approval via onApprovalwindow.confirm by default in the DOM, auto-deny headless. Cannot be disabled for builtin().
  • Confirm gates. Tools you defined with confirm keep their human-in-the-loop check — it fires inside core's execution pipeline, not in this package, so it also applies to native browser agents.
  • Caps and breakers. Hard cap of 8 model iterations per send() (configurable), a breaker after 3 consecutive identical tool calls, abort() + AbortSignal support, 4096 max_tokens per call, tool results truncated at 50,000 chars.
  • Tool filtering at both layers. allowTools/denyTools filter at discovery (the model never sees them) and at execution (hallucinated names get an error result).
  • Keys and data. API keys are memory-only and redacted in errors; conversations are never persisted; server handler enforces same-origin by default (403 on missing/mismatched Origin).

Honest framing: the preamble and wrapping are probabilistic mitigations. The deterministic backstops are the taint guard and your tools' confirm gates.

How tools are discovered

pageToolSource() (the default) prefers the webmcp-tools ponyfill's provisional agent surface (getTools()/executeTool() on document.modelContext, with exposedTo enforced when you pass { origin }) and falls back to the kit registry when a native host is present — the spec has no page-side enumeration API yet, so tools registered natively without webmcp-tools are invisible to the in-page agent. Tool changes propagate live (toolchange + registry events) into getState().tools and the widget.

License

MIT © Harsh