Skip to content

Latest commit

 

History

History
400 lines (308 loc) · 9.44 KB

File metadata and controls

400 lines (308 loc) · 9.44 KB

RO-JRPC — Resource-Oriented JSON-RPC

JSON-RPC evolved for AI tools and local systems.

Status Version License

RO-JRPC is a backward-compatible extension to JSON-RPC 2.0 that adds structured routing fields to request messages:

Field Purpose
resource Logical entity (user, task, repo)
target Specific instance of resource (42, "abc")
verb Action (get, create, cancel)
subresource Nested entity owned by resource (repoissue)
parent Specific instance of resource that owns the subresource

Every RO-JRPC request is a valid JSON-RPC 2.0 request. Nothing breaks.


Why?

JSON-RPC method strings already encode this structure informally:

user.create
task.cancel
tool.execute

RO-JRPC makes it explicit — enabling:

  • ✅ Structured authorization (allow user:create, deny file:delete)
  • ✅ Automatic capability discovery via rpc.describe
  • ✅ Clean router APIs without string parsing
  • ✅ Better tooling for AI agents, CLIs, and IPC systems
  • ✅ Code generation and analytics
  • ✅ Async result routing via resource-oriented result messages

Quick Example

{
  "jsonrpc": "2.0",
  "method": "user.get",
  "resource": "user",
  "target": "42",
  "verb": "get",
  "id": 1
}
{
  "jsonrpc": "2.0",
  "result": { "id": "42", "name": "Alice" },
  "cache": { "max-age": 60 },
  "id": 1
}

Getting Started

JavaScript

npm install ro-jrpc
import { createRouter, buildRequest, buildResult } from 'ro-jrpc';

const router = createRouter();

router.resource('user').verb('get', async ({ target }) => {
  return { id: target, name: 'Alice' };
});

router.resource('user').verb('create', async ({ params }) => {
  return { id: '99', ...params };
});

// Handle a request
const response = await router.handle({
  jsonrpc: '2.0',
  method: 'user.get',
  resource: 'user',
  target: '42',
  verb: 'get',
  id: 1,
});

Sub-resources use a chained .subresource() call on the resource builder:

router.resource('repo').subresource('issue').verb('get', async ({ parent, target }) => {
  return { repoId: parent, issueId: target };
});

router.resource('project').subresource('task').verb('list', async ({ parent }) => {
  return [{ id: '1', projectId: parent }];
});

See implementations/js/ for the full reference implementation.

Rust

use ro_jrpc::RequestBuilder;

// Flat resource
let req = RequestBuilder::new("user", "get")
    .target("42")
    .id(serde_json::json!(1))
    .build();

assert_eq!(req.method, "user.get");

// Sub-resource
let req = RequestBuilder::new("repo", "get")
    .subresource("issue")
    .parent("99")
    .target("7")
    .id(serde_json::json!(2))
    .build();

assert_eq!(req.method, "repo.issue.get");

See implementations/rust-stub/ for the Rust crate stub.


Caching

Both client and server manage their own cache independently — no negotiation is required for the basic case. The natural cache key is resource + verb + target (plus subresource + parent when present).

Server hints TTL to client via cache on the response:

{
  "jsonrpc": "2.0",
  "result": { "id": "42", "name": "Alice" },
  "cache": { "max-age": 60 },
  "id": 1
}

Client bypasses server cache via cache on the request:

{
  "jsonrpc": "2.0",
  "method": "user.get",
  "resource": "user",
  "verb": "get",
  "target": "42",
  "cache": "no-cache",
  "id": 1
}

Both fields are optional. Silence means each side does whatever it likes locally.


Async Results

Async result messages are themselves resource-oriented — the receiver routes them using the same router as any other message. The result resource does not need to match the originating request's resource, allowing long-running operations to respond as a job or any other resource.

Two reserved verbs carry async results:

Verb Meaning
yield Intermediate result; more messages expected
return Final result; no further messages will be sent

The result object should include a status field:

Status Meaning
accepted Received and queued, not yet started
pending In progress, more results expected
done Completed successfully
error Failed; no further messages will be sent

Request-result pattern

Use request_id to correlate a result back to a specific originating request, especially when the result is delivered on a different channel or as a different resource:

// Originating request
const req = buildRequest({
  resource: 'attachment',
  verb: 'create',
  params: { filename: 'report.pdf' },
  id: 'req-001',
});

// Intermediate result delivered later as a job resource
const progress = buildResult({
  resource: 'job',
  verb: 'yield',
  target: 'job-123',
  result: { status: 'pending', progress: 60, stage: 'scanning' },
  request_id: 'req-001',
});

// Final result
const final = buildResult({
  resource: 'job',
  verb: 'return',
  target: 'job-123',
  result: { status: 'done', attachmentId: 'att-456' },
  request_id: 'req-001',
});

Event-driven pattern

For session or stream events where there is no single originating request, omit request_id and scope by target:

// Intermediate session event
const chunk = buildResult({
  resource: 'session',
  verb: 'yield',
  target: 'session-9',
  result: { status: 'pending', content: 'Thinking...' },
});

// Final session result
const done = buildResult({
  resource: 'session',
  verb: 'return',
  target: 'session-9',
  result: { status: 'done', content: 'Here is your answer.' },
});

Specification

📄 spec.md — Full specification

Key rules at a glance

  • method MUST equal <resource>.<verb> (or <resource>.<subresource>.<verb>) when structured fields are present
  • target SHOULD NOT be embedded in method
  • subresource and parent MUST NOT appear without resource
  • parent MUST NOT appear without subresource
  • Mismatched method/resource/verb SHOULD be rejected with -32600
  • yield and return are reserved for async result messages (server → client)
  • Servers SHOULD support rpc.describe for discovery
  • Servers MUST NOT trust client-supplied meta

Examples

Flat resource — get a user

{
  "jsonrpc": "2.0",
  "method": "user.get",
  "resource": "user",
  "target": "42",
  "verb": "get",
  "id": 1
}

Sub-resource — get an issue on a repo

{
  "jsonrpc": "2.0",
  "method": "repo.issue.get",
  "resource": "repo",
  "parent": "99",
  "subresource": "issue",
  "target": "7",
  "verb": "get",
  "id": 2
}
{
  "jsonrpc": "2.0",
  "result": { "id": "7", "repoId": "99", "title": "Fix null pointer", "state": "open" },
  "id": 2
}

Async — upload attachment with progress

{
  "jsonrpc": "2.0",
  "method": "attachment.create",
  "resource": "attachment",
  "verb": "create",
  "params": { "filename": "report.pdf" },
  "id": "req-001"
}
{
  "jsonrpc": "2.0",
  "method": "job.yield",
  "resource": "job",
  "verb": "yield",
  "target": "job-123",
  "result": { "status": "pending", "progress": 60, "stage": "scanning" },
  "request_id": "req-001"
}
{
  "jsonrpc": "2.0",
  "method": "job.return",
  "resource": "job",
  "verb": "return",
  "target": "job-123",
  "result": { "status": "done", "attachmentId": "att-456" },
  "request_id": "req-001"
}

Async — session stream (event-driven)

{
  "jsonrpc": "2.0",
  "method": "session.yield",
  "resource": "session",
  "verb": "yield",
  "target": "session-9",
  "result": { "status": "pending", "content": "Thinking..." }
}
{
  "jsonrpc": "2.0",
  "method": "session.return",
  "resource": "session",
  "verb": "return",
  "target": "session-9",
  "result": { "status": "done", "content": "Here is your answer." }
}

More examples: subresources.json · examples/user-crud.json · examples/agent-tool-call.json · examples/notifications.json · examples/discovery.json


Transports

RO-JRPC is transport-agnostic. Use it over:

  • stdio / pipes
  • Unix domain sockets
  • TCP
  • HTTP
  • WebSocket
  • Child-process IPC

Positioning

RO-JRPC is not a replacement for JSON-RPC 2.0. It is JSON-RPC 2.0 with optional structured fields. Existing JSON-RPC 2.0 servers and clients continue to work unchanged.


Contributing

  1. Open an issue to discuss proposed changes
  2. Spec changes require discussion in an issue before a PR
  3. Implementation PRs welcome for any language

License

MIT