Feature Request: JSON-RPC Support for Interactive Process Communication
Problem Statement
When working with processes that use JSON-RPC for communication (particularly Model Context Protocol (MCP) servers), the current approach requires verbose Process.Start() code with manual stream management, timeout handling, and request/response correlation. This defeats Amuru's core mission of eliminating process execution verbosity.
Current Pain Points
Here's a real example from testing an MCP server (like the one in TimeWarp.Nuru.Mcp):
// Current verbose approach - 200+ lines for basic JSON-RPC communication
ProcessStartInfo psi = new()
{
FileName = "dotnet",
Arguments = $"run --project \"{mcpPath}\" -c Release",
RedirectStandardInput = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process is null)
{
WriteLine("❌ Failed to start MCP server");
return 1;
}
try
{
// Manual delay for startup
await Task.Delay(2000);
// Send JSON-RPC request
string initRequest = """
{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"1.0.0","clientInfo":{"name":"test-client","version":"1.0.0"}},"id":1}
""";
await process.StandardInput.WriteLineAsync(initRequest);
await process.StandardInput.FlushAsync();
// Read response with manual timeout handling
string? response = null;
using (CancellationTokenSource cts = new(TimeSpan.FromSeconds(5)))
{
Task<string?> readTask = Task.Run(() => process.StandardOutput.ReadLineAsync());
try
{
response = await readTask.WaitAsync(cts.Token);
}
catch (OperationCanceledException)
{
WriteLine("⚠️ Timeout waiting for response");
}
}
if (response is not null)
{
// Manual JSON parsing
var json = JsonDocument.Parse(response);
// Check for result or error
if (json.RootElement.TryGetProperty("result", out JsonElement result))
{
// Handle success
}
else if (json.RootElement.TryGetProperty("error", out JsonElement error))
{
// Handle error
}
}
// Send another request
string listToolsRequest = """
{"jsonrpc":"2.0","method":"tools/list","id":2}
""";
await process.StandardInput.WriteLineAsync(listToolsRequest);
// ... more manual handling ...
}
finally
{
// Manual cleanup
process.Kill();
await process.WaitForExitAsync();
}
The Problem at Scale
This pattern becomes especially problematic when:
- Testing MCP servers - Every test needs this boilerplate
- Building MCP clients - Applications that consume MCP tools need robust communication
- Integration testing - Multiple round-trips of JSON-RPC communication
- Language servers - LSP also uses JSON-RPC over stdio
- Any interactive process protocol - Many modern tools use JSON-RPC for IPC
Proposed Solution
Add first-class JSON-RPC support to Amuru that handles the entire lifecycle of interactive process communication:
// Proposed Amuru API
var client = await Shell.Builder("dotnet")
.WithArguments("run", "--project", mcpPath, "-c", "Release")
.AsJsonRpcClient()
.StartAsync();
// Simple request/response
var initResponse = await client.SendRequestAsync("initialize", new {
protocolVersion = "1.0.0",
clientInfo = new { name = "test-client", version = "1.0.0" }
});
// List available tools
var toolsResponse = await client.SendRequestAsync("tools/list");
// Call a tool with parameters
var result = await client.SendRequestAsync("tools/call", new {
name = "get_documentation",
arguments = new { documentName = "Architecture" }
});
// Handle notifications (no response expected)
await client.SendNotificationAsync("initialized");
// Clean shutdown
await client.DisposeAsync();
Detailed API Design
public interface IJsonRpcClient : IAsyncDisposable
{
// Send request and wait for response
Task<JsonRpcResponse<TResult>> SendRequestAsync<TResult>(
string method,
object? parameters = null,
CancellationToken cancellationToken = default);
// Send request with dynamic response
Task<JsonRpcResponse> SendRequestAsync(
string method,
object? parameters = null,
CancellationToken cancellationToken = default);
// Send notification (no response expected)
Task SendNotificationAsync(
string method,
object? parameters = null,
CancellationToken cancellationToken = default);
// Event for unsolicited notifications from server
event EventHandler<JsonRpcNotification>? NotificationReceived;
// Process health
bool IsRunning { get; }
Task<int> WaitForExitAsync(CancellationToken cancellationToken = default);
}
public class JsonRpcResponse
{
public bool IsSuccess { get; }
public JsonElement? Result { get; }
public JsonRpcError? Error { get; }
public int Id { get; }
}
public class JsonRpcResponse<T> : JsonRpcResponse
{
public new T? Result { get; }
}
public class JsonRpcError
{
public int Code { get; }
public string Message { get; }
public JsonElement? Data { get; }
}
Builder Extensions
public static class ShellBuilderJsonRpcExtensions
{
public static JsonRpcClientBuilder AsJsonRpcClient(this RunBuilder builder)
{
return new JsonRpcClientBuilder(builder);
}
}
public class JsonRpcClientBuilder
{
public JsonRpcClientBuilder WithTimeout(TimeSpan timeout);
public JsonRpcClientBuilder WithStartupDelay(TimeSpan delay);
public JsonRpcClientBuilder WithEncoding(Encoding encoding);
public JsonRpcClientBuilder WithJsonSerializerOptions(JsonSerializerOptions options);
public JsonRpcClientBuilder WithRequestIdGenerator(Func<int> generator);
public Task<IJsonRpcClient> StartAsync(CancellationToken cancellationToken = default);
}
Implementation Considerations
1. Request/Response Correlation
- Automatic ID generation and correlation
- Support for concurrent requests
- Proper request queuing
2. Stream Management
- Handle newline-delimited JSON (most common for stdio)
- Support for Content-Length headers (LSP style)
- Buffering for partial messages
3. Error Handling
- Process crash detection
- Timeout handling per request
- Malformed JSON handling
- Connection state management
4. Lifecycle Management
- Graceful shutdown protocols
- Force kill as fallback
- Resource cleanup
5. Testing Support
// Should also provide testing utilities
var mockClient = new MockJsonRpcClient();
mockClient.SetupRequest("initialize").ReturnsJson(new {
capabilities = new { tools = true }
});
Real-World Use Cases
MCP Server Testing
var mcp = await Shell.Builder("mcp-server")
.AsJsonRpcClient()
.StartAsync();
var init = await mcp.SendRequestAsync("initialize", new {
protocolVersion = "1.0.0"
});
var tools = await mcp.SendRequestAsync("tools/list");
foreach (var tool in tools.Result.GetProperty("tools").EnumerateArray())
{
var name = tool.GetProperty("name").GetString();
var result = await mcp.SendRequestAsync("tools/call", new {
name = name,
arguments = new { }
});
}
Language Server Protocol
var lsp = await Shell.Builder("typescript-language-server")
.WithArguments("--stdio")
.AsJsonRpcClient()
.StartAsync();
await lsp.SendRequestAsync("initialize", new {
processId = Process.GetCurrentProcess().Id,
rootUri = "file:///workspace",
capabilities = new { }
});
await lsp.SendNotificationAsync("initialized");
var symbols = await lsp.SendRequestAsync("textDocument/documentSymbol", new {
textDocument = new { uri = "file:///workspace/main.ts" }
});
Custom Protocol Testing
var service = await Shell.Builder("./my-service")
.AsJsonRpcClient()
.WithTimeout(TimeSpan.FromSeconds(30))
.StartAsync();
// Service-specific protocol
var auth = await service.SendRequestAsync("auth.login", new {
username = "test",
password = "secret"
});
var token = auth.Result.GetProperty("token").GetString();
var data = await service.SendRequestAsync("data.query", new {
token = token,
query = "SELECT * FROM users"
});
Benefits
- Reduces boilerplate by 90% - From 200+ lines to 10-20 lines
- Type safety - Generic response types with proper deserialization
- Robust error handling - Built-in timeout, retry, and error handling
- Testability - Easy to mock for unit tests
- Reusability - Common pattern for many modern tools
- Consistency - Follows Amuru's existing patterns
Alternative Considerations
Streaming Responses
Some JSON-RPC servers send streaming responses. Consider support for:
await foreach (var chunk in client.StreamRequestAsync("generate", new { prompt = "..." }))
{
Console.Write(chunk.Result);
}
Batch Requests
JSON-RPC supports batch requests:
var responses = await client.SendBatchAsync(
new JsonRpcRequest("method1", new { }),
new JsonRpcRequest("method2", new { }),
new JsonRpcRequest("method3", new { })
);
Conclusion
Adding JSON-RPC support to Amuru would significantly reduce verbosity when working with interactive processes, particularly MCP servers which are becoming increasingly important in the AI tooling ecosystem. This feature would make Amuru the go-to solution for process communication in .NET, extending its value beyond simple command execution to complex interactive protocols.
The implementation would follow Amuru's existing patterns and philosophy while providing a robust, type-safe, and testable API for JSON-RPC communication.
Feature Request: JSON-RPC Support for Interactive Process Communication
Problem Statement
When working with processes that use JSON-RPC for communication (particularly Model Context Protocol (MCP) servers), the current approach requires verbose
Process.Start()code with manual stream management, timeout handling, and request/response correlation. This defeats Amuru's core mission of eliminating process execution verbosity.Current Pain Points
Here's a real example from testing an MCP server (like the one in TimeWarp.Nuru.Mcp):
The Problem at Scale
This pattern becomes especially problematic when:
Proposed Solution
Add first-class JSON-RPC support to Amuru that handles the entire lifecycle of interactive process communication:
Detailed API Design
Builder Extensions
Implementation Considerations
1. Request/Response Correlation
2. Stream Management
3. Error Handling
4. Lifecycle Management
5. Testing Support
Real-World Use Cases
MCP Server Testing
Language Server Protocol
Custom Protocol Testing
Benefits
Alternative Considerations
Streaming Responses
Some JSON-RPC servers send streaming responses. Consider support for:
Batch Requests
JSON-RPC supports batch requests:
Conclusion
Adding JSON-RPC support to Amuru would significantly reduce verbosity when working with interactive processes, particularly MCP servers which are becoming increasingly important in the AI tooling ecosystem. This feature would make Amuru the go-to solution for process communication in .NET, extending its value beyond simple command execution to complex interactive protocols.
The implementation would follow Amuru's existing patterns and philosophy while providing a robust, type-safe, and testable API for JSON-RPC communication.