systemprompt-mcp 0.11.2

Native Model Context Protocol (MCP) implementation for systemprompt.io. Orchestration, per-server OAuth2, RBAC middleware, and tool-call governance — the core of the AI governance pipeline.
Documentation
use super::html::{HtmlBuilder, base_styles, html_escape, mcp_app_bridge_script};
use crate::error::McpDomainResult;
use crate::services::ui_renderer::{CspPolicy, UiRenderer, UiResource};
use async_trait::async_trait;
use serde_json::Value as JsonValue;
use systemprompt_models::a2a::Artifact;
use systemprompt_models::artifacts::ArtifactType;

#[derive(Debug, Clone, Copy, Default)]
pub struct ListRenderer;

impl ListRenderer {
    pub const fn new() -> Self {
        Self
    }

    fn extract_items(artifact: &Artifact) -> Vec<ListItem> {
        let mut items = Vec::new();

        for part in &artifact.parts {
            if let Some(data) = part.as_data() {
                if let Some(arr) = data.as_array() {
                    for item in arr {
                        if let Some(list_item) = ListItem::from_json(item) {
                            items.push(list_item);
                        }
                    }
                } else if let Some(obj) = data.as_object() {
                    if let Some(items_arr) = obj.get("items").and_then(JsonValue::as_array) {
                        for item in items_arr {
                            if let Some(list_item) = ListItem::from_json(item) {
                                items.push(list_item);
                            }
                        }
                    }
                }
            }
        }

        items
    }

    fn extract_list_style(artifact: &Artifact) -> ListStyle {
        artifact
            .metadata
            .rendering_hints
            .as_ref()
            .and_then(|h| h.get("style"))
            .and_then(JsonValue::as_str)
            .map_or(ListStyle::Unordered, |s| match s {
                "ordered" | "numbered" => ListStyle::Ordered,
                "none" => ListStyle::None,
                _ => ListStyle::Unordered,
            })
    }
}

#[derive(Debug)]
struct ListItem {
    title: String,
    description: Option<String>,
    icon: Option<String>,
    link: Option<String>,
}

impl ListItem {
    fn from_json(value: &JsonValue) -> Option<Self> {
        if let Some(s) = value.as_str() {
            return Some(Self {
                title: s.to_owned(),
                description: None,
                icon: None,
                link: None,
            });
        }

        let title = value
            .get("title")
            .or_else(|| value.get("name"))
            .or_else(|| value.get("label"))
            .and_then(JsonValue::as_str)?
            .to_owned();

        Some(Self {
            title,
            description: value
                .get("description")
                .or_else(|| value.get("subtitle"))
                .and_then(JsonValue::as_str)
                .map(String::from),
            icon: value
                .get("icon")
                .and_then(JsonValue::as_str)
                .map(String::from),
            link: value
                .get("link")
                .or_else(|| value.get("url"))
                .and_then(JsonValue::as_str)
                .map(String::from),
        })
    }

    fn render_html(&self, index: usize) -> String {
        let icon_html = self.icon.as_ref().map_or_else(String::new, |i| {
            format!(r#"<span class="item-icon">{}</span>"#, html_escape(i))
        });

        let title_html = self.link.as_ref().map_or_else(
            || {
                format!(
                    r#"<span class="item-title">{}</span>"#,
                    html_escape(&self.title)
                )
            },
            |link| {
                format!(
                    r#"<a href="{}" class="item-link" target="_blank" rel="noopener">{}</a>"#,
                    html_escape(link),
                    html_escape(&self.title)
                )
            },
        );

        let description_html = self.description.as_ref().map_or_else(String::new, |d| {
            format!(r#"<p class="item-description">{}</p>"#, html_escape(d))
        });

        format!(
            r#"<li class="list-item" data-index="{index}">
    {icon}{title}
    {description}
</li>"#,
            index = index,
            icon = icon_html,
            title = title_html,
            description = description_html,
        )
    }
}

#[derive(Debug, Clone, Copy)]
enum ListStyle {
    Ordered,
    Unordered,
    None,
}

impl ListStyle {
    const fn tag(self) -> &'static str {
        match self {
            Self::Ordered => "ol",
            Self::Unordered | Self::None => "ul",
        }
    }

    const fn class(self) -> &'static str {
        match self {
            Self::Ordered => "ordered-list",
            Self::Unordered => "unordered-list",
            Self::None => "unstyled-list",
        }
    }
}

#[async_trait]
impl UiRenderer for ListRenderer {
    fn artifact_type(&self) -> ArtifactType {
        ArtifactType::List
    }

    async fn render(&self, artifact: &Artifact) -> McpDomainResult<UiResource> {
        let items = Self::extract_items(artifact);
        let style = Self::extract_list_style(artifact);
        let title = artifact.title.as_deref().unwrap_or("List");

        let items_html: String = items
            .iter()
            .enumerate()
            .map(|(i, item)| item.render_html(i))
            .collect();

        let body = format!(
            r#"<div class="container">
    {title_html}
    {description_html}
    <{tag} class="item-list {class}">
        {items}
    </{tag}>
    <div class="list-info">
        <span class="item-count">{count} items</span>
    </div>
</div>"#,
            title_html = if title.is_empty() {
                String::new()
            } else {
                format!(r#"<h1 class="mcp-app-title">{}</h1>"#, html_escape(title))
            },
            description_html = artifact
                .description
                .as_ref()
                .map_or_else(String::new, |d| format!(
                    r#"<p class="mcp-app-description">{}</p>"#,
                    html_escape(d)
                )),
            tag = style.tag(),
            class = style.class(),
            items = items_html,
            count = items.len(),
        );

        let script = mcp_app_bridge_script();

        let html = HtmlBuilder::new(title)
            .add_style(base_styles())
            .add_style(list_styles())
            .body(&body)
            .add_script(script)
            .build();

        Ok(UiResource::new(html).with_csp(self.csp_policy()))
    }

    fn csp_policy(&self) -> CspPolicy {
        CspPolicy::strict()
    }
}

const fn list_styles() -> &'static str {
    include_str!("assets/css/list.css")
}