Skip to main content
Back to Blog
Guide
2026-07-10

Test MCP Tool Schemas, Defaults, Invalid Inputs, and Error Types

Test MCP tool schemas, omitted defaults, invalid inputs, protocol and execution errors, structured output, and product semantics under the current spec.

MCP tool contract matrix separating JSON Schema validation, defaults, error channels, structured output, and product behavior

Effective MCP tool schema contract testing separates five questions that are often collapsed into one assertion: is the advertised JSON Schema valid, how does the server normalize omitted values, which failures belong in JSON-RPC errors, which failures belong in tool results with isError: true, and does a successful result satisfy both outputSchema and the product's meaning? Under the published 2025-11-25 MCP specification, these are related contracts with different authorities and different failure evidence.

Use the MCP server testing pillar for the complete architecture, then pair this guide with the official conformance server tutorial, GitHub Actions conformance baseline, and MCP Inspector CLI tutorial. Teams can find reusable QA instructions in /skills, install the Playwright CLI skill, and read the canonical Playwright MCP browser automation guide when tool contracts drive browser workflows.

Start with the current published contract

As of July 14, 2026, the MCP versioning documentation identifies 2025-11-25 as the current published protocol release. Draft material can describe future behavior, but it is not the baseline for a current release gate. Record the protocol revision, server SDK version, schema validator and dialect configuration, application commit, fixture revision, and tool catalog digest with every contract run.

The published tools specification establishes these relevant requirements and recommendations:

Contract areaPublished MCP behaviorWhat your tests must not assume
inputSchemaRequired JSON Schema object for every tool; defaults to dialect 2020-12 without $schemaA validator automatically inserts values from default
outputSchemaOptional JSON Schema for structuredContent; when supplied, server results must conform and clients should validateText content alone proves the structured contract
Unknown tool or malformed callProtocol errorEvery invalid domain value should be a protocol error
Input validation, API, or business failureTool result with isError: trueJSON-RPC success means the tool succeeded
Tool annotationsHints that clients must treat as untrusted from untrusted serversreadOnlyHint enforces authorization or prevents writes
Human control and securitySpecification contains SHOULD and MUST guidance for confirmation, validation, access control, rate limits, sanitization, and auditOne universal prompt, timeout, or rate threshold fits every product

Keep specification requirements, implementation behavior, security recommendations, and team policy labeled in test names and reports. For example, "output matches declared schema" is a protocol contract; "refund quotes expire after 15 minutes" is product semantics; "block release when any destructive tool lacks approval coverage" is team policy.

Define a representative tool contract

Use a real tool shape with enough constraints to expose type, omission, error, and output mistakes. The following orders.quote_refund definition uses explicit JSON Schema 2020-12, rejects unknown input properties as a team compatibility choice, annotates an optional currency, and declares structured output.

{
  "name": "orders.quote_refund",
  "title": "Quote an order refund",
  "description": "Calculate an eligible refund quote without issuing the refund.",
  "inputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "orderId": {
        "type": "string",
        "pattern": "^ORD-[0-9]{4}$"
      },
      "currency": {
        "type": "string",
        "enum": ["USD", "EUR"],
        "default": "USD"
      },
      "includeShipping": {
        "type": "boolean",
        "default": false
      }
    },
    "required": ["orderId"]
  },
  "outputSchema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "orderId": { "type": "string" },
      "eligible": { "type": "boolean" },
      "amount": { "type": "number", "minimum": 0 },
      "currency": { "type": "string", "enum": ["USD", "EUR"] },
      "reasonCode": { "type": "string" }
    },
    "required": ["orderId", "eligible", "amount", "currency", "reasonCode"]
  },
  "annotations": {
    "readOnlyHint": true,
    "destructiveHint": false,
    "idempotentHint": true,
    "openWorldHint": false
  }
}

The root additionalProperties: false is not a universal MCP requirement. It is a compatibility policy that prevents misspelled or newly introduced fields from being ignored. Some extensible tools intentionally allow additional properties. Decide per tool and test the decision.

The annotations are discovery hints, not security controls. A server must still authenticate the caller, authorize access to the order, prevent cross-tenant data exposure, and ensure the supposedly read-only implementation does not mutate state.

Test schema validity before instance validity

There are two validation layers:

  1. Schema validation asks whether inputSchema and outputSchema are legal schemas in the declared or default dialect.
  2. Instance validation asks whether a particular argument object or structuredContent value satisfies that legal schema.

The MCP base specification says schemas without $schema default to JSON Schema 2020-12, implementations must support at least that dialect, and unsupported explicit dialects must be handled gracefully. Configure the validator for 2020-12; do not let a library silently interpret the schema as draft-07.

Build catalog tests for every advertised tool:

  • name is unique and follows the published naming guidance;
  • inputSchema is non-null, valid for its dialect, and has an object root;
  • every name in required exists under properties when that is the intended schema structure;
  • examples and defaults validate against their containing subschemas;
  • references resolve without network access in CI unless remote retrieval is explicitly controlled;
  • outputSchema, when present, is valid and has an object root;
  • annotations match observed behavior, even though they remain hints.

Use an explicit case matrix rather than one happy-path object:

CaseInputSchema expectationProduct expectation
Minimum valid{"orderId":"ORD-1001"}ValidServer applies documented omission policy
Fully explicitOrder, USD, shipping falseValidSame normalized quote as minimum case if defaults are applied
Wrong JSON typeincludeShipping: "false"InvalidActionable tool execution error
Pattern violationorderId: "1001"InvalidActionable tool execution error
Unknown propertyincludeTax: trueInvalid under this schemaNo silent ignore
Unsupported enumcurrency: "GBP"InvalidError identifies supported choices without leaking data
Valid shape, missing domain entityORD-9999ValidProduct not-found tool execution error
Valid and eligibleFixture order ORD-1001ValidCorrect quote and schema-valid structured output

Schema validation tells you whether the data shape is accepted. It cannot prove that the quote amount, tenant, eligibility, reason code, or authorization decision is correct.

Treat default as an annotation, then define server policy

The official JSON Schema annotation documentation states that default does not fill missing values during validation. It can communicate that absence is semantically equivalent to a value, and tools such as forms may use it as a hint. Therefore, MCP schema validation alone does not turn an omitted currency into "USD".

Choose and document one normalization policy:

PolicyOmitted field behaviorContract test
Server applies schema defaultsServer materializes USD and false before business logicOmitted and explicit-default calls produce equivalent normalized audit input
Application defaults independentlyDomain service owns the default; schema mirrors itUnit test schema and application constants cannot drift
Absence has separate meaningOmitted differs from explicit valueTest both paths and remove misleading default annotation
Field is actually requiredServer rejects omissionAdd field to required; do not rely on prose

Test omission, explicit default, non-default, explicit null, and wrong type separately. Null is not absence. If the schema type is string, null is invalid unless the schema explicitly permits it.

{
  "cases": [
    {
      "id": "currency-omitted",
      "arguments": { "orderId": "ORD-1001" },
      "normalized": { "orderId": "ORD-1001", "currency": "USD", "includeShipping": false }
    },
    {
      "id": "currency-explicit-default",
      "arguments": { "orderId": "ORD-1001", "currency": "USD", "includeShipping": false },
      "normalized": { "orderId": "ORD-1001", "currency": "USD", "includeShipping": false }
    },
    {
      "id": "currency-explicit-null",
      "arguments": { "orderId": "ORD-1001", "currency": null },
      "expectedError": "tool-execution"
    }
  ]
}

The normalized field is a fixture expectation, not an MCP message field. Observe normalization through a test seam or sanitized audit event; do not add private diagnostic data to production tool results merely for testing.

Distinguish malformed requests from invalid tool inputs

The current tools specification defines two error mechanisms. Protocol errors are standard JSON-RPC errors for an unknown tool, a malformed CallToolRequest, or a server-level protocol failure. Tool execution errors are ordinary JSON-RPC results with isError: true for input validation, upstream API failures, and business logic errors.

That means "invalid input" needs a precise definition:

FailureExampleCorrect channel under current tools spec
Malformed request envelopeparams is a string, or required request structure is absentJSON-RPC protocol error
Unknown toolname: "orders.missing"JSON-RPC protocol error
Arguments violate tool schemaBoolean supplied as stringTool result with isError: true
Arguments pass schema but violate domain ruleRefund window closedTool result with isError: true
Dependency rejects or times outOrder service unavailableTool result with isError: true
Caller lacks product permissionValid identity cannot access orderProduct/security design; normally actionable execution error without leaked data
MCP endpoint lacks valid authorizationMissing or invalid HTTP credentialAuthorization or transport response before tool execution

Do not require -32602 for every schema-invalid argument. The 2025-11-25 changelog explicitly clarifies that input validation errors should be tool execution errors so a model can correct its call. Reserve protocol errors for the malformed or unknown protocol-level cases described by the specification.

These two JSON documents show the shape difference. The first is a protocol error for an unknown tool:

{
  "jsonrpc": "2.0",
  "id": 7,
  "error": {
    "code": -32602,
    "message": "Unknown tool: orders.missing"
  }
}

The second is a tool execution error for a known tool with a domain-invalid order identifier:

{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "orderId must use the form ORD-0000"
      }
    ],
    "isError": true
  }
}

Error messages should be actionable enough for correction but must not expose stack traces, credentials, tenant existence, database queries, or internal network details. Exact public error codes and redaction rules are product and security contracts.

Validate structuredContent and its text fallback

structuredContent is a JSON object in a tool result. When the tool advertises outputSchema, the server must provide structured results conforming to it, and clients should validate them. The tools specification also recommends returning serialized JSON in a text content block for backward compatibility.

A valid success fixture for the refund quote could be:

{
  "content": [
    {
      "type": "text",
      "text": "{"orderId":"ORD-1001","eligible":true,"amount":42.5,"currency":"USD","reasonCode":"WITHIN_WINDOW"}"
    }
  ],
  "structuredContent": {
    "orderId": "ORD-1001",
    "eligible": true,
    "amount": 42.5,
    "currency": "USD",
    "reasonCode": "WITHIN_WINDOW"
  },
  "isError": false
}

Test at least these output defects:

  • structuredContent is absent despite an advertised output schema;
  • amount is a numeric string instead of a number;
  • a required field is missing;
  • currency falls outside the enum;
  • an undeclared property appears when output forbids additional properties;
  • text fallback is missing or serializes different data;
  • result is schema-valid but refers to the wrong order;
  • isError is true while the payload looks like a successful quote.

If error results need structured machine-readable fields, define and document an error contract deliberately. Do not assume a success-only outputSchema automatically describes every isError: true payload. The current specification's broad conformance statement should be tested against your SDK behavior and tool design, and ambiguous error unions should be made explicit in product documentation.

Add transport-neutral contract assertions

Keep core assertions independent of stdio or HTTP so the same matrix can run through an SDK client, Inspector capture, or another harness. The following TypeScript is syntactically complete and receives an injected request function; it does not guess a changing SDK constructor.

import assert from 'node:assert/strict';

type JsonObject = Record<string, unknown>;
type Invoke = (request: JsonObject) => Promise<JsonObject>;

export async function verifyRefundContracts(invoke: Invoke) {
  const unknownTool = await invoke({
    jsonrpc: '2.0',
    id: 1,
    method: 'tools/call',
    params: { name: 'orders.missing', arguments: {} },
  });
  assert.equal(typeof unknownTool.error, 'object');

  const invalidInput = await invoke({
    jsonrpc: '2.0',
    id: 2,
    method: 'tools/call',
    params: {
      name: 'orders.quote_refund',
      arguments: { orderId: 'ORD-1001', includeShipping: 'false' },
    },
  });
  const invalidResult = invalidInput.result as JsonObject;
  assert.equal(invalidResult.isError, true);

  const valid = await invoke({
    jsonrpc: '2.0',
    id: 3,
    method: 'tools/call',
    params: {
      name: 'orders.quote_refund',
      arguments: { orderId: 'ORD-1001' },
    },
  });
  const result = valid.result as JsonObject;
  assert.notEqual(result.isError, true);

  const output = result.structuredContent as JsonObject;
  assert.equal(output.orderId, 'ORD-1001');
  assert.equal(output.currency, 'USD');
  assert.equal(typeof output.amount, 'number');
  assert.equal(typeof output.eligible, 'boolean');
}

Add your chosen JSON Schema 2020-12 validator before the semantic assertions. The injected invoke adapter should preserve the full JSON-RPC response so the test can distinguish error from result.isError. Do not write an adapter that converts every failure into a thrown JavaScript exception; that erases the protocol channel under test.

Test product semantics after schema success

A schema-valid response can still be dangerously wrong. For the quote tool, assert:

  • the returned order belongs to the authorized fixture tenant;
  • eligibility matches refund-window and order-state rules;
  • amount uses the approved decimal and rounding policy;
  • currency comes from the order or documented default policy;
  • shipping inclusion follows the explicit argument;
  • the quote operation creates no refund or payment side effect;
  • reason code maps to a documented user-safe explanation;
  • repeated calls are consistent with the advertised idempotency hint;
  • unavailable dependencies produce a safe execution error rather than a fabricated quote.

These are product requirements. MCP transports them but does not define refund arithmetic, tenant design, or idempotency keys. Assign business owners to expected values and security owners to authorization and leakage cases.

Use mutation tests against the fixture: change the order tenant, state, purchase date, currency, item total, shipping amount, and prior refund status one field at a time. A mutation should change only the outcomes implied by the rule. This catches handlers that return a hard-coded schema-valid fixture.

Include security and team policy without overstating the spec

The tools specification says servers must validate inputs, implement access controls, rate limit invocations, and sanitize outputs. It recommends that clients confirm sensitive operations, show inputs, validate results, use timeouts, and log tool use. Those statements establish direction, but exact limits remain implementation and team decisions.

ControlSpecification or recommendationExample team policy
Input validationServer MUST validateValidate before any dependency call and log only field names on rejection
Access controlServer MUST implement proper controlsEvery order lookup is tenant-scoped and deny-by-default
Rate limitingServer MUST rate limitLimit chosen from abuse model and service capacity, not a copied universal number
Output sanitizationServer MUST sanitizeNo stack, credential, raw SQL, or cross-tenant identifier in errors
ConfirmationClient SHOULD confirm sensitive operationsRefund execution requires explicit user confirmation; quote does not
TimeoutClient SHOULD implementPer-tool budget based on observed latency and safe cancellation behavior

Test both presence and effectiveness. A rate limiter that always permits calls is not proven by configuration. An annotation claiming read-only behavior is not proven until a state-diff assertion shows no mutation.

Run contracts in CI and release review

Organize the job into catalog, call-shape, error-channel, structured-output, product, and security phases. Save normalized request and response fixtures with secrets removed. Run contract tests on every tool schema or handler change, every MCP SDK upgrade, and every product rule change that affects tool output.

Block release on protocol contract failures. Whether a product-semantic or security test can be temporarily waived is a risk decision, but waivers need owner, impact, compensating control, and expiry. Never convert a wrong error channel or cross-tenant result into a broad expected snapshot.

Compare catalog digests across releases. A removed tool, newly required input, narrowed enum, changed default meaning, or incompatible output schema should trigger compatibility review. Adding an optional output property can still break strict consumers if they apply additionalProperties: false locally, so publish change notes even for theoretically additive updates.

Troubleshooting failures by layer

Schema compiles in one environment but not another. Confirm both validators use the declared dialect. MCP defaults an absent $schema to 2020-12; a draft-07 default in a library is a configuration mismatch.

Omitted values differ between Inspector and an SDK client. Compare the actual arguments sent. A form may materialize an explicit default while CLI invocation omits the key. Decide whether normalization belongs to the server and test both request shapes.

Invalid arguments return a JSON-RPC error. Check whether the request itself is malformed or only violates the tool's input schema. Under the current tools guidance, input validation belongs in a tool execution error so a model can retry.

Structured output validates but the text fallback differs. Serialize from one normalized result object instead of building text and structured forms independently. Test semantic equality after parsing the text JSON.

The success response validates but contains the wrong tenant's order. This is a critical product and access-control failure, not a schema problem. Stop the release and inspect authorization scope before changing expected fixtures.

Annotations disagree with behavior. Treat annotations as untrusted hints and fix either the implementation or metadata. Never use the hint as the only authorization or confirmation control.

Limitations

Contract tests cover declared tools and selected fixtures. They cannot prove every JSON Schema instance, every dependency response, or every attack path. Property-based generation and fuzzing can broaden input coverage, but generated cases still need domain oracles. Load and concurrency tests are separate because a functionally valid tool may race or exhaust resources under parallel calls.

Schema compatibility is also consumer-dependent. MCP defines message shapes and guidance, while a particular client may have stricter code generation or rendering behavior. Test the supported clients that matter to your product without rewriting those client quirks as universal protocol rules.

Contract checklist

  • Pin and record the published MCP protocol baseline 2025-11-25.
  • Validate each schema against its declared or default 2020-12 dialect.
  • Test valid, omitted, explicit-default, null, wrong-type, enum, pattern, and extra-field inputs.
  • Define who applies defaults; do not expect validation to insert them.
  • Assert malformed requests and unknown tools through protocol errors.
  • Assert schema, dependency, and business failures through isError: true results.
  • Validate structuredContent whenever outputSchema is advertised.
  • Compare the serialized text fallback with structured data.
  • Add product semantics and tenant authorization after schema validation.
  • Verify annotations against behavior while treating them as hints.
  • Preserve sanitized fixtures, responses, versions, and catalog digests in CI.
  • Review every schema change for backward compatibility and release impact.

Frequently asked questions

Which JSON Schema dialect does MCP use for tool schemas?

The published 2025-11-25 specification defaults schemas without $schema to JSON Schema 2020-12. Implementations must support that dialect and should document additional dialects. Explicitly configure your validator instead of relying on its library default.

Does the default keyword insert a missing tool argument?

No. JSON Schema defines default as an annotation, not a mutation performed by validation. A form or server may choose to apply it. Document that normalization policy and test omitted and explicit values separately.

Should a schema-invalid tool argument return JSON-RPC -32602?

Not under the current tools guidance when the tools/call request is structurally valid and the known tool's argument fails validation. Return a tool execution error with isError: true and actionable content. Reserve protocol errors for unknown tools, malformed requests, and protocol-level server failures.

Is structuredContent required for every tool?

Tools can return unstructured content without an output schema. When a tool advertises outputSchema, the server must provide conforming structured results, and clients should validate them. The specification recommends a serialized text block for backward compatibility.

Does outputSchema prove the result is correct?

No. It proves only that the structured value has the declared shape. A refund amount can be a valid number and still be wrong, unauthorized, or calculated from another tenant's order. Add product and security assertions after schema validation.

Are readOnlyHint and destructiveHint security controls?

No. Tool annotations are hints and must be treated as untrusted when they come from untrusted servers. Enforce authorization, confirmation, isolation, and side-effect controls in the client and server, then test observed behavior.

Should error results conform to the success output schema?

Do not assume a success-only schema explains every error payload. Define an explicit structured error design if clients need one, test it against your SDK behavior, and always preserve the required distinction between protocol errors and tool execution errors.

How often should MCP tool contracts run?

Run them on every change to tool registration, schemas, handlers, product rules, MCP SDK, authorization, or transport adapters. Include the stable conformance suite as a separate job and run broader security, fuzz, and load coverage according to release risk.

Conclusion

A durable MCP tool contract is layered. Validate the advertised schema and dialect, define omission and default behavior, preserve the protocol-versus-execution error boundary, verify structured results, and then assert the product's real meaning and security rules. Keeping those layers separate produces failures that are diagnosable and release claims that match what the tests actually prove.