1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(untagged)]
11pub enum McpMessage {
12 Request(McpRequest),
14 Response(McpResponse),
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct McpRequest {
21 pub jsonrpc: String,
23
24 #[serde(skip_serializing_if = "Option::is_none")]
26 pub id: Option<serde_json::Value>,
27
28 pub method: String,
30
31 #[serde(skip_serializing_if = "Option::is_none")]
33 pub params: Option<serde_json::Value>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct McpResponse {
39 pub jsonrpc: String,
41
42 pub id: serde_json::Value,
44
45 #[serde(skip_serializing_if = "Option::is_none")]
47 pub result: Option<serde_json::Value>,
48
49 #[serde(skip_serializing_if = "Option::is_none")]
51 pub error: Option<McpError>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct McpError {
57 pub code: i64,
59 pub message: String,
61 #[serde(skip_serializing_if = "Option::is_none")]
63 pub data: Option<serde_json::Value>,
64}
65
66#[derive(Debug, Clone)]
68pub struct ToolCallParams {
69 pub tool_name: String,
71 pub arguments: serde_json::Value,
73}
74
75impl McpRequest {
76 pub fn is_tool_call(&self) -> bool {
78 self.method == "tools/call"
79 }
80
81 pub fn extract_tool_call(&self) -> Option<ToolCallParams> {
86 if !self.is_tool_call() {
87 return None;
88 }
89
90 let params = self.params.as_ref()?;
91 let name = params.get("name")?.as_str()?.to_string();
92 let arguments = params
93 .get("arguments")
94 .cloned()
95 .unwrap_or(serde_json::Value::Object(serde_json::Map::new()));
96
97 Some(ToolCallParams {
98 tool_name: name,
99 arguments,
100 })
101 }
102}
103
104impl McpMessage {
105 pub fn parse(json: &str) -> Result<Self, serde_json::Error> {
107 serde_json::from_str(json)
108 }
109
110 pub fn as_request(&self) -> Option<&McpRequest> {
112 match self {
113 McpMessage::Request(req) => Some(req),
114 _ => None,
115 }
116 }
117
118 pub fn as_response(&self) -> Option<&McpResponse> {
120 match self {
121 McpMessage::Response(resp) => Some(resp),
122 _ => None,
123 }
124 }
125
126 pub fn is_tool_call(&self) -> bool {
128 self.as_request().is_some_and(|r| r.is_tool_call())
129 }
130
131 pub fn to_json(&self) -> Result<String, serde_json::Error> {
133 serde_json::to_string(self)
134 }
135}
136
137pub fn deny_response(
146 request_id: serde_json::Value,
147 reason: &str,
148 tool_name: &str,
149 rule_id: &str,
150) -> McpResponse {
151 let code = if rule_id == "_default_deny" {
152 "POLICY_DEFAULT_DENY"
153 } else {
154 "POLICY_DENY"
155 };
156 let message = format!(
157 "[BLOCKED BY KVLAR]\n\
158 Tool: {tool_name}\n\
159 Policy rule: {rule_id}\n\
160 Reason: {reason}\n\n\
161 This action was blocked by the Kvlar security policy. \
162 Tell the user exactly what was blocked and why.",
163 );
164 McpResponse {
165 jsonrpc: "2.0".into(),
166 id: request_id,
167 result: Some(serde_json::json!({
168 "content": [{"type": "text", "text": message}],
169 "isError": true,
170 "_kvlar": {
171 "code": code,
172 "decision": "deny",
173 "rule_id": rule_id,
174 "reason": reason,
175 "tool": tool_name
176 }
177 })),
178 error: None,
179 }
180}
181
182pub fn approval_required_response(
190 request_id: serde_json::Value,
191 reason: &str,
192 tool_name: &str,
193 rule_id: &str,
194) -> McpResponse {
195 let message = format!(
196 "[KVLAR — APPROVAL REQUIRED]\n\
197 Tool: {tool_name}\n\
198 Policy rule: {rule_id}\n\
199 Reason: {reason}\n\n\
200 This action requires explicit human approval before it can proceed. \
201 Tell the user what action needs their approval and why.",
202 );
203 McpResponse {
204 jsonrpc: "2.0".into(),
205 id: request_id,
206 result: Some(serde_json::json!({
207 "content": [{"type": "text", "text": message}],
208 "isError": true,
209 "_kvlar": {
210 "code": "POLICY_APPROVAL_REQUIRED",
211 "decision": "require_approval",
212 "rule_id": rule_id,
213 "reason": reason,
214 "tool": tool_name
215 }
216 })),
217 error: None,
218 }
219}
220
221pub fn upstream_error_response(request_id: serde_json::Value, message: &str) -> McpResponse {
227 McpResponse {
228 jsonrpc: "2.0".into(),
229 id: request_id,
230 result: None,
231 error: Some(McpError {
232 code: -32000,
233 message: format!("[KVLAR] Upstream error: {}", message),
234 data: Some(serde_json::json!({
235 "_kvlar": {
236 "code": "UPSTREAM_ERROR",
237 "reason": message
238 }
239 })),
240 }),
241 }
242}
243
244pub fn parse_error_response(message: &str) -> McpResponse {
248 McpResponse {
249 jsonrpc: "2.0".into(),
250 id: serde_json::json!(null),
251 result: None,
252 error: Some(McpError {
253 code: -32700,
254 message: format!("[KVLAR] Parse error: {}", message),
255 data: Some(serde_json::json!({
256 "_kvlar": {
257 "code": "PARSE_ERROR",
258 "reason": message
259 }
260 })),
261 }),
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 #[test]
270 fn test_parse_tool_call_request() {
271 let json = r#"{
272 "jsonrpc": "2.0",
273 "id": 1,
274 "method": "tools/call",
275 "params": {
276 "name": "bash",
277 "arguments": {
278 "command": "ls -la"
279 }
280 }
281 }"#;
282
283 let msg = McpMessage::parse(json).unwrap();
284 assert!(msg.is_tool_call());
285
286 let req = msg.as_request().unwrap();
287 let tool_call = req.extract_tool_call().unwrap();
288 assert_eq!(tool_call.tool_name, "bash");
289 assert_eq!(
290 tool_call
291 .arguments
292 .get("command")
293 .unwrap()
294 .as_str()
295 .unwrap(),
296 "ls -la"
297 );
298 }
299
300 #[test]
301 fn test_parse_non_tool_call_request() {
302 let json = r#"{
303 "jsonrpc": "2.0",
304 "id": 2,
305 "method": "resources/read",
306 "params": {
307 "uri": "file:///tmp/test.txt"
308 }
309 }"#;
310
311 let msg = McpMessage::parse(json).unwrap();
312 assert!(!msg.is_tool_call());
313
314 let req = msg.as_request().unwrap();
315 assert!(req.extract_tool_call().is_none());
316 }
317
318 #[test]
319 fn test_parse_response() {
320 let json = r#"{
321 "jsonrpc": "2.0",
322 "id": 1,
323 "result": {
324 "content": [{"type": "text", "text": "hello"}]
325 }
326 }"#;
327
328 let msg = McpMessage::parse(json).unwrap();
329 assert!(!msg.is_tool_call());
330 assert!(msg.as_response().is_some());
331 assert!(msg.as_request().is_none());
332 }
333
334 #[test]
335 fn test_parse_error_response() {
336 let json = r#"{
337 "jsonrpc": "2.0",
338 "id": 1,
339 "error": {
340 "code": -32600,
341 "message": "Invalid request"
342 }
343 }"#;
344
345 let msg = McpMessage::parse(json).unwrap();
346 let resp = msg.as_response().unwrap();
347 assert!(resp.result.is_none());
348 assert!(resp.error.is_some());
349 assert_eq!(resp.error.as_ref().unwrap().code, -32600);
350 }
351
352 #[test]
353 fn test_deny_response() {
354 let resp = deny_response(
355 serde_json::json!(42),
356 "bash is not allowed",
357 "bash",
358 "deny-shell",
359 );
360 assert_eq!(resp.id, serde_json::json!(42));
361 assert!(resp.error.is_none());
362 let result = resp.result.unwrap();
363 assert_eq!(result["isError"], true);
364 let text = result["content"][0]["text"].as_str().unwrap();
365 assert!(text.contains("BLOCKED BY KVLAR"));
366 assert!(text.contains("bash is not allowed"));
367 assert!(text.contains("deny-shell"));
368 assert!(text.contains("Tool: bash"));
369
370 let kvlar = &result["_kvlar"];
372 assert_eq!(kvlar["code"], "POLICY_DENY");
373 assert_eq!(kvlar["decision"], "deny");
374 assert_eq!(kvlar["rule_id"], "deny-shell");
375 assert_eq!(kvlar["reason"], "bash is not allowed");
376 assert_eq!(kvlar["tool"], "bash");
377 }
378
379 #[test]
380 fn test_deny_response_default_deny() {
381 let resp = deny_response(
382 serde_json::json!(1),
383 "no matching rule",
384 "dangerous_tool",
385 "_default_deny",
386 );
387 let result = resp.result.unwrap();
388 let kvlar = &result["_kvlar"];
389 assert_eq!(kvlar["code"], "POLICY_DEFAULT_DENY");
390 assert_eq!(kvlar["rule_id"], "_default_deny");
391 }
392
393 #[test]
394 fn test_approval_required_response() {
395 let resp = approval_required_response(
396 serde_json::json!(7),
397 "email requires approval",
398 "send_email",
399 "approve-email",
400 );
401 assert!(resp.error.is_none());
402 let result = resp.result.unwrap();
403 assert_eq!(result["isError"], true);
404 let text = result["content"][0]["text"].as_str().unwrap();
405 assert!(text.contains("APPROVAL REQUIRED"));
406 assert!(text.contains("email requires approval"));
407 assert!(text.contains("approve-email"));
408
409 let kvlar = &result["_kvlar"];
411 assert_eq!(kvlar["code"], "POLICY_APPROVAL_REQUIRED");
412 assert_eq!(kvlar["decision"], "require_approval");
413 assert_eq!(kvlar["rule_id"], "approve-email");
414 assert_eq!(kvlar["reason"], "email requires approval");
415 assert_eq!(kvlar["tool"], "send_email");
416 }
417
418 #[test]
419 fn test_tool_call_no_arguments() {
420 let json = r#"{
421 "jsonrpc": "2.0",
422 "id": 1,
423 "method": "tools/call",
424 "params": {
425 "name": "list_files"
426 }
427 }"#;
428
429 let msg = McpMessage::parse(json).unwrap();
430 let req = msg.as_request().unwrap();
431 let tool_call = req.extract_tool_call().unwrap();
432 assert_eq!(tool_call.tool_name, "list_files");
433 assert!(tool_call.arguments.is_object());
434 }
435
436 #[test]
437 fn test_message_roundtrip() {
438 let json = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"bash","arguments":{"cmd":"ls"}}}"#;
439 let msg = McpMessage::parse(json).unwrap();
440 let back = msg.to_json().unwrap();
441 let msg2 = McpMessage::parse(&back).unwrap();
442 assert!(msg2.is_tool_call());
443 }
444}