Skip to content

fix(mcp): rewrite draft-only const keywords in published tool schemas - #265

Open
Sarfaraz85 wants to merge 2 commits into
psi-oss:mainfrom
Sarfaraz85:fix/portable-published-tool-schemas
Open

fix(mcp): rewrite draft-only const keywords in published tool schemas#265
Sarfaraz85 wants to merge 2 commits into
psi-oss:mainfrom
Sarfaraz85:fix/portable-published-tool-schemas

Conversation

@Sarfaraz85

@Sarfaraz85 Sarfaraz85 commented Jul 16, 2026

Copy link
Copy Markdown

Fixes #239.

Root cause

Published tool input schemas cross MCP clients that re-encode them for model providers with restricted schema dialects. The failing provider's schema proto types enum as a repeated string field and rejects the JSON Schema const keyword outright — so schema_version: {"type": "integer", "const": 1} fails registration with Invalid value ... (TYPE_STRING), 1, and the commonly suggested {"type": "integer", "enum": [1]} fails identically (non-string enum member). Details in this comment on #239. Ten const fragments ship in published schemas today, all via the verification server's contract schemas.

Fix

Sanitize once at the publish chokepoint — set_published_tool_input_schema — instead of spot-fixing each literal. New portable_published_schema() deep-copies the schema and rewrites draft-only constructs into forms valid in both dialects while keeping exactness machine-checkable:

Fragment Rewritten to Why
string const single-member enum string enum members are valid in the restricted dialect
numeric const equal minimum/maximum bounds avoids the string-typed enum proto entirely; exactness stays machine-checkable
boolean const (occurs only inside if/then conditionals) single-member enum dialect-restricted clients strip conditional subtrees wholesale, so these never reach a provider proto
unrepresentable (e.g. null) "Must be exactly …" description note last resort, still self-documenting

Internal schemas stay draft-expressive; server-side argument validation remains the enforcement layer, unchanged.

Regression guard

tests/mcp/test_portable_published_schemas.py walks every FastMCP server's actually-published tool schemas (mcp.list_tools()) and fails on any const or non-string enum member outside conditional subtrees — keeping the whole class fixed, not just these instances. At current main it red-fails with 10 violations, including the exact properties.contract.properties.schema_version path from the issue's error trace. (arxiv_bridge builds a lowlevel mcp.Server in its entry point and publishes no draft-only keywords; it's exercised by its own suite.)

Plus unit tests for each rewrite rule, and test_tool_contract_visibility.py updated for the must_surface fragment's published form.

Validation

  • Full suite: 12,953 passed
  • ruff format / repo meta-tests (test inventory, runtime-name policy) green

Summary by CodeRabbit

  • Bug Fixes
    • Improved published tool input schemas to be runtime-portable across MCP clients by rewriting strict value constraints into broadly supported JSON Schema constructs.
    • Added clearer schema descriptions for exact values that can’t be represented directly in portable form.
  • Tests
    • Added validation to ensure all published tool schemas avoid restricted-dialect constructs.
    • Expanded unit coverage for portable schema rewriting, including nested and conditional structures, plus adjusted contract expectations.

Published tool input schemas cross MCP clients that re-encode them for
model providers with restricted schema dialects. Providers that reject
the JSON Schema const keyword and type enum members as strings fail
tool registration with 'Invalid value ... (TYPE_STRING), 1' whenever a
published schema carries {"const": 1} — ten such fragments ship today
via the verification server's contract schemas (schema_version and the
must_surface surface rule). Fixes psi-oss#239.

Rather than spot-fixing each literal, sanitize once at the publish
chokepoint (set_published_tool_input_schema): exact string values
become single-member enums, exact numeric values become equal
minimum/maximum bounds, and boolean consts inside conditional subtrees
become single-member enums that dialect-restricted clients strip along
with the enclosing conditional. Internal schemas stay draft-expressive;
server-side argument validation remains the enforcement layer.

A repo-wide regression test walks every FastMCP server's published
tool schemas and fails on const or non-string enum members outside
conditional subtrees, so the whole class stays fixed.
@CLAassistant

CLAassistant commented Jul 16, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a6aa3e79-0f96-4c14-b561-f826eabe4f8e

📥 Commits

Reviewing files that changed from the base of the PR and between c51c556 and 8103602.

📒 Files selected for processing (2)
  • src/gpd/mcp/servers/__init__.py
  • tests/mcp/test_portable_published_schemas.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/gpd/mcp/servers/init.py
  • tests/mcp/test_portable_published_schemas.py

📝 Walkthrough

Walkthrough

Published MCP tool schemas now recursively convert unsupported const constructs into portable JSON Schema forms before exposure through FastMCP. New tests validate published schemas across servers and cover scalar, conditional, nested, and null-value rewrites.

Changes

Schema portability

Layer / File(s) Summary
Portable schema publication
src/gpd/mcp/servers/__init__.py
Adds recursive const rewriting, publishes deep-copied portable schemas through both FastMCP schema surfaces, and exports the new helper.
Portability validation
tests/mcp/test_portable_published_schemas.py, tests/mcp/test_tool_contract_visibility.py
Scans published schemas across server modules and tests scalar, conditional, nested, null, and boolean schema representations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: making published MCP tool schemas portable by rewriting draft-only const keywords.
Description check ✅ Passed The description covers the root cause, fix, regression guard, and validation, though it doesn't follow the template headings exactly.
Linked Issues check ✅ Passed The PR addresses #239 by sanitizing published schemas and removing draft-only const and non-string enum issues in the verification server.
Out of Scope Changes check ✅ Passed The helper, regression tests, and contract visibility update all support the schema-portability fix and appear in scope.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Sarfaraz85

Copy link
Copy Markdown
Author

Gentle bump on this one — CLA is signed and CodeRabbit's review came back clean. I think it's just waiting on a maintainer to approve the workflow run.

Happy to rebase or adjust scope if that makes review easier — the guard test can also be split out into a separate PR if a smaller diff is preferred.

Brings docstring coverage on this PR's new functions to 100%, addressing
the CodeRabbit pre-merge warning (25% against an 80% threshold).

Documents the recursive rewriter in servers/__init__.py and the helpers
and regression guards in the new portable-schema suite.
@Sarfaraz85

Copy link
Copy Markdown
Author

Correction to my note above — I said the review came back clean, but I'd only looked at the check summary and missed the docstring-coverage warning inside it. Sorry about that.

Fixed in 8103602: docstrings on the recursive rewriter in servers/__init__.py and on the helpers and guards in the new suite, so every function this PR adds is documented. ruff check and ruff format --check pass; test_portable_published_schemas.py (6) and test_tool_contract_visibility.py (37) pass locally.

One judgment call worth flagging: the existing test suite doesn't use function docstrings (test_tool_contract_visibility.py is 0/57), so I've kept them to the new file rather than reformatting anything of yours. Happy to drop them if you'd rather the new suite match the surrounding convention than the coverage check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: GPD Verification Fails with "Invalid value at 'tools0.function_declarations40...enum0' (TYPE_STRING), 1" Error

2 participants