use std::collections::BTreeMap;
use serde_json::Value;
use crate::mcp_oauth::StoredMcpToken;
use crate::mcp_presets::{self, IdentityProbeDescriptor, IdentityProbeKind};
pub fn descriptor_for(server_url: &str) -> Option<&'static IdentityProbeDescriptor> {
mcp_presets::presets()
.iter()
.find(|preset| preset.url == server_url)?
.identity
.as_ref()
}
pub fn display_identity(server_url: &str, token: &StoredMcpToken) -> Option<String> {
let descriptor = descriptor_for(server_url)?;
render_identity(descriptor, token.token_response_extra.as_ref())
}
pub fn render_identity(
descriptor: &IdentityProbeDescriptor,
token_response: Option<&Value>,
) -> Option<String> {
for source in &descriptor.sources {
if source.kind != IdentityProbeKind::TokenResponse {
continue;
}
let Some(payload) = token_response else {
continue;
};
let captures = capture(payload, &source.fields);
if let Some(rendered) = render(&descriptor.display_template, &captures) {
return Some(rendered);
}
}
None
}
fn capture(payload: &Value, fields: &BTreeMap<String, String>) -> BTreeMap<String, String> {
let mut out = BTreeMap::new();
for (name, path) in fields {
if let Some(text) = lookup(payload, path).and_then(scalar_to_string) {
if !text.is_empty() {
out.insert(name.clone(), text);
}
}
}
out
}
fn lookup<'a>(value: &'a Value, path: &str) -> Option<&'a Value> {
let mut current = value;
for segment in path.split('.') {
current = current.get(segment)?;
}
Some(current)
}
fn scalar_to_string(value: &Value) -> Option<String> {
match value {
Value::String(text) => Some(text.clone()),
Value::Number(number) => Some(number.to_string()),
Value::Bool(flag) => Some(flag.to_string()),
_ => None,
}
}
fn render(template: &str, captures: &BTreeMap<String, String>) -> Option<String> {
let mut out = String::new();
let mut any = false;
for (index, part) in template.split('{').enumerate() {
if index == 0 {
out.push_str(part);
continue;
}
match part.split_once('}') {
Some((key, rest)) => {
if let Some(value) = captures.get(key) {
out.push_str(value);
any = true;
}
out.push_str(rest);
}
None => {
out.push('{');
out.push_str(part);
}
}
}
if !any {
return None;
}
let tidied = tidy(&out);
(!tidied.is_empty()).then_some(tidied)
}
fn tidy(text: &str) -> String {
let without_empty = text.replace("<>", "").replace("()", "");
let collapsed = without_empty
.split_whitespace()
.collect::<Vec<_>>()
.join(" ");
collapsed
.trim_matches(|c: char| c.is_whitespace() || matches!(c, '—' | '-' | '|' | ',' | ':'))
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mcp_presets::{IdentityProbeKind, IdentityProbeSource};
use serde_json::json;
fn notion_descriptor() -> IdentityProbeDescriptor {
IdentityProbeDescriptor {
display_template: "{name} <{email}> — {workspace}".to_string(),
sources: vec![IdentityProbeSource {
kind: IdentityProbeKind::TokenResponse,
tool: None,
url: None,
fields: BTreeMap::from([
("name".to_string(), "owner.user.name".to_string()),
("email".to_string(), "owner.user.person.email".to_string()),
("workspace".to_string(), "workspace_name".to_string()),
]),
}],
}
}
#[test]
fn lookup_walks_dotted_paths() {
let payload = json!({"owner": {"user": {"name": "Jane"}}});
assert_eq!(
lookup(&payload, "owner.user.name").and_then(scalar_to_string),
Some("Jane".to_string())
);
assert!(lookup(&payload, "owner.user.missing").is_none());
assert!(lookup(&payload, "nope").is_none());
}
#[test]
fn renders_full_identity() {
let payload = json!({
"workspace_name": "Acme",
"owner": {"user": {"name": "Jane Doe", "person": {"email": "jane@acme.com"}}}
});
let rendered = render_identity(¬ion_descriptor(), Some(&payload));
assert_eq!(rendered.as_deref(), Some("Jane Doe <jane@acme.com> — Acme"));
}
#[test]
fn elides_missing_trailing_field() {
let payload = json!({
"owner": {"user": {"name": "Jane Doe", "person": {"email": "jane@acme.com"}}}
});
let rendered = render_identity(¬ion_descriptor(), Some(&payload));
assert_eq!(rendered.as_deref(), Some("Jane Doe <jane@acme.com>"));
}
#[test]
fn elides_missing_email_brackets() {
let payload = json!({"workspace_name": "Acme", "owner": {"user": {"name": "Jane"}}});
let rendered = render_identity(¬ion_descriptor(), Some(&payload));
assert_eq!(rendered.as_deref(), Some("Jane — Acme"));
}
#[test]
fn renders_only_workspace() {
let payload = json!({"workspace_name": "Acme"});
let rendered = render_identity(¬ion_descriptor(), Some(&payload));
assert_eq!(rendered.as_deref(), Some("Acme"));
}
#[test]
fn none_when_no_fields_resolve() {
let payload = json!({"unrelated": "x"});
assert!(render_identity(¬ion_descriptor(), Some(&payload)).is_none());
assert!(render_identity(¬ion_descriptor(), None).is_none());
}
#[test]
fn skips_live_probe_only_sources() {
let descriptor = IdentityProbeDescriptor {
display_template: "{name}".to_string(),
sources: vec![IdentityProbeSource {
kind: IdentityProbeKind::Tool,
tool: Some("whoami".to_string()),
url: None,
fields: BTreeMap::from([("name".to_string(), "name".to_string())]),
}],
};
let payload = json!({"name": "Jane"});
assert!(render_identity(&descriptor, Some(&payload)).is_none());
}
#[test]
fn display_identity_uses_catalog_descriptor_for_notion() {
let token = StoredMcpToken {
access_token: "a".into(),
refresh_token: None,
expires_at_unix: None,
token_endpoint: "https://auth/token".into(),
client_id: "c".into(),
client_secret: None,
token_endpoint_auth_method: "none".into(),
issuer: "https://auth".into(),
resource: "https://mcp.notion.com/mcp".into(),
scopes: None,
token_response_extra: Some(json!({
"workspace_name": "Acme",
"owner": {"user": {"name": "Jane Doe", "person": {"email": "jane@acme.com"}}}
})),
};
assert_eq!(
display_identity("https://mcp.notion.com/mcp", &token).as_deref(),
Some("Jane Doe <jane@acme.com> — Acme")
);
let mut other = token;
other.resource = "https://unknown.example/mcp".into();
assert!(display_identity("https://unknown.example/mcp", &other).is_none());
}
}