1use crate::mcp::types::*;
2use anyhow::{Context, Result};
3
4pub fn parse_mcp_transcript(text: &str, format: McpInputFormat) -> Result<Vec<McpEvent>> {
6 match format {
7 McpInputFormat::JsonRpc => parse_jsonrpc_jsonl(text),
8 McpInputFormat::Inspector => parse_inspector_best_effort(text),
9 }
10}
11
12fn parse_jsonrpc_jsonl(text: &str) -> Result<Vec<McpEvent>> {
13 let mut out = Vec::new();
14
15 for (lineno, line) in text.lines().enumerate() {
16 let line = line.trim();
17 if line.is_empty() {
18 continue;
19 }
20
21 let v: serde_json::Value = serde_json::from_str(line)
22 .with_context(|| format!("invalid JSON on line {}", lineno + 1))?;
23
24 let event = parse_single_event(v, (lineno + 1) as u64)?;
25 out.push(event);
26 }
27
28 Ok(out)
29}
30
31fn parse_inspector_best_effort(text: &str) -> Result<Vec<McpEvent>> {
32 let v: serde_json::Value = serde_json::from_str(text).context("invalid inspector JSON")?;
33
34 let arr = v
38 .get("events")
39 .cloned()
40 .or_else(|| v.as_array().cloned().map(serde_json::Value::Array))
41 .and_then(|x| x.as_array().cloned())
42 .unwrap_or_default();
43
44 let mut out = Vec::new();
45 for (idx, item) in arr.into_iter().enumerate() {
46 let event = parse_single_event(item, (idx + 1) as u64)?;
48 out.push(event);
49 }
50
51 Ok(out)
52}
53
54fn parse_single_event(v: serde_json::Value, source_line: u64) -> Result<McpEvent> {
55 let ts_ms = extract_ts_ms(&v);
56
57 let id_str = v
59 .get("id")
60 .map(|x| x.to_string().trim_matches('"').to_string());
61
62 let method = v
64 .get("method")
65 .and_then(|m| m.as_str())
66 .map(|s| s.to_string());
67
68 let payload = if let Some(method) = method {
69 match method.as_str() {
70 "tools/list" => McpPayload::ToolsListRequest { raw: v.clone() },
71 "tools/call" => {
72 let params = v.get("params").cloned().unwrap_or(serde_json::Value::Null);
73 let name = params
74 .get("name")
75 .and_then(|x| x.as_str())
76 .unwrap_or("unknown_tool")
77 .to_string();
78 let arguments = params
79 .get("arguments")
80 .cloned()
81 .unwrap_or(serde_json::Value::Null);
82 McpPayload::ToolCallRequest {
83 name,
84 arguments,
85 raw: v.clone(),
86 }
87 }
88 _ => McpPayload::Other { raw: v.clone() },
90 }
91 } else {
92 if v.get("result").is_some() {
94 if looks_like_tools_list_result(&v) {
95 let tools = parse_tools_list_result(&v)?;
96 McpPayload::ToolsListResponse {
97 tools,
98 raw: v.clone(),
99 }
100 } else {
101 McpPayload::ToolCallResponse {
102 result: v.get("result").cloned().unwrap_or(serde_json::Value::Null),
103 is_error: false,
104 raw: v.clone(),
105 }
106 }
107 } else if v.get("error").is_some() {
108 McpPayload::ToolCallResponse {
109 result: v.get("error").cloned().unwrap_or(serde_json::Value::Null),
110 is_error: true,
111 raw: v.clone(),
112 }
113 } else {
114 McpPayload::Other { raw: v.clone() }
117 }
118 };
119
120 Ok(McpEvent {
121 source_line,
122 timestamp_ms: ts_ms,
123 jsonrpc_id: id_str,
124 payload,
125 })
126}
127
128fn extract_ts_ms(v: &serde_json::Value) -> Option<u64> {
129 if let Some(t) = v.get("timestamp_ms").and_then(|t| t.as_u64()) {
131 return Some(t);
132 }
133 if let Some(t) = v.get("timestamp").and_then(|t| t.as_u64()) {
134 return Some(t); }
137 None
138}
139
140fn looks_like_tools_list_result(v: &serde_json::Value) -> bool {
141 v.get("result")
142 .and_then(|r| r.get("tools"))
143 .and_then(|t| t.as_array())
144 .is_some()
145}
146
147fn parse_tools_list_result(v: &serde_json::Value) -> Result<Vec<McpToolDef>> {
148 let tools = v
149 .get("result")
150 .and_then(|r| r.get("tools"))
151 .and_then(|t| t.as_array())
152 .cloned()
153 .unwrap_or_default();
154
155 let mut out = Vec::new();
156 for tool in tools {
157 let name = tool
158 .get("name")
159 .and_then(|x| x.as_str())
160 .unwrap_or("unknown")
161 .to_string();
162 let description = tool
163 .get("description")
164 .and_then(|x| x.as_str())
165 .map(|s| s.to_string());
166 let input_schema = tool
168 .get("inputSchema")
169 .cloned()
170 .or_else(|| tool.get("input_schema").cloned());
171 out.push(McpToolDef {
172 name,
173 description,
174 input_schema,
175 tool_identity: None,
176 });
177 }
178 Ok(out)
179}