DEV Community

Bharathvaj
Bharathvaj

Posted on • Originally published at bharathvaj.com

JSON-RPC Uncovered

While tinkering with Agentic MCP servers at work, I ran into JSON-RPC, the slick tech driving parts of the MCP system. Talk about a *remote*ly cool find! It fired up my curiosity, so I dug in.

What’s JSON-RPC?

JSON-RPC is like texting your server to run a function. It’s a lightweight protocol using JSON for messages:

  • Request: Names a method, sends params, and tracks with an optional ID.
  • Response: Returns a result or error, tied to the ID.
  • Notification: A request without an ID—no reply needed.

It vibes over HTTP, WebSocket, or TCP, sticking to JSON-RPC 2.0 for rpc-king simplicity.

Why’s It Awesome?

JSON-RPC makes remote calls a breeze by:

  1. Playing Nice: JSON works everywhere.
  2. Staying Lean: Ditches the bloat of SOAP or complex REST.
  3. Feeling Local: Remote functions act like they’re next door.
  4. Flexing: Handles requests and one-way notifications.

Perfect for microservices, IoT, or any app needing a fast rpc-ket to the server.

HandsOn

Let’s build a JSON-RPC server and client for cleaner message handling. The server supports:

  • add: Sums two numbers.
  • greet: Returns a greeting.
  • log: A notification to log a message.

We’ll use nodejs with HTTP for easy implementation.

1. Setup the project

mkdir json-rpc-jam cd json-rpc-jam npm init -y npm install express body-parser jsonrpc-lite 
Enter fullscreen mode Exit fullscreen mode

2. Create client and server files.

client.js

const jsonrpc = require("jsonrpc-lite"); async function sendRpcRequest(body, skipResponse = false) { const res = await fetch("http://localhost:3000/rpc", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (skipResponse) { return res.status; } return await res.json(); } async function test() { console.log("add([5, 3]):", await sendRpcRequest(jsonrpc.request(1, "add", [5, 3]))); console.log("greet({ name: 'Alice' }):", await sendRpcRequest(jsonrpc.request(2, "greet", { name: "Alice" }))); console.log("notification:", await sendRpcRequest(jsonrpc.notification("log", { message: "Hello" }), true)); console.log("bogus method:", await sendRpcRequest(jsonrpc.request(3, "not-a-method", [5, 3]))); } test(); 
Enter fullscreen mode Exit fullscreen mode

server.js

const express = require("express"); const jsonrpc = require("jsonrpc-lite"); const bodyParser = require("body-parser"); const app = express(); app.use(bodyParser.json()); const methods = { add: (params) => { if (!Array.isArray(params) || params.length !== 2) throw jsonrpc.JsonRpcError.invalidParams("Bad params, no sum!"); return params[0] + params[1]; }, greet: (params) => { if (!params?.name) throw jsonrpc.JsonRpcError.invalidParams("No name, no fame!"); return `Yo, ${params.name}, what’s good?`; }, log: (params) => { if (!params?.message) throw jsonrpc.JsonRpcError.invalidParams("No message to log!"); console.log(`Notification: ${params.message}`); return null; }, }; app.post("/rpc", (req, res) => { // Parse incoming request with jsonrpc-lite const parsed = jsonrpc.parseObject(req.body); console.log(parsed); // Handle invalid JSON-RPC if (parsed.type === "invalid") { return res .status(400) .json(jsonrpc.error(null, jsonrpc.JsonRpcError.invalidRequest())); } const { type, payload } = parsed; // Handle notifications (no ID) if (type === "notification") { try { if (!methods[payload.method]) throw jsonrpc.JsonRpcError.methodNotFound(); methods[payload.method](payload.params); return res.status(204).send(); } catch (error) { return res.status(204).send(); // Silent for notifications } } // Handle requests if (type === "request") { try { if (!methods[payload.method]) throw jsonrpc.JsonRpcError.methodNotFound(); const result = methods[payload.method](payload.params); res.json(jsonrpc.success(payload.id, result)); } catch (error) { res.status(400).json(jsonrpc.error(payload.id, error)); } } }); app.listen(3000, () => console.log("Server’s JSON-RPC at http://localhost:3000") ); 
Enter fullscreen mode Exit fullscreen mode

3. Run both server and client

node server.js # In a different terminal node client.js 
Enter fullscreen mode Exit fullscreen mode

We will be seeing the following logs in the client terminal

add([5, 3]): { jsonrpc: '2.0', id: 1, result: 8 } greet({ name: 'Alice' }): { jsonrpc: '2.0', id: 2, result: 'Yo, Alice, what’s good?' } notification: 204 bogus method: { jsonrpc: '2.0', id: 3, error: { message: 'Method not found', code: -32601 } } 
Enter fullscreen mode Exit fullscreen mode

Wrap-Up

JSON-RPC, is a total game-changer for zipping remote calls and notifications with minimal fuss. It’s like sending a quick DM to your server and getting a reply before you can say rpc-ord time!

What’s next? Here are a couple of ideas to take this forward.

  • Add Authentication: Secure your JSON-RPC endpoint with JWT or API keys for safe vibes.
  • Go WebSocket: Swap HTTP for WebSocket to unlock real-time, two-way rpc-king action.
  • Expand Methods: Toss in more complex methods to handle bigger tasks and flex JSON-RPC’s power.

Keep rpc-king it!

Top comments (1)

Collapse
 
nitish_kumar_909fdd24f2a6 profile image
Nitish Kumar

I've also written a separate blog post covering authentication in more detail. You can check it out here: nkumar37.vercel.app/posts/securing...