1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13use tracing::debug;
14
15pub struct JsonRpcProtocol {}
17
18impl JsonRpcProtocol {
19 pub fn new() -> Self {
20 Self {}
21 }
22}
23
24impl Protocol for JsonRpcProtocol {
26 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
30 vec![]
31 }
32 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
37 vec![]
38 }
39 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
40 vec![jsonrpc_success_action(), jsonrpc_error_action()]
41 }
42 fn protocol_name(&self) -> &'static str {
43 "JSON-RPC"
44 }
45 fn get_event_types(&self) -> Vec<EventType> {
46 get_jsonrpc_event_types()
47 }
48 fn stack_name(&self) -> &'static str {
49 "ETH>IP>TCP>HTTP>JSONRPC"
50 }
51 fn keywords(&self) -> Vec<&'static str> {
52 vec!["jsonrpc", "json-rpc", "json rpc", "rpc"]
53 }
54 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
55 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
56
57 ProtocolMetadataV2::builder()
58 .state(DevelopmentState::Experimental)
59 .implementation("Manual JSON-RPC 2.0 over HTTP POST (hyper 1)")
60 .llm_control("Every method call: result value or error code/message/data")
61 .e2e_testing("curl and mocked in-process HTTP")
62 .notes(
63 "Single requests, batches and notifications. The response id is taken \
64 from the request, never from the model. Notifications (no id member) \
65 get HTTP 204 and, in a batch, no array entry. Every batch member is a \
66 separate model call and batch length is not capped, so a large batch is \
67 expensive. Any path is accepted; there is no routing, auth or rate \
68 limiting, and non-POST gets an Invalid Request body rather than 405.",
69 )
70 .max_inbound_bytes(crate::server::jsonrpc::MAX_REQUEST_BODY_BYTES)
71 .request_only("JSON-RPC over HTTP is HTTP request/response; a server cannot send a peer anything unprompted")
72 .build()
73 }
74 fn description(&self) -> &'static str {
75 "JSON-RPC 2.0 server"
76 }
77 fn example_prompt(&self) -> &'static str {
78 "Start a JSON-RPC 2.0 server on port 8000"
79 }
80 fn group_name(&self) -> &'static str {
81 "AI & API"
82 }
83 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
84 use crate::llm::actions::StartupExamples;
85
86 StartupExamples::new(
87 json!({
89 "type": "open_server",
90 "port": 8000,
91 "base_stack": "jsonrpc",
92 "instruction": "JSON-RPC 2.0 server. Implement add(a,b), subtract(a,b), and echo(message) methods. Return -32601 for unknown methods"
93 }),
94 json!({
96 "type": "open_server",
97 "port": 8000,
98 "base_stack": "jsonrpc",
99 "event_handlers": [{
100 "event_pattern": "jsonrpc_method_call",
101 "handler": {
102 "type": "script",
103 "language": "python",
104 "code": "method = event.get('method', '')\nparams = event.get('params', [])\nif method == 'add' and len(params) >= 2:\n action('jsonrpc_success', result=params[0] + params[1])\nelse:\n action('jsonrpc_error', code=-32601, message='Method not found')"
105 }
106 }]
107 }),
108 json!({
110 "type": "open_server",
111 "port": 8000,
112 "base_stack": "jsonrpc",
113 "event_handlers": [{
114 "event_pattern": "jsonrpc_method_call",
115 "handler": {
116 "type": "static",
117 "actions": [{
118 "type": "jsonrpc_success",
119 "result": {"status": "ok", "version": "1.0.0"}
120 }]
121 }
122 }]
123 }),
124 )
125 }
126}
127
128impl Server for JsonRpcProtocol {
130 fn spawn(
131 &self,
132 ctx: crate::protocol::SpawnContext,
133 ) -> std::pin::Pin<
134 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
135 > {
136 Box::pin(async move {
137 use crate::server::jsonrpc::JsonRpcServer;
138 let send_first = ctx
144 .startup_params
145 .as_ref()
146 .map(|p| p.get_optional_bool("send_first"))
147 .transpose()?
148 .flatten()
149 .unwrap_or(false);
150 if send_first {
151 return Err(anyhow::anyhow!(
152 "send_first is not supported by the JSON-RPC server: a request must precede any response, and an unsolicited notification is not what this flag was offering"
153 ));
154 }
155
156 JsonRpcServer::spawn_with_llm_actions(
157 ctx.legacy_listen_addr(),
158 ctx.llm_client,
159 ctx.state,
160 ctx.status_tx,
161 send_first,
162 ctx.server_id,
163 )
164 .await
165 })
166 }
167 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
168 let action_type = action
169 .get("type")
170 .and_then(|v| v.as_str())
171 .context("Missing 'type' field in action")?;
172
173 match action_type {
174 "jsonrpc_success" => self.execute_jsonrpc_success(action),
175 "jsonrpc_error" => self.execute_jsonrpc_error(action),
176 _ => Err(anyhow::anyhow!("Unknown JSON-RPC action: {}", action_type)),
177 }
178 }
179}
180
181impl JsonRpcProtocol {
182 fn execute_jsonrpc_success(&self, action: serde_json::Value) -> Result<ActionResult> {
183 let result = action
184 .get("result")
185 .context("Missing 'result' field")?
186 .clone();
187
188 let id = action.get("id").cloned().unwrap_or(serde_json::Value::Null);
189
190 debug!(
191 "JSON-RPC success response: result={:?}, id={:?}",
192 result, id
193 );
194
195 let response = json!({
197 "jsonrpc": "2.0",
198 "result": result,
199 "id": id
200 });
201
202 Ok(ActionResult::Custom {
203 name: "jsonrpc_response".to_string(),
204 data: response,
205 })
206 }
207
208 fn execute_jsonrpc_error(&self, action: serde_json::Value) -> Result<ActionResult> {
209 let code = action.get("code").and_then(|v| v.as_i64()).context(
212 "Missing or non-integer 'code' field (JSON-RPC error codes are integers, e.g. -32601)",
213 )?;
214
215 let message = action
216 .get("message")
217 .and_then(|v| v.as_str())
218 .context("Missing 'message' field")?;
219
220 let data = action.get("data").cloned();
221
222 let id = action.get("id").cloned().unwrap_or(serde_json::Value::Null);
223
224 debug!(
225 "JSON-RPC error response: code={}, message={}, id={:?}",
226 code, message, id
227 );
228
229 let mut error = json!({
231 "code": code,
232 "message": message
233 });
234
235 if let Some(data_val) = data {
236 error
237 .as_object_mut()
238 .unwrap()
239 .insert("data".to_string(), data_val);
240 }
241
242 let response = json!({
243 "jsonrpc": "2.0",
244 "error": error,
245 "id": id
246 });
247
248 Ok(ActionResult::Custom {
249 name: "jsonrpc_response".to_string(),
250 data: response,
251 })
252 }
253}
254
255pub fn jsonrpc_success_action() -> ActionDefinition {
257 ActionDefinition {
258 name: "jsonrpc_success".to_string(),
259 description: "Answer a JSON-RPC method call with a result. The response id is \
260 copied from the request automatically, so do not set one."
261 .to_string(),
262 parameters: vec![Parameter {
263 name: "result".to_string(),
264 type_hint: "any".to_string(),
265 description: "Whatever the method returns. Any JSON type: an object, an \
266 array, a string, a number, a boolean, or null."
267 .to_string(),
268 required: true,
269 }],
270 example: json!({
271 "type": "jsonrpc_success",
272 "result": {"status": "ok", "data": [1, 2, 3]}
273 }),
274 log_template: Some(
275 LogTemplate::new()
276 .with_info("-> JSON-RPC success id={id}")
277 .with_debug("JSON-RPC jsonrpc_success: id={id}"),
278 ),
279 }
280}
281
282pub fn jsonrpc_error_action() -> ActionDefinition {
284 ActionDefinition {
285 name: "jsonrpc_error".to_string(),
286 description: "Answer a JSON-RPC method call with an error. The response id is \
287 copied from the request automatically, so do not set one."
288 .to_string(),
289 parameters: vec![
290 Parameter {
291 name: "code".to_string(),
292 type_hint: "number".to_string(),
293 description: "Error code (JSON-RPC standard: -32700 Parse error, -32600 Invalid Request, -32601 Method not found, -32602 Invalid params, -32603 Internal error, -32000 to -32099 Server error)".to_string(),
294 required: true,
295 },
296 Parameter {
297 name: "message".to_string(),
298 type_hint: "string".to_string(),
299 description: "Human-readable error message".to_string(),
300 required: true,
301 },
302 Parameter {
303 name: "data".to_string(),
304 type_hint: "any".to_string(),
305 description: "Optional extra detail about the failure, e.g. which \
306 parameter was wrong. Any JSON value."
307 .to_string(),
308 required: false,
309 },
310 ],
311 example: json!({
312 "type": "jsonrpc_error",
313 "code": -32601,
314 "message": "Method not found"
315 }),
316 log_template: Some(
317 LogTemplate::new()
318 .with_info("-> JSON-RPC error {code}: {message}")
319 .with_debug("JSON-RPC jsonrpc_error: code={code} message={message}"),
320 ),
321 }
322}
323
324pub static JSONRPC_METHOD_CALL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
326 EventType::new(
327 "jsonrpc_method_call",
328 "JSON-RPC 2.0 method call received",
329 json!({"type": "jsonrpc_success", "result": 8}),
332 )
333 .with_parameters(vec![
334 Parameter {
335 name: "method".to_string(),
336 type_hint: "string".to_string(),
337 description: "The RPC method name being called".to_string(),
338 required: true,
339 },
340 Parameter {
341 name: "params".to_string(),
342 type_hint: "any".to_string(),
343 description: "Method parameters (can be array, object, or omitted)".to_string(),
344 required: false,
345 },
346 Parameter {
347 name: "id".to_string(),
348 type_hint: "string|number|null".to_string(),
349 description: "The request's correlation id, with its original JSON type. \
350 You never need to echo it: it is copied into the response for \
351 you. Null when the request carried no id."
352 .to_string(),
353 required: false,
354 },
355 Parameter {
356 name: "is_notification".to_string(),
357 type_hint: "boolean".to_string(),
358 description: "True when the request had no id member. A notification must \
359 not be answered: anything you return is discarded and the \
360 client gets HTTP 204."
361 .to_string(),
362 required: true,
363 },
364 ])
365 .with_actions(vec![jsonrpc_success_action(), jsonrpc_error_action()])
366 .with_log_template(
367 LogTemplate::new()
368 .with_info("JSON-RPC {method}")
369 .with_debug("JSON-RPC method={method}, id={id}")
370 .with_trace("JSON-RPC: {json_pretty(.)}"),
371 )
372});
373
374pub fn get_jsonrpc_event_types() -> Vec<EventType> {
376 vec![JSONRPC_METHOD_CALL_EVENT.clone()]
377}