mcpr-integrations 0.4.70

External integrations for mcpr: cloud event sink, API client, and SQLite request storage
Documentation
//! Event types sent from the proxy hot path to the background storage writer.
//!
//! These are the "write side" of the storage engine. The proxy constructs events
//! from MCP request/response data and sends them through an mpsc channel —
//! never blocking the hot path.
//!
//! # Separation from McprEvent
//!
//! `mcpr-integrations` has its own `McprEvent` for cloud/stdout emission.
//! `StoreEvent` is intentionally independent — different schema, different
//! lifecycle, no coupling between crates. The conversion happens at the
//! proxy's emit stage (`mcpr_core::proxy::pipeline::emit`) where both are
//! built from the same locals.

/// Top-level event enum sent through the storage channel.
///
/// The background writer dispatches on this to decide which table to write to
/// and whether to INSERT, UPDATE, or close a session.
#[derive(Debug)]
pub enum StoreEvent {
    /// A completed MCP request — one row in the `requests` table.
    Request(RequestEvent),

    /// A new MCP session from an `initialize` handshake — one row in `sessions`.
    Session(SessionEvent),

    /// A session was cleanly closed (transport disconnect).
    /// Updates `ended_at` on the existing session row.
    SessionClosed {
        session_id: String,
        /// Unix milliseconds (UTC) when the close was detected.
        ended_at: i64,
    },

    /// A new `SchemaVersion` was created by the proxy's SchemaManager.
    /// Writer UPSERTs `server_schema` and appends change rows.
    SchemaVersion(SchemaVersionEvent),
}

/// One completed MCP request, ready to be written to the `requests` table.
///
/// Built from the same data the proxy already computes for logging and cloud
/// emission (method, tool name, latency, status, sizes). No extra parsing needed.
#[derive(Debug)]
pub struct RequestEvent {
    /// UUIDv7 generated by mcpr — time-ordered, globally unique.
    /// Used for cloud sink correlation and as the primary lookup key.
    pub request_id: String,

    /// Unix milliseconds (UTC) when the request was received.
    /// Millisecond resolution is sufficient for latency tracking and avoids
    /// i64 overflow concerns that nanosecond timestamps would introduce.
    pub ts: i64,

    /// Proxy name from config (e.g., "api-server").
    /// Tags every row so a shared database can hold data from multiple proxies.
    pub proxy: String,

    /// MCP session ID from the `mcp-session-id` header.
    /// Nullable: pre-handshake probes or malformed clients may not have one.
    /// Soft foreign key to `sessions.session_id` (no hard FK constraint —
    /// avoids ordering edge cases in the async writer).
    pub session_id: Option<String>,

    /// MCP JSON-RPC method (e.g., "tools/call", "resources/read", "initialize").
    /// Stored as-is from the protocol layer — no normalization.
    pub method: String,

    /// Tool name for `tools/call` requests, None for other methods.
    /// Extracted from the JSON-RPC params by the proxy's MCP parser.
    pub tool: Option<String>,

    /// Resource URI for `resources/{read,subscribe,unsubscribe}`.
    pub resource_uri: Option<String>,

    /// Prompt name for `prompts/get`.
    pub prompt_name: Option<String>,

    /// Wall-clock time from proxy receiving the request to getting the upstream response.
    /// Includes network round-trip to upstream — this is what the AI client experiences.
    /// Stored in microseconds for sub-millisecond precision.
    pub latency_us: i64,

    /// Whether the request succeeded, failed, or timed out.
    pub status: RequestStatus,

    /// MCP error code if `status` is `Error` (e.g., "-32600", "-32601").
    /// None for successful requests.
    pub error_code: Option<String>,

    /// Human-readable error message, truncated to 512 chars at the call site.
    /// None for successful requests.
    pub error_msg: Option<String>,

    /// Request payload size in bytes. None if not measured.
    pub bytes_in: Option<i64>,

    /// Response payload size in bytes. None if not measured.
    pub bytes_out: Option<i64>,
}

/// Emitted once per MCP session, when the proxy intercepts the `initialize` handshake.
///
/// The `clientInfo` field in the MCP `initialize` request is the authoritative source
/// of client identity — we don't guess from headers or user-agent strings.
#[derive(Debug)]
pub struct SessionEvent {
    /// MCP session ID — primary key in the `sessions` table.
    pub session_id: String,

    /// Which proxy this session connected through.
    pub proxy: String,

    /// Unix milliseconds (UTC) of the `initialize` request.
    pub started_at: i64,

    /// Client name from `clientInfo.name` (e.g., "claude-desktop", "cursor").
    /// None if the client omits `clientInfo` (non-compliant but possible).
    pub client_name: Option<String>,

    /// Client version from `clientInfo.version` (e.g., "1.2.0").
    pub client_version: Option<String>,

    /// Normalized platform identifier derived from client_name.
    /// Values: "claude", "chatgpt", "vscode", "cursor", "unknown".
    /// Normalization happens at write time via a static lookup table.
    pub client_platform: Option<String>,
}

/// A new `SchemaVersion` produced by the proxy's `SchemaManager`.
///
/// The writer trusts the event: `SchemaManager` guarantees the payload
/// differs from the previous version (unchanged ingests return `None`
/// and produce no event). The writer only diffs against the prior row
/// to populate `schema_changes`.
#[derive(Debug)]
pub struct SchemaVersionEvent {
    pub ts: i64,
    /// Proxy name from config (upstream identity).
    pub proxy: String,
    pub upstream_url: String,
    pub method: String,
    /// Merged `result` payload as JSON string.
    pub payload: String,
    /// SHA-256 hex digest of the canonical payload.
    pub content_hash: String,
}

/// Outcome of a single MCP request.
///
/// Maps to the `status` CHECK constraint in the `requests` table:
/// `CHECK (status IN ('ok', 'error', 'timeout'))`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestStatus {
    /// Upstream returned a valid MCP response (may still contain application-level errors).
    Ok,
    /// Upstream returned an MCP error response (JSON-RPC error object).
    Error,
    /// The request timed out before the upstream responded.
    Timeout,
}

impl RequestStatus {
    /// SQL-safe string representation, matching the CHECK constraint values.
    pub fn as_str(&self) -> &'static str {
        match self {
            RequestStatus::Ok => "ok",
            RequestStatus::Error => "error",
            RequestStatus::Timeout => "timeout",
        }
    }
}