webmcp 0.1.0

WebMCP manifest v1 definitions, safe HTML helpers, browser runtime, and optional Tower middleware.
Documentation
//! Framework-agnostic WebMCP manifest v1 definitions and safe HTML helpers.
//!
//! Spec baseline: WebMCP Draft CG Report 2026-10-02. Define immutable [`Tool`]s
//! at boot, then expose only the tools explicitly selected for each page.
//! Endpoints remain responsible for authorization, CSRF, and input validation.
//!
//! ```
//! use serde_json::json;
//! use webmcp::{Endpoint, Manifest, ScriptTagOptions, Tool, ToolDef, Transport};
//! let mut definition = ToolDef::new("list_tasks", "List my tasks.",
//!     json!({"type": "object"}), Endpoint::new("/api/tasks", "GET"));
//! definition.annotations.insert("read_only".into(), true);
//! let tool = Tool::new(definition)?;
//! let manifest = Manifest::build(&[&tool], Transport::default())?;
//! let html = manifest.to_script_tag(&ScriptTagOptions::default());
//! assert!(html.contains("data-webmcp-autostart"));
//! # Ok::<(), webmcp::DefinitionError>(())
//! ```
//!
//! With `tower`, `OriginTrialLayer` fills only missing response headers.
//! The `axum` feature enables Tower support for this documentation-only example;
//! the application supplies its own axum dependency:
//!
//! ```ignore
//! use axum::{routing::get, Router};
//! use webmcp::OriginTrialLayer;
//! let app: Router = Router::new().route("/", get(|| async { "ok" }))
//!     .layer(OriginTrialLayer::new("YOUR_TOKEN")?.warn_on_oac_opt_out(true));
//! ```

#![deny(missing_docs)]
#![deny(missing_debug_implementations)]

mod definition;
mod manifest;
mod origin_trial;
mod schema;

pub use definition::{DefinitionError, Endpoint, Registry, Tool, ToolDef};
pub use manifest::{Csrf, Manifest, ScriptTagOptions, Transport};
pub use origin_trial::*;

/// An error in a tool definition or declarative form's metadata.
pub type Error = DefinitionError;

/// The byte-for-byte copy of the shared browser runtime.
pub const RUNTIME_JS: &str = include_str!("../runtime/webmcp-runtime.js");
/// SHA-256 of the shipped runtime (also pinned in `conformance/RUNTIME.sha256`).
pub const RUNTIME_SHA256: &str = "287200d4ef89e16ccd0f0bbb035d9b484c3a761503f3df40f8f1cf5f3baae452";

pub(crate) fn escape_html(value: &str) -> String {
    value
        .replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#39;")
}

/// Render escaped, normative declarative form attributes.
///
/// Metadata must be trusted literals: HTML escaping does not stop prompt injection.
pub fn form_attrs(tool: &str, description: &str, autosubmit: bool) -> Result<String, Error> {
    definition::validate_name(tool)?;
    if description.is_empty() {
        return Err(DefinitionError::new(
            "form description must be a nonempty string",
        ));
    }
    Ok(format!(
        "toolname=\"{}\" tooldescription=\"{}\"{}",
        escape_html(tool),
        escape_html(description),
        if autosubmit { " toolautosubmit" } else { "" }
    ))
}

/// Render an escaped `toolparamdescription` attribute, even for pre-escaped input.
pub fn param_attr(description: &str) -> String {
    format!("toolparamdescription=\"{}\"", escape_html(description))
}

/// Render an external module script tag with an optional escaped CSP nonce.
/// Serve [`RUNTIME_JS`] at `src` using your framework's asset pipeline.
pub fn runtime_script_tag(src: &str, nonce: Option<&str>) -> String {
    format!(
        "<script type=\"module\" src=\"{}\"{}></script>",
        escape_html(src),
        nonce_attr(nonce)
    )
}

pub(crate) fn nonce_attr(nonce: Option<&str>) -> String {
    nonce
        .map(|value| format!(" nonce=\"{}\"", escape_html(value)))
        .unwrap_or_default()
}