Actionable reform-input validation for the MCP/CLI tools#38
Merged
Conversation
Two defects found in live end-to-end testing of the hosted server:
1. Date-range reform keys ("2026-01-01.2029-12-31") surfaced a raw pydantic
dump: "unconverted data remains: .2029-12-31". Root cause is upstream:
policyengine's compile_reform parses each key with
strptime(key, "%Y-%m-%d") and builds ParameterValue(end_date=None), so a
reform value applies from its effective date and never expires. The
reform-dict API has no way to express an end date, and silently dropping
it would score a permanent reform when the caller asked for a temporary
one. Ranges are therefore rejected up front with an explanation, the
correct single-date form, and a per-year workaround -- not faked.
2. calculate_household returned a raw "2 validation errors ... Field
required" dump when country/people were omitted. The MCP arguments are
now optional at the schema level and validated in core, so callers get a
sentence instead of a pydantic trace. country deliberately keeps NO
default: uk and us are different tax-benefit models and guessing would
silently score the wrong country.
Adds core.validate_reform() as the single validator behind every tool that
accepts a reform (household, household impact, population, score_reform, the
OG and OBR bridges). It accepts flat values and single-date keys, returns the
normalised dict, and raises ValueError naming the supported shapes with a
worked example. _og_build_policy now honours dated values too.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The previous commit was made from a stale checkout and unintentionally reverted the workflow changes from #35. This restores all four workflow files to their state on main, leaving this branch's diff scoped to the reform-input validation in integration/. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
vahid-ahmadi
added a commit
that referenced
this pull request
Jul 19, 2026
#42) The hosted tool surface was only ever checked as a SUBSET (`EXPECTED_TOOLS <= names`), and the in-process check lived in a slow-marked test that PR CI skips entirely. Adding, removing or renaming a tool could therefore ship to clients with a fully green pipeline. - tests/tool_surface.py: single golden list of the 10 published tool names, shared by the in-process, stdio and live-deployment tests, with an assert_surface() helper that reports added/removed names. - tests/test_tool_surface.py: fast, PolicyEngine-free contract on exact names, exact count, no duplicates, per-tool description/schema, plus a regression guard that country/people stay schema-optional (#38). - test_remote_mcp.py / test_mcp_server.py: subset checks upgraded to exact-surface equality against the same golden list. - test_remote_mcp.py: reform-input validation (#38) now asserted AS SERVED — date-range rejection, missing country, empty reform, and a valid dated reform that must be accepted and score correctly. - test-integration.yml: the tool-surface and reform-input suites run as named hard gates before the general suite. - validate-deployment.yml: weekly Sunday deep run so the slow investment-closure and OBR bridge legs are exercised on a schedule instead of only on a manual dispatch. Also fixes a genuinely broken job: contract.yml pinned astral-sh/setup-uv@v8, but that action publishes no moving `v8` tag, so the nightly contract run had been failing in ~5s at "Set up job" since the action bump in #36. Pinned to v8.3.2. Verified: fast suite and the full live remote suite (14 passed) run locally against the deployed server. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Fixes two developer-experience defects found in live end-to-end testing of the hosted MCP server.
1. Date-range reform keys returned a raw pydantic dump
{"gov.hmrc.income_tax.rates.uk[0].rate": {"2026-01-01.2029-12-31": 0.21}}returned:Root cause (upstream, confirmed in the installed package).
policyengine/tax_benefit_models/common/reform.pycompiles each reform entry toParameterValue(start_date=datetime.strptime(effective_date, "%Y-%m-%d"), end_date=None, ...). Every reform value is applied from an effective date and stays in force indefinitely —end_dateis hardcoded toNone, and the reform-dict API exposes no field for it.strptime("2026-01-01.2029-12-31", "%Y-%m-%d")reproduces the reported error exactly.Decision: reject explicitly, do not fake. Accepting the range and silently using only the start date would score a permanent reform while the caller asked for a temporary one — a wrong number returned confidently, the worst failure mode for a costing tool. The error now names the end date it cannot express, shows the correct single-date form, and gives a workaround:
If upstream ever exposes
end_datethrough the reform dict,validate_reformis the single place to change.2.
calculate_householdreturned a raw pydantic dump on missing argumentsOmitting
countryproduced2 validation errors for calculate_householdArguments: country Field required... people Field required. The MCP arguments are now optional at the schema level and validated incore, so callers get a sentence:No default for
country.pe_population_impactkeeps its historicalcountry="uk"default for compatibility, but defaulting the household tools would silently run a US household through UK rules. The error message is made good instead of guessing a default.Changes
core.validate_reform()— one validator behind every tool that accepts a reform (pe_household,pe_household_impact,pe_population_impact,score_reform, and the OG/OBR bridges). Accepts flat scalars and single-date keys, returns the normalised dict, and raisesValueErrornaming the supported shapes with a worked example._validate_reformkept as a thin alias._validate_country/_validate_people— actionable messages for the other required arguments. Validation runs before the heavy PolicyEngine import, so bad input fails in milliseconds._og_build_policy— now honours dated values (previously assumed scalars), emitting oneParameterValueper effective date.score_reform— a missing/unknownmodelnames the three valid options and what each is for.--reformhelp document the two supported shapes and state that ranges are unsupported, so agents get it right on the first call.Tests
New
integration/tests/test_reform_input.py(28 tests): flat value, integer/bool values, single-date key, multiple dates on one parameter, mixed flat+dated; date-range rejection (both.and:separators) asserting the message names the end date, shows the correct form, and leaks no upstream text; and each malformed input's message (empty, non-dict, bad date key, non-numeric value, non-string path, missing country/people, unknown model). Also asserts PolicyEngine is never imported when input is invalid.The single failure is
test_core.py::test_summary_parses, pre-existing and environmental —boe_varis not installed locally andMACROMOD_BOE_VAR_REPOis unset. Untouched by this change; passes in CI where the model extras install.ruff check src testsis clean (also fixed one pre-existingF541incli.py).Not deployed to Modal — CI handles that on merge.
🤖 Generated with Claude Code