Skip to main content

mcpr_integrations/store/
event.rs

1//! Event types sent from the proxy hot path to the background storage writer.
2//!
3//! These are the "write side" of the storage engine. The proxy constructs events
4//! from MCP request/response data and sends them through an mpsc channel —
5//! never blocking the hot path.
6//!
7//! # Separation from McprEvent
8//!
9//! `mcpr-integrations` has its own `McprEvent` for cloud/stdout emission.
10//! `StoreEvent` is intentionally independent — different schema, different
11//! lifecycle, no coupling between crates. The conversion happens at the
12//! call site in `mcp_handler.rs` where both are built from the same locals.
13
14/// Top-level event enum sent through the storage channel.
15///
16/// The background writer dispatches on this to decide which table to write to
17/// and whether to INSERT, UPDATE, or close a session.
18#[derive(Debug)]
19pub enum StoreEvent {
20    /// A completed MCP request — one row in the `requests` table.
21    Request(RequestEvent),
22
23    /// A new MCP session from an `initialize` handshake — one row in `sessions`.
24    Session(SessionEvent),
25
26    /// A session was cleanly closed (transport disconnect).
27    /// Updates `ended_at` on the existing session row.
28    SessionClosed {
29        session_id: String,
30        /// Unix milliseconds (UTC) when the close was detected.
31        ended_at: i64,
32    },
33
34    /// A captured schema discovery response, ready for storage.
35    SchemaCapture(SchemaCaptureEvent),
36
37    /// Mark a schema method as stale (e.g., `notifications/tools/list_changed`).
38    SchemaStale {
39        proxy: String,
40        upstream_url: String,
41        method: String,
42        ts: i64,
43    },
44}
45
46/// One completed MCP request, ready to be written to the `requests` table.
47///
48/// Built from the same data the proxy already computes for logging and cloud
49/// emission (method, tool name, latency, status, sizes). No extra parsing needed.
50#[derive(Debug)]
51pub struct RequestEvent {
52    /// UUIDv7 generated by mcpr — time-ordered, globally unique.
53    /// Used for cloud emitter correlation and as the primary lookup key.
54    pub request_id: String,
55
56    /// Unix milliseconds (UTC) when the request was received.
57    /// Millisecond resolution is sufficient for latency tracking and avoids
58    /// i64 overflow concerns that nanosecond timestamps would introduce.
59    pub ts: i64,
60
61    /// Proxy name from config (e.g., "api-server").
62    /// Tags every row so a shared database can hold data from multiple proxies.
63    pub proxy: String,
64
65    /// MCP session ID from the `mcp-session-id` header.
66    /// Nullable: pre-handshake probes or malformed clients may not have one.
67    /// Soft foreign key to `sessions.session_id` (no hard FK constraint —
68    /// avoids ordering edge cases in the async writer).
69    pub session_id: Option<String>,
70
71    /// MCP JSON-RPC method (e.g., "tools/call", "resources/read", "initialize").
72    /// Stored as-is from the protocol layer — no normalization.
73    pub method: String,
74
75    /// Tool name for `tools/call` requests, None for other methods.
76    /// Extracted from the JSON-RPC params by the proxy's MCP parser.
77    pub tool: Option<String>,
78
79    /// Wall-clock time from proxy receiving the request to getting the upstream response.
80    /// Includes network round-trip to upstream — this is what the AI client experiences.
81    /// Stored in microseconds for sub-millisecond precision.
82    pub latency_us: i64,
83
84    /// Whether the request succeeded, failed, or timed out.
85    pub status: RequestStatus,
86
87    /// MCP error code if `status` is `Error` (e.g., "-32600", "-32601").
88    /// None for successful requests.
89    pub error_code: Option<String>,
90
91    /// Human-readable error message, truncated to 512 chars at the call site.
92    /// None for successful requests.
93    pub error_msg: Option<String>,
94
95    /// Request payload size in bytes. None if not measured.
96    pub bytes_in: Option<i64>,
97
98    /// Response payload size in bytes. None if not measured.
99    pub bytes_out: Option<i64>,
100}
101
102/// Emitted once per MCP session, when the proxy intercepts the `initialize` handshake.
103///
104/// The `clientInfo` field in the MCP `initialize` request is the authoritative source
105/// of client identity — we don't guess from headers or user-agent strings.
106#[derive(Debug)]
107pub struct SessionEvent {
108    /// MCP session ID — primary key in the `sessions` table.
109    pub session_id: String,
110
111    /// Which proxy this session connected through.
112    pub proxy: String,
113
114    /// Unix milliseconds (UTC) of the `initialize` request.
115    pub started_at: i64,
116
117    /// Client name from `clientInfo.name` (e.g., "claude-desktop", "cursor").
118    /// None if the client omits `clientInfo` (non-compliant but possible).
119    pub client_name: Option<String>,
120
121    /// Client version from `clientInfo.version` (e.g., "1.2.0").
122    pub client_version: Option<String>,
123
124    /// Normalized platform identifier derived from client_name.
125    /// Values: "claude", "chatgpt", "vscode", "cursor", "unknown".
126    /// Normalization happens at write time via a static lookup table.
127    pub client_platform: Option<String>,
128}
129
130/// A schema discovery response captured from the proxy, ready for storage.
131#[derive(Debug)]
132pub struct SchemaCaptureEvent {
133    /// Unix milliseconds (UTC).
134    pub ts: i64,
135    /// Proxy name from config.
136    pub proxy: String,
137    /// Upstream MCP server URL.
138    pub upstream_url: String,
139    /// MCP method (e.g., "initialize", "tools/list").
140    pub method: String,
141    /// JSON string of the `result` field.
142    pub payload: String,
143    /// Pagination state.
144    pub page_status: mcpr_core::protocol::schema::PageStatus,
145}
146
147/// Outcome of a single MCP request.
148///
149/// Maps to the `status` CHECK constraint in the `requests` table:
150/// `CHECK (status IN ('ok', 'error', 'timeout'))`.
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum RequestStatus {
153    /// Upstream returned a valid MCP response (may still contain application-level errors).
154    Ok,
155    /// Upstream returned an MCP error response (JSON-RPC error object).
156    Error,
157    /// The request timed out before the upstream responded.
158    Timeout,
159}
160
161impl RequestStatus {
162    /// SQL-safe string representation, matching the CHECK constraint values.
163    pub fn as_str(&self) -> &'static str {
164        match self {
165            RequestStatus::Ok => "ok",
166            RequestStatus::Error => "error",
167            RequestStatus::Timeout => "timeout",
168        }
169    }
170}