Skip to content

Schema & Inference

Source of truth: schema (whole) → inferred (runtime best-effort, optional build-time) → fields patch → metadata (name/description/annotations). Learn Browser Support and Getting Started.

Hierarchy

1. schema (whole) — Zod / Valibot / ArkType (StandardSchema) or raw JSON Schema
2. inferred — runtime best-effort or optional build-time (TypeScript types before erasure)
3. fields patch — Partial<JsonSchema> or per-field StandardSchema (e.g. Zod per field)
4. metadata — name, description, annotations

fields decorates, does not silently replace core type. Whole schema wins. See webmcp() options and Guide — Hooks for validation timing (beforevalidatefn).

Field patch (Partial<JsonSchema>)

ts
webmcp(search, {
  description: 'Search customers by name or email',
  fields: {
    query: { description: 'Name, email, or ID' },
    limit: { type: 'integer', minimum: 1, maximum: 50 },
  },
});

Use required:false to make added field optional:

ts
fields: { note: { description: 'optional', required: false } as any }

Whole vs per-field

ts
// whole — JSON Schema
webmcp(fn, { schema: { type:'object', properties:{query:{type:'string'}}, required:['query'] } });

// whole — Zod (requires side-effect import)
import 'simple-webmcp/zod';
import { z } from 'zod';
webmcp(fn, { schema: z.object({ query: z.string().min(1) }) });

// per-field mix
webmcp(fn, { fields: { query: z.string().describe('Name'), limit: { type:'integer' } } });

StandardSchema is supported for any vendor exposing ~standard — see StandardSchema spec. For Zod details, see below.

Inference — best-effort at runtime, richer at build

Infer what JavaScript can know at runtime. Get richer TypeScript/JSDoc inference with the optional build plugin.

Runtime (confidence:'low')

Honest about limits — it does not recover query: string magically. TypeScript is erased at runtime.

Parses fn.toString():

  • async ({query, limit=20}){query: {required}, limit: {default:20, optional}} (type string/number only from literal defaults)
  • fn(query){query: {}} — warn; need fields/schema or strict:true throws ConfigurationError.
  • function search(query: string) → at runtime still {query:{}} — add fields: {query:{description}} or schema.

Build-time — optional

A build plugin can read TypeScript types + JSDoc before erasure and emit richer inputSchema without code change — same webmcp(fn) call. Check the changelog and GitHub discussions for current status; runtime inference works without it.

Zod & StandardSchema adapter — simple-webmcp/zod

Core stays lean (~8KB gz) without Zod; Zod is opt-in to keep the bundle lean (~1.4KB gz separate).

Enable

ts
import 'simple-webmcp/zod'; // side-effect registers Zod → JSON converter globally
import { z } from 'zod';
import { webmcp } from 'simple-webmcp';

webmcp(fn, { schema: z.object({ query: z.string().min(1) }) });
webmcp(fn, { fields: { query: z.string().describe('Name') } });

Without the side-effect import, StandardSchema schema falls back to inferred/runtime placeholder and per-field Zod falls back to {type:'string'} — tests still pass but less accurate.

Helpers

ts
import { zodToJsonSchema, convertZodDef } from 'simple-webmcp/zod';
zodToJsonSchema(z.string().describe('x')); // → {type:'string', description:'x'}

Supports ZodString (checks min/max/regex/email/url/uuid), ZodNumber (min/max/int), ZodBoolean, ZodEnum, ZodObject (shape, optional detection), ZodArray, ZodUnion, ZodOptional/Default/Nullable/Effects, etc. Valibot/ArkType via StandardSchema.validate pass through when converter not matched (fallback).

Whole vs per-field with Zod

ts
// whole
webmcp(fn, { schema: z.object({ a: z.string(), b: z.number().optional() }) });
// per-field
webmcp(fn, { fields: { a: z.string(), b: z.number() } });

Whole schema wins; fields patches descriptions/min/max. Keep dist/zod.js (5.32KB raw, 1.40KB gz) separate — import simple-webmcp/zod only where needed. See Zod spec and Reference — Core.

Annotations

Extensible Record<string,unknown>:

ts
webmcp(fn, { annotations: { readOnlyHint: true, destructiveHint: false, title: 'Search' } });

Annotations are forwarded to the WebMCP registerTool definition as hints for the agent.

See also

Released under the MIT License.