feat(test-servers): a CIMD showcase fixture that serves its own client metadata document - #2307
Conversation
…t metadata document In CIMD (SEP-991) the `client_id` *is* a URL the authorization server dereferences to learn the client's metadata. Nothing in this repo served such a document, so a fixture could advertise `client_id_metadata_document_supported` and still be unusable — exercising CIMD meant standing up a second host by hand. That is why #2242 shipped verified by its tests alone, and why the v2.6.0 release smoke recorded it as the one contribution with an observable UI surface that could not be reached. The composable server now hosts the document itself when `oauth.clientMetadata` is set, gated on `supportCIMD` — advertising a client the server would then refuse to honour is a worse fixture than serving nothing. The document's `client_id` is derived from the **request** rather than from a configured issuer, so it stays correct when the harness picks the port, as the integration test does. `supportDCR: false` in the fixture is load-bearing rather than incidental. With DCR available a CIMD failure silently succeeds via dynamic registration and the reproduction proves nothing — during development a misconfigured run connected happily and reported `Dynamic (DCR)` with a `test_client_…` id, which reads as success until you check the client id. With DCR off, CIMD is the only path that can complete, so reaching a connected state is itself the assertion; the test pins the absent `registration_endpoint` so it stays that way. Verified end to end against the fixture: Connection Info read `Client registration — Client ID Metadata (CIMD)` with the client id equal to the metadata URL. That drive needed a self-signed HTTPS listener, because the Inspector requires the CIMD metadata URL to be HTTPS with no loopback exemption — filed separately as #2305, since it is a validation gap rather than a fixture one. Closes #2306 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RsmR1iQstcrEzJFmgZXGLi Signed-off-by: cliffhall <cliff@futurescale.com>
There was a problem hiding this comment.
🟡 Changes recommended
The configurable path validation and exact query-bearing client_id handling must be corrected before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds a CIMD showcase fixture that serves its own client metadata document.
Changes:
- Adds configurable CIMD metadata serving.
- Adds a DCR-disabled showcase fixture.
- Adds integration coverage and documentation.
File summaries
| File | Review |
|---|---|
test-servers/src/test-server-oauth.ts |
Serves CIMD metadata. Requires path validation and preserving query parameters in the returned client_id (moderate). |
test-servers/src/load-config.ts |
Adds CIMD configuration fields; clientMetadataPath validation is required (moderate). |
test-servers/src/composable-test-server.ts |
Exposes CIMD options; programmatic path validation is required (moderate). |
test-servers/configs/oauth-cimd-http.json |
Defines the CIMD showcase fixture. |
docs/test-servers.md |
Documents CIMD testing; contains an inaccurate fixed-port warning (nit). |
clients/web/src/test/integration/mcp/oauth-cimd-fixture.test.ts |
Tests fixture metadata and DCR exclusion. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Validate `oauth.clientMetadataPath` the way the other two metadata paths are validated — in `loadConfig` for the JSON route and again at server setup for a `ServerConfig` built in code. This path is not merely advertised: it becomes the served document's own `client_id`, so an off-origin or query-bearing value would publish a client id this server cannot honour. Derive that `client_id` from `req.originalUrl` rather than from the registered route, so a document fetched with a query string answers with a `client_id` byte-identical to the URL it was fetched from — the equality CIMD turns on. Correct the docs' fixed-port warning: this fixture configures no `issuerUrl` and derives the `client_id` from the request, which is exactly why the integration test can drive it on a harness-chosen port. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWGH2ytPzSPxnp4cibaV3Z Signed-off-by: cliffhall <cliff@futurescale.com>
Copilot review round 1 — all three findings addressed (a8cbc50)Replies are inline on each thread; mirrored here because inline replies go hidden once the fix is pushed. 1. 2. 3. Docs fixed-port warning — removed. It was carried over from the fixture above and wrong twice (no Test strengthThree tests added to
One guard removed fails exactly one test, so each is detected and none is redundant. Restored: 7 passed. Verification
🤖 Generated with Claude Code |
There was a problem hiding this comment.
🟢 Approval recommended
The implementation satisfies the linked issue’s acceptance criteria with appropriate validation, integration coverage, and documentation.
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Closes #2306
Why
In CIMD (SEP-991) the
client_idis a URL that the authorization server dereferences to learn the client's metadata. Nothing in this repo served such a document, so a fixture could advertiseclient_id_metadata_document_supportedand still be unusable — exercising CIMD meant standing up a second host by hand.That is why #2242 shipped verified by its tests alone, and why the v2.6.0 release smoke recorded it as the one contribution with an observable UI surface that could not be reached. This PR is that missing piece, and #2242 has now been driven by hand against it.
What's here
oauth.clientMetadata/oauth.clientMetadataPathand a route serving the document whensupportCIMDis on. Gated onsupportCIMDas well as on the document's presence: advertising a client the server would then refuse to honour is a worse fixture than serving nothing.client_idis derived from the request, not from a configured issuer, so it stays correct when the harness picks the port — which the integration test does.test-servers/configs/oauth-cimd-http.json.src/test/integration/mcp/shape.docs/test-servers.mdsection.supportDCR: falseis load-bearing, not tidinessWith DCR available, a CIMD failure silently succeeds via dynamic registration and the reproduction proves nothing.
This is not hypothetical — it happened while building this. A misconfigured run connected happily and Connection Info read
Dynamic (DCR)with atest_client_…id. It reads as success until you check the client id, which is precisely the confusion #2242 is about. With DCR off, CIMD is the only path that can complete, so reaching a connected state is itself the assertion.The test pins it so it stays that way:
Verification
npm run local:gate— pass, in a dedicated worktree with its own fullnpm install. Web tests went 418 files / 8001 tests → 419 / 8005, i.e. exactly the four added here; 11 smokes OK.Driven end to end against the fixture (this is what cleared the v2.6.0 ledger finding):
The authorize request carried
client_id=https%3A%2F%2F127.0.0.1%3A8443%2Fclient-metadata.jsonagainst a server advertising noregistration_endpoint, so nothing about it could have been DCR.What this does not fix — #2305
That drive needed a self-signed HTTPS listener holding nothing but the JSON document, plus
NODE_TLS_REJECT_UNAUTHORIZED=0in the test server's environment so its own fetch of the document would succeed.The reason is our own validation, not the SDK's:
getCimdClientMetadataUrlErrorrejects any CIMD metadata URL that is not HTTPS, with no loopback exemption, and applies that toclient.jsonon disk as well as to the settings form. So this server'shttp://document is not a legalclientMetadataUrl. It exists for the authorization-server side of the flow and for the assertions in this PR's test.Filed as #2305 — the same over-narrow allow-list shape as #2280 / #1944, from the other side. This PR delivers the fixture; #2305 is what would make it pleasant to use.
Two things worth knowing, now in the docs
client.json→cimd.{enabled,clientMetadataUrl}, reachable from Client settings), not per-server. AclientMetadataUrlwritten into a catalog entry'soauthblock is silently ignored.No UI or TUI surface changes, so no screenshots apply.
🤖 Generated with Claude Code
https://claude.ai/code/session_01RsmR1iQstcrEzJFmgZXGLi