feat(test-servers): a CIMD showcase fixture that serves its own client 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>
This commit is contained in:
parent
86f25bd9e1
commit
18bda28ce1
4 changed files with 302 additions and 0 deletions
37
test-servers/configs/oauth-cimd-http.json
Normal file
37
test-servers/configs/oauth-cimd-http.json
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{
|
||||
"serverInfo": {
|
||||
"name": "oauth-cimd-showcase",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"tools": [
|
||||
{
|
||||
"preset": "echo"
|
||||
}
|
||||
],
|
||||
"oauth": {
|
||||
"enabled": true,
|
||||
"mode": "combined",
|
||||
"requireAuth": true,
|
||||
"scopesSupported": [
|
||||
"mcp"
|
||||
],
|
||||
"supportCIMD": true,
|
||||
"supportDCR": false,
|
||||
"supportRefreshTokens": true,
|
||||
"clientMetadata": {
|
||||
"clientName": "MCP Inspector (CIMD test fixture)",
|
||||
"scope": "mcp",
|
||||
"redirectUris": [
|
||||
"http://127.0.0.1:6274/oauth/callback",
|
||||
"http://localhost:6274/oauth/callback",
|
||||
"http://127.0.0.1:6330/oauth/callback",
|
||||
"http://localhost:6330/oauth/callback",
|
||||
"http://127.0.0.1:6276/oauth/callback"
|
||||
]
|
||||
}
|
||||
},
|
||||
"transport": {
|
||||
"type": "streamable-http",
|
||||
"port": 8092
|
||||
}
|
||||
}
|
||||
|
|
@ -734,6 +734,34 @@ export interface ServerConfig {
|
|||
*/
|
||||
supportCIMD?: boolean;
|
||||
|
||||
/**
|
||||
* Serve a CIMD client metadata document from this server, so a CIMD
|
||||
* fixture is self-contained.
|
||||
*
|
||||
* CIMD makes the `client_id` a URL that the authorization server fetches
|
||||
* to learn the client's metadata (SEP-991). Nothing in this repo served
|
||||
* such a document, so exercising CIMD meant standing up a second host by
|
||||
* hand — which is why #2242 shipped verified only by its tests. With this
|
||||
* set, the server hosts the document at `clientMetadataPath` (default
|
||||
* `/client-metadata.json`) and that URL is a usable `client_id`.
|
||||
*
|
||||
* `redirectUris` MUST list the Inspector's callback for the port you run
|
||||
* it on (`<web origin>/oauth/callback`) — the authorization server checks
|
||||
* the incoming `redirect_uri` against this list, and a mismatch fails the
|
||||
* flow with `Invalid redirect_uri` rather than anything CIMD-specific.
|
||||
*
|
||||
* Only served when `supportCIMD` is true: a document advertising a client
|
||||
* the server would then refuse is a worse fixture than none.
|
||||
*/
|
||||
clientMetadata?: {
|
||||
redirectUris: string[];
|
||||
clientName?: string;
|
||||
scope?: string;
|
||||
};
|
||||
|
||||
/** Where to serve `clientMetadata` (default `/client-metadata.json`). */
|
||||
clientMetadataPath?: string;
|
||||
|
||||
/**
|
||||
* Token expiration time in seconds (default: 3600)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -37,6 +37,18 @@ export interface ConfigFileOAuth {
|
|||
}>;
|
||||
supportDCR?: boolean;
|
||||
supportCIMD?: boolean;
|
||||
/**
|
||||
* Serve a CIMD client metadata document, making a CIMD fixture
|
||||
* self-contained. `redirectUris` must list the Inspector callback for the
|
||||
* web port under test. See the field's doc comment in
|
||||
* `composable-test-server.ts`.
|
||||
*/
|
||||
clientMetadata?: {
|
||||
redirectUris: string[];
|
||||
clientName?: string;
|
||||
scope?: string;
|
||||
};
|
||||
clientMetadataPath?: string;
|
||||
tokenExpirationSeconds?: number;
|
||||
supportRefreshTokens?: boolean;
|
||||
/** RFC 7009 revocation endpoint; default true (#2144). */
|
||||
|
|
|
|||
|
|
@ -283,6 +283,38 @@ function setupMetadataEndpoints(
|
|||
});
|
||||
}
|
||||
|
||||
// CIMD client metadata document (SEP-991). The `client_id` in a CIMD flow is
|
||||
// a URL the authorization server dereferences, so a fixture that advertises
|
||||
// `client_id_metadata_document_supported` without hosting a document
|
||||
// anywhere is only half a fixture — it needs a second host to be usable at
|
||||
// all. Serving it here makes a CIMD run self-contained.
|
||||
//
|
||||
// Gated on `supportCIMD` as well as on the document's presence: advertising
|
||||
// a client this server would then refuse to honour is worse than serving
|
||||
// nothing.
|
||||
if (config.supportCIMD && config.clientMetadata) {
|
||||
const doc = config.clientMetadata;
|
||||
const metadataPath = config.clientMetadataPath ?? "/client-metadata.json";
|
||||
app.get(metadataPath, (req: Request, res: Response) => {
|
||||
// Derived from the request rather than from `issuerUrl`, so the
|
||||
// document's own `client_id` always equals the URL it was fetched from
|
||||
// — which is what CIMD requires, and what stays true if the server
|
||||
// walked to another port on EADDRINUSE.
|
||||
const requestBaseUrl = `${req.protocol}://${req.get("host")}`;
|
||||
res.json({
|
||||
client_id: new URL(metadataPath, requestBaseUrl).href,
|
||||
client_name: doc.clientName ?? "MCP Inspector (CIMD test fixture)",
|
||||
redirect_uris: doc.redirectUris,
|
||||
// CIMD clients are public and authenticate with nothing; the server's
|
||||
// own CIMD branch assumes exactly this (no client_secret is issued).
|
||||
token_endpoint_auth_method: "none",
|
||||
grant_types: ["authorization_code", "refresh_token"],
|
||||
response_types: ["code"],
|
||||
...(doc.scope ? { scope: doc.scope } : {}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// OAuth Protected Resource Metadata. `resourceMetadataPath` moves the
|
||||
// document off the well-known path entirely (rather than serving both), so
|
||||
// a client that ignores the advertised `resource_metadata` URL gets a 404
|
||||
|
|
|
|||
Loading…
Reference in a new issue