1use crate::Method;
19use serde_json::Value;
20use std::fmt;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum LegacyClassificationError {
25 Malformed,
27 Batch,
30 MissingMethod,
32 MissingName(Method),
34}
35
36impl fmt::Display for LegacyClassificationError {
37 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38 match self {
39 Self::Malformed => {
40 f.write_str("request body is not JSON, and this client sent no trusted MCP headers")
41 }
42 Self::Batch => f.write_str(
43 "JSON-RPC batches cannot be metered as a single call; send one request per call",
44 ),
45 Self::MissingMethod => f.write_str("request body has no JSON-RPC \"method\""),
46 Self::MissingName(method) => write!(
47 f,
48 "{} requires params.name or params.uri in the body",
49 method.as_str()
50 ),
51 }
52 }
53}
54
55impl std::error::Error for LegacyClassificationError {}
56
57pub fn classify_body(body: &[u8]) -> Result<(Method, Option<String>), LegacyClassificationError> {
67 let value: Value =
68 serde_json::from_slice(body).map_err(|_| LegacyClassificationError::Malformed)?;
69 if value.is_array() {
70 return Err(LegacyClassificationError::Batch);
71 }
72 let method = value
73 .get("method")
74 .and_then(Value::as_str)
75 .ok_or(LegacyClassificationError::MissingMethod)?;
76 let method = Method::parse(method);
77
78 if !method.carries_name() {
79 return Ok((method, None));
80 }
81 let name = value
84 .get("params")
85 .and_then(|params| {
86 params
87 .get("name")
88 .or_else(|| params.get("uri"))
89 .and_then(Value::as_str)
90 })
91 .ok_or_else(|| LegacyClassificationError::MissingName(method.clone()))?;
92 Ok((method, Some(name.to_owned())))
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn tools_call_is_priced_on_the_name_in_the_body() {
101 let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call",
102 "params":{"name":"check_merchant","arguments":{"domain":"a.com"}}}"#;
103 let (method, name) = classify_body(body).expect("classifies");
104 assert_eq!(method, Method::ToolsCall);
105 assert_eq!(name.as_deref(), Some("check_merchant"));
106 }
107
108 #[test]
112 fn the_body_name_is_the_one_the_origin_will_run() {
113 let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call",
114 "params":{"name":"expensive_tool"}}"#;
115 let (_, name) = classify_body(body).expect("classifies");
116 assert_eq!(name.as_deref(), Some("expensive_tool"));
117 }
118
119 #[test]
120 fn resources_read_is_named_by_uri() {
121 let body = br#"{"jsonrpc":"2.0","id":1,"method":"resources/read",
122 "params":{"uri":"file:///a.txt"}}"#;
123 let (method, name) = classify_body(body).expect("classifies");
124 assert_eq!(method, Method::ResourcesRead);
125 assert_eq!(name.as_deref(), Some("file:///a.txt"));
126 }
127
128 #[test]
129 fn listing_methods_need_no_name() {
130 for raw in ["tools/list", "prompts/list", "resources/list"] {
131 let body = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"{raw}"}}"#);
132 let (method, name) = classify_body(body.as_bytes()).expect("classifies");
133 assert_eq!(method, Method::parse(raw));
134 assert!(name.is_none(), "{raw} should carry no name");
135 }
136 }
137
138 #[test]
139 fn unknown_extension_methods_pass_through() {
140 let body = br#"{"jsonrpc":"2.0","id":1,"method":"vendor/thing"}"#;
141 let (method, _) = classify_body(body).expect("classifies");
142 assert_eq!(method, Method::parse("vendor/thing"));
143 }
144
145 #[test]
146 fn a_name_bearing_method_without_a_name_is_refused() {
147 let body = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{}}"#;
148 assert!(matches!(
149 classify_body(body),
150 Err(LegacyClassificationError::MissingName(Method::ToolsCall))
151 ));
152 }
153
154 #[test]
155 fn batches_are_refused_rather_than_priced_as_one_call() {
156 let body = br#"[{"jsonrpc":"2.0","id":1,"method":"tools/list"}]"#;
157 assert_eq!(classify_body(body), Err(LegacyClassificationError::Batch));
158 }
159
160 #[test]
161 fn malformed_and_methodless_bodies_are_distinguished() {
162 assert_eq!(
163 classify_body(b"not json"),
164 Err(LegacyClassificationError::Malformed)
165 );
166 assert_eq!(
167 classify_body(br#"{"jsonrpc":"2.0","id":1}"#),
168 Err(LegacyClassificationError::MissingMethod)
169 );
170 }
171}