Skip to content

feat: add conformance test for authorization server metadata#170

Open
Michito-Okai wants to merge 1 commit intomodelcontextprotocol:mainfrom
Hitachi:authorization-server-metadata
Open

feat: add conformance test for authorization server metadata#170
Michito-Okai wants to merge 1 commit intomodelcontextprotocol:mainfrom
Hitachi:authorization-server-metadata

Conversation

@Michito-Okai
Copy link

Motivation and Context

Described in #169

How Has This Been Tested?

Breaking Changes

  • add authorization server mode (-- authorization)
  • add url option for authorization server mode (--url )
  • add a conformance test for authorization server metadata

Types of changes

  • New feature (non-breaking change which adds functionality)

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Copy link

@tnorimat tnorimat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Michito-Okai Hello, I added some review comments. Could you check them?

implements ClientScenarioForAuthorizationServer
{
name = 'authorization-server-metadata-endpoint';
specVersions: SpecVersion[] = ['2025-03-26', '2025-06-18', '2025-11-25'];

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid duplication, instead of defining spec version here, could you import SpecVersion of types.ts like

import {
  ClientScenarioForAuthorizationServer,
  ConformanceCheck,
  SpecVersion 
} from '../../types';

?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed. Could you check this?

export function getClientScenarioForAuthorizationServer(
name: string
): ClientScenarioForAuthorizationServer | undefined {
return clientScenariosForAuthorizationServer.get(name);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getClientScenarioForAuthorizationServer declares its return type as ClientScenarioForAuthorizationServer but that type isn't imported. Could you add it to the import from '../types' on line 1 ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed. Could you check this?

versionScenarios = listClientScenariosForSpec(version);
} else if (command === 'authorization') {
versionScenarios =
listClientScenariosForAuthorizationServerForSpec(version);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function is called inside filterScenariosBySpecVersion but was never imported from './scenarios'. It's exported from index.ts but missing from the import block at line18-34 of index.ts. Could you import that ? like

import {
  listScenarios,
  listClientScenarios,
  listActiveClientScenarios,
  listPendingClientScenarios,
  listAuthScenarios,
  listMetadataScenarios,
  listCoreScenarios,
  listExtensionScenarios,
  listBackcompatScenarios,
  listScenariosForSpec,
  listClientScenariosForSpec,
  getScenarioSpecVersions,
  listClientScenariosForAuthorizationServer,
  listClientScenariosForAuthorizationServerForSpec,
  ALL_SPEC_VERSIONS
} from './scenarios';

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed. Could you check this?

src/index.ts Outdated
authorizationServerScenarios = filterScenariosBySpecVersion(
authorizationServerScenarios,
specVersionFilter,
'client'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

authorization instead of client ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed. Could you check this?

src/index.ts Outdated
console.error(` ${err.path.join('.')}: ${err.message}`);
});
console.error('\nAvailable authorization server scenarios:');
listClientScenarios().forEach((s) => console.error(` - ${s}`));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

listClientScenariosForAuthorizationServer instead of listClientScenarios ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed. Could you check this?

src/index.ts Outdated
console.log('');
}
console.log(
'Authorization server scenarios (test against a authorization server):'

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an authorization server instead of a authorization server ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed. Could you check this?

run(serverUrl: string): Promise<ConformanceCheck[]>;
}

export interface ClientScenarioForAuthorizationServer {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both interfaces ClientScenario and ClientScenarioForAuthorizationServer have the exact same shape. Consider using a type alias (type ClientScenarioForAuthorizationServer = ClientScenario) or just reusing ClientScenario directly?.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The shape of ClientScenarioForAuthorizationServer is likely to change with enhancements, so I defined ClientScenarioForAuthorizationServer.

let errorMessage: string | undefined;
let details: any;

const fail = (msg: string) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fail is never called.
All validation methods throw instead, and the catch block sets status and errorMessage directly.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example,

    } catch (error) {
      status = 'FAILURE';
      errorMessage = error instanceof Error ? error.message : String(error);
    }

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed. Could you check this?

);
}

const expectedIssuer = serverUrl.replace(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It only removes the first occurrence. If the URL were e.g. https://example.com/.well-known/oauth-authorization-server/tenant, the replace would produce https://example.com//tenant (note the path portion is kept). Per RFC 8414 §3, the issuer for a path-aware metadata URL /.well-known/oauth-authorization-server/<path> should be https://example.com/<path>. The current approach works correctly for the root case but may be incorrect for path-based issuers.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example,

const url = new URL(serverUrl);
const wellKnownPrefix = '/.well-known/oauth-authorization-server';
const suffix = url.pathname.startsWith(wellKnownPrefix + '/')
  ? url.pathname.slice(wellKnownPrefix.length)
  : '';
const expectedIssuer = `${url.origin}${suffix}`;

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that if the URL were e.g. https://example.com/.well-known/oauth-authorization-server/tenant, the replace would produce not https://example.com//tenant but https://example.com/tenant. Therefore I think that the current approach works correctly for path-based issuers.

if (options.server || (!options.client && !options.server)) {
if (
options.server ||
(!options.client && !options.server && !options.authorization)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server command supports running a single scenario (--scenario), suite selection (suite), expected-failures baselines, and verbose output (--verbose). The new authorization command has none of these, making it less flexible. This may be intentional for an initial implementation but is worth noting. It is up to you to consider this point by this PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scenario, suite, expected-failures and verbose options are outside the scope of this PR. I am considering to add these options in a future enhancement. How does that sound?

@Michito-Okai Michito-Okai force-pushed the authorization-server-metadata branch from 9617458 to eacc9e7 Compare February 27, 2026 02:37
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.

2 participants