jsonrpc
js/jsonrpc.ts
fino:jsonrpc - JSON-RPC 2.0 request dispatch and peer transports.
Useful references:
- JSON-RPC 2.0 Specification: https://www.jsonrpc.org/specification
This module provides the common JSON-RPC pieces used by Fino integrations: a method registry for request handling, a bidirectional peer for request and notification exchange over string transports, and a small HTTP server adapter for services that only need a JSON-RPC endpoint.
Protocol model
JsonRpcService handles one JSON-RPC message string at a time. Requests with
an id produce a JSON response string, while notifications without an id
run the handler in the background and return null. The dispatcher emits the
standard error codes exported by this module and can validate params with
fino:validate schemas before calling a method.
JsonRpcPeer builds on the same service model for long-lived, bidirectional
transports. The transport contract is deliberately minimal: framing can be
newline-delimited streams, pipes, WebSockets, stdio processes, or in-memory
queues as long as the transport sends and receives complete JSON-RPC strings.
Current coverage
The implementation targets the single-message request, response, error, and
notification flow from JSON-RPC 2.0. Batch requests are not implemented, and
callers that require strict preflight validation of every JSON-RPC envelope
field should validate before dispatch. Methods named with the reserved
rpc. prefix are not blocked by the registry.
import { JsonRpcService, JsonRpcServer } from 'fino:jsonrpc';
import { v } from 'fino:validate';
const service = new JsonRpcService();
service.method('math.add')
.description('Add two numbers.')
.params(v.object({ a: v.number(), b: v.number() }))
.handle((params) => {
const { a, b } = params as { a: number; b: number };
return a + b;
});
const server = new JsonRpcServer(service).listen({ port: 3000, path: '/rpc' });
await server.ready;
Constants
const PARSE_ERROR
Standard JSON-RPC parse error code for invalid JSON payloads.
const INVALID_REQUEST
Standard JSON-RPC invalid request code for malformed request objects.
const METHOD_NOT_FOUND
Standard JSON-RPC method lookup failure code.
const INVALID_PARAMS
Standard JSON-RPC parameter validation failure code.
const INTERNAL_ERROR
Standard JSON-RPC internal error code for unexpected handler failures.
Classes
class JsonRpcError extends Error {
Error type that lets handlers choose the JSON-RPC error code and optional
data value returned to a caller.
Throw this from a JsonRpcService handler when an application-level failure
should be represented as a JSON-RPC error response instead of the default
INTERNAL_ERROR. On the calling side, JsonRpcPeer.call() also rejects with a
JsonRpcError reconstructed from an inbound error response, so the same type
carries failures in both directions.
import { JsonRpcService, JsonRpcError } from 'fino:jsonrpc';
const service = new JsonRpcService();
service.method('account.withdraw').handle((params) => {
const { amount, balance } = params as { amount: number; balance: number };
if (amount > balance) {
throw new JsonRpcError('Insufficient funds', -32001, { balance });
}
return balance - amount;
});
Properties
code: number
JSON-RPC error code to serialize in the response.
data?: unknown
Optional application-specific error details for the response data field.
Constructors
constructor(message: string, code: number, data?: unknown)
Create an error response payload.
message is also used as the JavaScript Error.message; code is copied
into the JSON-RPC error object; data, when present, is serialized as the
JSON-RPC error data member.
class JsonRpcService {
Registry and dispatcher for JSON-RPC methods.
A service maps method names to handlers and handles one inbound JSON-RPC
message string at a time. It is transport-agnostic: callers can feed messages
from HTTP, stdio, sockets, test queues, or JsonRpcPeer.
Register methods with the method(name) builder, then dispatch raw message
strings through handle(). The same service can be mounted onto HTTP via
httpHandler(), served standalone through JsonRpcServer, or attached to a
bidirectional JsonRpcPeer to answer inbound requests.
import { JsonRpcService } from 'fino:jsonrpc';
import { v } from 'fino:validate';
const service = new JsonRpcService();
service.method('math.add')
.params(v.object({ a: v.number(), b: v.number() }))
.handle((params) => {
const { a, b } = params as { a: number; b: number };
return a + b;
});
const response = await service.handle(JSON.stringify({
jsonrpc: '2.0',
method: 'math.add',
params: { a: 2, b: 3 },
id: 1,
}));
// response === '{"jsonrpc":"2.0","result":5,"id":1}'
Methods
method(name: string): MethodBuilder
Start registering a method by name.
Chain .description() or .params() before .handle() when the method
needs registry metadata or input validation.
list(): Array<{
name: string;
description?: string;
params?: Record<string, unknown>;
}>
Return the registered method metadata in insertion order.
The result is useful for local discovery endpoints, MCP-style tool conversion, or diagnostics. Handler functions are intentionally omitted.
async handle(raw: string, signal?: AbortSignal): Promise<string | null>
Dispatch one JSON-RPC message string.
Invalid JSON returns a PARSE_ERROR response. Malformed single request
objects return INVALID_REQUEST. Unknown methods return METHOD_NOT_FOUND
for requests and no response for notifications. When method metadata
includes a params schema, failed validation returns INVALID_PARAMS.
The return value is a serialized JSON-RPC response for requests, or null
when no response should be sent.
httpHandler(): (req: Request) => Promise<Response>
Create a Fetch-compatible HTTP handler for this service.
Only POST requests are accepted. Requests that dispatch to notifications
receive 204 No Content; request messages receive an application/json
response body containing the serialized JSON-RPC response.
class JsonRpcPeer {
Bidirectional JSON-RPC endpoint over a Transport.
The peer starts reading as soon as it is constructed. It resolves pending
calls from inbound responses and dispatches inbound requests to the optional
local service. Use done to observe the receive loop and close() to stop
the transport.
Unlike JsonRpcService, a peer can originate traffic: call() sends a request
and awaits its result, while notify() sends a fire-and-forget notification.
Passing a JsonRpcService makes the connection symmetric so both ends can call
each other over the single transport.
import { JsonRpcPeer, JsonRpcService } from 'fino:jsonrpc';
import type { Transport } from 'fino:jsonrpc';
declare const transport: Transport;
const local = new JsonRpcService();
local.method('log').handle((params) => { console.log(params); });
const peer = new JsonRpcPeer(transport, local);
const sum = await peer.call('math.add', { a: 1, b: 2 });
await peer.notify('log', { level: 'info', message: 'done' });
await peer.close();
Constructors
constructor(transport: Transport, service?: JsonRpcService, opts: {
signal?: AbortSignal;
} = {})
Create a peer over transport.
service, when provided, handles inbound requests and notifications from
the same connection. opts.signal is passed through to those handlers.
Methods
call(method: string, params?: unknown): Promise<unknown>
Send a request and wait for its response result.
Request ids are generated as increasing numbers local to this peer. Rejected
JSON-RPC responses become JsonRpcError instances.
async notify(method: string, params?: unknown): Promise<void>
Send a notification.
Notifications do not include an id, so no response is expected and remote handler failures are not reported to this peer.
async close(): Promise<void>
Mark the peer closed and close the underlying transport.
Getters
get done(): Promise<void>
Promise for the background receive loop.
It settles when the transport receive iterator ends or throws.
class JsonRpcServer {
Serve a JsonRpcService over transports or a convenience HTTP listener.
For composed HTTP applications, prefer JsonRpcService.httpHandler() or the
App.rpc() helper so authentication, routing, and middleware can live in the
application layer. JsonRpcServer is useful for tests, local tools, and simple
standalone JSON-RPC endpoints.
The same server can drive an arbitrary Transport through serve() or spin up
a dedicated HTTP listener through listen().
import { JsonRpcServer, JsonRpcService } from 'fino:jsonrpc';
const service = new JsonRpcService();
service.method('time.now').handle(() => Date.now());
const server = new JsonRpcServer(service);
const handle = server.listen({ port: 3000, path: '/rpc' });
await handle.ready;
Constructors
constructor(service: JsonRpcService)
Wrap a service for transport or HTTP serving.
Methods
async serve(transport: Transport): Promise<void>
Serve a service over one message-string transport until it closes.
listen(opts: ListenOptions): ServerHandle
Start a standalone HTTP JSON-RPC endpoint.
Only requests whose pathname matches opts.path are dispatched; all other
paths return 404. Method filtering and response formatting are delegated
to JsonRpcService.httpHandler().
Interfaces
interface Transport {
Minimal bidirectional carrier for complete JSON-RPC message strings.
A transport owns framing. send() must write one complete JSON-RPC message,
receive() must yield complete JSON-RPC messages, and close() should stop
both directions. The peer and server do not impose newline, WebSocket, or
stream framing by themselves.
Implement this interface to bridge any duplex byte or message stream into
JsonRpcPeer or JsonRpcServer. The example below adapts a WebSocket-like
object that already delivers discrete text frames, so no extra framing is
needed.
import { JsonRpcPeer } from 'fino:jsonrpc';
import type { Transport } from 'fino:jsonrpc';
function wsTransport(ws: {
send(data: string): void;
close(): void;
messages(): AsyncIterable<string>;
}): Transport {
return {
send: (message) => ws.send(message),
receive: () => ws.messages(),
close: () => ws.close(),
};
}
const peer = new JsonRpcPeer(wsTransport(socket));
const result = await peer.call('ping');
Methods
send(message: string): void | Promise<void>
Send one complete JSON-RPC message string.
receive(): AsyncIterable<string>
Yield complete JSON-RPC message strings until the connection closes.
close(): void | Promise<void>
Close the transport and unblock pending receivers when possible.
interface RequestContext {
Request-scoped data passed to method handlers.
The second argument to every JsonRpcHandler carries the current request id
(or undefined for notifications) and an AbortSignal that fires when the
dispatch or underlying HTTP request is cancelled. Handlers should forward
signal to downstream async work so long-running methods can be aborted.
import { JsonRpcService } from 'fino:jsonrpc';
const service = new JsonRpcService();
service.method('fetch.upstream').handle(async (params, ctx) => {
const { url } = params as { url: string };
const res = await fetch(url, { signal: ctx.signal });
return res.status;
});
Properties
id: number | string | null | undefined
Request id, notification marker, or undefined for notifications.
signal: AbortSignal
Abort signal associated with the current dispatch or HTTP request.
interface MethodMeta {
Optional metadata attached to a registered method.
Metadata is returned by JsonRpcService.list() and is also used for automatic
params validation when a schema is provided. Metadata is accumulated through
the MethodBuilder returned by JsonRpcService.method() rather than
constructed directly, but the shape is exported so discovery code that consumes
list() can type the entries it reads.
import { JsonRpcService } from 'fino:jsonrpc';
import { v } from 'fino:validate';
const service = new JsonRpcService();
service.method('user.rename')
.description('Change a user display name.')
.params(v.object({ id: v.string(), name: v.string() }))
.handle((params) => params);
for (const method of service.list()) {
console.log(method.name, method.description);
}
Properties
description?: string
Human-readable method description for registries or discovery endpoints.
params?: JsonSchema | SchemaBuilder
JSON Schema or fino:validate builder used to validate request params.
interface ListenOptions {
Options for serving JSON-RPC over HTTP.
Passed to JsonRpcServer.listen() to control the bound port, host, and the URL
pathname that accepts JSON-RPC POST bodies. Requests to any other pathname
receive 404.
import { JsonRpcServer, JsonRpcService } from 'fino:jsonrpc';
import type { ListenOptions } from 'fino:jsonrpc';
const opts: ListenOptions = { port: 0, host: '127.0.0.1', path: '/rpc' };
const handle = new JsonRpcServer(new JsonRpcService()).listen(opts);
await handle.ready;
Properties
port: number
TCP port for the HTTP listener. Use 0 to request an ephemeral port.
host?: string
Optional bind host passed to the HTTP server.
path?: string
URL pathname that should receive JSON-RPC POST requests. Defaults to /.
interface ServerHandle {
Handle returned by JsonRpcServer.listen().
Await ready before sending requests, read the resolved port (useful when
0 was requested and an ephemeral port was assigned), and call close() to
shut the listener down.
import { JsonRpcServer, JsonRpcService } from 'fino:jsonrpc';
import type { ServerHandle } from 'fino:jsonrpc';
const handle: ServerHandle = new JsonRpcServer(new JsonRpcService())
.listen({ port: 0, path: '/rpc' });
await handle.ready;
console.log(`listening on ${handle.port}`);
await handle.close();
Readonly Properties
readonly port: number
Actual bound port, including the assigned ephemeral port when 0 was used.
readonly ready: Promise<void>
Settles when the HTTP listener is ready to accept requests.
Methods
close(): Promise<void>
Stop accepting HTTP requests and release the listener.
Types
type JsonRpcHandler = (params: unknown, ctx: RequestContext) => unknown | Promise<unknown>
JSON-RPC method implementation.
params is the raw request params value after optional schema validation.
The return value is serialized as the response result for requests. For
notifications, returned values are ignored. Throwing a JsonRpcError controls
the error code sent to the caller; any other thrown value becomes an
INTERNAL_ERROR response.
import { JsonRpcService } from 'fino:jsonrpc';
import type { JsonRpcHandler } from 'fino:jsonrpc';
const greet: JsonRpcHandler = (params) => {
const { name } = params as { name: string };
return `Hello, ${name}`;
};
new JsonRpcService().method('greet').handle(greet);