elicitation 0.10.0

Conversational elicitation of strongly-typed Rust values via MCP
Documentation
//! `http::HeaderMap` elicitation (Collection pattern).
//!
//! Elicits headers as a newline-separated list of `Name: Value` pairs,
//! then parses into a `http::HeaderMap`.

use crate::{
    ElicitCommunicator, ElicitError, ElicitErrorKind, ElicitIntrospect, ElicitResult, Elicitation,
    ElicitationPattern, PatternDetails, Prompt, TypeMetadata, mcp,
};
use http::{HeaderMap, HeaderName, HeaderValue};

crate::default_style!(HeaderMap => HeaderMapStyle);

impl Prompt for HeaderMap {
    fn prompt() -> Option<&'static str> {
        Some(
            "Enter HTTP headers as newline-separated `Name: Value` pairs \
             (e.g. `Content-Type: application/json`). Leave blank for no headers:",
        )
    }
}

impl Elicitation for HeaderMap {
    type Style = HeaderMapStyle;

    #[tracing::instrument(skip(communicator))]
    async fn elicit<C: ElicitCommunicator>(communicator: &C) -> ElicitResult<Self> {
        tracing::debug!("Eliciting http::HeaderMap");

        let params = mcp::text_params(Self::prompt().unwrap_or("Enter HTTP headers:"));

        let result = communicator
            .call_tool(
                rmcp::model::CallToolRequestParams::new(mcp::tool_names::elicit_text())
                    .with_arguments(params),
            )
            .await?;

        let value = mcp::extract_value(result)?;
        let raw = mcp::parse_string(value)?;

        parse_header_map(&raw)
    }

    fn kani_proof() -> proc_macro2::TokenStream {
        crate::verification::proof_helpers::kani_trusted_opaque("header_map")
    }

    fn verus_proof() -> proc_macro2::TokenStream {
        crate::verification::proof_helpers::verus_trusted_opaque("header_map")
    }

    fn creusot_proof() -> proc_macro2::TokenStream {
        crate::verification::proof_helpers::creusot_trusted_opaque("header_map")
    }
}

impl ElicitIntrospect for HeaderMap {
    fn pattern() -> ElicitationPattern {
        ElicitationPattern::Primitive
    }

    fn metadata() -> TypeMetadata {
        TypeMetadata {
            type_name: "http::HeaderMap",
            description: Self::prompt(),
            details: PatternDetails::Primitive,
        }
    }
}

/// Parse a newline-separated `Name: Value` string into a `HeaderMap`.
fn parse_header_map(raw: &str) -> ElicitResult<HeaderMap> {
    let mut map = HeaderMap::new();

    for line in raw.lines() {
        let line = line.trim();
        if line.is_empty() {
            continue;
        }

        let Some((name, value)) = line.split_once(':') else {
            return Err(ElicitError::new(ElicitErrorKind::ParseError(format!(
                "Invalid header line (expected 'Name: Value'): {line}"
            ))));
        };

        let name = name.trim();
        let value = value.trim();

        let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "Invalid header name '{name}': {e}"
            )))
        })?;

        let header_value = HeaderValue::from_str(value).map_err(|e| {
            ElicitError::new(ElicitErrorKind::ParseError(format!(
                "Invalid header value '{value}': {e}"
            )))
        })?;

        map.insert(header_name, header_value);
    }

    Ok(map)
}