@ashraf009/webmcp-kit
v0.1.0
Published
Typed helpers for building WebMCP tool surfaces: registration, React hooks for scoped/dynamic tools, an activity log, and confirmation gating.
Readme
webmcp-kit
Typed helpers for building WebMCP tool surfaces: schema-inferred tool definitions, React hooks for dynamic/scoped tool sets, an activity log, and confirmation gating for consequential actions.
Built while implementing three apps for the OpenAI WebMCP Challenge (Cadence, Consequence, Relay). All three needed the same registration, lifecycle, and confirmation logic, so it got pulled out here instead of copy-pasted three times.
Install
npm install @ashraf009/webmcp-kitCore idea
A tool's logic is a plain async function: handler(input) => output, no WebMCP-specific code inside it. defineTool is the only place that knows about the { content: [...] } result shape:
import { defineTool } from "@ashraf009/webmcp-kit";
export const searchIssues = defineTool({
name: "search_issues",
description: "Search issues by title and body text.",
inputSchema: {
type: "object",
properties: {
query: { type: "string", description: "Search text" },
limit: { type: "number" },
},
required: ["query"],
additionalProperties: false,
} as const,
annotations: { readOnlyHint: true },
async handler({ query, limit }) {
// fully typed: query: string, limit: number | undefined
return searchIssuesInStore(query, limit);
},
});Because the handler returns a plain value instead of a wrapped WebMCP result, the same function drives both the real document.modelContext.registerTool call and a simulated-agent fallback UI. That's what keeps an app fully explorable in a browser without WebMCP support. Put the logic inside a raw execute closure instead, and now you're maintaining two implementations that can quietly drift apart.
Registering tools
import { registerTools } from "@ashraf009/webmcp-kit";
const controller = new AbortController();
registerTools([searchIssues, createIssue], { signal: controller.signal });
// controller.abort() unregisters everything registered in this callNo-ops cleanly when document.modelContext isn't present. Callers never need to branch on isWebMCPAvailable() themselves.
Dynamic, scoped tool sets (React)
import { useScopedTools } from "@ashraf009/webmcp-kit/react";
function IssueDetail({ issue }: { issue: Issue }) {
useScopedTools(
true,
() => [addComment(issue.id), splitIssue(issue.id), setEstimate(issue.id)],
{},
[issue.id],
);
// ...
}The tool set is registered only while active is true, and unregisters the moment it becomes false or a dependency changes. This is the primitive behind every dynamic tool surface in this repo: an issue selected, a filter applied, a reviewer role granted, each exposing a different tool set. A static server-side MCP tool list can't do that. The set here is a function of live page state.
Confirmation gating
import { withConfirmation } from "@ashraf009/webmcp-kit";
export const bulkUpdate = withConfirmation(bulkUpdateDefinition, async (input) => {
return await showConfirmDialog(`Apply this change to ${input.issueIds.length} issues?`);
});If the human declines, the handler never runs and the agent receives a clear refusal rather than a silent no-op.
Activity log
import { createActivityLog } from "@ashraf009/webmcp-kit";
const log = createActivityLog();
registerTools(tools, { onInvoke: (entry) => log.log({ ...entry, actor: "agent" }) });
log.subscribe((entry, all) => renderFeed(all));Feed this into a visible "agent activity feed." It's the thing that makes an invisible protocol legible to a person watching over the agent's shoulder.
API
defineTool(definition): typed tool definition with schema-inferred handler input.registerTools(tools, options?): batch registration with feature detection and anonInvokehook.withConfirmation(tool, confirm): wraps a tool to require human approval before it runs.createActivityLog(options?): subscribable store of tool invocations.text(value)/json(value)/refusal(reason): WebMCP result-shape helpers, for tools written against the raw API directly.isWebMCPAvailable(): feature detection.- React:
useWebMCPTool,useWebMCPTools,useScopedTools. Lifecycle-managed registration hooks.
License
MIT
