Skip to content

Testing

Helpers for a consumer's test suite, shipped in the wheel and importable from rest_framework_mcp.testing. Nothing here is imported by the package at runtime, and the dependency they need is an extra:

pip install "djangorestframework-mcp-server[test]"

Does a result match the schema the server advertised?

The package already refuses a binding whose settings disagree — include_output_schema=True with include_structured_content=False raises at registration, because the MCP spec requires conforming structuredContent whenever outputSchema is declared (see Omitting structuredContent and outputSchema).

That is coherence, decided from the configuration alone. Whether a real response conforms is a different question, and the check a suite reaches for on its own answers a third one:

advertised = tool["outputSchema"]["items"]["properties"]
assert set(advertised) == set(result["structuredContent"][0])

Comparing key sets catches a field that vanished. It passes unchanged when a property advertised as integer arrives as a string, or a date-time arrives as "soon" — which is what a typed client actually breaks on, and what an output serializer's to_representation override introduces without touching a single key.

assert_tool_result_conforms validates the payload against the schema, types and formats included, and names each disagreement:

from rest_framework_mcp.testing import assert_tool_result_conforms

page = server.list_tools(user=user)
tool = next(entry for entry in page["tools"] if entry["name"] == "invoices.list")
result = await server.acall_tool("invoices.list", {}, user=user)

assert_tool_result_conforms(tool, result)
AssertionError: Tool 'invoices.list' returned 'structuredContent' that does not
conform to the 'outputSchema' it advertises (2 problems):
  - $[0].amount_cents: advertised type 'integer', got string '1240.00'
  - $[1]: 'number' is a required property

Both arguments are plain mappings off the wire — one tools/list entry and one tools/call result — so the same call works against list_tools / acall_tool, an HTTP round trip, or a client library.

A tool that advertises no schema is a failure, not a pass: there would be nothing to conform to, and an assertion that holds for every possible result is the thing this replaces.

How much of a format is checked depends on the install

jsonschema registers a format checker only when the library that performs it is importable, so the test extra pulls those in. Install it as jsonschema alone and "format": "date-time" is not checked — and this still passes.

There is deliberately no setting that forces every tool to advertise a schema. For a service whose response shape is context-dependent, compelling advertisement is the wrong default; the existing on/off knob plus per-binding overrides is the right shape. Verification was the missing piece, not compulsion.

assert_tool_result_conforms

assert_tool_result_conforms(tool: Mapping[str, Any], result: Mapping[str, Any]) -> None

Assert result's structuredContent satisfies tool's outputSchema.

Parameters:

Name Type Description Default
tool Mapping[str, Any]

One entry from a tools/list response -- the dict carrying name and outputSchema, not the response that wraps them.

required
result Mapping[str, Any]

The tools/call result for that tool: the result member of the JSON-RPC envelope, carrying structuredContent.

required

Raises:

Type Description
AssertionError

If the payload does not conform, naming every property that disagrees, what the schema advertised, and what arrived. Also if the tool advertises no schema, or advertises one and returns no structuredContent -- both would otherwise make this assertion pass for any result at all, which is the failure mode it exists to remove.

ImportError

If jsonschema is not installed.

from rest_framework_mcp.testing import assert_tool_result_conforms

page = server.list_tools(user=user)
tool = next(entry for entry in page["tools"] if entry["name"] == "invoices.list")
result = await server.acall_tool("invoices.list", {}, user=user)

assert_tool_result_conforms(tool, result)

Both arguments are plain mappings straight off the wire, so this works against any way of reaching a tool -- the in-process transport above, an HTTP round trip, or a client library -- with no fixture of its own.

Formats are checked, not only types, and how thoroughly depends on the install: jsonschema registers a format checker only when the library that performs it is importable, so the test extra pulls those in. Without them "format": "date-time" is not checked and this still passes.