Skip to main content

netget/server/jsonrpc/
actions.rs

1//! JSON-RPC protocol actions implementation
2
3use 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
15/// JSON-RPC protocol action handler
16pub struct JsonRpcProtocol {}
17
18impl JsonRpcProtocol {
19    pub fn new() -> Self {
20        Self {}
21    }
22}
23
24// Implement Protocol trait (common functionality)
25impl Protocol for JsonRpcProtocol {
26    /// None. `send_first` used to be declared here and was bound as `_send_first`
27    /// in the server, i.e. accepted and ignored: JSON-RPC over HTTP is strictly
28    /// client-initiated, so there is nothing to send first.
29    fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
30        vec![]
31    }
32    /// None. `list_rpc_methods` used to be declared here; it ignored its input,
33    /// always returned an empty list, and its result was consumed by nobody, so it
34    /// cost prompt tokens to advertise a method-discovery feature that does not
35    /// exist. There is no method registry: the model answers every call.
36    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            // LLM mode: instruction-based
88            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            // Script mode: event_handlers with script handler
95            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            // Static mode: event_handlers with static actions
109            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
128// Implement Server trait (server-specific functionality)
129impl 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            // `send_first` stays declared so a caller that passes `false` still validates - an
139            // undeclared key is refused outright - but `true` is refused rather than ignored.
140            // It was read here, threaded through spawn, and dropped at the far end as
141            // `_send_first`: a knob advertised to the model, plumbed through three hops, and
142            // silently doing nothing. JSON-RPC: a request must precede any response, and an unsolicited notification is not what this flag was offering.
143            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        // Build JSON-RPC 2.0 success response
196        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        // Kept as i64: the spec says "integer", and `as i32` silently wrapped, so
210        // a model emitting 4294967296 produced code 0.
211        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        // Build JSON-RPC 2.0 error response
230        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
255/// Action definition: Send JSON-RPC success response
256pub 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
282/// Action definition: Send JSON-RPC error response
283pub 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
324/// JSON-RPC method call event - triggered when client sends a JSON-RPC request
325pub 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        // A bare action, not a {"actions": [...]} wrapper — a response_example is a
330        // single action object or an array of them, never the outer envelope.
331        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
374/// Get JSON-RPC event types
375pub fn get_jsonrpc_event_types() -> Vec<EventType> {
376    vec![JSONRPC_METHOD_CALL_EVENT.clone()]
377}