feat(server): advertise outputSchema on tools/list (JS parity)#396
Draft
feat(server): advertise outputSchema on tools/list (JS parity)#396
Conversation
`tools/list` now carries `outputSchema` per ADCP tool, matching the TypeScript SDK's behaviour. Pydantic response types from `adcp.types` drive the schemas via `_generate_pydantic_output_schemas()`, parallel to the existing `_generate_pydantic_schemas()` for `inputSchema`. Key design decisions: - anyOf union response types (e.g. CreateMediaBuyResponse) are included — outputSchema is informational and anyOf is valid MCP contract here, unlike the inputSchema generator which skips them. - `_inline_refs` flattens all $ref nodes for client compatibility. - `_register_tool` accepts a new `output_schema` kwarg; the per-tool schema replaces FastMCP's generic dict inference in fn_metadata while the runtime `output_model` (permissive dict acceptor) is preserved so structuredContent keeps working. - `register_test_controller` gains `structured_output=True` so its fn_metadata has a non-None `output_model` required by FastMCP's assertion in `convert_result`. https://claude.ai/code/session_019UpkNNacQ4QS8zcVTRfFQV
…ld names
Mirror the inputSchema spot-check pattern: assert specific known fields
('products' in get_products, 'anyOf' in create_media_buy, 'adcp' +
'supported_protocols' in get_adcp_capabilities) rather than just
isinstance checks. Structural regressions on response models now surface
in the spot-check test, not only in the full drift comparison.
https://claude.ai/code/session_019UpkNNacQ4QS8zcVTRfFQV
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #386
Summary
tools/listnow carriesoutputSchemaper ADCP tool, matching the TypeScript SDK's behaviour. Pydantic response types fromadcp.typesdrive the schemas at import time via_generate_pydantic_output_schemas(), parallel to the existing_generate_pydantic_schemas()forinputSchema.Four files changed:
src/adcp/server/mcp_tools.py— adds_generate_pydantic_output_schemas(),_PYDANTIC_OUTPUT_SCHEMAS, and_apply_pydantic_output_schemas()which writesoutputSchemainto every entry inADCP_TOOL_DEFINITIONSat import time.src/adcp/server/serve.py—_register_toolgains anoutput_schemakwarg;_register_handler_toolsextractstool_def.get("outputSchema")and passes it through. The per-tool schema replaces FastMCP's generic dict inference infn_metadata.output_schema(thetools/listadvertisement source), whileoutput_model(the runtimestructuredContentpath) is left unchanged.src/adcp/server/test_controller.py— addsstructured_output=Trueto theTool.from_functioncall so FastMCP produces a non-Noneoutput_model(required by theassertinFuncMetadata.convert_result), then overridesoutput_schemawith the spec-accurate schema fromADCP_TOOL_DEFINITIONS.tests/test_mcp_schema_drift.py— 5 new drift tests mirror the inputSchema coverage: every-tool mapping, byte-level drift,$ref-free guarantee, and spot-checks of actual known fields.Design decisions:
anyOfunion response types (e.g.CreateMediaBuyResponse = Success | Error) are included —outputSchemais informational andanyOfis valid MCP contract for what a tool may return, unlike theinputSchemagenerator which skips them becauseanyOfconfuses some MCP input-parsing clients.output_modelis intentionally kept as FastMCP's generic dict-accepting model for runtimestructuredContentpopulation. Per MCP §5.4.3, server-side enforcement thatstructuredContentconforms tooutputSchemais a SHOULD; the existing opt-invalidationconfig increate_tool_calleralready handles that for callers who want it.Known pre-existing issue (not introduced by this PR)
Several tools with discriminated-union schemas have
discriminator.mappingstring values like"all": "#/$defs/PublisherPropertySelector1"that reference$defsentries. After_inline_refsflattens those entries, the mapping strings become dangling pointers. This is identical to the pre-existing behaviour ininputSchemafor ~16 tools. Tracked separately; this PR does not regress or worsen the issue.What was tested
pytest tests/test_mcp_schema_drift.py— all 24 tests pass (5 new output schema tests included)pytest tests/(non-integration) — 2711 passed, 17 skipped, 0 failuresruff check src/adcp/server/{mcp_tools,serve,test_controller}.py tests/test_mcp_schema_drift.py— cleanmypy src/adcp/server/{mcp_tools,serve,test_controller}.py— cleanPre-PR review:
discriminator.mappingissue noted aboveanyOffor union responses is canonical; runtime vs. advertised schema separation acceptable per §5.4.3Nits (not fixed):
anyOfoutput schemas toany— client quality gap, not a protocol violation._fallbackpath intest_controller.pyis defensive;comply_test_controllerwill always have an output schema in practice.Session: https://claude.ai/code/session_019UpkNNacQ4QS8zcVTRfFQV