{"id":49933169,"url":"https://github.com/aussierobots/turul-rpc","last_synced_at":"2026-05-24T21:00:46.943Z","repository":{"id":356870470,"uuid":"1234264113","full_name":"aussierobots/turul-rpc","owner":"aussierobots","description":"Typed JSON-RPC 2.0 framework for Rust. Handlers return domain errors; the dispatcher owns the wire. Includes spec-conformant batch processing.","archived":false,"fork":false,"pushed_at":"2026-05-24T09:50:07.000Z","size":106,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-24T11:26:15.639Z","etag":null,"topics":["async","json-rpc","jsonrpc-2-0","mcp","model-context-protocol","rpc","rust","tokio"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/turul-rpc","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aussierobots.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-10T00:37:29.000Z","updated_at":"2026-05-24T09:50:08.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aussierobots/turul-rpc","commit_stats":null,"previous_names":["aussierobots/turul-rpc"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/aussierobots/turul-rpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aussierobots%2Fturul-rpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aussierobots%2Fturul-rpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aussierobots%2Fturul-rpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aussierobots%2Fturul-rpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aussierobots","download_url":"https://codeload.github.com/aussierobots/turul-rpc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aussierobots%2Fturul-rpc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33450402,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-24T19:21:36.376Z","status":"ssl_error","status_checked_at":"2026-05-24T19:21:10.562Z","response_time":57,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","json-rpc","jsonrpc-2-0","mcp","model-context-protocol","rpc","rust","tokio"],"created_at":"2026-05-17T04:53:51.520Z","updated_at":"2026-05-24T21:00:46.935Z","avatar_url":"https://github.com/aussierobots.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# turul-rpc\n\n\u003e Typed JSON-RPC 2.0 framework for Rust. Handlers return domain errors; the dispatcher owns the wire.\n\n`turul-rpc` is a generic, transport-agnostic JSON-RPC 2.0 framework for Rust.\nYou get spec-conformant wire types and an async dispatcher; you bring the\ntransport. Handlers return your own error type and the dispatcher owns the\nconversion to the wire.\n\n## Crate layout\n\n| Crate | Purpose |\n|---|---|\n| [`turul-rpc-core`](crates/turul-rpc-core)       | Pure JSON-RPC 2.0 wire types — request, response, notification, error. No async, no codec helpers. |\n| [`turul-rpc-jsonrpc`](crates/turul-rpc-jsonrpc) | JSON-RPC 2.0 codec — parser, batch, error mapping per spec. Depends on `core`. |\n| [`turul-rpc-server`](crates/turul-rpc-server)   | Async dispatcher, handler trait, session context, optional streaming. Depends on `core` + `jsonrpc`. |\n| [`turul-rpc`](crates/turul-rpc)                 | Facade — single import path. `pub use` of the three crates above. |\n\nMost consumers depend on `turul-rpc` (the facade). The split crates exist so you can pull in only the wire types (e.g. for a client) without dragging in the async runtime.\n\n## Why\n\nMost JSON-RPC crates either hand you raw envelopes or hide the wire entirely.\n`turul-rpc` keeps a hard line between the two:\n\n1. **Handlers return `Result\u003cValue, YourError\u003e`** — never `JsonRpcError`.\n2. **Dispatcher converts `YourError → JsonRpcError`** via your `ToJsonRpcError` impl. One boundary, one direction.\n3. **Transport-agnostic.** Bring your own HTTP/SSE/stdio/Lambda. The crate is pure dispatch and types.\n4. **JSON-RPC 2.0 batch** is implemented and tested per spec (§6).\n\n## Core types\n\nThe wire model is a small set of enums you compose directly:\n\n| Type | Shape | Role |\n|---|---|---|\n| `RequestId` | `String \\| Number(i64) \\| Null` | request/response id (§4.2) |\n| `ResponseResult` | `Success(Value) \\| Null` | the `result` payload of a success response |\n| `JsonRpcResponse` | `Success(JsonRpcSuccessResponse) \\| Error(JsonRpcError)` | the §5 Response object union |\n| `JsonRpcMessage` | `Request \\| Notification` | what a **server** reads off the wire |\n| `JsonRpcWireMessage` | `Request \\| Notification \\| Response` | what a **client or peer** reads — the schema's `JSONRPCMessage` |\n\n`JsonRpcMessage` is the inbound subset; `JsonRpcWireMessage` is the full\nbidirectional union, its `Response` arm carrying the success-or-error\n`JsonRpcResponse`. Parse incoming bytes with `parse_json_rpc_message` (server\nside) or `parse_json_rpc_wire_message` (any peer that reads responses) — both\nvalidate structure and reject malformed ids per spec rather than guessing via\nuntagged deserialization. `JsonRpcRequest` and `JsonRpcNotification` are the\nmatching outbound shapes.\n\n## Compliance posture\n\n`turul-rpc` implements JSON-RPC 2.0 id handling per §4.2: incoming\nrequests with `\"id\": null` are **accepted** (`RequestId = {String, Number,\nNull}`); null ids are permitted by the spec though discouraged. The only\nnarrowing is that fractional numeric ids are rejected (the spec itself\nSHOULD-NOTs them; only `i64` is representable). Server-emitted error\nresponses use `id: null` for unparseable or unidentifiable requests as\nthe spec requires. See\n[ADR-002](docs/adr/002-json-rpc-2-compliance.md) for the full posture.\n\n## Quick start\n\nDefine a handler — it returns your own error type, never a `JsonRpcError`:\n\n```rust\nuse turul_rpc::{JsonRpcDispatcher, JsonRpcHandler, RequestParams, SessionContext};\nuse turul_rpc::error::JsonRpcErrorObject;\nuse turul_rpc::r#async::ToJsonRpcError;\nuse async_trait::async_trait;\nuse serde_json::{json, Value};\n\n#[derive(thiserror::Error, Debug)]\nenum CalcError {\n    #[error(\"bad params: {0}\")]\n    BadParams(String),\n}\n\nimpl ToJsonRpcError for CalcError {\n    fn to_error_object(\u0026self) -\u003e JsonRpcErrorObject {\n        match self {\n            CalcError::BadParams(m) =\u003e JsonRpcErrorObject::invalid_params(m),\n        }\n    }\n}\n\nstruct Calc;\n\n#[async_trait]\nimpl JsonRpcHandler for Calc {\n    type Error = CalcError;\n\n    async fn handle(\n        \u0026self,\n        method: \u0026str,\n        params: Option\u003cRequestParams\u003e,\n        _session: Option\u003cSessionContext\u003e,\n    ) -\u003e Result\u003cValue, CalcError\u003e {\n        match method {\n            \"add\" =\u003e {\n                let p = params.ok_or_else(|| CalcError::BadParams(\"missing\".into()))?;\n                let m = p.to_map();\n                let a = m.get(\"a\").and_then(|v| v.as_f64())\n                    .ok_or_else(|| CalcError::BadParams(\"a\".into()))?;\n                let b = m.get(\"b\").and_then(|v| v.as_f64())\n                    .ok_or_else(|| CalcError::BadParams(\"b\".into()))?;\n                Ok(json!({ \"result\": a + b }))\n            }\n            _ =\u003e Err(CalcError::BadParams(format!(\"unknown method {method}\"))),\n        }\n    }\n\n    fn supported_methods(\u0026self) -\u003e Vec\u003cString\u003e { vec![\"add\".into()] }\n}\n```\n\nRegister it on a dispatcher and hand it a request — you get back a\n`JsonRpcResponse` (the §5 `Success | Error` union):\n\n```rust\nuse turul_rpc::{JsonRpcDispatcher, JsonRpcRequest, JsonRpcResponse, RequestId};\nuse serde_json::json;\nuse std::collections::HashMap;\n\nasync fn run() {\n    let mut dispatcher: JsonRpcDispatcher\u003cCalcError\u003e = JsonRpcDispatcher::new();\n    dispatcher.register_method(\"add\".into(), Calc);\n\n    let req = JsonRpcRequest::new_with_object_params(\n        RequestId::Number(1),\n        \"add\".into(),\n        HashMap::from([(\"a\".into(), json!(2)), (\"b\".into(), json!(3))]),\n    );\n\n    match dispatcher.handle_request(req).await {\n        JsonRpcResponse::Success(ok) =\u003e println!(\"result: {:?}\", ok.result.as_value()),\n        JsonRpcResponse::Error(err)  =\u003e eprintln!(\"error {}: {}\", err.error.code, err.error.message),\n    }\n\n    // Or feed a raw batch/single body straight in; returns the response\n    // JSON string, or None for an all-notifications body.\n    let _: Option\u003cString\u003e =\n        dispatcher.handle_batch(r#\"{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"add\",\"params\":{\"a\":1,\"b\":1}}\"#).await;\n}\n```\n\nIf you're below the dispatcher — parsing wire bytes or building responses\nby hand — use the message helpers:\n\n```rust\nuse turul_rpc::{JsonRpcWireMessage, parse_json_rpc_wire_message, RequestId};\nuse turul_rpc::dispatch::{create_success_response, create_error_response};\nuse serde_json::json;\n\n// Classify whatever a peer sent: request, notification, or response.\nmatch parse_json_rpc_wire_message(r#\"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"sum\":5}}\"#).unwrap() {\n    JsonRpcWireMessage::Request(req)       =\u003e { /* dispatch it */ }\n    JsonRpcWireMessage::Notification(note) =\u003e { /* fire-and-forget */ }\n    JsonRpcWireMessage::Response(resp)     =\u003e { /* correlate to a request you sent */ }\n}\n\n// Build a ready-to-serialize result without a dispatcher.\nlet ok  = create_success_response(RequestId::Number(1), json!({ \"sum\": 5 }));\nlet err = create_error_response(Some(RequestId::Number(2)), -32601, \"method not found\");\nlet body: Option\u003cString\u003e = ok.to_json_string();\n```\n\n(Use `parse_json_rpc_message` instead of `parse_json_rpc_wire_message` when you\nonly ever receive requests/notifications — a server that never reads responses.)\n\n## Ecosystem\n\n`turul-rpc` is the JSON-RPC layer beneath\n[`turul-mcp-server`](https://crates.io/crates/turul-mcp-server) — reach for that\ncrate if you want MCP semantics. The `turul-mcp-json-rpc-server` (0.3.x) shim\nre-exports `turul-rpc`, so existing consumers keep working; new code should\ndepend on `turul-rpc` directly ([ADR-003](docs/adr/003-compatibility-with-turul-mcp-json-rpc-server.md)).\n\n## Architecture decisions\n\nSee [`docs/adr/`](docs/adr/) for the four ADRs that govern this workspace:\n\n- [ADR-001 — Crate boundaries](docs/adr/001-crate-boundaries.md)\n- [ADR-002 — JSON-RPC 2.0 compliance](docs/adr/002-json-rpc-2-compliance.md)\n- [ADR-003 — Compatibility with turul-mcp-json-rpc-server](docs/adr/003-compatibility-with-turul-mcp-json-rpc-server.md)\n- [ADR-004 — Non-goals for v0.1](docs/adr/004-non-goals-for-v0-1.md)\n\n## License\n\nLicensed under either of\n\n- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or\n  \u003chttp://www.apache.org/licenses/LICENSE-2.0\u003e)\n- MIT license ([LICENSE-MIT](LICENSE-MIT) or\n  \u003chttp://opensource.org/licenses/MIT\u003e)\n\nat your option.\n\n### Contribution\n\nUnless you explicitly state otherwise, any contribution intentionally submitted\nfor inclusion in the work by you, as defined in the Apache-2.0 license, shall\nbe dual licensed as above, without any additional terms or conditions.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faussierobots%2Fturul-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faussierobots%2Fturul-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faussierobots%2Fturul-rpc/lists"}