PHPackages                             fabpot/json-rpc-peer - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Queues &amp; Workers](/categories/queues)
4. /
5. fabpot/json-rpc-peer

ActiveLibrary[Queues &amp; Workers](/categories/queues)

fabpot/json-rpc-peer
====================

An asynchronous, bidirectional JSON-RPC 2.0 peer over line-delimited streams, built on amphp

v0.2(yesterday)03↑2900%MITPHP &gt;=8.4

Since Jul 19Compare

[ Source](https://github.com/fabpot/json-rpc-peer)[ Packagist](https://packagist.org/packages/fabpot/json-rpc-peer)[ GitHub Sponsors](https://github.com/fabpot)[ RSS](/packages/fabpot-json-rpc-peer/feed)WikiDiscussions Synced today

READMEChangelogDependencies (5)Versions (3)Used By (0)

JSON-RPC
========

[](#json-rpc)

A minimal, asynchronous, bidirectional [JSON-RPC 2.0](https://www.jsonrpc.org/specification)peer over line-delimited JSON streams, built on [amphp](https://amphp.org) byte streams and the [Revolt](https://revolt.run) event loop.

Unlike the HTTP-shaped JSON-RPC libraries common in PHP, this is a **peer**: a single long-lived connection over which both sides send requests and notifications, answer inbound requests, and resolve responses out of order. It is the primitive behind stdio protocols such as the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/)and the [Model Context Protocol](https://modelcontextprotocol.io).

Why this exists
---------------

[](#why-this-exists)

The PHP ecosystem has plenty of JSON-RPC libraries, but they model one HTTP request mapping to one response, are strictly a server or a client, run synchronously, and cannot let the endpoint initiate a call back to the other side. Bidirectional stdio protocols need none of that shape and all of what is missing:

- **Peer, not server-or-client**: the same endpoint serves inbound requests and emits its own requests and notifications.
- **Persistent duplex transport**: line-delimited JSON over any amphp `ReadableStream`/`WritableStream` (stdio, a socket, or in-memory streams for tests), not one-shot HTTP.
- **Deferred responses**: a request handler may hold its responder and resolve it later from another coroutine, so a long-running call can answer while the reader keeps processing inbound messages (for example an interrupt) on the same connection.

Installation
------------

[](#installation)

```
composer require fabpot/json-rpc-peer
```

Usage
-----

[](#usage)

### Wiring a peer

[](#wiring-a-peer)

```
use Amp\ByteStream;
use Fabpot\JsonRpc\JsonRpcDispatcher;
use Fabpot\JsonRpc\JsonRpcPeer;

$peer = new JsonRpcPeer(
    ByteStream\getStdin(),
    ByteStream\getStdout(),
);

$dispatcher = new JsonRpcDispatcher($peer);
```

### Handling requests and notifications

[](#handling-requests-and-notifications)

Register handlers by method name. A request handler receives the params and a `RequestResponder` it must resolve or reject. A notification handler receives only the params and never produces a response.

```
use Fabpot\JsonRpc\RequestResponder;

$dispatcher->onRequest('sum', function (array $params, RequestResponder $responder): void {
    $responder->resolve(['total' => array_sum($params['values'])]);
});

$dispatcher->onNotification('log', function (array $params): void {
    fwrite(\STDERR, $params['message']."\n");
});
```

Throwing a `JsonRpcException` from a request handler is turned into a JSON-RPC error response:

```
use Fabpot\JsonRpc\Exception\JsonRpcException;
use Fabpot\JsonRpc\JsonRpcError;
use Fabpot\JsonRpc\RequestResponder;

$dispatcher->onRequest('divide', function (array $params, RequestResponder $responder): void {
    if (0 === $params['by']) {
        throw new JsonRpcException(JsonRpcError::INVALID_PARAMS, 'Cannot divide by zero.');
    }

    $responder->resolve($params['value'] / $params['by']);
});
```

### Deferred responses

[](#deferred-responses)

A handler may keep its responder and resolve it later. The reader keeps dispatching inbound messages in the meantime, so a cancellation can arrive and settle the same request while its work is still running.

```
$dispatcher->onRequest('run', function (array $params, RequestResponder $responder) use (&$active): void {
    $active = $responder;

    \Amp\async(function () use ($responder): void {
        $result = doLongRunningWork();
        $responder->resolve($result);
    });
});

$dispatcher->onNotification('cancel', function () use (&$active): void {
    $active?->resolve(['stopReason' => 'cancelled']);
});
```

A responder settles at most once: a late `resolve()`/`reject()` after a cancel is silently ignored.

### Emitting requests and notifications

[](#emitting-requests-and-notifications)

Outbound requests return an Amp `Future`. Responses are matched by ID, so they can arrive in any order. Remote JSON-RPC errors throw a `JsonRpcException` when the future is awaited. The listener must be running in another coroutine while a request is pending.

```
$listener = \Amp\async($peer->listen(...));
$result = $peer->request('workspace/status', ['workspace' => '/project'])->await();
```

Close the input stream to stop `listen()`. Closing the input also fails every outstanding request with a `Fabpot\JsonRpc\Exception\ConnectionClosedException`.

The peer can also push notifications to the other side at any time:

```
$peer->notify('progress', ['percent' => 42]);
```

For protocols that use JSON-RPC batches, pass explicit request and notification entries. The returned array contains futures for request entries only, in the same order as those requests:

```
use Fabpot\JsonRpc\BatchNotification;
use Fabpot\JsonRpc\BatchRequest;

[$status, $configuration] = $peer->batch(
    new BatchRequest('workspace/status'),
    new BatchNotification('progress', ['percent' => 42]),
    new BatchRequest('workspace/configuration'),
);

$status = $status->await();
$configuration = $configuration->await();
```

### Running the loop

[](#running-the-loop)

`listen()` reads inbound lines until the stream closes, dispatching each message. Malformed lines are answered with a JSON-RPC parse error and skipped, so a single bad line never tears down the connection.

```
$peer->listen();
```

Spec conformance
----------------

[](#spec-conformance)

The peer implements JSON-RPC 2.0, including mixed request and notification batches. Batch responses are emitted once every request has settled and may be ordered by settlement rather than input order, as allowed by the specification.

Traffic logging
---------------

[](#traffic-logging)

Pass a `TrafficLoggerInterface` to the peer to record raw inbound and outbound lines. `PsrTrafficLogger` forwards them to a PSR-3 logger at the `debug` level:

```
use Fabpot\JsonRpc\PsrTrafficLogger;

$peer = new JsonRpcPeer($input, $output, new PsrTrafficLogger($logger));
```

Install `psr/log` to use this optional adapter. JSON-RPC payloads may contain secrets, so configure protocol-specific redaction in your logger before persisting them.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance100

Actively maintained with recent releases

Popularity4

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~0 days

Total

2

Last Release

1d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/47313?v=4)[Fabien Potencier](/maintainers/fabpot)[@fabpot](https://github.com/fabpot)

---

Tags

asyncmcpamphpjsonrpcjson-rpcstdioLSP

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/fabpot-json-rpc-peer/health.svg)

```
[![Health](https://phpackages.com/badges/fabpot-json-rpc-peer/health.svg)](https://phpackages.com/packages/fabpot-json-rpc-peer)
```

###  Alternatives

[amphp/parallel

Parallel processing component for Amp.

85251.9M100](/packages/amphp-parallel)[amphp/byte-stream

A stream abstraction to make working with non-blocking I/O simple.

394125.7M142](/packages/amphp-byte-stream)[amphp/dns

Async DNS resolution for Amp.

19346.8M57](/packages/amphp-dns)[amphp/http-server

A non-blocking HTTP application server for PHP based on Amp.

1.3k6.7M115](/packages/amphp-http-server)[amphp/socket

Non-blocking socket connection / server implementations based on Amp and Revolt.

26646.6M153](/packages/amphp-socket)[amphp/file

Non-blocking access to the filesystem based on Amp and Revolt.

1113.4M130](/packages/amphp-file)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
