feat(sep-2549): add absence-assert and explicit-zero wire-distinction checks#310
Conversation
… checks The merged CachingScenario asserts presence of ttlMs and cacheScope on the five cacheable list-response endpoints against a fixture that emits them by default. Two complementary edge cases aren't covered by that single-fixture model: 1. Servers configured without cache hints SHOULD NOT emit ttlMs or cacheScope (both fields are OPTIONAL per the merged spec). A negative test against a fixture configured with no hints catches servers that emit garbage defaults. 2. Servers that intentionally surface "explicit zero" vs "no hint at all" need a wire-shape assertion that ttlMs:0 appears on the wire rather than being omitted. The merged spec treats absent ≡ 0 client-side, so this is stricter than the spec mandates — useful for server implementations that deliberately distinguish the two states. Both checks opt in via secondary fixture URLs (CACHING_NO_HINTS_URL, CACHING_EXPLICIT_ZERO_URL). They emit SKIPPED with a reason when the env var is unset, so the default `npm test` against the everything- server gets the seven existing checks unchanged. The pattern mirrors TASKS_SERVER_URL / MRTR_SERVER_URL / LIST_TTL_*_URL elsewhere in the suite. A matching reference implementation lives in mcpkit's examples/list-ttl (three demo servers — positive / explicit-zero / unset). The three fixtures exercise all nine CachingScenario checks (seven existing + two new) by varying the server's caching-hint configuration.
|
@CaitieM20 - this is a small additive follow-up to your CachingScenario in PR 275. I added two opt-in checks for the wire-shape edge cases that the single-fixture model doesn't reach: |
|
@LucaButBoring - cc'ing you as the explicit-zero check has a flavor similar to the retroactive-negative-test conversation we had on PR 262 (where you correctly steered me to drop the One thing worth flagging upfront is that the TypeScript SDK's current |
The merged CachingScenario asserts presence of ttlMs and cacheScope on the five cacheable list-response endpoints against a fixture that emits them by default. Two complementary edge cases aren't covered by that single-fixture model:
Both checks opt in via secondary fixture URLs (
CACHING_NO_HINTS_URL,CACHING_EXPLICIT_ZERO_URL). They emit SKIPPED with a reason when the env var is unset, so the defaultnpm testagainst the everything-server gets the seven existing checks unchanged. The pattern mirrorsTASKS_SERVER_URL/MRTR_SERVER_URL/LIST_TTL_*_URLelsewhere in the suite.A matching reference implementation lives in mcpkit's
examples/list-ttl(three demo servers - positive / explicit-zero / unset). The three fixtures exercise all nineCachingScenariochecks (seven existing + two new) by varying the server's caching-hint configuration.Motivation and Context
SEP-2549 defines
ttlMsandcacheScopeas OPTIONAL caching hints on cacheable list responses. PR 275 (the mergedCachingScenario) covers the happy path where a fixture emits both fields with non-zero TTLs, but leaves two wire-shape edge cases untested:ttlMs: 300000hardcoded into the response builder regardless of configuration) passes conformance silently.ttlMs: 0, the field MUST appear on the wire rather than being omitted.Both checks have existed downstream in mcpkit's local conformance suite for several weeks; PR 275 making
CachingScenariocanonical is the natural moment to fold them upstream so they're available to every conformant server.The TypeScript SDK's current
ttlMs?: numbertype can't represent the explicit-zero distinction (an explicit0is indistinguishable from absent at the type level), but SDK ergonomics shouldn't gate what the conformance suite asserts - the suite asserts what the spec permits, and the SDK can catch up. Both checks use rawclient.request(..., schema)rather than relying on the SDK's typed accessors, so no SDK-side change is needed.How Has This Been Tested?
Verified locally against two fixture configurations:
Default
npm test(no env vars set, runs againsteverything-server):With both env vars set (
CACHING_NO_HINTS_URLandCACHING_EXPLICIT_ZERO_URLpointing at the reference mcpkit fixtures inexamples/list-ttl):npx tsc --noEmitclean. No new tests were added to the conformance suite's own test harness because the new checks are SCENARIO-level (run by the existing all-scenarios.test.ts dispatcher), not standalone unit tests.Breaking Changes
None. Pure additive coverage. The existing seven
CachingScenariochecks against the everything-server are untouched and continue to pass on the defaultnpm testrun. Both new checks are opt-in via env vars; they SKIPPED-cleanly when the env vars are unset.Types of changes
Checklist
Additional context
Design decisions:
CachingScenario. Splitting into a separatecaching-edge-cases.tswould force a second connection helper and duplicate the import surface. The env-var-driven secondary URL pattern matches howTASKS_SERVER_URL,MRTR_SERVER_URL, and other multi-fixture suites already work in this repo.npm testagainst the everything-server can't exercise either path (the fixture emits hints with non-zero TTLs), so SKIPPED is the truthful status - neither a pass nor a fail when the fixture doesn't exercise the assertion. FAILURE is reserved for "env var set, connection failed" so a misconfigured opt-in surfaces loudly.listCachingFieldsForAllEndpointsre-implements the five endpoint calls rather than refactoring the existing inline ones. The duplication keeps the diff scoped to additive lines; refactoring the mergedrun()to share listing would touch the existing surface more invasively than is warranted for two new checks.client.request(..., schema)against the existing helpers, so they work today without any SDK changes.Risk / blast radius:
hasTtlMs || hasCacheScope) - a server that emits one but not the other still triggers FAILURE. That's intentional (the assertion is "neither MUST be present"), but worth a sanity check.Out of scope (deliberately deferred):
everything-serverdoesn't expose a configuration knob to set up the no-hints or explicit-zero modes. Adding such a knob would letnpm testexercise the new checks directly without an out-of-tree fixture; worth a separate PR if there's appetite.