typesec_agent/interop/
mcp.rs1use serde_json::{Value, json};
10
11use super::call::{GuardedToolCall, InteropError, ToolCallRequest};
12use super::wire;
13
14pub const DIALECT: &str = "mcp";
16
17pub fn parse_tool_calls(payload: &Value) -> Result<Vec<ToolCallRequest>, InteropError> {
21 let requests: &[Value] = match payload {
22 Value::Array(items) => items,
23 Value::Object(_) => std::slice::from_ref(payload),
24 _ => {
25 return Err(InteropError::malformed(
26 DIALECT,
27 "expected a JSON-RPC request object or an array of them",
28 ));
29 }
30 };
31 requests
32 .iter()
33 .filter(|request| is_tools_call(request))
34 .map(parse_request)
35 .collect()
36}
37
38pub fn is_tools_call(message: &Value) -> bool {
40 message.get("method").and_then(Value::as_str) == Some("tools/call")
41}
42
43fn parse_request(request: &Value) -> Result<ToolCallRequest, InteropError> {
44 let params = request
45 .get("params")
46 .ok_or_else(|| InteropError::malformed(DIALECT, "tools/call request has no 'params'"))?;
47 let name = wire::required_str(params, "name", DIALECT)?;
48 let arguments = wire::parse_arguments(params.get("arguments"), DIALECT, name)?;
49 let mut call = ToolCallRequest::new(name, arguments);
50 if let Some(id) = request.get("id").filter(|id| !id.is_null()) {
51 call = call.with_call_id(match id {
54 Value::String(s) => s.clone(),
55 other => other.to_string(),
56 });
57 }
58 Ok(call)
59}
60
61pub fn filter_tools(payload: &Value, keep: &dyn Fn(&str) -> bool) -> Value {
64 let name_of = |item: &Value| wire::optional_str(item, "name");
65 match payload.get("tools") {
66 Some(tools) => {
67 let mut result = payload.clone();
68 result["tools"] = wire::filter_tool_array(tools, name_of, keep);
69 result
70 }
71 None => wire::filter_tool_array(payload, name_of, keep),
72 }
73}
74
75pub fn denial(call: &GuardedToolCall) -> Option<Value> {
81 let id = match &call.request.call_id {
82 Some(raw) => raw
83 .parse::<i64>()
84 .map_or_else(|_| json!(raw), |number| json!(number)),
85 None => Value::Null,
86 };
87 denial_with_id(call, id)
88}
89
90pub fn denial_with_id(call: &GuardedToolCall, id: Value) -> Option<Value> {
93 call.denial_message().map(|text| {
94 json!({
95 "jsonrpc": "2.0",
96 "id": id,
97 "result": {
98 "content": [{"type": "text", "text": text}],
99 "isError": true,
100 },
101 })
102 })
103}