Skip to content

Hosted conformance: deploy + scheduled client runs (CI) and conformance remote - #502

Draft
claude[bot] wants to merge 4 commits into
claude/hosted-client-conformancefrom
claude/hosted-ci-workflows
Draft

Hosted conformance: deploy + scheduled client runs (CI) and conformance remote#502
claude[bot] wants to merge 4 commits into
claude/hosted-client-conformancefrom
claude/hosted-ci-workflows

Conversation

@claude

@claude claude Bot commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Requested by Paul Carleton · Slack thread

Before: the hosted client-conformance server (#501) is deployed to val.town by hand with examples/hosted/deploy-valtown.ts, nothing exercises the live deployment after a deploy, and the only way to drive a client against it is to mint a run with curl and wire the URL/env into the client yourself.

After: pushes to main that touch src/**, examples/hosted/** or package*.json redeploy the rs and relay vals in place and smoke-test them; a scheduled matrix of eight scenarios (core + auth/*) runs the vendored TypeScript everything-client against the live deployment four times a day, after every deploy, on demand, and on PRs that touch the hosted code, with a scenario/result table linking every HTML report in the step summary. A new conformance remote subcommand is the reusable piece both workflows (and humans) use.

This PR adds the CI for the hosted deployment plus the conformance remote CLI command that drives a client against it, and fixes three live-deployment bugs the first matrix runs surfaced.

How: conformance remote --url <RS> --scenario <name> --command "<client>" [--run-id <id>] [--timeout <ms>] [-o result.json] does GET <RS>/s/<scenario> to mint a run, then spawns the client through the very same executeClient() path conformance client --command uses (MCP URL appended as the last argument, MCP_CONFORMANCE_SCENARIO / MCP_CONFORMANCE_PROTOCOL_VERSION in env, MCP_CONFORMANCE_CONTEXT set to the minted context), reads <RS>/results/<runId> back, prints it with the existing printClientResults reporter plus the /results/<runId>.html link, and exits 1 when summary.failed > 0 (or the client timed out). The hosted mint route gained ?runId=<id> so --run-id can pick a predictable id. deploy-hosted.yml (concurrency hosted-deploy, environment hosted-conformance) runs npm ci && npm test, then deploy-valtown.ts --push rs relay, then curls /scenarios and runs one remote --scenario initialize. hosted-conformance.yml has a gate job that probes /scenarios and turns an outage into a skipped (never red) run, a fail-fast: false matrix over initialize, tools_call, json-schema-ref-no-deref, auth/metadata-default, auth/metadata-var1, auth/metadata-var3, auth/basic-cimd, auth/pre-registration (each leg retries once with a fresh run id and uploads its JSON result), and a summary job that aggregates the artifacts into a table. The valtown-manifest.json now carries only the two mcp_conformance org vals (rs, relay) with their live ids so deploys update in place. Unit tests cover remote against a mocked hosted server (mint, URL-as-last-arg, env, ?runId=, results origin, failure exit, unreachable/404) and end to end against the real createHostedApp().

Found by the PR-triggered matrix runs against the live deployment and fixed here:

  • The AS relay (examples/hosted/valtown-relay.ts) forwarded the upstream content-length after fetch() had transparently decompressed the body, so every AS-metadata fetch reached the client as full JSON with the compressed length and was truncated (Unterminated string in JSON at position N). The relay now buffers the body, drops the framing/encoding headers, and asks upstream for identity encoding; regression test with a gzipping mock RS.
  • The merge of main into the hosted branch pulled src/spec-types/draft.ts (98K chars) and the four spec JSON schemas (90–180K) into the RS closure, and val.town rejects files over 80,000 characters, so a deploy aborted halfway through the upload. deploy-valtown.ts now strips comments from oversized TS modules, stages .json imports as generated .json.ts modules split into string chunks when needed, and refuses to push if anything is still over the cap; stage-only regression test. The live rs and relay vals were redeployed from this branch, which also brought the issuer field (added on main in fix(auth): add issuer to pre-registration context #423) into the hosted auth/pre-registration context that the vendored client requires.
  • On val.town the isolate serving GET /results is usually not the one that saw the OAuth flow; SessionManager.results() re-judges the persisted raw log in a fresh scenario instance. The metadata-discovery and token-endpoint-auth scenarios kept their RFC 8707 resource observations in private fields, so the fresh instance reported resource-parameter-in-authorization / resource-parameter-in-token as "not provided" even though the persisted incoming-auth-request checks show the client sent it in both places. observeResourceParameters() now reads those facts back from the log (the PRM route also records the identifier it served), and getChecks() falls back to it; single-process judging is unchanged. The hosted relay test snapshots the raw log and re-judges it through finalizeChecks(), failing without the fallback.

Setup needed from a maintainer:

  • GitHub environment hosted-conformance with secret VAL_TOWN_TOKEN (a val.town API token for the mcp_conformance org)
  • Repository variables HOSTED_RS_URL (RS origin) and HOSTED_AS_URL (AS relay origin); both workflows fall back to the current RS val URL when HOSTED_RS_URL is unset
  • workflow_run and schedule triggers only fire once the workflow files are on main; the pull_request trigger on this PR runs the matrix against the live deployment now

Stacked on #501 (claude/hosted-client-conformance); merge that first.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FXWixCiyW8eEfwFeZcADEK

`conformance remote --url <RS> --scenario <name> --command "<client>"`
mints a run via GET <RS>/s/<scenario>, spawns the client through the same
executeClient() path `conformance client --command` uses (MCP URL as the
last argv, MCP_CONFORMANCE_SCENARIO / MCP_CONFORMANCE_CONTEXT in env),
reads <RS>/results/<runId> back, prints it with the usual client reporter
plus the HTML report link, and exits 1 when summary.failed > 0 (or the
client timed out). `-o` writes the JSON result for CI aggregation.

The hosted mint route now accepts ?runId=<id> so --run-id can pick a
predictable id. The val.town manifest now carries only the two org vals
(mcp-client-conformance / mcp-client-conformance-as) so deploys update
the live deployment in place.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXWixCiyW8eEfwFeZcADEK
…narios

deploy-hosted.yml pushes the rs + relay vals on every push to main that
touches src/**, examples/hosted/** or package*.json (environment
`hosted-conformance`, secret VAL_TOWN_TOKEN), then smoke-tests the live
service with GET /scenarios and a `conformance remote --scenario initialize`
run using the vendored TypeScript everything-client.

hosted-conformance.yml runs an 8-scenario matrix (core + auth) against the
live deployment 4x/day, after each deploy, on demand, and on PRs touching
the hosted code. A gate job skips the matrix (never red) when the service
is unreachable; each leg retries once with a fresh run id; a summary job
writes a scenario/result table with links to every HTML report.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXWixCiyW8eEfwFeZcADEK
@pkg-pr-new

pkg-pr-new Bot commented Sep 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

npx https://pkg.pr.new/@modelcontextprotocol/conformance@502

commit: 617cce8

…n's file cap

Two live-deployment failures surfaced by the first hosted-conformance matrix
run against the mcp_conformance vals:

1. The AS relay copied the upstream content-length while fetch() had already
   transparently decompressed the body, so clients received the full AS
   metadata JSON with the *compressed* length and truncated it
   ("Unterminated string in JSON at position N"). Buffer the upstream body,
   drop every framing/encoding header, ask upstream for identity encoding,
   and let the runtime derive content-length. Regression test with a
   gzipping mock RS.

2. The merge of main into the hosted branch pulled src/spec-types/draft.ts
   (98K chars) and the four spec JSON schemas (90-180K) into the RS import
   closure; val.town rejects any file over 80,000 characters, which aborted
   the deploy halfway through the upload. deploy-valtown.ts now strips
   comments from oversized TS modules (draft.ts drops to 20K, keeping its
   runtime constants), stages .json imports as generated .json.ts modules
   split into string chunks when needed, and refuses to push anything if a
   staged file is still over the cap. Stage-only regression test checks
   sizes and that the generated modules round-trip.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXWixCiyW8eEfwFeZcADEK
…ed elsewhere

On val.town the isolate answering GET /results is usually not the one that
served the OAuth flow. SessionManager.results() re-judges the persisted raw
log in a fresh scenario instance, but the metadata-discovery and
token-endpoint-auth scenarios kept their `resource` observations in private
fields, so the fresh instance reported "Client MUST include resource
parameter" even though the persisted incoming-auth-request checks show the
client sent it in both the authorize query and the token body (live runs
QFi03fSC, XueJLrTc, WL8NG1m2).

observeResourceParameters() reads the same facts back from the log: the
/authorize query, the /token body, and the identifier the PRM route served
(now recorded in prm-pathbased-requested.details.resource). getChecks()
falls back to it when the private fields are empty, so single-process
judging is unchanged and the multi-isolate re-judge agrees with it. The
hosted relay test snapshots the raw log before results and re-judges it via
finalizeChecks(); the assertion fails without the fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FXWixCiyW8eEfwFeZcADEK
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.

1 participant