use core::fmt;
use crate::domain::mcp::authorization::AuthorizationModel;
use crate::domain::mcp::resource_metadata::ResourceUri;
#[derive(Clone, Default, Eq)]
pub struct ResourceParameters(Vec<ResourceUri>);
impl PartialEq for ResourceParameters {
fn eq(&self, other: &Self) -> bool {
self.len() == other.len() && self.0.iter().all(|uri| other.0.contains(uri))
}
}
impl ResourceParameters {
#[must_use]
pub fn single(resource: &ResourceUri) -> Self {
Self(vec![resource.clone()])
}
#[must_use]
pub fn many(resources: impl IntoIterator<Item = ResourceUri>) -> Self {
let mut kept: Vec<ResourceUri> = Vec::new();
for resource in resources {
if !kept.contains(&resource) {
kept.push(resource);
}
}
Self(kept)
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.0.iter().map(ResourceUri::as_str)
}
#[must_use]
pub const fn len(&self) -> usize {
self.0.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl fmt::Debug for ResourceParameters {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.debug_list().entries(self.iter()).finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct ResourceBinding {
requested: ResourceParameters,
}
impl ResourceBinding {
pub fn bind(
model: &AuthorizationModel,
requested: ResourceParameters,
) -> Result<Self, ResourceIndicatorError> {
if requested.is_empty() && model.resource_indicators_required {
return Err(ResourceIndicatorError::Missing);
}
Ok(Self { requested })
}
#[must_use]
pub const fn token_request(&self) -> &ResourceParameters {
&self.requested
}
#[must_use]
pub fn matches_token_request(&self, offered: &ResourceParameters) -> bool {
self.requested == *offered
}
}
impl fmt::Debug for ResourceBinding {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("ResourceBinding")
.field("requested", &self.requested)
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ResourceIndicatorError {
#[error(
"this revision requires a resource indicator (RFC 8707), and the request named no resource"
)]
Missing,
}
#[cfg(test)]
mod tests {
use super::{ResourceBinding, ResourceIndicatorError, ResourceParameters};
use crate::domain::ProtocolVersion;
use crate::domain::mcp::authorization::{AuthorizationModel, authorization_model};
use crate::domain::mcp::resource_metadata::{ResourceUri, ResourceUriError};
#[derive(Debug)]
enum Failure {
Uri(ResourceUriError),
Indicator(ResourceIndicatorError),
}
impl From<ResourceUriError> for Failure {
fn from(error: ResourceUriError) -> Self {
Self::Uri(error)
}
}
impl From<ResourceIndicatorError> for Failure {
fn from(error: ResourceIndicatorError) -> Self {
Self::Indicator(error)
}
}
impl core::fmt::Display for Failure {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Uri(error) => write!(formatter, "{error}"),
Self::Indicator(error) => write!(formatter, "{error}"),
}
}
}
fn resource(value: &str) -> Result<ResourceUri, ResourceUriError> {
ResourceUri::new(value)
}
fn model_for(version: ProtocolVersion) -> AuthorizationModel {
authorization_model(version)
}
#[test]
fn a_duplicated_resource_is_one_value() -> Result<(), Failure> {
let api = resource("https://api.example.com/app/")?;
let parameters = ResourceParameters::many([api.clone(), api]);
assert_eq!(parameters.len(), 1);
assert!(!parameters.is_empty());
Ok(())
}
#[test]
fn distinct_resources_are_kept_in_request_order() -> Result<(), Failure> {
let first = resource("https://api.example.com/one/")?;
let second = resource("https://api.example.com/two/")?;
let parameters = ResourceParameters::many([first, second]);
assert_eq!(parameters.len(), 2);
assert_eq!(
parameters.iter().collect::<Vec<_>>(),
vec!["https://api.example.com/one", "https://api.example.com/two"]
);
Ok(())
}
#[test]
fn order_does_not_change_what_a_request_names() -> Result<(), Failure> {
let one = resource("https://api.example.com/one")?;
let two = resource("https://api.example.com/two")?;
let binding = ResourceBinding::bind(
&model_for(ProtocolVersion::V2025_06_18),
ResourceParameters::many([one.clone(), two.clone()]),
)?;
let reordered = ResourceParameters::many([two, one]);
assert!(
binding.matches_token_request(&reordered),
"a reordered set names the same resources"
);
Ok(())
}
#[test]
fn a_redemption_for_a_different_resource_does_not_match() -> Result<(), Failure> {
let granted = resource("https://api.example.com/granted")?;
let smuggled = resource("https://evil.example.com")?;
let binding = ResourceBinding::bind(
&model_for(ProtocolVersion::V2025_06_18),
ResourceParameters::single(&granted),
)?;
assert!(
!binding.matches_token_request(&ResourceParameters::single(&smuggled)),
"a token request for another resource must not match the grant"
);
assert!(
!binding.matches_token_request(&ResourceParameters::default()),
"omitting the parameter must not match a bound grant"
);
assert!(binding.matches_token_request(&ResourceParameters::single(&granted)));
Ok(())
}
#[test]
fn a_revision_that_requires_indicators_refuses_an_empty_request() {
for version in [
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_11_25,
ProtocolVersion::V2026_07_28,
] {
let model = model_for(version);
assert!(
model.resource_indicators_required,
"{version:?} must require resource indicators"
);
assert_eq!(
ResourceBinding::bind(&model, ResourceParameters::default()),
Err(ResourceIndicatorError::Missing),
"{version:?} must refuse an authorization request naming no resource"
);
}
}
#[test]
fn a_revision_that_does_not_require_indicators_accepts_an_empty_request() {
for version in [ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_03_26] {
let model = model_for(version);
assert!(
!model.resource_indicators_required,
"{version:?} must not require resource indicators"
);
let binding = ResourceBinding::bind(&model, ResourceParameters::default());
assert!(
binding.is_ok(),
"{version:?} must allow a request naming no resource"
);
}
}
#[test]
fn the_binding_repeats_its_own_values_at_redemption() -> Result<(), Failure> {
let api = resource("https://api.example.com/app")?;
let requested = ResourceParameters::single(&api);
let binding =
ResourceBinding::bind(&model_for(ProtocolVersion::V2025_06_18), requested.clone())?;
assert_eq!(binding.token_request(), &requested);
assert!(binding.matches_token_request(&requested));
Ok(())
}
}