systemprompt-mcp 0.9.1

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
//! HTML form artifact renderer (`ArtifactType::Form`).

use super::form_field::FormField;
use super::html::{
    HtmlBuilder, base_styles, html_escape, json_to_js_literal, 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 FormRenderer;

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

    fn extract_fields(artifact: &Artifact) -> Vec<FormField> {
        let mut fields = Vec::new();

        if let Some(hints) = &artifact.metadata.rendering_hints {
            if let Some(field_defs) = hints.get("fields").and_then(JsonValue::as_array) {
                for def in field_defs {
                    if let Some(field) = FormField::from_json(def) {
                        fields.push(field);
                    }
                }
            }
        }

        for part in &artifact.parts {
            if let Some(data) = part.as_data() {
                if let Some(form_fields) = data.get("fields").and_then(JsonValue::as_array) {
                    for def in form_fields {
                        if let Some(field) = FormField::from_json(def) {
                            if !fields.iter().any(|f| f.name == field.name) {
                                fields.push(field);
                            }
                        }
                    }
                }
            }
        }

        fields
    }

    fn extract_submit_tool(artifact: &Artifact) -> Option<String> {
        artifact
            .metadata
            .rendering_hints
            .as_ref()
            .and_then(|h| h.get("submit_tool"))
            .and_then(JsonValue::as_str)
            .map(String::from)
    }
}

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

    async fn render(&self, artifact: &Artifact) -> McpDomainResult<UiResource> {
        let fields = Self::extract_fields(artifact);
        let submit_tool = Self::extract_submit_tool(artifact);
        let title = artifact.title.as_deref().unwrap_or("Form");

        let fields_html: String = fields.iter().map(FormField::render_html).collect();

        let fields_json: Vec<JsonValue> = fields
            .iter()
            .map(|f| {
                serde_json::json!({
                    "name": f.name,
                    "type": f.field_type,
                    "required": f.required
                })
            })
            .collect();

        let body = format!(
            r#"<div class="container">
    {title_html}
    {description_html}
    <form id="mcp-form" class="mcp-form">
        {fields}
        <div class="form-actions">
            <button type="submit" class="submit-btn">Submit</button>
            <button type="reset" class="reset-btn">Reset</button>
        </div>
    </form>
    <div id="form-message" class="form-message" style="display: none;"></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)
                )),
            fields = fields_html,
        );

        let script = format!(
            "{bridge}\nwindow.FORM_FIELDS = {fields_json};\nwindow.FORM_SUBMIT_TOOL = \
             {submit_tool};\n{app}",
            bridge = mcp_app_bridge_script(),
            fields_json = json_to_js_literal(&serde_json::json!(fields_json)),
            submit_tool = submit_tool.map_or_else(|| "null".to_string(), |t| format!("\"{t}\"")),
            app = include_str!("assets/js/form.js"),
        );

        let html = HtmlBuilder::new(title)
            .add_style(base_styles())
            .add_style(form_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 form_styles() -> &'static str {
    include_str!("assets/css/form.css")
}