use crate::error::DispatchError;
use crate::targets::{
CapabilityEntry, CapabilityOutcome, CapabilityProfile, Criticality, ProvidedBy, TargetId,
};
use serde::Deserialize;
use std::borrow::Cow;
use std::collections::HashMap;
pub const ENGINE: &str = "claude-code";
#[derive(Debug, Clone, Deserialize)]
pub struct ProfileEntrySpec {
pub outcome: CapabilityOutcome,
#[serde(default)]
pub via: Option<String>,
pub provided_by: ProvidedBy,
pub criticality: Criticality,
#[serde(default)]
pub note: Option<String>,
}
impl From<ProfileEntrySpec> for CapabilityEntry {
fn from(spec: ProfileEntrySpec) -> Self {
CapabilityEntry {
outcome: spec.outcome,
via: spec.via.map(Cow::Owned),
provided_by: spec.provided_by,
criticality: spec.criticality,
note: spec.note.map(Cow::Owned),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct ToolBindingSpec {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub names: Vec<String>,
pub source: String,
}
impl ToolBindingSpec {
fn all_names(&self) -> Vec<String> {
self.name
.iter()
.cloned()
.chain(self.names.iter().cloned())
.collect()
}
}
#[derive(Debug, Clone)]
pub struct ToolBinding {
pub names: Vec<String>,
pub source: String,
}
pub type ToolMap = HashMap<String, ToolBinding>;
#[derive(Debug, Clone, Deserialize)]
pub struct ProviderFragment {
pub fragment_version: String,
pub provider: String,
pub engine: String,
#[serde(default)]
pub mode: Option<String>,
#[serde(default)]
pub capabilities: HashMap<String, ProfileEntrySpec>,
#[serde(default)]
pub tools: HashMap<String, ToolBindingSpec>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
enum ProviderFile {
List { providers: Vec<ProviderFragment> },
Single(ProviderFragment),
}
pub fn parse_provider_fragments(raw: &str) -> Result<Vec<ProviderFragment>, DispatchError> {
let file: ProviderFile = serde_yaml::from_str(raw)
.map_err(|e| DispatchError(format!("failed to parse provider fragment: {e}")))?;
Ok(match file {
ProviderFile::Single(fragment) => vec![fragment],
ProviderFile::List { providers } => providers,
})
}
pub struct ComposedTarget {
pub profile: CapabilityProfile,
pub tool_map: ToolMap,
}
fn validate_source_grammar(
provider_id: &str,
capability: &str,
source: &str,
) -> Result<(), DispatchError> {
if let Some(rest) = source.strip_prefix("mcp:") {
let mut parts = rest.splitn(2, '/');
let server = parts.next().unwrap_or("");
let tool = parts.next();
if server.is_empty() || tool.is_none_or(str::is_empty) {
return Err(DispatchError(format!(
"provider '{provider_id}': capability '{capability}' has a malformed tool source \
'{source}' — expected 'mcp:<server>/<tool>'"
)));
}
} else if source.is_empty() || source.contains(':') || source.contains('/') {
return Err(DispatchError(format!(
"provider '{provider_id}': capability '{capability}' has a malformed tool source \
'{source}' — a bare mechanism label must not contain ':' or '/'"
)));
}
Ok(())
}
fn validate_via_source_coherence(
provider_id: &str,
capability: &str,
via: Option<&str>,
source: &str,
) -> Result<(), DispatchError> {
let Some(rest) = source.strip_prefix("mcp:") else {
return Ok(());
};
let Some(server) = rest.split('/').next().filter(|s| !s.is_empty()) else {
return Ok(());
};
let expected = format!("mcp:{server}");
if via != Some(expected.as_str()) {
return Err(DispatchError(format!(
"provider '{provider_id}': capability '{capability}' has tool source '{source}' \
(server '{server}') but via is {via:?} — via should be '{expected}' so the report and \
the tool grant agree on the backing server"
)));
}
Ok(())
}
fn validate_names_match_source(
provider_id: &str,
capability: &str,
names: &[String],
source: &str,
) -> Result<(), DispatchError> {
if names.is_empty() {
return Err(DispatchError(format!(
"provider '{provider_id}': tool binding for '{capability}' grants no tool — set `name` \
or `names`"
)));
}
let Some(rest) = source.strip_prefix("mcp:") else {
return Ok(());
};
let server = rest.split('/').next().unwrap_or("");
let expected_prefix = format!("mcp__{server}__");
for name in names {
if !name.starts_with(&expected_prefix) {
return Err(DispatchError(format!(
"provider '{provider_id}': tool binding for '{capability}' grants '{name}', which \
cannot come from server '{server}' — on {ENGINE} an MCP tool is spelled \
'{expected_prefix}<tool>'"
)));
}
}
Ok(())
}
fn base_locks_capability(base: &CapabilityProfile, capability: &str) -> bool {
base.get(capability).is_some_and(|e| {
e.criticality == Criticality::SafetyCritical || e.outcome != CapabilityOutcome::Fail
})
}
pub fn compose_target(
base_profile: CapabilityProfile,
base_tool_map: ToolMap,
providers: &[ProviderFragment],
target_id: TargetId,
) -> Result<ComposedTarget, DispatchError> {
let base_profile_snapshot = base_profile.clone();
let base_tool_keys: Vec<String> = base_tool_map.keys().cloned().collect();
let mut profile = base_profile;
let mut tool_map = base_tool_map;
let mut claimed: HashMap<String, usize> = HashMap::new();
for (idx, fragment) in providers.iter().enumerate() {
if fragment.engine != ENGINE {
return Err(DispatchError(format!(
"provider '{}': engine '{}' does not match target engine '{ENGINE}'",
fragment.provider, fragment.engine
)));
}
if let Some(mode) = fragment.mode.as_deref() {
if mode != target_id.mode() {
continue;
}
}
for (capability, spec) in &fragment.capabilities {
if base_locks_capability(&base_profile_snapshot, capability) {
return Err(DispatchError(format!(
"provider '{}': capability '{capability}' is already provided by the base \
target and cannot be redefined",
fragment.provider
)));
}
claim_capability(&mut claimed, providers, capability, idx, fragment)?;
profile.insert(capability.clone(), spec.clone().into());
}
for (capability, spec) in &fragment.tools {
let names = spec.all_names();
validate_source_grammar(&fragment.provider, capability, &spec.source)?;
validate_via_source_coherence(
&fragment.provider,
capability,
profile.get(capability).and_then(|e| e.via.as_deref()),
&spec.source,
)?;
validate_names_match_source(&fragment.provider, capability, &names, &spec.source)?;
if base_locks_capability(&base_profile_snapshot, capability)
|| base_tool_keys.iter().any(|k| k == capability)
{
return Err(DispatchError(format!(
"provider '{}': tool binding for '{capability}' is already provided by the \
base target and cannot be redefined",
fragment.provider
)));
}
claim_capability(&mut claimed, providers, capability, idx, fragment)?;
if !profile.contains_key(capability) {
return Err(DispatchError(format!(
"provider '{}': tool binding for '{capability}' has no corresponding \
capability profile entry (from the base or this provider)",
fragment.provider
)));
}
tool_map.insert(
capability.clone(),
ToolBinding {
names,
source: spec.source.clone(),
},
);
}
}
Ok(ComposedTarget { profile, tool_map })
}
fn claim_capability(
claimed: &mut HashMap<String, usize>,
providers: &[ProviderFragment],
capability: &str,
idx: usize,
fragment: &ProviderFragment,
) -> Result<(), DispatchError> {
match claimed.get(capability) {
Some(&owner) if owner != idx => Err(DispatchError(format!(
"capability '{capability}' is claimed by both provider '{}' and provider '{}' — at \
most one provider source per capability",
providers[owner].provider, fragment.provider
))),
Some(_) => Ok(()),
None => {
claimed.insert(capability.to_string(), idx);
Ok(())
}
}
}
pub fn compose_for_conformance(
base_profile: CapabilityProfile,
providers: &[ProviderFragment],
target_id: TargetId,
) -> Result<Vec<String>, DispatchError> {
let composed = compose_target(base_profile, ToolMap::new(), providers, target_id)?;
Ok(composed.profile.into_keys().collect())
}