PRACTICAL GUIDE / MCP JSON-RPC error testing

Distinguish MCP JSON-RPC and Tool Errors

Learn MCP JSON-RPC error testing by separating protocol failures from tool execution errors, then asserting client behavior, retries, and evidence.

By The Testing AcademyUpdated July 25, 202618 min read
All field guides
In this guide11 sections
  1. What does MCP JSON-RPC error testing prove?
  2. How are protocol errors different from tool execution errors?
  3. Which JSON-RPC fields must every error assertion cover?
  4. When should an MCP tool return isError true?
  5. How do you build an error matrix for tools/call?
  6. How do you test unknown tools and invalid arguments?
  7. How should clients retry, stop, or show each failure?
  8. How do you test error content without leaking secrets?
  9. Which contract checks belong in CI?
  10. Test the adapter at both directions
  11. Preserve side-effect uncertainty
  12. Make observability agree with the wire
  13. Use mutation tests for the taxonomy
  14. FAQ
  15. Is isError a JSON-RPC error?
  16. Should invalid arguments be protocol errors?
  17. Can clients retry a tool error?
  18. What error code should an unknown tool use?
  19. Should error content be passed to the model?
  20. How do MCP errors appear in traces?
  21. Conclusion

What you will learn

  • What does MCP JSON-RPC error testing prove?
  • How are protocol errors different from tool execution errors?
  • Which JSON-RPC fields must every error assertion cover?
  • When should an MCP tool return isError true?

MCP JSON-RPC error testing must distinguish two contracts. A protocol failure uses the JSON-RPC error response. A valid tools/call request whose tool execution fails returns a JSON-RPC result containing isError: true. Tests should assert the envelope, code, request ID, content, sanitization, and client retry decision as separate facts.

This distinction follows the JSON-RPC specification and the MCP 2025-11-25 schema. It is also the narrow focus missing from a general guide to testing MCP servers. The JSON-RPC, tool-result, schema, and contract-audit challenges in src/db/seed-data/challenges/expansion-mcp-ai.ts provide the repository evidence for treating the envelope as an executable contract rather than a log format.

What does MCP JSON-RPC error testing prove?

It proves that every failure is represented at the correct protocol layer and that the client reacts according to an explicit policy. A parser failure, an unknown method, an invalid MCP request, a tool's business rejection, and a temporary provider outage are all failures. They are not interchangeable.

A complete test answers five questions:

  1. Did the peer receive a JSON-RPC response with the correct request ID?
  2. Did the response contain exactly one of result or error?
  3. If it was a protocol error, were code, message, and optional data shaped correctly?
  4. If tool execution failed, did the tool result carry isError: true and useful sanitized content?
  5. Did the client stop, retry, revise arguments, ask the user, or report the failure according to policy?

The last question prevents a common false pass. A response can be schema-valid while the client loops, exposes a secret, or retries a side effect. Protocol conformance is necessary evidence, but application behavior completes the test.

This article does not own version negotiation or tool discovery. Those contracts belong in MCP capability negotiation tests and debugging missing MCP tools. Here the tool has already been selected or the request has already reached the error boundary.

How are protocol errors different from tool execution errors?

A protocol error means the JSON-RPC or MCP method could not be processed as a valid request. The response has the same id when an identifier is available and uses an error member. It does not also contain result. JSON-RPC defines the top-level response rules and standard error object shape.

A tool execution error means the server understood and processed a valid tools/call request far enough to invoke the tool contract, but the requested operation failed. MCP's tool guidance represents that outcome in a CallToolResult with isError: true. From JSON-RPC's perspective, method processing produced a result. From the tool user's perspective, the operation failed.

TypeScript
type JsonRpcId = string | number | null;

type JsonRpcFailure = {
  jsonrpc: "2.0";
  id: JsonRpcId;
  error: {
    code: number;
    message: string;
    data?: unknown;
  };
};

type CallToolFailure = {
  jsonrpc: "2.0";
  id: string | number;
  result: {
    content: Array<{ type: "text"; text: string }>;
    isError: true;
  };
};

Do not infer the layer from English wording. A tool result may say "invalid account" but remain a valid JSON-RPC result. A protocol error may mention a tool name because no registered tool matched it. Assert the member that exists, then assert its inner contract.

The distinction also preserves model behavior. Tool error content may be returned to an agent so it can revise a call or explain a domain rejection. Protocol errors are handled by the client's protocol machinery. Passing every protocol defect to the model encourages retries that cannot repair malformed framing or an unsupported method.

Which JSON-RPC fields must every error assertion cover?

Start with jsonrpc. A response must identify version "2.0". Next assert the request identifier. Correlation is essential when several operations are in flight; accepting the right error under the wrong ID can attach failure handling to another request.

For a protocol failure, assert:

  • error.code is the expected numeric code from the applicable contract.
  • error.message is a concise, stable classification, not a secret-bearing stack trace.
  • error.data, when present, has a documented schema and remains optional for clients that do not need it.
  • result is absent.
  • The response is not treated as a notification.

For a tool execution failure, assert:

  • result.isError is exactly true.
  • result.content is valid MCP result content.
  • error is absent.
  • The content is safe for the intended consumer.
  • Any structured result obeys the tool's advertised output contract.

Avoid assertions against an entire serialized message when only a few fields are stable. Full snapshots make harmless wording changes noisy and can normalize unknown fields without review. A targeted assertion also produces a clearer failure when the server accidentally swaps result and error.

The repository's JSON-RPC basics and error challenges emphasize request and response shape. Extend that evidence into trace correlation. Agent tool-call trace grading is useful after protocol correctness is established, but a trajectory grader should not be responsible for discovering an invalid envelope.

When should an MCP tool return isError true?

Use isError: true when a valid tool invocation reaches tool execution and the operation cannot produce its successful result. Examples include a provider outage encountered by the tool, a business rule that rejects the operation, a domain resource that no longer exists, or an authorization decision made by the invoked service.

The current MCP tools specification distinguishes protocol errors from tool execution errors and gives unknown tools as a protocol-error example. A request is malformed at this layer when it does not satisfy the MCP CallToolRequest shape, for example because params is not an object or the required tool name is absent. Check the selected SDK and protocol version for exact error codes and result types. Do not invent a project-specific numeric code that collides with standard meanings.

Once the server has a well-formed tools/call request, validation against the selected tool's advertised inputSchema is part of tool execution. A missing required tool argument, a date in the wrong format, an out-of-range value, or a domain rule rejection therefore belongs in a CallToolResult with isError: true. This gives a model actionable feedback while preserving the JSON-RPC distinction between an invalid protocol request and an unsuccessful tool operation.

Authorization also has two possible locations. A transport or session that cannot authenticate may fail before the method is processed. A valid session may invoke a tool whose domain policy denies a specific record. Tests should name which component made the decision. The broader control design belongs in secure agent tool execution architecture; this article verifies its wire result.

How do you build an error matrix for tools/call?

Build the matrix from failure ownership and client action. Avoid a single test named "handles errors," because it cannot show whether retries, user messages, or side-effect protection differ across cases.

FailureExpected envelopeKey assertionTypical client decision to test
Unknown toolJSON-RPC errorCode, ID, and no resultRefresh catalog or stop
Malformed requestJSON-RPC error when response is possibleParser or request classificationFix client, do not model-retry
Tool input-schema failureresult with isError: trueActionable validation detail and no JSON-RPC errorRevise arguments or stop
Provider outage during executionresult with isError: trueSanitized transient classificationBounded retry if safe
Business-rule rejectionresult with isError: trueDomain reason without sensitive dataShow or request user action
Timeout during executionOwned tool or transport policyTerminal state and side-effect uncertaintyReconcile before retry
Authorization failureDepends on rejecting layerNo protected data in contentReauthorize or deny

"Typical" is not an implementation command. Each product must freeze its own retry and user-interaction policy. A payment timeout is different from a read-only search timeout because the first may have completed remotely. A business rejection should not be retried with identical arguments merely because it arrived in a tool result.

Add dimensions only when they affect behavior: idempotent versus side-effecting tools, authenticated versus expired sessions, text versus structured content, and first attempt versus exhausted retry budget. Evaluate AI agent tool use can then judge selection and recovery without hiding the underlying contract matrix.

How do you test unknown tools and invalid arguments?

Use table-driven fixtures so each case states its boundary, response class, and client action. The following example deliberately avoids pinning a numeric code for every MCP implementation. The fixture supplies the expected code selected by the project's protocol and SDK contract.

TypeScript
import { describe, expect, it } from "vitest";

type ErrorCase = {
  name: string;
  response: JsonRpcFailure | CallToolFailure;
  requestId: string;
  kind: "protocol" | "tool";
  code?: number;
};

const cases: ErrorCase[] = [
  {
    name: "unknown tool",
    requestId: "req-41",
    kind: "protocol",
    code: -32602,
    response: {
      jsonrpc: "2.0",
      id: "req-41",
      error: { code: -32602, message: "Unknown tool" },
    },
  },
  {
    name: "domain validation rejection",
    requestId: "req-42",
    kind: "tool",
    response: {
      jsonrpc: "2.0",
      id: "req-42",
      result: {
        content: [{ type: "text", text: "Account is not eligible" }],
        isError: true,
      },
    },
  },
];

describe.each(cases)("$name", ({ response, requestId, kind, code }) => {
  it("preserves the correct failure contract", () => {
    expect(response.jsonrpc).toBe("2.0");
    expect(response.id).toBe(requestId);

    if (kind === "protocol") {
      expect("error" in response).toBe(true);
      expect("result" in response).toBe(false);
      expect((response as JsonRpcFailure).error.code).toBe(code);
    } else {
      expect("result" in response).toBe(true);
      expect("error" in response).toBe(false);
      expect((response as CallToolFailure).result.isError).toBe(true);
    }
  });
});

Replace the illustrative code with the exact value required by the project's unknown-tool contract. The important test design is not the number alone. It is the evidence that the method was rejected before tool execution and that the client did not treat the failure as an ordinary model observation.

For invalid arguments, test both sides of the boundary. First send a malformed CallToolRequest, such as one whose params value cannot satisfy the MCP request shape, and assert a JSON-RPC error. Then send a well-formed request whose arguments violate the selected tool's advertised schema, followed by a schema-valid value that violates a domain rule. Both tool-level failures should return result.isError: true, with safe content that distinguishes structural tool input validation from domain rejection. Contract testing tool schema evolution covers versioned input changes; runtime error classification remains a separate assertion.

How should clients retry, stop, or show each failure?

Make retry classification an explicit function whose inputs include operation semantics, not just an error string. The client should know whether the request was protocol-invalid, whether execution may have caused a side effect, whether the failure is transient, and whether user action can change the result.

TypeScript
type ClientAction =
  | "stop-protocol"
  | "retry-bounded"
  | "ask-user"
  | "reconcile-side-effect"
  | "show-tool-error";

type FailureContext = {
  kind: "protocol" | "tool";
  transient: boolean;
  sideEffectMayHaveOccurred: boolean;
  userActionRequired: boolean;
};

export function classifyFailure(failure: FailureContext): ClientAction {
  if (failure.kind === "protocol") return "stop-protocol";
  if (failure.sideEffectMayHaveOccurred) return "reconcile-side-effect";
  if (failure.userActionRequired) return "ask-user";
  if (failure.transient) return "retry-bounded";
  return "show-tool-error";
}

This is an application policy example, not an MCP requirement. Test it with cases that make the order matter. A transient timeout after a side-effecting call should reconcile before retry. A user-correctable business rejection should not consume the transient retry budget. A malformed request should go to client diagnostics rather than the model.

The workflow for every new failure is:

  1. Classify which layer rejected the operation.
  2. Build the correct JSON-RPC envelope and inner payload.
  3. Preserve the original request ID when a response can be correlated.
  4. Sanitize model-facing and user-facing content before serialization.
  5. Assert client stop, retry, reconciliation, or user-action behavior.
  6. Record diagnostic evidence with access appropriate to its sensitivity.
  7. Add the observed failure as a named regression case.

Bound retries and record attempt identity. An agent that repeatedly submits the same denied request is not recovering. MCP sampling and elicitation testing can cover human input flows when the correct action is to ask, while the error contract still proves why the ask occurred.

How do you test error content without leaking secrets?

Create fixtures containing canary secrets, internal paths, tokens, provider request bodies, and personal fields. Make the server produce each failure, then assert that none of those values appears in JSON-RPC message, error.data, tool result content, model context, user messages, ordinary logs, or CI artifacts.

Preserve enough diagnostic structure to act. A safe tool error can include a stable category, correlation identifier, retryability classification, and public remediation. Restricted evidence can store a provider code or sanitized stack under tighter access. Do not force one payload to serve the model, end user, developer, and auditor.

Also test content injection. A downstream error string may contain instructions that should not become trusted model policy. Treat tool error text as untrusted observation. MCP server evaluation for safety and compliance provides the broader abuse and policy context, while these tests assert the exact redaction boundary.

Which contract checks belong in CI?

Run deterministic shape tests on every change to protocol adapters, tool registration, validation, result serialization, or client retry code. Include one valid success, one unknown tool, one schema rejection, one domain rejection, one sanitized provider failure, and one side-effect uncertainty case. Add regressions from incidents rather than expanding only with invented variants.

CI should fail when a response contains both result and error, loses its request ID, emits an undocumented code, omits isError for a designated tool failure, leaks a canary secret, or selects a forbidden retry action. Preserve the sanitized request and response plus the client decision. Do not publish raw credentials or entire provider bodies as proof.

Keep notification tests separate because notifications do not have response IDs. Testing MCP list-changed resource subscriptions exercises that lifecycle. Use MCP testing interview questions to practice explaining why a green schema check does not prove safe client behavior.

Test the adapter at both directions

Most clients have two error adapters. The inbound adapter converts a wire response into an application failure. The outbound adapter converts a server or tool failure into a JSON-RPC response. Test both independently, then include one integration case across the pair. Otherwise two wrong mappings can cancel each other in an in-process test while an external peer receives the wrong envelope.

For the server direction, throw or return one controlled failure at each boundary: request decoding, method dispatch, input-schema validation, tool invocation, domain validation, provider call, and result serialization. Capture whether the tool body was entered and whether a side effect began. The expected response class follows the owned boundary, not the exception class name. A generic ValueError could represent parser input, tool input, or a programming defect depending on where it occurred.

For the client direction, provide raw fixtures through the actual decoder. Test a correct protocol error, a correct tool error, a response containing both members, a response containing neither, a mismatched ID, invalid result content, and an unknown error code. The client should not crash its entire session because one response is invalid, but it must not manufacture a tool success either. Record the affected request and leave unrelated in-flight requests intact.

Preserve side-effect uncertainty

Timeouts and connection loss deserve a state beyond success and ordinary failure when the tool can change external state. The caller may not know whether the operation completed after the last observable event. Mapping that condition to a generic retryable tool error can duplicate a charge, booking, message, or deletion.

Model the outcome as unknown until an idempotency key, status endpoint, or reconciliation tool establishes what happened. Test that the client preserves the original operation identifier and does not issue a fresh side effect while status is unresolved. If the tool has no reconciliation mechanism, the policy may require user or operator review rather than automatic retry.

This uncertainty can be carried in sanitized structured content or in local client state, depending on the advertised schema. Do not claim MCP defines the business representation. The test should assert the product's chosen representation and prove it is not flattened into "try again."

Make observability agree with the wire

Logs, traces, and user messages should derive from the classified failure, not reclassify it through string matching. A trace span for a JSON-RPC error should record protocol failure and its numeric code. A span for isError: true should record that method processing returned a tool failure. Both can have an error status operationally, but the original envelope class must remain queryable.

Test trace production with canary values. Provide a secret in a nested provider error, a public remediation string, a correlation ID, and a tool name. The model-facing observation should keep only the public remediation and safe identifier. The restricted diagnostic record can retain approved fields. The trace exporter must not serialize the whole exception object.

Also verify attempt relationships. A retry should create a new request ID and link to the prior failed attempt through local metadata. It should not reuse a completed JSON-RPC request ID as if the exchange never happened. When a user changes arguments, record that as a revised attempt so reviewers can tell recovery from repeated looping.

Use mutation tests for the taxonomy

Small deliberate mutations make the contract test credible. Swap error for result, remove isError, change the response ID, insert both top-level members, leak a canary token into message, and mark a side-effect timeout as retryable. Each mutation should fail at a named assertion with evidence about the violated layer.

These negative controls are especially useful after SDK upgrades because generated types can keep code compiling while serializer defaults change. Keep the mutation fixtures local and deterministic. Their purpose is to prove the test can see the defect, not to imitate every possible peer.

Review public error wording independently from classification. Clients may localize or revise a user message while preserving the same code and action. Keep machine decisions tied to stable fields, not exact prose. Tests can still assert that a message is nonempty, sanitized, and appropriate for its audience without turning punctuation into a protocol requirement.

When a new SDK maps errors automatically, run the raw-wire fixtures through that adapter before deleting local tests. Generated classes prove type compatibility only for data that reaches them. The suite must still show what happens when a peer sends a valid but unfamiliar code, a malformed envelope, or a tool error with unexpected content.

FAQ

Is isError a JSON-RPC error?

No. An MCP tool result with isError: true is still the result of a successfully processed JSON-RPC tools/call request. The envelope uses result, not error. The flag tells the client and model that tool execution failed while preserving failure content inside the tool result.

Should invalid arguments be protocol errors?

Only when the CallToolRequest itself is malformed at the MCP protocol layer. Once tools/call is well formed, an argument that fails the selected tool's input schema or domain validation is a tool execution error. Return a CallToolResult inside JSON-RPC result with isError: true, then test the validation message and client correction behavior.

Can clients retry a tool error?

Only when policy classifies the failure as transient and the operation is safe to repeat. The envelope alone does not grant retry permission. Consider side effects, idempotency, authorization, provider status, retry limits, and whether changed arguments or user action are required before another call.

What error code should an unknown tool use?

MCP tool documentation identifies unknown tools as protocol errors, but tests should use the code required by the implemented specification and SDK rather than inventing a private meaning. Assert the error object, request ID, sanitized message, and client behavior beside tool-catalog tests.

Should error content be passed to the model?

Only sanitized, decision-relevant content should enter model context. Remove credentials, tokens, stack traces, internal paths, personal data, and provider payloads. Preserve sensitive diagnostics in controlled engineering evidence when needed. Test both the model-facing message and the separately retained operational record.

How do MCP errors appear in traces?

A useful trace records request ID, method, tool name, sanitized arguments, envelope class, JSON-RPC code when present, tool isError state, client decision, and attempt metadata. It should distinguish protocol rejection from executed-tool failure without copying secrets or treating every failure as retryable.

Conclusion

The reliable mental model has two layers. JSON-RPC reports whether the method request was processed as a protocol operation. The MCP tool result reports whether the invoked tool achieved its domain operation. Tests must preserve that distinction from server serialization through client action and trace evidence.

Once the envelope, inner fields, sanitization, and retry policy are asserted independently, failures become diagnosable. The client can repair what is repairable, stop what is invalid, and avoid turning every error into another unsafe tool call.

// LIVE COURSE / THE TESTING ACADEMY

AI Tester Blueprint

Master GenAI, AI Agents, MCP, RAG, CrewAI. Build 23+ real AI projects.

From the instructor behind this guide.

AI testing roles are up 180% and pay 12-22 LPA. 12+ weeks / 65+ live hrs / Sat-Sun 8:30 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed July 25, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official modelcontextprotocol.io reference

    modelcontextprotocol.io

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official modelcontextprotocol.io reference

    modelcontextprotocol.io

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official jsonrpc.org reference

    jsonrpc.org

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Model Context Protocol documentation

    Model Context Protocol

    Canonical architecture, transport, server, client, and security concepts.

FAQ / QUICK ANSWERS

Questions testers ask

Is isError a JSON-RPC error?

No. An MCP tool result with isError true is still the result of a successfully processed JSON-RPC tools/call request. The JSON-RPC envelope uses result, not error. The flag tells the client and model that tool execution failed, while preserving structured or textual failure content inside the tool result.

Should invalid tool arguments be protocol errors?

Only a malformed CallToolRequest belongs at the protocol layer. Once tools/call is a well-formed MCP request, failures against the selected tool's input schema or domain validation are tool execution errors returned inside result with isError true. Test malformed request shape separately from wrong formats, missing tool fields, and out-of-range values.

Can clients retry a tool error?

Only when product policy classifies the failure as transient and the operation is safe to repeat. The envelope alone does not grant retry permission. Consider side effects, idempotency, authorization, provider status, retry limits, and whether changed arguments or user action are required before another tools/call request.

What error code should an unknown tool use?

MCP tool documentation identifies unknown tools as protocol errors, but tests should use the code required by the implemented specification and SDK rather than inventing a private meaning. Assert the error object, request ID, sanitized message, and client behavior. Keep the tool catalog and capability tests beside this case.

Should error content be passed to the model?

Only sanitized, decision-relevant content should enter model context. Remove credentials, tokens, stack traces, internal paths, personal data, and provider payloads. Preserve sensitive diagnostics in access-controlled engineering evidence when needed. Tests should verify both the model-facing message and the separately retained operational record.

How do MCP errors appear in traces?

A useful trace records the request ID, method, tool name, sanitized arguments, envelope class, JSON-RPC code when present, tool isError state, client decision, and timing or attempt metadata. It should make protocol rejection distinguishable from executed-tool failure without copying secrets or treating every failure as retryable.