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