An open API service indexing awesome lists of open source software.

https://github.com/williamw520/zigjr

A Lightweight Zig Library for JSON-RPC 2.0
https://github.com/williamw520/zigjr

json-rpc mcp protocol zig

Last synced: 12 days ago
JSON representation

A Lightweight Zig Library for JSON-RPC 2.0

Awesome Lists containing this project

README

          

# ZigJR - JSON-RPC 2.0 Library for Zig

(Note: ZigJR 2.0 is under development. It will introduce some breaking changes.
The main changes are for adding session based handling support and multi-thread support.)

ZigJR is a lightweight Zig library providing a full implementation of the JSON-RPC 2.0 protocol,
with message streaming on top, and a smart function dispatcher that turns native Zig functions
into RPC handlers. It aims to make building JSON-RPC applications in Zig simple and straightforward.

This small library is packed with the following features:

* Parsing and composing JSON-RPC 2.0 messages.
* Support for Request, Response, Notification, and Error JSON-RPC 2.0 messages.
* Support for batch requests and batch responses in JSON-RPC 2.0.
* Message streaming via delimiter based streams (`\n`, etc.).
* Message streaming via `Content-Length` header-based streams.
* RPC pipeline to process the full request-to-response lifecycle.
* Native Zig functions as message handlers with automatic type mapping.
* Flexible logging mechanism for inspecting the JSON-RPC messages.

## Content

* [Quick Usage](#quick-usage)
* [Installation](#installation)
* [Usage](#usage)
* [Dispatcher](#dispatcher)
* [RpcDispatcher](#rpcdispatcher)
* [Custom Dispatcher](#custom-dispatcher)
* [Invocation and Cleanup](#invocation-and-cleanup)
* [Handler Function](#handler-function)
* [Extended Handlers](#extended-handlers)
* [Universal Message Handling](#universal-message-handling)
* [Transport](#transport)
* [Project Build](#project-build)
* [Examples](#examples)
* [License](#license)
* [References](#references)

## Quick Usage

The following example shows a JSON-RPC server registering native Zig functions
as RPC handlers in a dispatcher's registry and using it to handle
JSON-RPC messages in a stream from `stdin` to `stdout`.

The handler functions take in native Zig data types and return native result values
or errors, which are automatically mapped to the JSON data types.

```zig
{
var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc);
defer rpc_dispatcher.deinit();

try rpc_dispatcher.add"say", say);
try rpc_dispatcher.add("hello", hello);
try rpc_dispatcher.add("hello-name", helloName);
try rpc_dispatcher.add("substr", substr);
try rpc_dispatcher.add("weigh-cat", weigh);

try zigjr.stream.runByDelimiter(alloc, stdin, stdout, &rpc_dispatcher, .{});
}

fn say(msg: []const u8) void {
std.debug.print("Message to say: {s}\n", .{msg});
}

fn hello() []const u8 {
return "Hello world";
}

fn helloName(alloc: Allocator, name: [] const u8) ![]const u8 {
return try std.fmt.allocPrint(alloc, "Hello {s}", .{name});
}

fn substr(name: [] const u8, start: i64, len: i64) []const u8 {
return name[@intCast(start) .. @intCast(len)];
}

fn weigh(cat: CatInfo) f64 {
return cat.weight;
}
```
Check out [hello.zig](examples/hello.zig) for a complete example.
See the example on how to obtain the `std.Io.Reader` based `stdin` and `std.Io.Writer` based `stdout`.

Here are some sample request and response JSON messages for testing.
```
Request: {"jsonrpc": "2.0", "method": "hello", "id": 1}
Response: {"jsonrpc": "2.0", "result": "Hello world", "id": 1}
```
```
Request: {"jsonrpc": "2.0", "method": "hello-name", "params": ["Spiderman"], "id": 2}
Response: {"jsonrpc": "2.0", "result": "Hello Spiderman", "id": 2}
```

## Installation

Select a version of the library in the [Releases](https://github.com/williamw520/zigjr/releases) page,
and copy its asset URL. E.g. https://github.com/williamw520/zigjr/archive/refs/tags/1.9.0.zip

Use `zig fetch` to add the ZigJR package to your project's dependencies. Replace `` with the version you selected.
```shell
zig fetch --save https://github.com/williamw520/zigjr/archive/refs/tags/.tar.gz
```

This command updates your `build.zig.zon` file, adding ZigJR to the `dependencies` section with its URL and content hash.

```diff
.{
.name = "my-project",
...
.dependencies = .{
+ .zigjr = .{
+ .url = "zig fetch https://github.com/williamw520/zigjr/archive/refs/tags/.tar.gz",
+ .hash = "zigjr-...",
+ },
},
}
```

Next, update your `build.zig` to add the ZigJR module to your executable.

```diff
pub fn build(b: *std.Build) void {
...
+ const zigjr_pkg = b.dependency("zigjr", .{ .target = target, .optimize = optimize });
+ const zigjr_mod = zigjr_pkg.module("zigjr"); // get the module defined in the pkg.
...
const exe1 = b.addExecutable(.{
.name = "my_project",
.root_module = exe1_mod,
});
...
+ exe1.root_module.addImport("zigjr", zigjr_module);
+ exe2.root_module.addImport("zigjr", zigjr_module);
+ lib1.root_module.addImport("zigjr", zigjr_module);
```

The `.addImport("zigjr")` call makes the library's module available to your executable,
allowing you to import it in your source files:
```zig
const zigjr = @import("zigjr");
```

## Usage

You can build JSON-RPC 2.0 applications with ZigJR at different levels of abstraction:
* **Streaming API:** Handle message frames for continuous communication (recommended).
* **RPC Pipeline:** Process individual requests and responses.
* **Parsers and Composers:** Manually build and parse JSON-RPC messages for maximum control.

For most use cases, the Streaming API is the simplest and most powerful approach.

### Streaming API
The following example handles a stream of messages prefixed with a `Content-Length` header,
reading requests from `stdin` and writing responses to `stdout`.
```zig
{
var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc);
defer rpc_dispatcher.deinit();
try rpc_dispatcher.add("add", addTwoNums);

try zigjr.stream.runByContentLength(alloc, stdin, stdout, &rpc_dispatcher, .{});
}

fn addTwoNums(a: i64, b: i64) i64 { return a + b; }
```
See [hello.zig](examples/hello.zig) on how to obtain the `std.Io.Reader` based `stdin` and `std.Io.Writer` based `stdout`.

This example streams messages from one in-memory buffer to another,
using a newline character (`\n`) as a delimiter.
```zig
{
var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc);
defer rpc_dispatcher.deinit();
try rpc_dispatcher.add("add", addTwoNums);

const req_jsons =
\\{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}
\\{"jsonrpc": "2.0", "method": "add", "params": [3, 4], "id": 2}
\\{"jsonrpc": "2.0", "method": "add", "params": [5, 6], "id": 3}
;
var reader = std.Io.Reader.fixed(req_jsons);

var out_buf = std.Io.Writer.Allocating.init(alloc);
defer out_buf.deinit();

try zigjr.stream.runByDelimiter(alloc, &reader, &out_buf.writer, &rpc_dispatcher, .{});

std.debug.print("output_jsons: {s}\n", .{out_buf.written()});
}
```

### RPC Pipeline
To handle individual requests, use the `RequestPipeline`. It abstracts away message parsing,
dispatching, and response composition.

```zig
{
// Set up the rpc_dispatcher as the dispatcher.
var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc);
defer rpc_dispatcher.deinit();
try rpc_dispatcher.add("add", addTwoNums);
const dispatcher = zigjr.RequestDispatcher.implBy(&rpc_dispatcher);

// Set up the request pipeline with the dispatcher.
var pipeline = try zigjr.RequestPipeline.init(alloc, dispatcher, null);
defer pipeline.deinit();

// Run the individual requests to the pipeline.
const response_json1 = try pipeline.runRequestToJson(alloc,
\\{"jsonrpc": "2.0", "method": "add", "params": [1, 2], "id": 1}
);
defer alloc.free(response_json1);

const response_json2 = try pipeline.runRequestToJson(alloc,
\\{"jsonrpc": "2.0", "method": "add", "params": [3, 4], "id": 2}
);
defer alloc.free(response_json2);

const response_json3 = try pipeline.runRequestToJson(alloc,
\\{"jsonrpc": "2.0", "method": "add", "params": [5, 6], "id": 3}
);
defer alloc.free(response_json3);
}
```

### Parse JSON-RPC Messages
For lower-level control, you can parse messages directly into `RpcRequest` objects,
where the request's method, parameters, and request ID can be accessed.
```zig
const zigjr = @import("zigjr");
{
var result = zigjr.parseRpcRequest(alloc,
\\{"jsonrpc": "2.0", "method": "func42", "params": [42], "id": 1}
);
defer result.deinit();
const req = try result.request();
try testing.expect(std.mem.eql(u8, req.method, "func42"));
try testing.expect(req.arrayParams().?.items.len == 1);
try testing.expect(req.arrayParams().?.items[0].integer == 42);
try testing.expect(req.id.num == 1);
}
```
`parseRpcRequest()` can parse a single message or a batch of messages. Use `result.batch()` to get the list of requests in the batch.

### Compose JSON-RPC Messages
The `composer` API helps to build valid JSON-RPC messages.
```zig
const zigjr = @import("zigjr");
{
const msg1 = try zigjr.composer.makeRequestJson(alloc, "hello", null, zigjr.RpcId { .num = 1 });
defer alloc.free(msg1);

const msg2 = try zigjr.composer.makeRequestJson(alloc, "hello-name", ["Spiderman"], zigjr.RpcId { .num = 1 });
defer alloc.free(msg2);
}
```
See [composer.zig](src/jsonrpc/composer.zig) for other API methods.

## Dispatcher
The dispatcher is the entry point for handling incoming RPC messages.
After a message is parsed, the RPC pipeline feeds it to the dispatcher,
which routes it to a handler function based on the message's `method`.
The `RequestDispatcher` and `ResponseDispatcher` interfaces define the required dispatching functions.

## RpcDispatcher
The built-in `RpcDispatcher` is a dispatcher with a registry of RPC handlers that also implements the `RequestDispatcher` interface and
serves as a powerful, ready-to-use dispatcher. Use `RpcDispatcher.add(method_name, function)`
to register a handler function for a specific JSON-RPC method. When a request comes in,
the it looks up the handler from its registry, maps the request's parameters to the function's arguments,
calls the function, and captures the result or error to formulate a response.

```zig
{
var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc);
defer rpc_dispatcher.deinit();

try rpc_dispatcher.add("add", addTwoNums);
try rpc_dispatcher.add("sub", subTwoNums);
...
const dispatcher = zigjr.RequestDispatcher.implBy(&rpc_dispatcher);
...
}
```

## Custom Dispatcher
You can provide a custom dispatcher as long as it implements the `dispatch()` and `dispatchEnd()`
functions of the `RequestDispatcher` interface. See the `dispatcher_hello.zig` example for details.

## Invocation and Cleanup
Each request is processed in two phases: `dispatch()`, which executes the handler,
and `dispatchEnd()`, which performs per-invocation cleanup (such as freeing memory).

## Handler Function
Message handler functions are native Zig functions.

### Scopes
Handler functions can be defined in the global scope, a struct scope, or a struct instance scope.

For instance-scoped methods, pass a pointer to the struct instance as the context
when registering the handler. This context pointer will be passed as the first parameter
to the handler function when it is invoked.

```zig
{
try rpc_dispatcher.add("global-fn", global_fn);
try rpc_dispatcher.add("group-fn", Group.group_fn);
...
var counter = Counter{};
try rpc_dispatcher.addWithCtx("counter-inc", &counter, Counter.inc);
try rpc_dispatcher.addWithCtx("counter-get", &counter, Counter.get);
...
}

fn global_fn() void { }

const Group = struct {
fn group_fn() void { }
};

const Counter = struct {
count: i64 = 0;

fn inc(self: *Counter) void { self.count += 1; }
fn get(self: *Counter) i64 { return self.count; }
};
```

### Parameters
Handler function parameters are native Zig types, with a few limitations related
to JSON compatibility. Parameter types should generally map to JSON types:

* `bool`: JSON boolean
* `i64`: JSON number (compatible with JavaScript's 53-bit safe integer range)
* `f64`: JSON number (64-bit float)
* `[]const u8`: JSON string
* `struct`: JSON object

There're some light automatic type conversion when the function parameter's type
and the JSON message's parameter type are closely related. (See `ValueAs()` in json_call.zig for details).

Struct parameters must be deserializable from JSON. The corresponding handler
parameter's struct must have fields that match the JSON object. ZigJR uses `std.json`
for deserialization. Nested objects are supported, and you can implement custom
parsing by adding a `jsonParseFromValue` function to your struct. See the `std.json`
documentation for details.

Here's an example on using `struct` as parameter and return value of a RPC handler.
```zig
{
var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc);
try rpc_dispatcher.add("weigh-cat", weighCat);
try rpc_dispatcher.add("make-cat", makeCat);
try zigjr.stream.runByDelimiter(alloc, stdin, stdout, &rpc_dispatcher, .{});
}

const CatInfo = struct {
cat_name: []const u8,
weight: f64,
eye_color: []const u8,
};

fn weighCat(cat: CatInfo) []const u8 {
if (std.mem.eql(u8, cat.cat_name, "Garfield")) return "Fat Cat!";
if (std.mem.eql(u8, cat.cat_name, "Odin")) return "Not a cat!";
if (0 < cat.weight and cat.weight <= 2.0) return "Tiny cat";
if (2.0 < cat.weight and cat.weight <= 10.0) return "Normal weight";
if (10.0 < cat.weight ) return "Heavy cat";
return "Something wrong";
}

fn makeCat(name: []const u8, eye_color: []const u8) CatInfo {
const seed: u64 = @truncate(name.len);
var prng = std.Random.DefaultPrng.init(seed);
return .{
.cat_name = name,
.weight = @floatFromInt(prng.random().uintAtMost(u32, 20)),
.eye_color = eye_color,
};
}
```

### Special Parameters

#### Context

If a context pointer is supplied to `RpcDispatcher.addWithCtx()`, it is passed as
the first parameter to the handler function, effectively serving as a `self` pointer.

The first parameter's type and the context type need to be the same.

#### Allocator
If an `std.mem.Allocator` is the first parameter of a handler (or the second,
if a context is used as the first), an arena allocator is passed in. The handler does
not need to free memory allocated with it; the arena is automatically reset after the request completes.
The arena memory is reset in dispatchEnd() when the dispatching of a request has completed.

#### Value
To handle parameters manually, you can use `std.json.Value`:
* As the **only** parameter: The entire `params` field from the request (`array` or `object`) is passed as a single `std.json.Value`.
```zig
fn h1(params: std.json.Value) void { /* ... */ }
```
* As **one of several** parameters: The corresponding JSON-RPC parameter is passed as a `std.json.Value` without being converted to a native Zig type.
```zig
fn h3(a: std.json.Value, b: i64, c: std.json.Value) void { /* ... */ }
```

### Return Value
The return value of a handler function is serialized to JSON and becomes the `result`
of the JSON-RPC response. You can return any Zig type that can be serialized by `std.json`.

If your function returns a `void`, it is treated as a Notification, and no response message is generated.

#### JSON Return Value
If the return value is already a JSON string, you can wrap it in `zigjr.JsonStr` to avoid double-serialization.
Declare `zigjr.JsonStr` as the return type of the handler function.

#### `DispatchResult` Return Value
For lower-level control, you can return a `DispatchResult` from the handler function.
You can set a returning JSON result or set an error in `DispatchResult`.
Typically the normal error handling just returns the error code to the client.
`DispatchResult` let you set the error code, error message, and additional error data
to send back to the client.

#### End-Stream Return Value
Sometimes you want to end the streaming session of JSON-RPC requests and responses
by a user's command coming from the request. E.g. the client sends a request as,
```
Request: {"jsonrpc": "2.0", "method": "end-session"}
```
Your handler handles the `end-session` method and can return a `DispatchResult.end_stream` value
to tell the calling streaming service to end its session. E.g.
```zig
...
try rpc_dispatcher.add("end-session", endSession);
...

fn endSession() zigjr.DispatchResult {
return zigjr.DispatchResult.asEndStream();
}
```
See [hello_net.zig](examples/hello_net.zig) in TCP model for example.
Send a `{"jsonrpc": "2.0", "method": "end-session"}` message to it for test.

#### Process Termination via JSON-RPC Message

See the `endServer()` handler in [hello_net.zig](examples/hello_net.zig) for example.
Send a `{"jsonrpc": "2.0", "method": "end-server"}` message to it for test.

### Error
A handler function can have an error union with the return type. Any error returned will be
packaged into a JSON-RPC error response with the `InternalError` code.

### Memory Management
When using `RpcDispatcher`, memory management is straightforward. Any memory
obtained from the allocator passed to a handler is automatically freed
after the request completes. Handlers do not need to perform manual cleanup.
Memory is freed in the `dispatcher.dispatchEnd()` phase.

If you implement a custom dispatcher, you are responsible for managing the memory's lifecycle.

### Logging

Logging is a great way to learn about a protocol by watching the messages exchanged between
the client and server. ZigJR has a built-in logging mechanism to help you inspect messages
and debug handlers. You can use a pre-built logger or implement a custom one.

#### DbgLogger
This example uses a `DbgLogger` in a request pipeline. This logger prints to `stderr`.
```zig
var d_logger = zigjr.DbgLogger{};
const pipeline = zigjr.pipeline.RequestPipeline.init(alloc,
RequestDispatcher.implBy(&rpc_dispatcher), d_logger.asLogger());

```
#### FileLogger
This example uses a `FileLogger` in a request stream. This logger writes to a file.
File based logging is great in situations where the stdout is not available, e.g.
when running as a sub-process in a MCP host.

```zig
var f_logger = try zigjr.FileLogger.init(alloc, "log.txt");
defer f_logger.deinit();
try zigjr.stream.runByDelimiter(alloc, stdin, stdout, &rpc_dispatcher, .{ .logger = f_logger.asLogger() });
```
#### Custom Logger
This example uses a custom logger in a request pipeline.
```zig
{
var my_logger = MyLogger{};
const pipeline = zigjr.pipeline.RequestPipeline.init(alloc,
RequestDispatcher.implBy(&rpc_dispatcher), zigjr.Logger.implBy(&my_logger));
}

const MyLogger = struct {
count: usize = 0,

pub fn start(_: MyLogger, _: []const u8) void {}
pub fn log(self: *MyLogger, source:[] const u8, operation: []const u8, message: []const u8) void {
self.count += 1;
std.debug.print("LOG {}: {s} - {s} - {s}\n", .{self.count, source, operation, message});
}
pub fn stop(_: MyLogger, _: []const u8) void {}
};
```

## Extended Handlers

`RpcDispatcher` supports adding of pre-handler, post handler, error handler and fallback handler
for the requests. Before dispatching a request to its method's handler, the
`OnBeforeFn` extended handler is called, allowing any pre-handling of the request.
After the method's handler for a request has returned successfully, the `OnAfterFn`
extended handler is called, allowing any post-handling on the request.
When the method's handler returns error, the `OnErrorFn` extended handler is called,
allowing any error handling on the request before sending it back to the client.

If a request's method has no registered handler, the `OnFallbackFn` extended handler is called,
allowing handling of any unknown requests.

The extended handlers are set up via `setOnBefore()`, `setOnAfter()`, `setOnError()`, and `setOnFallback()` on `RpcDispatcher`.
Once set, these extended handlers are applied to all requests, regardless of the request methods.

An extended handler takes in a context and an Allocator parameters. The context is passed
in as the first parameter of the setOnXX() functions. The Allocator is the same arena allocator
for request handling.

For example,

```zig
{
var rpc_dispatcher = try zigjr.RpcDispatcher.init(alloc);
defer rpc_dispatcher.deinit();
rpc_dispatcher.setOnBefore(null, onBefore);
rpc_dispatcher.setOnAfter(null, onAfter);
rpc_dispatcher.setOnError(null, onError);
rpc_dispatcher.setOnFallback(null, onFallback);
}

fn onBefore(_: *anyopaque, _: Allocator, req: RpcRequest) void {
std.debug.print("Before handling request, method: {s}, id: {any}\n", .{req.method, req.id});
}

fn onAfter(_: *anyopaque, _: Allocator, req: RpcRequest, res: DispatchResult) void {
std.debug.print("After handling request, method: {s}, id: {any}, result: {any}\n", .{req.method, req.id, res});
}

fn onError(_: *anyopaque, _: Allocator, req: RpcRequest, err: anyerror) void {
std.debug.print("After handling request, method: {s}, id: {any}, error: {any}\n", .{req.method, req.id, err});
}

fn onFallback(_: *anyopaque, _: Allocator, req: RpcRequest) anyerror!DispatchResult {
std.debug.print("Unknown request, method: {s}, id: {any}\n", .{req.method, req.id});
return DispatchResult.asNone(); // return a .none result to discard the request.
}
```

## Universal Message Handling

Some servers (e.g. LSP server) can send both responses to a client's requests and
its own server-to-client requests in the same channel. A client needs to be able
to handle both JSON RPC responses and requests from the server in the same channel.
ZigJR provides universal message handling functions to handle message that is either a request or a response.

* `stream.messagesByContentLength()`: handles a stream of mixed requests and responses.
* `rpc_pipeline.runMessage()`: handles one message of either a request or response.
* `message.parseRpcMessage()`: parses one message of either a request or response.

See the [lsp_client.zig](examples/lsp_client.zig) example on how to handle a mix of requests and responses
in a stream.

## Transport

A few words on message transport. ZigJR doesn't deal with transport at all.
It sits on top of any transport, network or others.
It's assumed the JSON-RPC messages are sent over some transport before arriving at ZigJR.

## Project Build

You do not need to build this project if you are only using it as a library
via `zig fetch`. To run the examples, clone the repository and run `zig build` to build the project.
The example binaries will be located in `zig-out/bin/`.

## Examples

The project has a number of examples showing how to build applications with ZigJR.

* [hello.zig](examples/hello.zig): Showcase the basics of handler function registration and the streaming API.
* [calc.zig](examples/calc.zig): Showcase different kinds of handler functions.
* [dispatcher_hello.zig](examples/dispatcher_hello.zig): Custom dispatcher.
* [mcp_hello.zig](examples/mcp_hello.zig): A basic MCP server written from the ground up.
* [lsp_client.zig](examples/lsp_client.zig): A LSP client interacting with LSP server.
* [hello_net.zig](examples/hello_net.zig): A JSON-RPC server over network (HTTP or TCP).

Check out [examples](examples) for other examples.

### Run Examples Interactively
Running the programs interactively is a great way to experiment with the handlers.
Just type in the JSON requests and see the result.
```
zig-out/bin/hello
```
The program will wait for input. Type or paste the JSON-RPC request and press Enter.
```
{"jsonrpc": "2.0", "method": "hello", "id": 1}
```
It will print the JSON result.
```
{"jsonrpc": "2.0", "result": "Hello world", "id": 1}
```

Other sample requests,
```
{"jsonrpc": "2.0", "method": "hello-name", "params": ["Foobar"], "id": 1}
```
```
{"jsonrpc": "2.0", "method": "hello-name", "params": ["Spiderman"], "id": 1}
```
```
{"jsonrpc": "2.0", "method": "hello-xtimes", "params": ["Spiderman", 3], "id": 1}
```
```
{"jsonrpc": "2.0", "method": "say", "params": ["Abc Xyz"], "id": 1}
```

### Run Examples with Data Files
You can also run the examples by piping test data from a file, which is useful for creating repeatable tests.
```
zig-out/bin/hello < data/hello.json
zig-out/bin/hello < data/hello_name.json
zig-out\bin/hello < data/hello_xtimes.json
zig-out/bin/hello < data/hello_say.json
zig-out/bin/hello < data/hello_stream.json
```

Some more sample data files. Examine the data files in the [Data](data) directory to
see how they exercise the message handlers.
```
zig-out/bin/calc.exe < data/calc_add.json
zig-out/bin/calc.exe < data/calc_weight.json
zig-out/bin/calc.exe < data/calc_sub.json
zig-out/bin/calc.exe < data/calc_multiply.json
zig-out/bin/calc.exe < data/calc_divide.json
zig-out/bin/calc.exe < data/calc_divide_99.json
zig-out/bin/calc.exe < data/calc_divide_by_0.json
```

### Run the MCP Server Example

The `mcp_hello` executible can be run standalone on a console for testing its message handling,
or run as an embedded subprocess in a MCP host.

#### Standalone Run

Run it standalone. Feed the MCP requests by hand.

```
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"mcphost","version":"1.0.0"},"capabilities":{}}}
```
```
{"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
```
```
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
```
```
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hello","arguments":{}}}
```
```
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hello-name","arguments":{"name":"Mate"}}}
```

#### Embedded in a MCP Host

This uses [MCP Host](https://github.com/mark3labs/mcphost) as an example.

Create a configuration file `config-mcp-hello.json` with `command` pointing to the mcp_hello executible.
```json
{
"mcpServers": {
"mcp-hello": {
"command": "/zigjr/zig-out/bin/mcp_hello.exe",
"args": []
}
}
}
```

Run `mcphost` with one of the LLM providers.
```
mcphost --config config-mcp-hello.json --provider-api-key YOUR-API-KEY --model anthropic:claude-3-5-sonnet-latest
mcphost --config config-mcp-hello.json --provider-api-key YOUR-API-KEY --model openai:gpt-4
mcphost --config config-mcp-hello.json --provider-api-key YOUR-API-KEY --model google:gemini-2.0-flash
```

Type `hello`, `hello Joe` or `hello Joe 10` in the prompt for testing. The `log.txt` file captures the interaction.

### Run the LSP Client Example

The LSP client

**(Note: This example is not working after migrating to Zig 0.15.1, pending update.)**

The LSP client example is a rudimentary LSP client illustrating how to build a JSON RPC client.
It spawns a LSP server as a sub-process, communicating to it via its stdin and stdout.
It creates a thread for `request_worker()` to send LSP requests to the server's stdin
and another thread for `response_worker()` to read LSP responses and requests from the server's stdout.

Since a LSP server can send both responses to client's requests and its own server-to-client requests,
lsp_client uses `stream.messagesByContentLength()` to handle both incoming JSON RPC responses and requests.

It uses `RpcDispatcher`, `ExtHandlers` and `ResponseDispatcher` to handle the requests and responses in
a central place.

The `request_worker()` sends a number of LSP requests to the server to illustrate how the LSP protocol works.
- `initialize` - tells the LSP server the client's capabilities and starts the session.
- `initialized` - tells the server the client is ready; server will send requests to client after this.
- `textDocument/didOpen` - tells the server a file has been loaded; send the file content over.
- `textDocument/definition` - gets the definition at a position of the file.
- `textDocument/hover` - gets hovering information at a position of the file.
- `textDocument/signatureHelp` - gets function signature description.
- `textDocument/completion` - gets completion information to complete typing by the user.
- `shutdown`
- `exit`

#### Sample Runs of lsp_client

This runs the ZLS executible as the embedded LSP server. Runs the lsp_client in minimum mode.
```shell
lsp_client /opt/zls/zls.exe
```

This dumps the LSP message's payload as JSON string.
```shell
lsp_client --json /opt/zls/zls.exe
```

This pretty-prints the LSP message's JSON payload.
```shell
lsp_client --pp-json /opt/zls/zls.exe
```

This dumps the LSP message in raw format, i.e. with the message frame headers.
```shell
lsp_client --dump /opt/zls/zls.exe
```

The `--stderr` option pipes the LSP server subprocess' stderr to stderr.
```shell
lsp_client --dump --stderr /opt/zls/zls.exe
```

### Run the Network Server Example

```shell
hello_net --tcp
```
Runs the `hello_net` server in TCP mode. Use clients like `telnet` or `netcat` to interact with the server.
The JSON-RPC messages are delimited with LF. See `data/hello.json` for sample data.
```shell
nc64 localhost 35354 < data\hello.json
nc64 localhost 35354 < data\hello_name.json
```

```shell
hello_net --http
```
Runs the `hello_net` server in HTTP mode. Use a client like `curl` for testing.
```shell
curl localhost:35354 --request POST --json @data/hello.json
curl localhost:35354 --request POST --json @data/hello_name.json
```

## License

ZigJR is [MIT licensed](./LICENSE).

## References

- [JSON-RPC 2.0 Specification](https://www.jsonrpc.org/specification)
- [MCP Schema](https://github.com/modelcontextprotocol/modelcontextprotocol/tree/main/schema)
- [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/)