discoverMcpProtectedResourceMetadata function
Future<McpProtectedResourceDiscovery>
discoverMcpProtectedResourceMetadata(
- Uri endpoint, {
- HttpClient? httpClient,
- Map<
String, String> headers = const <String, String>{}, - bool closeHttpClient = false,
- int maxMetadataBytes = _defaultMaxMetadataBytes,
- Duration timeout = _defaultDiscoveryTimeout,
- void onRequestOpened(
- 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);
}
}
}