https://github.com/asouqi/webmcp-adapter
Lightweight adapter for WebMCP — define, register, and manage browser tools with built-in schema validation and error handling
https://github.com/asouqi/webmcp-adapter
ai ai-agent browser function-calling llm mcp model-context-protocol standard-schema tool-calling type-safe typescript validation webmcp zod
Last synced: 4 days ago
JSON representation
Lightweight adapter for WebMCP — define, register, and manage browser tools with built-in schema validation and error handling
- Host: GitHub
- URL: https://github.com/asouqi/webmcp-adapter
- Owner: asouqi
- License: mit
- Created: 2026-04-14T09:18:12.000Z (about 2 months ago)
- Default Branch: master
- Last Pushed: 2026-05-29T22:02:51.000Z (14 days ago)
- Last Synced: 2026-05-29T23:12:50.967Z (14 days ago)
- Topics: ai, ai-agent, browser, function-calling, llm, mcp, model-context-protocol, standard-schema, tool-calling, type-safe, typescript, validation, webmcp, zod
- Language: TypeScript
- Homepage:
- Size: 125 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# WebMCP Adapter
A lightweight utility designed to register, validate, and manage tool lifecycles within the `document.modelContext` (or `navigator.modelContext` for backward compatibility). By leveraging the official [@mcp-b/webmcp-types](https://github.com/mcp-b/webmcp-types), it provides a type-safe, framework-agnostic way to expose browser-side tools to AI models.
## 🧠 What is WebMCP?
The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open standard that enables AI models to securely connect to local and remote tools. [WebMCP](https://webmcp.dev/) brings this protocol directly into the browser — letting web applications expose tools to AI models via `document.modelContext`.
## 🚀 Installation
```bash
npm install webmcp-adapter
# or
yarn add webmcp-adapter
```
---
## 🛠 API Reference
### `defineTool(config)`
A type-safe helper to create a tool definition. Does not register the tool — it just ensures the `execute` handler receives the correct types based on `inputSchema`.
```typescript
import { defineTool } from 'webmcp-adapter'
const calculateTool = defineTool({
name: 'calculate_area',
description: 'Calculates the area of a rectangle',
inputSchema: {
type: 'object',
properties: {
width: { type: 'number' },
height: { type: 'number' }
},
required: ['width', 'height']
},
execute: ({ width, height }) => {
// 'width' and 'height' are fully typed as number
return {
content: [{ type: 'text', text: `The area is ${width * height}` }]
}
}
})
```
---
### `registerTool(tool)`
Registers a single tool with (`document.modelContext` or `navigator.modelContext`). The `execute` function is automatically wrapped with validation and error handling.
- **Returns**: A cleanup function `() => void` to unregister that specific tool.
- **Silent no-op** if WebMCP is not supported in the current environment.
```typescript
import { defineTool, registerTool } from 'webmcp-adapter'
const tool = defineTool({ ... })
const unregister = registerTool(tool)
// Later, to clean up:
unregister()
```
---
### `registerBatch(tools)`
Registers an array of tools simultaneously and returns a single cleanup function.
- **Returns**: A single `() => void` that unregisters all tools in the batch.
```typescript
import { defineTool, registerBatch } from 'webmcp-adapter'
const searchTool = defineTool({ name: 'search_products', ... })
const cartTool = defineTool({ name: 'add_to_cart', ... })
const checkoutTool = defineTool({ name: 'checkout', ... })
const unregisterAll = registerBatch([searchTool, cartTool, checkoutTool])
// Later, to clean up all three:
unregisterAll()
```
```typescript
// React usage
useEffect(() => {
const unregister = registerBatch([searchTool, cartTool, checkoutTool])
return unregister // Cleanup on unmount
}, [])
```
---
### `unregisterTool(name)`
Unregisters a specific tool by its string name.
- **Returns**: `true` if the tool was found and removed, `false` otherwise.
```typescript
import { unregisterTool } from 'webmcp-adapter'
unregisterTool('calculate_area') // true
unregisterTool('unknown_tool') // false
```
---
### `unregisterAllTools()`
A global reset that removes every tool registered through this adapter. Useful for logout or page-transition scenarios.
```typescript
import { unregisterAllTools } from 'webmcp-adapter'
// On page unload
window.addEventListener('beforeunload', () => {
unregisterAllTools()
})
// On user logout
function handleLogout() {
unregisterAllTools()
// ... other cleanup
}
```
---
### `hasTool(name)`
Checks whether a tool with the given name is currently registered.
- **Returns**: `true` if the tool is registered, `false` otherwise.
- Useful for **conditional registration** — avoids re-registering a tool that is already active, e.g. when user state or cart state changes.
```typescript
import { hasTool, registerTool } from 'webmcp-adapter'
if (!hasTool('checkout_form')) {
registerTool(checkoutTool)
}
```
---
### `getRegisteredTools()`
Returns the names of all tools currently registered through this adapter as a `string[]`.
Useful for debugging, logging, or verifying which tools are active at any point in the application lifecycle.
```typescript
import { getRegisteredTools } from 'webmcp-adapter'
console.log(getRegisteredTools())
// ['search_products', 'add_to_cart', 'checkout_form']
```
```typescript
import { registerBatch, getRegisteredTools } from 'webmcp-adapter'
// Verify tools registered after a batch
registerBatch([searchTool, cartTool, checkoutTool])
console.log(getRegisteredTools().length) // 3
```
---
### `isWebMCPSupported()`
Returns `true` if the current environment supports the (`document.modelContext` or `navigator.modelContext`) API.
```typescript
import { isWebMCPSupported } from 'webmcp-adapter'
if (isWebMCPSupported()) {
registerTool(myTool)
} else {
console.warn('WebMCP is not available in this environment')
}
```
---
## 🛡️ Runtime Validation
The browser's native WebMCP API (`document.modelContext` or `navigator.modelContext`) performs only basic JSON Schema validation. `webmcp-adapter` adds an **additional validation layer** on top — wrapping every tool's `execute` function so that input is validated more thoroughly before your code ever runs.
When validation fails, instead of the model receiving a generic error, it gets a structured `isError: true` response with field-level details. This allows the AI to see exactly which fields failed and why, correct its input, and retry — rather than simply crashing.
Additionally, the adapter natively supports [Standard Schema](https://github.com/standard-schema/standard-schema), meaning you can use libraries like **Zod** or **Valibot** to enforce strict constraints (like regex, emails, or custom cross-field logic) that basic JSON Schema cannot handle.
### 1. Built-in JSON Schema validation (default)
If no `validator` is provided, the adapter validates input against the `inputSchema` using a lightweight built-in validator. It covers:
- Type checking (`string`, `number`, `boolean`, `array`, `object`, `null`)
- String constraints: `minLength`, `maxLength`, `pattern`, `enum`
- Number constraints: `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`
- Array constraints: `minItems`, `maxItems`, `items`
- Object constraints: `required`, `properties`
- Composites: `oneOf`, `anyOf`, `allOf`, `if/then/else`
```typescript
import { defineTool, registerTool } from 'webmcp-adapter'
const calculateTool = defineTool({
name: 'calculate_area',
description: 'Calculates the area of a rectangle',
inputSchema: {
type: 'object',
properties: {
width: { type: 'number', minimum: 0 },
height: { type: 'number', minimum: 0 }
},
required: ['width', 'height']
},
// No validator provided — built-in JSON Schema validation runs automatically
execute: ({ width, height }) => {
return {
content: [{ type: 'text', text: `The area is ${width * height}` }]
}
}
})
registerTool(calculateTool)
```
### 2. Standard Schema validation (Zod, Valibot, ArkType)
Pass any [Standard Schema](https://github.com/standard-schema/standard-schema)-compatible library via the `validator` option for strict runtime validation. When provided, this **replaces** the built-in JSON Schema validation.
Use this when you need constraints that JSON Schema cannot express — such as regex patterns, email formats, cross-field rules, or custom `refine` logic.
```typescript
import { defineTool, registerTool } from 'webmcp-adapter'
import { z } from 'zod'
const schema = z.object({
width: z.number().positive('Width must be a positive number'),
height: z.number().positive('Height must be a positive number')
})
const calculateTool = defineTool({
name: 'calculate_area',
description: 'Calculates the area of a rectangle',
inputSchema: {
type: 'object',
properties: {
width: { type: 'number' },
height: { type: 'number' }
},
required: ['width', 'height']
},
validator: schema, // ← Zod schema replaces built-in validation
execute: ({ width, height }) => {
// Reaches here only if Zod validation passed
return {
content: [{ type: 'text', text: `The area is ${width * height}` }]
}
}
})
registerTool(calculateTool)
```
### Validation error response
When validation fails the adapter returns a structured response back to the AI model so it can correct its input and retry:
```typescript
// Validation failure — AI should fix input and retry
{
content: [{ type: 'text', text: 'Validation error: width: Expected number, got string' }],
isError: true,
structuredContent: {
success: false,
validationFailed: true, // ← input was invalid, not a crash
error: 'width: Expected number, got string', // top-level summary
errors: {
width: 'Expected number, got string' // field-keyed for targeted correction
}
}
}
```
Compare this to a **runtime error** (an exception thrown inside `execute`), which has `validationFailed: false`:
```typescript
// Runtime crash — retrying with the same input won't help
{
content: [{ type: 'text', text: 'Error: Something went wrong' }],
isError: true,
structuredContent: {
success: false,
validationFailed: false, // ← something crashed, not a validation issue
error: 'Something went wrong',
errors: {}
}
}
```
The `validationFailed` flag lets the model distinguish between **"fix your input and retry"** vs **"something crashed on the server side"**.
---
## 🔧 Validation Utilities
These are exported for use in higher-level libraries (e.g. `webmcp-forms`) that need to run validation outside of the tool registration flow.
### `validateJsonSchema(schema, input)`
Validates `input` against a JSON Schema object. Returns a `ValidationResult`.
```typescript
import { validateJsonSchema } from 'webmcp-adapter'
const result = validateJsonSchema(
{ type: 'string', minLength: 3 },
'hi'
)
// { valid: false, error: 'String must be at least 3 characters, got 2' }
```
### `validateWithStandardSchema(schema, input)`
Validates `input` against a Standard Schema (Zod, Valibot, etc.). Returns a `Promise` with a full field-keyed `errors` map.
- Issues with a `path` are keyed by `path.join('.')` (e.g. `"address.zip"`)
- Issues with no path (top-level `refine`, cross-field rules) are keyed under `_form`
```typescript
import { validateWithStandardSchema } from 'webmcp-adapter'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
age: z.number().min(18)
})
const result = await validateWithStandardSchema(schema, { email: 'bad', age: 16 })
// {
// valid: false,
// error: 'email: Invalid email, age: Number must be greater than or equal to 18',
// errors: {
// email: 'Invalid email',
// age: 'Number must be greater than or equal to 18'
// }
// }
```
### `isStandardSchema(value)`
Type guard that returns `true` if `value` implements the Standard Schema interface. Used internally to detect Zod, Valibot, and ArkType validators.
```typescript
import { isStandardSchema } from 'webmcp-adapter'
import { z } from 'zod'
isStandardSchema(z.object({ name: z.string() })) // true
isStandardSchema({ type: 'object' }) // false — plain JSON Schema
```
---
## 📦 Exported Types
```typescript
// From @mcp-b/webmcp-types
export type { InputSchema, JsonValue, ToolResponse }
// From webmcp-adapter
export type { ToolDefinition, ToolConfig, UnregisterFn, ValidationResult, StandardSchema }
```
### `ValidationResult`
```typescript
interface ValidationResult {
valid: boolean
error?: string // top-level summary string
errors?: Record // field-keyed errors map
}
```
### `ToolConfig`
```typescript
interface ToolConfig {
name: string
description: string
inputSchema: InputSchema
annotations?: ToolAnnotations
validator?: StandardSchema // optional Zod/Valibot/ArkType schema
execute: (input: InferArgsFromInputSchema) => Promise | ToolResponse
}
```
### `ToolDefinition`
The normalized, readonly shape returned by `defineTool()` and consumed by `registerTool()` / `registerBatch()`.
```typescript
interface ToolDefinition {
readonly name: string
readonly description: string
readonly inputSchema: TSchema
readonly annotations: ToolAnnotations
readonly validator?: StandardSchema
readonly execute: (input: InferArgsFromInputSchema) => Promise | ToolResponse
}
```