discoverMcpProtectedResourceMetadata function

Future<McpProtectedResourceDiscovery> discoverMcpProtectedResourceMetadata(
  1. Uri endpoint, {
  2. HttpClient? httpClient,
  3. Map<String, String> headers = const <String, String>{},
  4. bool closeHttpClient = false,
  5. int maxMetadataBytes = _defaultMaxMetadataBytes,
  6. Duration timeout = _defaultDiscoveryTimeout,
  7. void onRequestOpened(
    1. HttpClientRequest request
    )?,
})

Discovers and validates RFC 9728 metadata for an MCP HTTP endpoint.

Requests intentionally omit MCP session and authorization headers. Explicit headers are accepted for metadata-specific routing, but credential and session headers are rejected. timeout is one total deadline across the endpoint probe and every metadata fallback. onRequestOpened observes each request before it is sent so an owning client can bind it to a larger lifecycle.

Implementation

Future<McpProtectedResourceDiscovery> discoverMcpProtectedResourceMetadata(
  Uri endpoint, {
  HttpClient? httpClient,
  Map<String, String> headers = const <String, String>{},
  bool closeHttpClient = false,
  int maxMetadataBytes = _defaultMaxMetadataBytes,
  Duration timeout = _defaultDiscoveryTimeout,
  void Function(HttpClientRequest request)? onRequestOpened,
}) async {
  _validateProtectedResourceUri(endpoint, 'endpoint');
  if (maxMetadataBytes <= 0) {
    throw ArgumentError.value(
      maxMetadataBytes,
      'maxMetadataBytes',
      'must be greater than zero',
    );
  }
  if (timeout <= Duration.zero) {
    throw ArgumentError.value(timeout, 'timeout', 'must be greater than zero');
  }
  _validateDiscoveryHeaders(headers);

  final client = httpClient ?? HttpClient();
  final ownsClient = httpClient == null || closeHttpClient;
  final stopwatch = Stopwatch()..start();
  try {
    final probe = await _getDiscoveryDocument(
      client,
      endpoint,
      headers: headers,
      maxMetadataBytes: maxMetadataBytes,
      stopwatch: stopwatch,
      timeout: timeout,
      onRequestOpened: onRequestOpened,
    );
    final challenges = parseMcpBearerChallenges(
      probe.headers[HttpHeaders.wwwAuthenticateHeader] ?? const <String>[],
    );
    final challenge = _preferredBearerChallenge(challenges);
    _validateChallengeScope(challenge);

    final directMetadata = _directMetadataJson(probe);
    if (directMetadata != null) {
      return McpProtectedResourceDiscovery(
        metadataUri: endpoint,
        metadata: _metadataFromJson(directMetadata, endpoint),
        challenge: challenge,
      );
    }

    final challengedMetadataValue = challenge?.resourceMetadataValue;
    if (challengedMetadataValue != null) {
      final metadataUri = challenge!.resourceMetadata;
      if (metadataUri == null) {
        throw McpAuthorizationDiscoveryException(
          'Bearer resource_metadata is not a valid absolute URL.',
          uri: endpoint,
        );
      }
      _validateMetadataUri(metadataUri);
      final metadata = await _fetchRequiredMetadata(
        client,
        metadataUri,
        endpoint,
        headers: headers,
        maxMetadataBytes: maxMetadataBytes,
        stopwatch: stopwatch,
        timeout: timeout,
        onRequestOpened: onRequestOpened,
      );
      return McpProtectedResourceDiscovery(
        metadataUri: metadataUri,
        metadata: metadata,
        challenge: challenge,
      );
    }

    final attempted = <Uri>[];
    for (final metadataUri in _wellKnownMetadataUris(endpoint)) {
      attempted.add(metadataUri);
      final response = await _getDiscoveryDocument(
        client,
        metadataUri,
        headers: headers,
        maxMetadataBytes: maxMetadataBytes,
        stopwatch: stopwatch,
        timeout: timeout,
        onRequestOpened: onRequestOpened,
      );
      if (response.statusCode == HttpStatus.notFound ||
          response.statusCode == HttpStatus.gone) {
        continue;
      }
      if (response.statusCode != HttpStatus.ok) {
        throw McpAuthorizationDiscoveryException(
          'Protected Resource Metadata request failed.',
          uri: metadataUri,
          statusCode: response.statusCode,
        );
      }
      final metadata = _metadataFromResponse(response, metadataUri, endpoint);
      return McpProtectedResourceDiscovery(
        metadataUri: metadataUri,
        metadata: metadata,
        challenge: challenge,
      );
    }

    throw McpAuthorizationDiscoveryException(
      'Protected Resource Metadata was not found at ${attempted.join(', ')}.',
      uri: endpoint,
    );
  } finally {
    stopwatch.stop();
    if (ownsClient) {
      client.close(force: true);
    }
  }
}