Resource library

QA How-To

Fuzz MCP Tool Arguments with JSON Schema

Learn to fuzz MCP tool arguments with JSON Schema using Ajv, fast-check, boundary mutations, protocol assertions, shrinking, and secure CI test suites.

20 min read | 2,417 words

TL;DR

Compile each MCP tool inputSchema with Ajv, generate valid argument objects with fast-check, and mutate one schema rule at a time. Send every case through the real request boundary, then assert consistent rejection, sanitized errors, and zero handler side effects for invalid input.

Key Takeaways

  • Compile the same JSON Schema that the MCP tool publishes instead of maintaining a separate test contract.
  • Generate valid arguments first, then apply one controlled mutation so every rejection has a clear cause.
  • Test structural, boundary, Unicode, numeric, nesting, and unexpected-property cases.
  • Assert stable protocol errors and absence of side effects, not only validator return values.
  • Preserve fast-check seeds and minimized counterexamples so every failure is reproducible.
  • Cap input size and execution time before running fuzz tests in CI.

To fuzz mcp tool arguments with json schema, use the tool's published inputSchema as both the generator contract and the validation oracle. Generate many valid objects, change one property at a time to violate a known constraint, and assert that invalid requests never reach the tool handler.

This tutorial builds a runnable TypeScript fuzz harness for an MCP-style tool boundary. It fits into the broader MCP security testing complete guide for 2026, which covers threat modeling, transport, authorization, prompt injection, and data exposure.

You will use Ajv's JSON Schema 2020-12 validator and fast-check's property-based generators. The final suite produces reproducible failures, shrinks them to small counterexamples, verifies protocol-safe errors, and runs with bounded resources in CI.

What You Will Build

You will build a focused argument-fuzzing lab that can be adapted to a real MCP server:

  • A strict JSON Schema for a search_tickets tool.
  • A compiled validation boundary that blocks handler execution on invalid input.
  • Valid generators derived from the schema's constraints.
  • Targeted invalid mutations for types, ranges, strings, arrays, objects, and extra keys.
  • Property tests with deterministic seeds and minimized counterexamples.
  • MCP-shaped request tests and a safe GitHub Actions job.

The goal is not random noise. A useful fuzzer explores equivalence classes around the contract and tells you exactly which safety property failed.

Prerequisites

Use Node.js 22 or a newer supported LTS release. Create an isolated ESM project and install Ajv 8, fast-check 4, TypeScript, and Vitest:

mkdir mcp-argument-fuzz-lab
cd mcp-argument-fuzz-lab
npm init -y
npm install ajv fast-check
npm install --save-dev typescript vitest @types/node
mkdir -p src test

Add these fields to package.json:

{
  "type": "module",
  "scripts": {
    "typecheck": "tsc --noEmit",
    "test": "vitest run",
    "test:fuzz": "vitest run test/fuzz.test.ts"
  }
}

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "types": ["node", "vitest/globals"]
  },
  "include": ["src", "test"]
}

Run npm test. Vitest may report that no test files exist yet. That confirms the runner starts correctly. Keep the lab offline and use synthetic arguments only.

Step 1: Define a Strict MCP Tool JSON Schema

Start with a small but realistic tool. Create src/schema.ts:

export const searchTicketsSchema = {
  $schema: "https://json-schema.org/draft/2020-12/schema",
  type: "object",
  additionalProperties: false,
  required: ["query", "limit"],
  properties: {
    query: { type: "string", minLength: 1, maxLength: 120 },
    limit: { type: "integer", minimum: 1, maximum: 50 },
    labels: {
      type: "array",
      maxItems: 5,
      uniqueItems: true,
      items: { type: "string", minLength: 1, maxLength: 24 }
    },
    options: {
      type: "object",
      additionalProperties: false,
      properties: {
        includeClosed: { type: "boolean" },
        sort: { type: "string", enum: ["created", "updated"] }
      }
    }
  }
} as const;

export type SearchTicketsArgs = {
  query: string;
  limit: number;
  labels?: string[];
  options?: { includeClosed?: boolean; sort?: "created" | "updated" };
};

Strictness matters. Without additionalProperties: false, misspelled or attacker-controlled keys may pass validation and later influence logging, authorization, or downstream APIs. Bounds keep the accepted state space intentional and reduce denial-of-service risk.

Treat JSON Schema as a wire contract, not as TypeScript documentation. TypeScript disappears at runtime and cannot protect requests received from a model or remote client.

Verification: run npm run typecheck. Confirm that required fields, maximum sizes, enums, and nested-object rules are explicit. Search for any unbounded string or array and decide whether it truly needs to be unbounded.

Step 2: Compile the Validation and Side-Effect Boundary

Compile the schema with Ajv's 2020 entry point. Create src/tool.ts:

import Ajv2020, { type ErrorObject } from "ajv/dist/2020.js";
import { searchTicketsSchema, type SearchTicketsArgs } from "./schema.js";

const ajv = new Ajv2020({ allErrors: true, strict: true });
const validate = ajv.compile<SearchTicketsArgs>(searchTicketsSchema);

export const effects: SearchTicketsArgs[] = [];
export const resetEffects = () => effects.splice(0, effects.length);

export class InvalidArgumentsError extends Error {
  constructor(public readonly issues: ErrorObject[]) {
    super("Invalid tool arguments");
  }
}

export async function invokeSearchTickets(input: unknown) {
  if (!validate(input)) {
    throw new InvalidArgumentsError(validate.errors ?? []);
  }

  effects.push(structuredClone(input));
  return { content: [{ type: "text" as const, text: "No synthetic tickets found" }] };
}

Validation happens immediately before the side effect. This placement prevents alternate callers from bypassing a controller-level check. Ajv's allErrors option improves diagnostics, while strict mode exposes ambiguous or ignored schema keywords during compilation.

Do not return raw inputs, stack traces, schema internals, or environment data in client errors. The custom error carries structured issues for test assertions, but the protocol adapter added later will expose only safe fields.

Verification: run npm run typecheck. Then check that effects.push occurs only after validate(input) succeeds. Temporarily pass { query: "x", limit: 0 } in a local test and confirm effects stays empty.

Step 3: Generate Valid Arguments with fast-check

A good invalid-input fuzzer begins with known-valid objects. Create src/generators.ts:

import fc from "fast-check";
import type { SearchTicketsArgs } from "./schema.js";

const boundedText = (minLength: number, maxLength: number) =>
  fc.string({ minLength, maxLength });

const optionsArb = fc.record(
  {
    includeClosed: fc.boolean(),
    sort: fc.constantFrom<"created" | "updated">("created", "updated")
  },
  { requiredKeys: [] }
);

export const validArgsArb: fc.Arbitrary<SearchTicketsArgs> = fc.record(
  {
    query: boundedText(1, 120),
    limit: fc.integer({ min: 1, max: 50 }),
    labels: fc.uniqueArray(boundedText(1, 24), { maxLength: 5 }),
    options: optionsArb
  },
  { requiredKeys: ["query", "limit"] }
);

This is a manual schema-to-generator mapping. It is transparent, easy to review, and suitable when security-sensitive constraints deserve deliberate coverage. A generic schema generator can accelerate broad discovery, but verify its support for your dialect and keywords before trusting it.

The generator includes optional properties, nested objects, Unicode strings, unique arrays, and exact numeric bounds. fast-check will shrink a failing object toward a simpler value, which makes debugging much faster than inspecting a large random payload.

Add test/valid.test.ts:

import fc from "fast-check";
import { describe, expect, it } from "vitest";
import { effects, invokeSearchTickets, resetEffects } from "../src/tool.js";
import { validArgsArb } from "../src/generators.js";

describe("valid tool arguments", () => {
  it("accepts every generated schema-valid object", async () => {
    await fc.assert(fc.asyncProperty(validArgsArb, async (args) => {
      resetEffects();
      await expect(invokeSearchTickets(args)).resolves.toBeDefined();
      expect(effects).toEqual([args]);
    }), { numRuns: 300, seed: 20260715 });
  });
});

Verification: run npm test -- test/valid.test.ts. Expect one passing property after 300 runs. If it fails, compare the minimized counterexample with the schema. The generator and runtime contract disagree, which is exactly the drift this test should expose.

Step 4: Build Targeted Invalid Mutations

Blind random JSON spends most runs on obviously invalid shapes. Controlled mutations provide better coverage and diagnostics. Add this to src/generators.ts:

export type InvalidCase = { rule: string; value: unknown };

export const invalidCaseArb: fc.Arbitrary<InvalidCase> = validArgsArb.chain((base) =>
  fc.oneof(
    fc.constant({ rule: "required.query", value: omit(base, "query") }),
    fc.constant({ rule: "required.limit", value: omit(base, "limit") }),
    fc.constant({ rule: "type.query", value: { ...base, query: 42 } }),
    fc.constant({ rule: "minLength.query", value: { ...base, query: "" } }),
    fc.constant({ rule: "maxLength.query", value: { ...base, query: "x".repeat(121) } }),
    fc.constant({ rule: "minimum.limit", value: { ...base, limit: 0 } }),
    fc.constant({ rule: "maximum.limit", value: { ...base, limit: 51 } }),
    fc.constant({ rule: "integer.limit", value: { ...base, limit: 1.5 } }),
    fc.constant({ rule: "additionalProperties", value: { ...base, admin: true } }),
    fc.constant({ rule: "maxItems.labels", value: { ...base, labels: ["a", "b", "c", "d", "e", "f"] } }),
    fc.constant({ rule: "uniqueItems.labels", value: { ...base, labels: ["bug", "bug"] } }),
    fc.constant({ rule: "enum.options.sort", value: { ...base, options: { sort: "priority" } } })
  )
);

function omit<T extends object, K extends keyof T>(value: T, key: K): Omit<T, K> {
  const copy = { ...value };
  delete copy[key];
  return copy;
}

Each case changes one rule and retains a label. That gives you a coverage report by schema keyword and prevents a case meant for maximum from being rejected earlier for a missing required field.

Use a coverage matrix while expanding the harness:

Schema rule Valid boundary Invalid examples Security concern
required key present key absent unsafe defaults
type exact JSON type number for string, array for object parser confusion
minimum and maximum 1 and 50 0, 51, huge number resource abuse
maxLength 120 code points 121, very large text memory and log pressure
uniqueItems distinct labels duplicate labels repeated downstream work
additionalProperties known keys only role, admin, prototype-like keys mass assignment
enum listed value unknown value, wrong case unintended routing

Verification: run npm run typecheck. Review every constraint in searchTicketsSchema and ensure at least one invalid mutation targets it. Add a tracking test if your production schemas change frequently.

Step 5: Fuzz MCP Tool Arguments with JSON Schema Assertions

Create test/fuzz.test.ts and assert three properties: every invalid case is rejected, the handler records no effect, and the error contains at least one validator issue.

import fc from "fast-check";
import { beforeEach, describe, expect, it } from "vitest";
import { invalidCaseArb } from "../src/generators.js";
import { effects, InvalidArgumentsError, invokeSearchTickets, resetEffects } from "../src/tool.js";

beforeEach(resetEffects);

describe("MCP argument fuzzing", () => {
  it("rejects invalid arguments before side effects", async () => {
    await fc.assert(fc.asyncProperty(invalidCaseArb, async ({ rule, value }) => {
      resetEffects();
      try {
        await invokeSearchTickets(value);
        throw new Error(`accepted invalid case: ${rule}`);
      } catch (error) {
        expect(error, rule).toBeInstanceOf(InvalidArgumentsError);
        expect((error as InvalidArgumentsError).issues.length, rule).toBeGreaterThan(0);
        expect(effects, rule).toHaveLength(0);
      }
    }), { numRuns: 1000, seed: 20260715 });
  });
});

The explicit seed makes the normal CI sequence stable. fast-check also prints the seed and path for failures, so a newly discovered counterexample can be replayed exactly. Do not catch and discard that output in a custom reporter.

One subtle mistake is validating after coercion. Avoid Ajv options such as coerceTypes unless the protocol explicitly defines coercion. A string "50" is not a JSON integer, and accepting it in one component but rejecting it in another creates differential behavior. Avoid mutating options such as removeAdditional at a security boundary because silent repair can hide attacks.

Verification: run npm run test:fuzz. Expect the property to pass 1,000 generated cases and effects to remain empty for each invalid input. Change maximum: 50 to maximum: 51 temporarily and confirm the suite finds that 51 is unexpectedly accepted, then restore the schema.

Step 6: Exercise an MCP-Shaped Protocol Adapter

Unit-testing the validator is necessary but insufficient. Serialization, request routing, and error translation can introduce bypasses. Create src/protocol.ts:

import { InvalidArgumentsError, invokeSearchTickets } from "./tool.js";

type CallToolRequest = {
  method: "tools/call";
  params: { name: string; arguments?: unknown };
};

export async function handleRequest(request: CallToolRequest) {
  if (request.params.name !== "search_tickets") {
    return { error: { code: -32602, message: "Unknown tool" } };
  }

  try {
    return { result: await invokeSearchTickets(request.params.arguments) };
  } catch (error) {
    if (error instanceof InvalidArgumentsError) {
      return {
        error: {
          code: -32602,
          message: "Invalid tool arguments",
          data: error.issues.map(({ instancePath, keyword }) => ({ instancePath, keyword }))
        }
      };
    }
    return { error: { code: -32603, message: "Internal error" } };
  }
}

This adapter uses MCP's tools/call method shape without pretending to replace an SDK conformance test. In your server, send generated cases through the official client and transport that production uses. Keep the same assertions at the handler boundary.

Add a second property to test/fuzz.test.ts:

import { handleRequest } from "../src/protocol.js";

it("returns a stable safe error for invalid protocol arguments", async () => {
  await fc.assert(fc.asyncProperty(invalidCaseArb, async ({ value }) => {
    resetEffects();
    const response = await handleRequest({
      method: "tools/call",
      params: { name: "search_tickets", arguments: value }
    });
    expect(response).toMatchObject({
      error: { code: -32602, message: "Invalid tool arguments" }
    });
    expect(JSON.stringify(response)).not.toContain("stack");
    expect(effects).toHaveLength(0);
  }), { numRuns: 500, seed: 20260715 });
});

Verification: run npm run test:fuzz. Expect both properties to pass. Inspect one response manually and confirm it contains only paths and schema keywords, never the full hostile value, stack trace, filesystem path, or secret.

Step 7: Add Parser, Size, and Unicode Cases

Property generators should be combined with a curated regression corpus. JavaScript JSON numbers, Unicode strings, and deeply nested data deserve explicit attention even when the schema rejects them. Add test/regression.test.ts:

import { describe, expect, it } from "vitest";
import { effects, invokeSearchTickets, resetEffects } from "../src/tool.js";

const cases: Array<[string, unknown]> = [
  ["null root", null],
  ["array root", []],
  ["missing arguments", undefined],
  ["unsafe integer", { query: "x", limit: Number.MAX_SAFE_INTEGER + 1 }],
  ["non-finite number", { query: "x", limit: Number.POSITIVE_INFINITY }],
  ["empty text", { query: "", limit: 1 }],
  ["very large text", { query: "x".repeat(1_000_000), limit: 1 }],
  ["nested extra key", { query: "x", limit: 1, options: { includeClosed: false, admin: true } }],
  ["prototype-like key", JSON.parse('{"query":"x","limit":1,"__proto__":{"admin":true}}')]
];

describe.each(cases)("regression: %s", (_name, value) => {
  it("rejects safely without a handler effect", async () => {
    resetEffects();
    await expect(invokeSearchTickets(value)).rejects.toThrow("Invalid tool arguments");
    expect(effects).toHaveLength(0);
  });
});

Keep schema and test expectations aligned. A null byte is valid under the current string schema, so test it separately as an accepted value with safe downstream encoding. If the ticket API forbids control characters, add an explicit schema pattern or semantic validator first, then move that value into the rejection corpus.

Apply a byte limit before JSON parsing at the transport boundary. JSON Schema only runs after parsing, so it cannot prevent a huge body from consuming memory first. Also set request deadlines and nesting limits where your parser or gateway supports them.

Verification: run npm test -- test/regression.test.ts. Expect every listed case to be rejected consistently. Monitor the large-text case locally and confirm it fails quickly without being copied into logs. Add a separate acceptance test for any unusual Unicode value that the published schema intentionally allows.

Step 8: Make Fuzzing Reproducible and CI-Safe

Use modest pull-request budgets and larger scheduled campaigns. Never equate run count with coverage. Track schema keywords, mutation operators, tool names, and boundary values reached.

Create .github/workflows/mcp-argument-fuzz.yml:

name: MCP argument fuzzing

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  fuzz:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run typecheck
      - run: npm test
        env:
          FC_SEED: "20260715"

If you read FC_SEED, parse it once and pass it to every fc.assert. Print the seed at test start. When a failure occurs, save the seed, path, tool schema hash, package lockfile, and minimized counterexample in the defect. Promote valuable failures into the curated regression corpus.

Do not give the fuzz job production credentials or unrestricted network access. The harness should use stub handlers or isolated test accounts. Bound numRuns, input sizes, per-test timeouts, and total job time. A fuzzer is itself an untrusted workload.

Verification: run npm ci, npm run typecheck, and npm test locally. In CI, confirm the job has read-only repository permission and no deployment environment. Force one invalid mutation to return success, verify the job fails with a seed and counterexample, then restore the code.

Troubleshooting

Problem: valid generated objects fail validation -> Compare the smallest counterexample with the schema dialect and generator. Check optional keys, Unicode length, uniqueness, and nested additionalProperties. Compile the exact schema published by the tool.

Problem: invalid cases are rejected for the wrong reason -> Begin with a valid object and mutate exactly one constraint. Assert the expected Ajv keyword for targeted tests, but allow multiple errors when one mutation necessarily violates related rules.

Problem: fast-check failures cannot be reproduced -> Record both the seed and shrink path printed by fast-check. Pin dependencies with the lockfile and keep generator ordering stable while investigating.

Problem: the server accepts values Ajv rejects -> Look for coercion, defaults, property removal, a second validator, or a route that bypasses the shared boundary. Send cases through the real transport and compare decisions byte for byte.

Problem: fuzz tests exhaust memory or time out -> Lower maximum string, array, and recursion sizes. Reject oversized request bodies before parsing, use per-case deadlines, and split expensive campaigns from pull-request checks.

Problem: errors expose hostile arguments or secrets -> Map validator errors to stable paths and keywords. Do not echo full values, raw exceptions, stacks, environment data, or request headers.

Where To Go Next

Use the complete MCP security testing guide to place schema fuzzing inside a full assessment. Argument validation limits malformed input, but it does not decide whether a valid call should be authorized. Add MCP tool permission boundary testing for identity, tenant, scope, approval, and side-effect controls.

Then run MCP prompt injection attack tests because perfectly schema-valid arguments can still carry adversarial instructions. Finish with MCP server secret leakage testing across successful responses, validation errors, logs, tracing, and transport failures.

In a mature suite, inventory every tool and schema version. Generate valid cases, controlled invalid mutations, semantic policy cases, and cross-tool sequences. Re-run a compact deterministic set on each pull request and a broader isolated campaign on a schedule. Apply the same boundary-first method from an API security testing guide, and strengthen the surrounding pipeline with CI/CD security testing practices.

Interview Questions and Answers

Q: Why use JSON Schema to fuzz MCP tool arguments?

JSON Schema defines the runtime shape that an MCP tool advertises to clients. It provides constraints for valid generation and an oracle for invalid mutations. Reusing it reduces drift between documentation, validation, and tests.

Q: What is the difference between generation and mutation fuzzing?

Generation builds values directly from a model of the input space. Mutation starts with an existing value and changes it. A strong schema fuzzer generates valid seeds, then applies one labeled mutation per constraint so failures are explainable.

Q: Why test valid values in a security fuzz suite?

Valid generation detects schema and implementation drift and exercises boundary values that must remain accepted. It also supplies well-formed bases for precise invalid mutations. Without valid seeds, random inputs mostly test the same shallow rejection paths.

Q: What should the oracle assert for invalid MCP arguments?

Assert a stable invalid-parameters response, zero handler side effects, safe error content, and bounded processing time. When useful, assert the schema keyword and instance path. Do not rely only on an HTTP status or thrown exception.

Q: How does shrinking help property-based testing?

Shrinking reduces a failing generated input to a smaller counterexample that still fails. The smaller object usually reveals the relevant constraint or bypass. Save the seed and shrink path to reproduce it exactly.

Q: Can JSON Schema stop prompt injection?

No. Schema validation controls structure, types, and declared value constraints. Adversarial natural-language content can remain schema-valid, so prompt injection defenses, authorization, least privilege, and output handling are still required.

Q: Where should MCP argument validation occur?

Validate at the shared tool invocation boundary immediately before handler execution. Also enforce byte limits before parsing and semantic authorization before side effects. This layered placement covers transport exhaustion, structural invalidity, and valid but unauthorized calls.

Best Practices

  • Compile the exact JSON Schema exposed by each tool and identify its dialect.
  • Set additionalProperties: false unless extensibility is an intentional contract.
  • Bound strings, arrays, numbers, nesting, request bytes, and execution time.
  • Generate accepted inputs as well as rejected inputs.
  • Mutate one rule at a time and label every operator.
  • Assert that invalid requests cause no handler, network, file, or database effect.
  • Keep validation errors structured, stable, sanitized, and small.
  • Record seeds, shrink paths, schema hashes, and dependency versions.
  • Promote minimized failures into a permanent regression corpus.
  • Test through the production transport and SDK in addition to validator unit tests.

Common mistakes include enabling silent type coercion, removing unknown properties instead of rejecting them, using TypeScript types as runtime validation, logging entire hostile payloads, and sending random JSON without a coverage model. Another mistake is assuming schema-valid means safe. Authorization and content security remain separate decisions.

Conclusion: Fuzz MCP Tool Arguments with JSON Schema

The practical way to fuzz mcp tool arguments with json schema is to turn the published contract into a two-sided test model. Generate values that must pass, create focused mutations that must fail, and drive both through the real tool boundary while observing errors, timing, and side effects.

Start with the eight-step lab, preserve every reproducible counterexample, and expand coverage tool by tool. Combine structural fuzzing with permission, prompt injection, and secret-leakage testing so a valid object can never bypass the controls that protect the action behind it.

Interview Questions and Answers

Why is JSON Schema useful as a fuzzing oracle for MCP tools?

It is the runtime contract that declares accepted types, required keys, bounds, and object rules. A fuzzer can generate conforming values and deliberate violations from the same source. The handler's decision can then be compared with the compiled schema decision.

Why should invalid argument tests assert side effects?

A request can appear rejected after a handler already wrote data or called an external API. The stronger oracle verifies that validation occurs before execution and that no network, file, database, or audit-sensitive effect happened.

What is shrinking in property-based testing?

Shrinking searches for a smaller input that preserves the failure. It turns a noisy generated object into an actionable counterexample. Engineers should save the seed and shrink path for exact replay.

Why avoid automatic type coercion at an MCP validation boundary?

Coercion can make different components interpret the same JSON differently and silently accept malformed model output. Unless the public contract explicitly defines coercion, exact JSON types produce clearer and safer behavior.

How would you cover additionalProperties in a schema fuzzer?

Start with a valid object and add one unknown key at the root and within each nested object. Include ordinary misspellings and security-relevant names. Assert rejection and confirm that the property is not silently removed before handler execution.

What is the difference between structural and semantic validation?

Structural validation checks declared shape, types, formats, and bounds. Semantic validation checks domain meaning, authorization, tenant ownership, and relationships that a schema alone may not express. Both must run before sensitive side effects.

How should MCP fuzzing run in CI?

Use pinned dependencies, deterministic seeds, bounded sizes, fixed run budgets, and strict timeouts. Give the job no production credentials and minimal network access. Preserve the seed, path, schema hash, and minimized input when it fails.

Frequently Asked Questions

How do you fuzz MCP tool arguments with JSON Schema?

Compile the tool's inputSchema, generate valid objects that satisfy its constraints, and apply one controlled invalid mutation at a time. Send both classes through the real tool-call boundary and assert validation decisions, safe errors, and absence of side effects.

Which TypeScript libraries work for MCP argument fuzzing?

Ajv is a widely used JSON Schema validator, and fast-check provides property-based generators, reproducible seeds, and shrinking. The official MCP SDK should be used when exercising the actual client, server, and transport boundary.

Should a fuzzer generate valid or invalid MCP arguments?

Generate both. Valid values detect contract drift and supply clean mutation seeds, while invalid values verify rejection paths, error safety, and absence of side effects.

Does JSON Schema validation prevent MCP prompt injection?

No. A malicious instruction can be a perfectly valid string. Schema validation must be combined with prompt injection tests, authorization, least privilege, approval controls, and safe output handling.

How many property-based fuzz runs should CI execute?

Choose a bounded number that completes reliably for your schemas and runners, then measure operator and constraint coverage. Use a compact suite on pull requests and broader scheduled campaigns rather than treating a large run count as proof of quality.

How do you reproduce a fast-check fuzz failure?

Keep the printed seed and shrink path, pin dependencies with the lockfile, and record the schema version or hash. Add the minimized counterexample to a deterministic regression corpus after fixing the defect.

Related Guides