h3-mcp logoh3-mcp

Input Validation

Validated at the boundary, not in your handlers.

Everything a client sends is validated before it reaches a handler — JSON-RPC shape, parameter types, header agreement, cursors, URIs, session ids. The rule the library follows internally is that no unvalidated value crosses into a handler, so what your code receives has already been type-checked.

#What Is Checked for You

InputChecked
JSON-RPC envelopejsonrpc, method, id presence and types (-32600)
paramsObject shape; required strings and objects per method (-32602)
Tool argumentsAgainst inputSchema when it is a Standard Schema
Prompt argumentsRequired arguments present
Resource uriParseable; matched against resources then URI templates
Pagination cursorOpaque format validated, never trusted as an offset
_meta (modern)Required protocol version and client capabilities; log level; trace
Mirrored headers (modern)Equality with the body (-32020)
Mcp-Session-Id (legacy)Length cap, character allowlist, then store lookup
MCP-Protocol-Version (legacy)Known version, before method dispatch
Body size and Content-TypeSee limits

Errors carry static messages and never echo the offending value back — an error response is not a reconnaissance tool.

#Tool Arguments

Runtime validation of tool arguments happens when inputSchema is a Standard Schema implementor (Zod, Valibot, ArkType). Then invalid input never reaches the handler, and args is typed:

import { z } from "zod";

defineMcpTool({
  name: "create-user",
  inputSchema: z.object({
    email: z.string().email(),
    role: z.enum(["admin", "member"]).default("member"),
  }),
  handler: async (args) => {
    // args.email is a valid email; args.role is one of two strings
    return { content: [{ type: "text", text: `ok` }] };
  },
});

How a failure is reported is version-aware, per SEP-1303:

Client protocol versionInvalid arguments produce
≤2025-06-18A JSON-RPC error
≥2025-11-25A tool result with isError: true, so the model can correct itself

Important

A plain JSON Schema inputSchema is advertised but not enforced — there is no validator to run. With plain JSON Schema, args arrives as whatever the client sent. Either use a Standard Schema implementor, or validate inside the handler before you use anything.

#Still Your Job

Validation is about shape. These are about meaning, and the library cannot do them for you:

  • Authorization. That args.documentId is a well-formed UUID says nothing about whether this caller may read it.
  • Injection. Never interpolate an argument, a template parameter, or a completion value into SQL, a shell command, a filesystem path, or an outbound URL. Parameterize, or resolve against an allowlist.
  • Resource URIs from templates. app://files/{path} will happily match ../../etc/passwd. Normalize and confine the extracted value to the directory you intended.
  • requestState and inputResponses. Both round-trip through the client. Sign what matters — see MRTR.
  • Semantic limits. A limit: 1e9 argument is valid JSON and a valid number. Cap it yourself.
defineMcpTool({
  name: "read-doc",
  inputSchema: z.object({ id: z.string().uuid(), limit: z.number().int().min(1).max(100) }),
  handler: async (args, event) => {
    const tenant = event.context.tenant; // set during auth
    const doc = await db.getDoc({ id: args.id, tenant }); // scoped, parameterized
    if (!doc) return { isError: true, content: [{ type: "text", text: "Not found" }] };
    return { content: [{ type: "text", text: doc.body.slice(0, args.limit) }] };
  },
});

Notice the last two lines of defense: the query is scoped to the caller's tenant, and a missing document is reported the same way as one that does not exist — so the tool cannot be used to probe for ids that do.

h3-mcp logo

h3-mcp  MCP servers, built on H3.