harn-vm 0.8.91

Async bytecode virtual machine for the Harn programming language
Documentation
use super::*;

pub(crate) fn parse_jsonrpc_result(msg: serde_json::Value) -> Result<serde_json::Value, VmError> {
    if let Some(error) = msg.get("error") {
        return Err(jsonrpc_error_to_vm_error(error));
    }
    Ok(msg
        .get("result")
        .cloned()
        .unwrap_or(serde_json::Value::Null))
}

pub(crate) fn jsonrpc_error_to_vm_error(error: &serde_json::Value) -> VmError {
    let message = error
        .get("message")
        .and_then(|v| v.as_str())
        .unwrap_or("Unknown MCP error");
    let code = error.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
    VmError::Thrown(VmValue::String(std::sync::Arc::from(format!(
        "MCP error ({code}): {message}"
    ))))
}

pub(crate) fn client_request_rejection(msg: &serde_json::Value) -> Option<serde_json::Value> {
    let request_id = msg.get("id")?.clone();
    let method = msg.get("method").and_then(|value| value.as_str())?;
    Some(crate::jsonrpc::error_response(
        request_id,
        -32601,
        &format!("Method not found: {method}"),
    ))
}

pub(crate) fn resolve_protocol_mode(
    protocol_mode: Option<&str>,
    protocol_version: Option<&str>,
) -> Result<McpProtocolMode, VmError> {
    let normalized = protocol_mode.map(|value| value.trim().to_ascii_lowercase());
    match normalized.as_deref() {
        Some("legacy") | Some("2025") | Some("2025-11-25") => Ok(McpProtocolMode::Legacy),
        Some("rc") | Some("modern") | Some("draft") | Some("draft-2026-v1") => {
            Ok(McpProtocolMode::Modern)
        }
        Some(other) => Err(VmError::Runtime(format!(
            "mcp_connect: unsupported protocol_mode {other:?}; expected \"legacy\" or \"rc\""
        ))),
        None if protocol_version == Some(DRAFT_PROTOCOL_VERSION) => Ok(McpProtocolMode::Modern),
        None => Ok(McpProtocolMode::Legacy),
    }
}

pub(crate) fn mcp_connect_options(value: Option<&VmValue>) -> Result<McpConnectOptions, VmError> {
    let Some(value) = value else {
        return Ok(McpConnectOptions {
            protocol_mode: McpProtocolMode::Legacy,
            protocol_version: PROTOCOL_VERSION.to_string(),
        });
    };
    let VmValue::Dict(options) = value else {
        return Err(VmError::Runtime(format!(
            "mcp_connect: options must be a dict, got {}",
            value.type_name()
        )));
    };
    let protocol_mode_value = options.get("protocol_mode").map(|value| value.display());
    let protocol_version_value = options.get("protocol_version").map(|value| value.display());
    let protocol_mode = resolve_protocol_mode(
        protocol_mode_value.as_deref(),
        protocol_version_value.as_deref(),
    )?;
    let protocol_version = protocol_version_value
        .unwrap_or_else(|| default_protocol_version(protocol_mode).to_string());
    Ok(McpConnectOptions {
        protocol_mode,
        protocol_version,
    })
}

pub(crate) fn default_protocol_version(mode: McpProtocolMode) -> &'static str {
    match mode {
        McpProtocolMode::Legacy => PROTOCOL_VERSION,
        McpProtocolMode::Modern => DRAFT_PROTOCOL_VERSION,
    }
}

pub(crate) fn legacy_initialize_params(protocol_version: &str) -> serde_json::Value {
    serde_json::json!({
        "protocolVersion": protocol_version,
        "capabilities": legacy_client_capabilities(),
        "clientInfo": client_info(),
    })
}

pub(crate) fn client_info() -> serde_json::Value {
    serde_json::json!({
        "name": "harn",
        "version": env!("CARGO_PKG_VERSION"),
    })
}

pub(crate) fn legacy_client_capabilities() -> serde_json::Value {
    serde_json::json!({
        "elicitation": {},
        "roots": {
            "listChanged": true,
        },
        "sampling": {},
    })
}

pub(crate) fn modern_client_capabilities() -> serde_json::Value {
    serde_json::json!({
        "elicitation": {},
        "roots": {},
        "sampling": {},
    })
}

pub(crate) fn request_params_for_protocol(
    protocol_mode: McpProtocolMode,
    protocol_version: &str,
    params: serde_json::Value,
) -> serde_json::Value {
    if protocol_mode == McpProtocolMode::Legacy {
        return params;
    }

    let mut object = match params {
        serde_json::Value::Object(object) => object,
        serde_json::Value::Null => serde_json::Map::new(),
        other => serde_json::Map::from_iter([("value".to_string(), other)]),
    };
    let mut meta = object
        .remove("_meta")
        .and_then(|value| value.as_object().cloned())
        .unwrap_or_default();
    meta.insert(
        RC_META_KEY_PROTOCOL_VERSION.to_string(),
        serde_json::Value::String(protocol_version.to_string()),
    );
    meta.insert(RC_META_KEY_CLIENT_INFO.to_string(), client_info());
    meta.insert(
        RC_META_KEY_CLIENT_CAPABILITIES.to_string(),
        modern_client_capabilities(),
    );
    object.insert("_meta".to_string(), serde_json::Value::Object(meta));
    serde_json::Value::Object(object)
}

pub(crate) fn maybe_retry_unsupported_protocol(
    protocol_mode: McpProtocolMode,
    protocol_version: &mut String,
    msg: &serde_json::Value,
) -> bool {
    if protocol_mode != McpProtocolMode::Modern {
        return false;
    }
    let Some(error) = msg.get("error") else {
        return false;
    };
    if error.get("code").and_then(|value| value.as_i64()) != Some(UNSUPPORTED_PROTOCOL_VERSION_CODE)
    {
        return false;
    }
    let supported = error
        .get("data")
        .and_then(|data| data.get("supported"))
        .and_then(|value| value.as_array())
        .into_iter()
        .flatten()
        .filter_map(|value| value.as_str())
        .collect::<Vec<_>>();
    let Some(selected) = select_supported_protocol_version(&supported) else {
        return false;
    };
    if selected == protocol_version {
        return false;
    }
    *protocol_version = selected.to_string();
    true
}

pub(crate) fn should_fallback_to_legacy_http_discovery(
    protocol_mode: McpProtocolMode,
    method: &str,
    status: u16,
) -> bool {
    protocol_mode == McpProtocolMode::Modern
        && method == "server/discover"
        && matches!(status, 400 | 404 | 405)
}

pub(crate) fn http_discovery_fallback_response(id: Option<u64>) -> serde_json::Value {
    crate::jsonrpc::error_response(
        id.map(serde_json::Value::from)
            .unwrap_or(serde_json::Value::Null),
        -32601,
        "Modern MCP discovery was not recognized",
    )
}

pub(crate) fn select_supported_protocol_version(supported: &[&str]) -> Option<&'static str> {
    [DRAFT_PROTOCOL_VERSION, PROTOCOL_VERSION]
        .into_iter()
        .find(|candidate| supported.iter().any(|value| value == candidate))
}

pub(crate) fn is_method_not_found_response(msg: &serde_json::Value) -> bool {
    msg.get("error")
        .and_then(|error| error.get("code"))
        .and_then(|code| code.as_i64())
        == Some(-32601)
}

pub(crate) fn extract_tool_headers(tool: &serde_json::Value) -> Result<Vec<McpToolHeader>, String> {
    let Some(properties) = tool
        .get("inputSchema")
        .and_then(|schema| schema.get("properties"))
        .and_then(|value| value.as_object())
    else {
        return Ok(Vec::new());
    };

    let mut headers = Vec::new();
    let mut seen = std::collections::BTreeSet::new();
    for (parameter, schema) in properties {
        let Some(header_name) = schema.get(X_MCP_HEADER).and_then(|value| value.as_str()) else {
            continue;
        };
        validate_mcp_header_annotation(parameter, header_name, schema, &mut seen)?;
        headers.push(McpToolHeader {
            parameter: parameter.clone(),
            header_name: format!("Mcp-Param-{header_name}"),
        });
    }
    Ok(headers)
}

pub(crate) fn filter_tools_for_client(tools: &[serde_json::Value]) -> Vec<serde_json::Value> {
    tools
        .iter()
        .filter_map(|tool| {
            let name = tool
                .get("name")
                .and_then(|value| value.as_str())
                .unwrap_or("<unnamed>");
            match extract_tool_headers(tool) {
                Ok(_) => Some(tool.clone()),
                Err(reason) => {
                    tracing::warn!(tool = name, %reason, "excluding MCP tool from tools/list");
                    None
                }
            }
        })
        .collect()
}

pub(crate) fn validate_mcp_header_annotation(
    parameter: &str,
    header_name: &str,
    schema: &serde_json::Value,
    seen: &mut std::collections::BTreeSet<String>,
) -> Result<(), String> {
    if header_name.is_empty() {
        return Err(format!("{parameter}: x-mcp-header must not be empty"));
    }
    if !header_name.is_ascii() || header_name.bytes().any(|byte| matches!(byte, b' ' | b':')) {
        return Err(format!(
            "{parameter}: x-mcp-header must be ASCII and exclude space or colon"
        ));
    }
    if reqwest::header::HeaderName::from_bytes(format!("Mcp-Param-{header_name}").as_bytes())
        .is_err()
    {
        return Err(format!(
            "{parameter}: x-mcp-header does not form a valid HTTP header name"
        ));
    }
    let lower = header_name.to_ascii_lowercase();
    if !seen.insert(lower) {
        return Err(format!(
            "{parameter}: duplicate x-mcp-header value {header_name:?}"
        ));
    }
    let is_primitive = match schema.get("type") {
        Some(serde_json::Value::String(value)) => {
            matches!(value.as_str(), "string" | "number" | "integer" | "boolean")
        }
        Some(serde_json::Value::Array(values)) => values.iter().any(|value| {
            value
                .as_str()
                .is_some_and(|ty| matches!(ty, "string" | "number" | "integer" | "boolean"))
        }),
        _ => false,
    };
    if !is_primitive {
        return Err(format!(
            "{parameter}: x-mcp-header is only valid on primitive schema types"
        ));
    }
    Ok(())
}

pub(crate) fn encode_mcp_header_value(value: &serde_json::Value) -> Option<String> {
    let raw = match value {
        serde_json::Value::String(value) => value.clone(),
        serde_json::Value::Number(value) => value.to_string(),
        serde_json::Value::Bool(value) => value.to_string(),
        _ => return None,
    };
    if is_plain_mcp_header_value(&raw) {
        Some(raw)
    } else {
        Some(format!(
            "=?base64?{}?=",
            base64::engine::general_purpose::STANDARD.encode(raw.as_bytes())
        ))
    }
}

pub(crate) fn is_plain_mcp_header_value(value: &str) -> bool {
    !value.is_empty()
        && value.trim() == value
        && value
            .bytes()
            .all(|byte| matches!(byte, b'\t' | b' '..=b'~'))
}

pub(crate) fn extract_content_text(result: &serde_json::Value) -> String {
    if let Some(content) = result.get("content").and_then(|c| c.as_array()) {
        let texts: Vec<&str> = content
            .iter()
            .filter_map(|item| {
                if item.get("type").and_then(|t| t.as_str()) == Some("text") {
                    item.get("text").and_then(|t| t.as_str())
                } else {
                    None
                }
            })
            .collect();
        if texts.is_empty() {
            json_to_vm_value(result).display()
        } else {
            texts.join("\n")
        }
    } else {
        json_to_vm_value(result).display()
    }
}