Merge pull request #2307 from modelcontextprotocol/v2/feat/2306-cimd-fixture
feat(test-servers): a CIMD showcase fixture that serves its own client metadata document
This commit is contained in:
commit
d8b4b06af9
6 changed files with 406 additions and 0 deletions
194
clients/web/src/test/integration/mcp/oauth-cimd-fixture.test.ts
Normal file
194
clients/web/src/test/integration/mcp/oauth-cimd-fixture.test.ts
Normal file
|
|
@ -0,0 +1,194 @@
|
|||
import { describe, it, expect, afterEach } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
createTestServerHttp,
|
||||
type TestServerHttp,
|
||||
createTestServerInfo,
|
||||
loadConfig,
|
||||
resolveConfig,
|
||||
} from "@modelcontextprotocol/inspector-test-server";
|
||||
|
||||
/**
|
||||
* Live coverage of `test-servers/configs/oauth-cimd-http.json` — the fixture
|
||||
* that makes #2242 reproducible by hand.
|
||||
*
|
||||
* #2242 (CIMD provenance surviving the SDK's issuer-binding write) shipped
|
||||
* verified by its own end-to-end tests, because nothing in this repo served a
|
||||
* **client metadata document**: in CIMD the `client_id` is a URL the
|
||||
* authorization server dereferences, so exercising it meant standing up a
|
||||
* second host. The v2.6.0 release ledger recorded that as the one row with an
|
||||
* observable UI surface and no way to reach it.
|
||||
*
|
||||
* What this file protects is the fixture itself, not the fix. The manual
|
||||
* reproduction depends on three things being true of the served document, and
|
||||
* each of them is the kind of thing that breaks silently:
|
||||
*
|
||||
* - the AS advertises `client_id_metadata_document_supported`, or the
|
||||
* Inspector's CIMD pre-registration bails out before storing anything;
|
||||
* - it advertises **no** `registration_endpoint`, or a CIMD failure quietly
|
||||
* falls back to DCR and the repro passes while proving nothing (this is
|
||||
* exactly how the fixture first fooled its own author);
|
||||
* - the document's `client_id` equals the URL it was fetched from, which is
|
||||
* what makes it a legal CIMD client id rather than an arbitrary blob.
|
||||
*
|
||||
* The document is served over plain HTTP here. That is deliberate and is *not*
|
||||
* a usable `clientMetadataUrl` for the Inspector, which requires HTTPS with no
|
||||
* loopback exemption (#2305) — driving the UI needs a self-signed HTTPS
|
||||
* listener, as `docs/test-servers.md` describes. This endpoint exists for the
|
||||
* authorization-server side of the flow and for exactly these assertions.
|
||||
*/
|
||||
describe("CIMD showcase fixture (#2242)", () => {
|
||||
let server: TestServerHttp | null = null;
|
||||
|
||||
const configPath = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
"../../../../../../test-servers/configs/oauth-cimd-http.json",
|
||||
);
|
||||
|
||||
afterEach(async () => {
|
||||
if (server) {
|
||||
try {
|
||||
await server.stop();
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
server = null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Boot the showcase config on a harness-chosen port, so this cannot collide
|
||||
* with a showcase server someone is running by hand.
|
||||
*/
|
||||
async function startShowcase(): Promise<TestServerHttp> {
|
||||
const resolved = resolveConfig(loadConfig(configPath));
|
||||
const started = createTestServerHttp({
|
||||
...resolved,
|
||||
serverInfo: createTestServerInfo("oauth-cimd-test", "1.0.0"),
|
||||
port: undefined,
|
||||
});
|
||||
await started.start();
|
||||
server = started;
|
||||
return started;
|
||||
}
|
||||
|
||||
/** The MCP endpoint's origin, which is also the AS and the document host. */
|
||||
function originOf(started: TestServerHttp): string {
|
||||
return new URL(started.url).origin;
|
||||
}
|
||||
|
||||
it("resolves the config with CIMD on and DCR deliberately off", () => {
|
||||
const resolved = resolveConfig(loadConfig(configPath));
|
||||
expect(resolved.oauth?.supportCIMD).toBe(true);
|
||||
// Not an oversight: with DCR available a CIMD failure silently succeeds
|
||||
// via dynamic registration, and the repro stops proving anything.
|
||||
expect(resolved.oauth?.supportDCR).toBe(false);
|
||||
expect(resolved.oauth?.clientMetadata?.redirectUris).toContain(
|
||||
"http://127.0.0.1:6276/oauth/callback",
|
||||
);
|
||||
});
|
||||
|
||||
it("advertises CIMD support and no registration endpoint", async () => {
|
||||
const started = await startShowcase();
|
||||
const res = await fetch(
|
||||
`${originOf(started)}/.well-known/oauth-authorization-server`,
|
||||
);
|
||||
expect(res.ok).toBe(true);
|
||||
const metadata = await res.json();
|
||||
|
||||
expect(metadata.client_id_metadata_document_supported).toBe(true);
|
||||
// The half that keeps the repro honest.
|
||||
expect(metadata.registration_endpoint).toBeUndefined();
|
||||
});
|
||||
|
||||
it("serves a client metadata document whose client_id is its own URL", async () => {
|
||||
const started = await startShowcase();
|
||||
const documentUrl = `${originOf(started)}/client-metadata.json`;
|
||||
|
||||
const res = await fetch(documentUrl);
|
||||
expect(res.ok).toBe(true);
|
||||
const doc = await res.json();
|
||||
|
||||
// A CIMD client id IS the document's URL. Deriving it from the request
|
||||
// rather than from a configured issuer is what keeps this true when the
|
||||
// harness picks the port, as it does here.
|
||||
expect(doc.client_id).toBe(documentUrl);
|
||||
expect(doc.redirect_uris).toContain("http://127.0.0.1:6276/oauth/callback");
|
||||
// CIMD clients are public; the server's own CIMD branch issues no secret.
|
||||
expect(doc.token_endpoint_auth_method).toBe("none");
|
||||
});
|
||||
|
||||
it("preserves a query-bearing document URL in the client_id it publishes", async () => {
|
||||
const started = await startShowcase();
|
||||
const documentUrl = `${originOf(started)}/client-metadata.json?profile=a`;
|
||||
|
||||
const res = await fetch(documentUrl);
|
||||
expect(res.ok).toBe(true);
|
||||
const doc = await res.json();
|
||||
|
||||
// CIMD turns on the document's `client_id` being the URL it was fetched
|
||||
// from. Answering `?profile=a` with the bare route would publish a
|
||||
// document that fails that equality for a client id the server just
|
||||
// served (Copilot).
|
||||
expect(doc.client_id).toBe(documentUrl);
|
||||
});
|
||||
|
||||
it("rejects a clientMetadataPath that would publish a foreign client_id", () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "cimd-config-"));
|
||||
const badPath = path.join(dir, "bad-cimd.json");
|
||||
const base = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
||||
fs.writeFileSync(
|
||||
badPath,
|
||||
JSON.stringify({
|
||||
...base,
|
||||
oauth: { ...base.oauth, clientMetadataPath: "//other-host/doc" },
|
||||
}),
|
||||
);
|
||||
|
||||
try {
|
||||
// The path is not merely advertised: it becomes the document's own
|
||||
// `client_id`, so an off-origin value publishes a client id naming a
|
||||
// host this server does not serve.
|
||||
expect(() => loadConfig(badPath)).toThrow(/clientMetadataPath/);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects the same path built programmatically rather than from JSON", async () => {
|
||||
const resolved = resolveConfig(loadConfig(configPath));
|
||||
const started = createTestServerHttp({
|
||||
...resolved,
|
||||
oauth: { ...resolved.oauth!, clientMetadataPath: "/doc?version=1" },
|
||||
serverInfo: createTestServerInfo("oauth-cimd-badpath-test", "1.0.0"),
|
||||
port: undefined,
|
||||
});
|
||||
server = started;
|
||||
|
||||
// `loadConfig` covers the JSON route only, so the server-setup check is
|
||||
// what catches a `ServerConfig` assembled in code — the same split the
|
||||
// two existing metadata paths have.
|
||||
await expect(started.start()).rejects.toThrow(/clientMetadataPath/);
|
||||
server = null;
|
||||
});
|
||||
|
||||
it("does not serve the document when CIMD is switched off", async () => {
|
||||
const resolved = resolveConfig(loadConfig(configPath));
|
||||
const started = createTestServerHttp({
|
||||
...resolved,
|
||||
oauth: { ...resolved.oauth!, supportCIMD: false },
|
||||
serverInfo: createTestServerInfo("oauth-cimd-off-test", "1.0.0"),
|
||||
port: undefined,
|
||||
});
|
||||
await started.start();
|
||||
server = started;
|
||||
|
||||
const res = await fetch(`${originOf(started)}/client-metadata.json`);
|
||||
// Advertising a client this server would then refuse to honour is a worse
|
||||
// fixture than serving nothing at all.
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
|
@ -55,6 +55,7 @@ as a missing capability rather than an error.
|
|||
| `oauth-revocation-http.json` / `oauth-no-revocation-http.json` **(legacy era)** | RFC 7009 token revocation on clear, with and without a `revocation_endpoint` | [#2144](https://github.com/modelcontextprotocol/inspector/issues/2144) |
|
||||
| `oauth-rfc8414-at-oidc-path-http.json` **(legacy era)** | Plain OAuth 2.0 AS metadata served at the OIDC well-known path | [#2172](https://github.com/modelcontextprotocol/inspector/issues/2172) |
|
||||
| `oauth-insecure-token-endpoint-http.json` **(legacy era)** | A token endpoint the SDK refuses to post credentials to | [#2280](https://github.com/modelcontextprotocol/inspector/issues/2280) |
|
||||
| `oauth-cimd-http.json` **(legacy era)** | URL-based client IDs (CIMD / SEP-991), DCR deliberately off | [#2242](https://github.com/modelcontextprotocol/inspector/issues/2242) |
|
||||
| `logging-{legacy,modern}-http.json` **(era per file)** | Logging, both eras | [#1629](https://github.com/modelcontextprotocol/inspector/issues/1629) |
|
||||
| `subscriptions-{legacy,modern}-http.json` **(era per file)** | Resource subscriptions, both eras | [#1630](https://github.com/modelcontextprotocol/inspector/issues/1630) |
|
||||
| `subscriptions-never-acknowledged-http.json` **(modern era)** | A `subscriptions/listen` answered with a bare result | [#2097](https://github.com/modelcontextprotocol/inspector/issues/2097) |
|
||||
|
|
@ -534,6 +535,61 @@ On the broken build you got a **"Re-authentication required"** banner with a **R
|
|||
|
||||
Note that the fix here is presentational only. Making a `*.localhost` token endpoint actually **work** has to land in the SDK — the assertion runs inside `executeTokenRequest`, takes no options, and there is no hook the Inspector could reach.
|
||||
|
||||
## URL-based client IDs (CIMD / SEP-991)
|
||||
|
||||
`oauth-cimd-http.json` is a combined AS + resource server that advertises
|
||||
`client_id_metadata_document_supported: true` and — the part that makes it usable — **hosts the client
|
||||
metadata document itself**, at `/client-metadata.json`. Plain streamable-HTTP; connect with the
|
||||
**default (legacy)** protocol era.
|
||||
|
||||
Hosting the document is the whole reason this fixture exists. In CIMD the `client_id` *is* a URL that
|
||||
the authorization server dereferences to learn the client's metadata, so a server that merely
|
||||
advertises support is only half a fixture: exercising it still meant standing up a second host by
|
||||
hand. That is why [#2242](https://github.com/modelcontextprotocol/inspector/issues/2242) shipped
|
||||
verified by its tests alone, and the v2.6.0 release ledger recorded it as the one row that had an
|
||||
observable UI surface but no way to reach it.
|
||||
|
||||
**`supportDCR` is `false` on purpose.** With both registration paths available a successful connection
|
||||
proves nothing about which one ran — precisely the confusion #2242 was about, where Connection Info
|
||||
reported `Dynamic (DCR)` for a connection that never issued a `POST /oauth/register`. With DCR off,
|
||||
CIMD is the only way the flow can complete, so reaching a connected state *is* the assertion.
|
||||
|
||||
⚠️ **CIMD is configured install-wide, not per server.** It lives in `client.json`
|
||||
(`~/.mcp-inspector/storage/client.json`) as `cimd: { enabled: true, clientMetadataUrl }`, reachable
|
||||
from **Client settings**, not from a server's own OAuth settings. A `clientMetadataUrl` written into a
|
||||
catalog entry's `oauth` block is silently ignored — and with `supportDCR: true` the connection then
|
||||
succeeds *via DCR*, which looks like CIMD working until you read the client id.
|
||||
|
||||
⚠️ **The Inspector requires that URL to be HTTPS, and there is no loopback exemption**
|
||||
(`getCimdClientMetadataUrlError` in `core/client/config-parse.ts`, applied to `client.json` on disk as
|
||||
well as to the settings form). So this server's own `http://` document is **not** usable as a
|
||||
`clientMetadataUrl`: it exists for the authorization-server side of the flow and for tests that drive
|
||||
the AS directly. To drive the Inspector end to end you need the document served over HTTPS —
|
||||
`https://127.0.0.1:8443/client-metadata.json` from a throwaway self-signed listener works, with
|
||||
`NODE_TLS_REJECT_UNAUTHORIZED=0` in the *test server's* environment so its own fetch of that document
|
||||
succeeds. That asymmetry is tracked in
|
||||
[#2305](https://github.com/modelcontextprotocol/inspector/issues/2305); it is the same over-narrow
|
||||
allow-list shape as the token-endpoint exemption above.
|
||||
|
||||
With that in place: set the metadata URL in Client settings, connect, and open **Connection Info**.
|
||||
It should read `Client registration — Client ID Metadata (CIMD)` with the **client id equal to the
|
||||
metadata URL**, which is what CIMD means and what distinguishes it from a DCR-issued
|
||||
`test_client_…`. On the broken build it read `Dynamic (DCR)` for exactly this flow
|
||||
([#2242](https://github.com/modelcontextprotocol/inspector/issues/2242)).
|
||||
|
||||
⚠️ **`clientMetadata.redirectUris` must list the callback for the port you are running.** The
|
||||
Inspector's browser redirect is `<web origin>/oauth/callback` and the CLI/TUI's is
|
||||
`http://127.0.0.1:6276/oauth/callback`; the authorization server checks the incoming `redirect_uri`
|
||||
against this list. The shipped fixture lists **6274** (the default), **6330** and **6276**; on any
|
||||
other port the flow fails with `Invalid redirect_uri`, which reads like a CIMD problem and is not one.
|
||||
Add your port to the config rather than debugging the registration path.
|
||||
|
||||
This fixture has **no** fixed-`issuerUrl` hazard, unlike several of the ones above: it configures no
|
||||
`issuerUrl`, and the document's `client_id` is derived from the request it was fetched over — query
|
||||
string included — so a server that walked to another port on `EADDRINUSE` still publishes a
|
||||
`client_id` equal to the URL you fetched, and the integration test drives it on a harness-chosen port
|
||||
for exactly that reason. The fixed-port dependency that *does* bite is `redirect_uris`, above.
|
||||
|
||||
## Revoking tokens on clear (RFC 7009)
|
||||
|
||||
`oauth-revocation-http.json` and `oauth-no-revocation-http.json` are the same OAuth-protected server (combined AS + resource, DCR, refresh tokens) differing in one thing: the first advertises a `revocation_endpoint`, the second advertises none. Plain streamable-HTTP — connect with the **default (legacy)** protocol era.
|
||||
|
|
|
|||
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,43 @@ 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`).
|
||||
*
|
||||
* Must be origin-relative with no query or fragment, and is validated as
|
||||
* such — both by `loadConfig` and again at server setup for a config built
|
||||
* in code. Unlike the other metadata paths this one is not merely
|
||||
* advertised: it becomes the document's own `client_id`, so an off-origin
|
||||
* or query-bearing value would publish a client id this server cannot
|
||||
* honour (Copilot).
|
||||
*/
|
||||
clientMetadataPath?: string;
|
||||
|
||||
/**
|
||||
* Token expiration time in seconds (default: 3600)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -37,6 +37,23 @@ 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;
|
||||
};
|
||||
/**
|
||||
* Where to serve `clientMetadata` (default `/client-metadata.json`);
|
||||
* validated as an origin-relative path, since it becomes the document's own
|
||||
* `client_id`. See `composable-test-server.ts`.
|
||||
*/
|
||||
clientMetadataPath?: string;
|
||||
tokenExpirationSeconds?: number;
|
||||
supportRefreshTokens?: boolean;
|
||||
/** RFC 7009 revocation endpoint; default true (#2144). */
|
||||
|
|
@ -248,6 +265,12 @@ function validateConfig(
|
|||
`Invalid config in ${filePath}: oauth.asMetadataPath must be an origin-relative path (e.g. "/.well-known/openid-configuration") — a value such as "//host/doc" would move the document off this server entirely`,
|
||||
);
|
||||
}
|
||||
const cimdPath = oauth.clientMetadataPath;
|
||||
if (cimdPath !== undefined && !isOriginRelativePath(cimdPath)) {
|
||||
throw new Error(
|
||||
`Invalid config in ${filePath}: oauth.clientMetadataPath must be an origin-relative path (e.g. "/client-metadata.json") — this path becomes the document's own client_id, so a value such as "//host/doc" would publish a client id naming a host this server does not serve`,
|
||||
);
|
||||
}
|
||||
if (transportType === "stdio" && oauth.enabled === true) {
|
||||
throw new Error(
|
||||
`Invalid config in ${filePath}: oauth requires streamable-http or sse transport`,
|
||||
|
|
|
|||
|
|
@ -101,6 +101,27 @@ function asMetadataPath(config: OAuthConfig): string {
|
|||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the CIMD client metadata document is served from, validated the same
|
||||
* way the two metadata paths above are — and for a sharper reason than either.
|
||||
* This path is not merely advertised: it becomes the document's own
|
||||
* `client_id`, so a value such as `//other-host/doc` would publish a client id
|
||||
* naming a host this server does not control, and `/doc?version=1` would
|
||||
* publish one that cannot reach the route Express registered (Copilot).
|
||||
*/
|
||||
function clientMetadataPath(config: OAuthConfig): string {
|
||||
const path = config.clientMetadataPath;
|
||||
if (path === undefined) {
|
||||
return "/client-metadata.json";
|
||||
}
|
||||
if (!isOriginRelativePath(path)) {
|
||||
throw new Error(
|
||||
`oauth.clientMetadataPath must be an origin-relative path (got ${JSON.stringify(path)})`,
|
||||
);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* The `WWW-Authenticate` challenge sent with every 401.
|
||||
*
|
||||
|
|
@ -283,6 +304,44 @@ 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 = clientMetadataPath(config);
|
||||
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.
|
||||
//
|
||||
// `originalUrl` rather than the registered route, so a client id that
|
||||
// carries a query string (`/client-metadata.json?profile=a`) still gets
|
||||
// a document whose `client_id` is byte-identical to the URL that was
|
||||
// fetched. Answering with the bare route instead would hand back a
|
||||
// document that fails the very equality CIMD turns on (Copilot).
|
||||
const requestBaseUrl = `${req.protocol}://${req.get("host")}`;
|
||||
res.json({
|
||||
client_id: new URL(req.originalUrl, 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