Skip to main content

mcpr_core/proxy/pipeline/
parser.rs

1//! Build a [`RequestContext`] from the raw HTTP request in a single pass.
2
3use std::time::Instant;
4
5use crate::protocol::session;
6use crate::protocol::{self as jsonrpc, McpMethod};
7use axum::body::Bytes;
8use axum::http::{HeaderMap, Method, header};
9
10use super::context::RequestContext;
11
12/// Parse every field we'll need later from `(method, path, headers, body)`.
13/// Never re-parsed downstream.
14pub fn build_request_context(
15    method: Method,
16    path: &str,
17    headers: &HeaderMap,
18    body: &Bytes,
19    start: Instant,
20) -> RequestContext {
21    let wants_sse = headers
22        .get(header::ACCEPT)
23        .and_then(|v| v.to_str().ok())
24        .map(|a| a.contains("text/event-stream"))
25        .unwrap_or(false);
26
27    let session_id = headers
28        .get("mcp-session-id")
29        .and_then(|v| v.to_str().ok())
30        .map(String::from);
31
32    let jsonrpc = jsonrpc::parse_body(body);
33    let mcp_method = jsonrpc.as_ref().map(|p| p.mcp_method());
34    let mcp_method_str = mcp_method.as_ref().map(|m| m.as_str().to_string());
35
36    let tool = jsonrpc.as_ref().and_then(|p| {
37        if p.mcp_method() == McpMethod::ToolsCall {
38            p.detail()
39        } else {
40            None
41        }
42    });
43    let is_batch = jsonrpc.as_ref().is_some_and(|p| p.is_batch);
44
45    let client_info_from_init = if mcp_method == Some(McpMethod::Initialize) {
46        jsonrpc
47            .as_ref()
48            .and_then(|p| p.first_params())
49            .and_then(session::parse_client_info)
50    } else {
51        None
52    };
53
54    RequestContext {
55        start,
56        http_method: method,
57        path: path.to_string(),
58        request_size: body.len(),
59        wants_sse,
60        session_id,
61        jsonrpc,
62        mcp_method,
63        mcp_method_str,
64        tool,
65        is_batch,
66        client_info_from_init,
67        client_name: None,
68        client_version: None,
69        tags: Vec::new(),
70    }
71}
72
73#[cfg(test)]
74#[allow(non_snake_case)]
75mod tests {
76    use super::*;
77
78    fn mk(method: Method, headers: HeaderMap, body: &[u8]) -> RequestContext {
79        build_request_context(
80            method,
81            "/mcp",
82            &headers,
83            &Bytes::copy_from_slice(body),
84            Instant::now(),
85        )
86    }
87
88    #[test]
89    fn initialize__extracts_client_info() {
90        let body = br#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"clientInfo":{"name":"claude-desktop","version":"1.2.0"}}}"#;
91        let ctx = mk(Method::POST, HeaderMap::new(), body);
92        let info = ctx.client_info_from_init.expect("client info");
93        assert_eq!(info.name, "claude-desktop");
94        assert_eq!(info.version.as_deref(), Some("1.2.0"));
95        assert_eq!(ctx.mcp_method, Some(McpMethod::Initialize));
96        assert_eq!(ctx.mcp_method_str.as_deref(), Some("initialize"));
97        assert!(ctx.tool.is_none());
98    }
99
100    #[test]
101    fn tools_call__sets_tool_name() {
102        let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"search"}}"#;
103        let ctx = mk(Method::POST, HeaderMap::new(), body);
104        assert_eq!(ctx.mcp_method, Some(McpMethod::ToolsCall));
105        assert_eq!(ctx.tool.as_deref(), Some("search"));
106    }
107
108    #[test]
109    fn batch__marks_is_batch() {
110        let body = br#"[{"jsonrpc":"2.0","id":1,"method":"tools/list"},{"jsonrpc":"2.0","id":2,"method":"resources/list"}]"#;
111        let ctx = mk(Method::POST, HeaderMap::new(), body);
112        assert!(ctx.is_batch);
113        assert!(ctx.jsonrpc.is_some());
114    }
115
116    #[test]
117    fn non_json__jsonrpc_is_none() {
118        let ctx = mk(Method::POST, HeaderMap::new(), b"not json at all");
119        assert!(ctx.jsonrpc.is_none());
120        assert!(ctx.mcp_method.is_none());
121        assert!(ctx.mcp_method_str.is_none());
122    }
123
124    #[test]
125    fn header__populates_session_id() {
126        let mut headers = HeaderMap::new();
127        headers.insert("mcp-session-id", "sess-abc".parse().unwrap());
128        let ctx = mk(Method::POST, headers, b"");
129        assert_eq!(ctx.session_id.as_deref(), Some("sess-abc"));
130    }
131
132    #[test]
133    fn accept_sse__sets_wants_sse() {
134        let mut headers = HeaderMap::new();
135        headers.insert(header::ACCEPT, "text/event-stream".parse().unwrap());
136        let ctx = mk(Method::GET, headers, b"");
137        assert!(ctx.wants_sse);
138    }
139
140    #[test]
141    fn request_size__reflects_body_bytes() {
142        let body = br#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#;
143        let ctx = mk(Method::POST, HeaderMap::new(), body);
144        assert_eq!(ctx.request_size, body.len());
145    }
146
147    #[test]
148    fn non_initialize__no_client_info() {
149        let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/list"}"#;
150        let ctx = mk(Method::POST, HeaderMap::new(), body);
151        assert!(ctx.client_info_from_init.is_none());
152    }
153}