mcpr_core/proxy/pipeline/
parser.rs1use 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
12pub 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}