tandem-server 0.6.2

HTTP server for Tandem engine APIs
use tandem_types::TenantContext;

use crate::{AppState, AutomationMcpConnectionGrant, AutomationMcpRunAs};

pub(crate) fn automation_mcp_connection_grant_for_server<'a>(
    server_name: &str,
    allowed_connections: &'a [AutomationMcpConnectionGrant],
) -> Option<&'a AutomationMcpConnectionGrant> {
    allowed_connections
        .iter()
        .find(|grant| grant.server.eq_ignore_ascii_case(server_name))
}

pub(crate) fn automation_mcp_preflight_tenant_context(
    tenant_context: &TenantContext,
    connection_grant: Option<&AutomationMcpConnectionGrant>,
) -> Result<TenantContext, &'static str> {
    match connection_grant.and_then(|grant| grant.run_as.as_ref()) {
        Some(AutomationMcpRunAs::CurrentActor) | None => Ok(tenant_context.clone()),
        Some(AutomationMcpRunAs::ServicePrincipal { .. }) => {
            let mut service_tenant = tenant_context.clone();
            service_tenant.actor_id = None;
            Ok(service_tenant)
        }
        Some(
            AutomationMcpRunAs::AutomationPrincipal { .. }
            | AutomationMcpRunAs::SharedConnection { .. },
        ) => Err("grant_principal_not_executable"),
    }
}

pub(crate) async fn automation_mcp_remote_tool_names_for_tenant(
    state: &AppState,
    server_name: &str,
    tenant_context: &TenantContext,
) -> Vec<String> {
    let mut names = state
        .mcp
        .list_tools_for_tenant(tenant_context)
        .await
        .into_iter()
        .filter(|tool| tool.server_name == server_name)
        .map(|tool| {
            if tool.namespaced_name.trim().is_empty() {
                format!(
                    "mcp.{}.{}",
                    crate::http::mcp::mcp_namespace_segment(server_name),
                    tool.tool_name
                )
            } else {
                tool.namespaced_name
            }
        })
        .collect::<Vec<_>>();
    names.sort();
    names.dedup();
    names
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn service_principal_grant_uses_service_tenant_context_for_preflight() {
        let tenant = TenantContext::explicit("org-a", "workspace-a", Some("alice".to_string()));
        let grant = AutomationMcpConnectionGrant {
            server: "notion".to_string(),
            connection_id: None,
            run_as: Some(AutomationMcpRunAs::ServicePrincipal {
                principal_id: "tenant-service".to_string(),
            }),
        };

        let resolved = automation_mcp_preflight_tenant_context(&tenant, Some(&grant))
            .expect("service principal preflight context");

        assert_eq!(resolved.org_id, tenant.org_id);
        assert_eq!(resolved.workspace_id, tenant.workspace_id);
        assert!(resolved.actor_id.is_none());
    }
}