systemprompt_api/services/proxy/audit/
jsonrpc.rs1use serde::Deserialize;
19use serde_json::{Value, json};
20use systemprompt_identifiers::{McpExecutionId, McpToolName};
21use systemprompt_models::artifacts::EXECUTION_META_KEY;
22
23const TOOLS_CALL_METHOD: &str = "tools/call";
24
25#[derive(Deserialize)]
26struct RequestFrame {
27 #[serde(default)]
29 id: Option<Value>,
30 method: String,
31 #[serde(default)]
32 params: Option<ToolCallParams>,
33}
34
35#[derive(Deserialize)]
36struct ToolCallParams {
37 #[serde(default)]
40 name: Option<Value>,
41 #[serde(default)]
43 arguments: Option<Value>,
44}
45
46#[derive(Debug)]
49pub enum ToolCallFrame {
50 NotToolCall,
51 Call(ToolCallInvocation),
52 InvalidName { raw_name: Option<String> },
53}
54
55#[derive(Debug)]
56pub struct ToolCallInvocation {
57 pub id: Value,
59 pub tool_name: McpToolName,
60 pub arguments: Value,
62}
63
64pub fn parse_tool_call(body: &[u8]) -> Option<ToolCallInvocation> {
65 match classify_tool_call(body) {
66 ToolCallFrame::Call(invocation) => Some(invocation),
67 ToolCallFrame::NotToolCall | ToolCallFrame::InvalidName { .. } => None,
68 }
69}
70
71pub fn classify_tool_call(body: &[u8]) -> ToolCallFrame {
72 let Ok(frame) = serde_json::from_slice::<RequestFrame>(body) else {
73 return ToolCallFrame::NotToolCall;
74 };
75 if frame.method != TOOLS_CALL_METHOD {
76 return ToolCallFrame::NotToolCall;
77 }
78 let Some(params) = frame.params else {
79 return ToolCallFrame::InvalidName { raw_name: None };
80 };
81 let raw_name = match params.name {
82 Some(Value::String(name)) => name,
83 Some(other) => {
84 return ToolCallFrame::InvalidName {
85 raw_name: Some(other.to_string()),
86 };
87 },
88 None => return ToolCallFrame::InvalidName { raw_name: None },
89 };
90 match McpToolName::try_new(raw_name.as_str()) {
91 Ok(tool_name) => ToolCallFrame::Call(ToolCallInvocation {
92 id: frame.id.unwrap_or(Value::Null),
93 tool_name,
94 arguments: params.arguments.unwrap_or(Value::Null),
95 }),
96 Err(_) => ToolCallFrame::InvalidName {
97 raw_name: Some(raw_name),
98 },
99 }
100}
101
102#[derive(Deserialize)]
103struct ResponseFrame {
104 #[serde(default)]
105 result: Option<ToolCallResult>,
106 #[serde(default)]
108 error: Option<Value>,
109}
110
111#[derive(Deserialize)]
112struct ToolCallResult {
113 #[serde(default, rename = "isError")]
114 is_error: bool,
115 #[serde(default, rename = "structuredContent")]
116 structured_content: Option<Value>,
117 #[serde(default)]
119 content: Option<Value>,
120}
121
122#[derive(Debug)]
123pub struct ToolCallOutcome {
124 pub output: Option<Value>,
126 pub error_message: Option<String>,
127 pub result: Option<Value>,
129}
130
131pub fn parse_response_frame(data: &str, request_id: &Value) -> Option<ToolCallOutcome> {
133 let frame: Value = serde_json::from_str(data).ok()?;
134 if frame.get("id") != Some(request_id) {
135 return None;
136 }
137 let parsed: ResponseFrame = serde_json::from_value(frame.clone()).ok()?;
138 if let Some(error) = parsed.error {
139 return Some(ToolCallOutcome {
140 error_message: Some(error.to_string()),
141 output: Some(error),
142 result: None,
143 });
144 }
145 let result = parsed.result?;
146 let output = result.structured_content.or(result.content);
147 let error_message = if result.is_error {
148 Some("MCP tool call returned isError".to_owned())
149 } else {
150 output
151 .as_ref()
152 .and_then(body_error)
153 .map(|error| format!("MCP tool result reported an error: {error}"))
154 };
155 Some(ToolCallOutcome {
156 output,
157 error_message,
158 result: frame.get("result").cloned(),
159 })
160}
161
162fn body_error(output: &Value) -> Option<String> {
164 let reported = |body: &Value| -> Option<String> {
165 match body.as_object()?.get("error")? {
166 Value::Null | Value::Bool(false) => None,
167 Value::String(text) => Some(text.clone()),
168 other => Some(other.to_string()),
169 }
170 };
171 match output {
172 Value::Array(blocks) => blocks.iter().find_map(|block| {
173 let text = block.get("text")?.as_str()?;
174 reported(&serde_json::from_str::<Value>(text).ok()?)
175 }),
176 body => reported(body),
177 }
178}
179
180pub fn frame_matches(data: &str, request_id: &Value) -> bool {
182 serde_json::from_str::<Value>(data)
183 .ok()
184 .is_some_and(|frame| frame.get("id") == Some(request_id))
185}
186
187pub fn stamp_execution(data: &str, mcp_execution_id: &McpExecutionId) -> Option<String> {
188 let mut frame: Value = serde_json::from_str(data).ok()?;
189 let result = frame.get_mut("result")?.as_object_mut()?;
190 let meta = result
191 .entry("_meta")
192 .or_insert_with(|| json!({}))
193 .as_object_mut()?;
194 let execution = meta
195 .entry(EXECUTION_META_KEY)
196 .or_insert_with(|| json!({}))
197 .as_object_mut()?;
198 execution
199 .entry("mcp_execution_id")
200 .or_insert_with(|| Value::String(mcp_execution_id.to_string()));
201 serde_json::to_string(&frame).ok()
202}
203
204pub fn extract_sse_data(frame: &str) -> Option<String> {
205 let mut data = String::new();
206 for line in frame.lines() {
207 if let Some(rest) = line.strip_prefix("data:") {
208 if !data.is_empty() {
209 data.push('\n');
210 }
211 data.push_str(rest.trim_start());
212 }
213 }
214 (!data.is_empty()).then_some(data)
215}
216
217pub fn replace_sse_data(frame: &str, data: &str) -> String {
218 let mut out = String::with_capacity(frame.len() + data.len());
219 let mut wrote = false;
220 for line in frame.trim_end_matches('\n').lines() {
221 if line.starts_with("data:") {
222 if !wrote {
223 out.push_str("data: ");
224 out.push_str(data);
225 out.push('\n');
226 wrote = true;
227 }
228 continue;
229 }
230 out.push_str(line);
231 out.push('\n');
232 }
233 if !wrote {
234 out.push_str("data: ");
235 out.push_str(data);
236 out.push('\n');
237 }
238 out.push('\n');
239 out
240}