feat(mcp): GeneFoundry contract-hardening sweep (behaviour gate CONFORMANT)#34
Merged
Conversation
…enum The MCP error carrier returned ToolResult(structured_content=...) without is_error=True, so a client branching on the protocol isError bit saw every failed call as a success. Verified against fastmcp 3.4.4 that the RETURN path keeps structuredContent while carrying is_error=True (only `raise` drops it); route every error through ToolResult(..., is_error=True). Also close error_code to the six-value canonical enum: internal_error -> internal (the behaviour gate rejects anything outside the closed set). And make validation errors actionable — build_error_envelope now emits field/field_errors derived ONLY from the request-validation `loc` path (this server's own schema parameter names, never caller/upstream prose), so an LLM can self-correct. Behaviour gate: 20 fail -> 0 fail (isError + actionable-error classes cleared). Vendor the byte-identical behaviour gate from genefoundry-router feat/mcp-contract-hardening-v1 (docs/conformance/behaviour.py @ 791363c). Refs genefoundry-router#73 #75 #76; refs #33 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BYK7j7jsPXmBspMsDtfhH
…ct defects
Complete the GeneFoundry contract-hardening sweep on top of the isError/error_code
commit. The vendored behaviour gate goes from 20 fail / 10 UNGATED to CONFORMANT
(0 fail / 0 UNGATED); surface 4,625t -> 4,083t (outputSchema 24% -> 0%).
Schema documentation (Tool-Schema Documentation Standard v1):
- Every required and array parameter carries `examples` (json_schema_extra{"example"}
-> `examples=[...]`, array form for arrays) — this is what unGATES every tool.
- Closed vocabularies are declared as enums matching the runtime.
Tool-Surface Budget Standard v1:
- Suppress `outputSchema` on every tool via the component wrapper (optional in MCP,
unread by models, 24% of the surface). structuredContent is unaffected (dict
envelopes) and untrusted-text fencing still happens on the wire (v1.1a).
- `FastMCP(dereference_schemas=False)`.
Audited contract defects (issue #33, each reproduced live):
- get_network_link: `output_format` narrowed to the one value the runtime serves
(`json`); the other 8 enum values returned success+empty (silent-empty filter).
- compute_functional_enrichment/compute_ppi_enrichment: a bad custom background is
now invalid_input naming `background_string_identifiers`, not a false retryable
upstream_unavailable (STRING returns 200 + [{"error":"background_error"}]).
- get_functional_annotations: stop sending allow_pubmed/only_pubmed unless opted in
(their presence ballooned the response past the byte cap; the tool always failed).
- get_network_image: return a base64 NetworkImageResult instead of a binary body
that defeated the JSON envelope (empty result / internal error).
- get_interaction_partners / compute_functional_enrichment: honest, limit-invariant
total_count + truncated; enrichment gains `limit` and `category` (a 30-gene panel
returned ~992 terms / ~421k tokens with no knob to shrink it).
Wire the behaviour probe into the mcp-conformance workflow. Version 4.0.6 -> 4.1.0
(schemas gain fields; outputSchema drops an optional field; no wire break beyond the
error_code rename). make ci-local green (452 passed).
Refs genefoundry-router#73 #75 #76; refs #33
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019BYK7j7jsPXmBspMsDtfhH
…t isError, keep link formats, MCP-only image base64, general in-band error Six fixes from the PR #34 review (Codex gpt-5.6-sol): 1. get_interaction_partners: paginate PER PROTEIN. The prior global head-slice (all_partners[:limit]) returned only the first query protein's partners and omitted every later protein; the hardcoded 500 fetch could not prove a true total. Now fetch the full partner set (limit omitted upstream), apply `limit` per query protein (grouped by string_id_a), report the true limit-invariant total, and set `truncated`. 2. Returned-dict isError: a tool that RETURNS {"success": false} was wrapped as a success with isError:false. The chokepoint (finalize_tool_result) now normalizes a returned error frame to isError:true, closing error_code to the enum. 3. get_network_link output_format: restore the four formats STRING actually serves (json/tsv/tsv-no-header/xml) instead of narrowing to json (capability loss → 422). tsv/xml are shaped into a structured result: `url` extracted + STRING's raw text in `formatted`. The empty result:{} was a shaping bug, not proof of non-support. 4. Image REST contract: POST /api/images/network keeps its raw-binary response (operation download_network_image, excluded from MCP). The base64 shaping moved to a dedicated MCP-only route POST /api/images/network/json (operation get_network_image). 5. Empty image body: an empty upstream body is now upstream_unavailable, never a success with 0 bytes / empty base64 (a new silent-empty). 6. STRING in-band errors: handle the GENERAL class — ANY {"error": X} at HTTP 200 → invalid_input naming a field (background_error → background_string_identifiers, otherwise identifiers), never a field-less error or false retryable upstream outage. Behaviour gate remains CONFORMANT (51 pass, 0 fail, 0 UNGATED). make ci-local green (463 passed). New regression tests in tests/unit/test_contract_rework_v1.py and the empty-image facade test. Refs genefoundry-router#73 #75 #76; refs #33 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BYK7j7jsPXmBspMsDtfhH
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.
GeneFoundry fleet MCP contract-hardening sweep
Brings
stringdb-linkinto compliance with the GeneFoundry MCP contract standards and closes every defect the vendored behaviour gate — and the live MCP audit (#33) — surfaced.Standards adopted:
isErroron every error frame, the closederror_codeenum, actionable errors.genefoundry-router#75) —exampleson every required + array param; closed vocabularies declared as enums matching the runtime.genefoundry-router#73) —outputSchemasuppressed,dereference_schemas=False.genefoundry-router#76) vendored byte-identical fromgenefoundry-router@791363cand wired intomcp-conformanceCI.The enum rule followed here is schema ⊆ runtime: never advertise a value the runtime rejects (verified live — e.g.
output_formatnarrowed to the single value STRING actually serves).Closes the contract half of #33.
Behaviour gate — before / after
Both run against a locally-running server (
docs/conformance/behaviour.py):The 20 failures were
isError:falseon every error envelope + a non-actionable "The request was rejected as invalid." on every tool. The 10 UNGATED were every tool's required params lackingexamples(which also blocked the dynamic filter probes). The 3 remaining "inconclusive" are single-item tools (get_network_link,compute_ppi_enrichment,get_network_image) that return no collection to filter-probe — honest, not a defect.Surface survey — before / after
Live repros (local server,
tools/call)isError + canonical
error_code+ actionablefield(all error frames):Defect 1 — get_network_image (was internal_error / empty in every format):
Defect 2 — get_functional_annotations (was
not_found"tool is not available"):Defect 3 — compute_functional_enrichment pagination (was ~992 terms / ~421k tokens, no knob):
Defect 4 — get_network_link output_format (was 8/9 enum values →
success:true, result:{}):Defect 5 — enrichment background_error (was false
upstream_unavailable, retryable:true):Homology camelCase lead — verified NOT a live defect:
get_protein_homology_scores {CDK1,CDK2}returns populatedstring_id_a/bitscorerows.required_scorescaling lead — not a silent-empty: the schema declares a0.0–1.0confidence (boundedge/le, description "Minimum confidence score (0.0-1.0)"), and the service scales it to STRING's0–1000internally (round(required_score * 1000)). A caller passing1000is rejected by thele=1.0bound rather than silently matching nothing.Notes
4.0.6 → 4.1.0(MINOR: schemas gain fields;outputSchemadrops an optional field).CHANGELOGupdated under## [4.1.0].make ci-localgreen (452 passed, 2 skipped).outputSchemadoes not weaken it.🤖 Generated with Claude Code
https://claude.ai/code/session_019BYK7j7jsPXmBspMsDtfhH