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. Here’s a quick, casual scoop on JSON-RPC, including notifications, using the jsonrpc-lite npm package in a Node.js server and client—puns included! 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: Playing Nice: JSON works everywhere. Staying Lean: Ditches the bloat of SOAP or complex REST. Feeling Local: Remote functions act like they’re next door. 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 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(); 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") ); 3. Run both server and client node server.js # In a different terminal node client.js 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 } } 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 Auth

Apr 22, 2025 - 12:17
 0
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. Here’s a quick, casual scoop on JSON-RPC, including notifications, using the jsonrpc-lite npm package in a Node.js server and client—puns included!

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

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();

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")
);

3. Run both server and client

node server.js

# In a different terminal
node client.js

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 }
}

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!