Lesson 5 / 31

JSON-RPC 2.0 in Five Minutes

Read the three message shapes MCP is built on.

Requests, responses and notifications

MCP messages are JSON-RPC 2.0. A request has jsonrpc: "2.0", a method, optional params and an id, and expects a response with the same id containing either a result or an error (with a numeric code and a message). A notification has a method but no id and gets no reply. Standard error codes include -32700 (parse error), -32600 (invalid request), -32601 (method not found), -32602 (invalid params) and -32603 (internal error). Because messages are plain JSON, MCP is easy to log, replay and test with ordinary tools.

JSON-RPC underneath

MCP is JSON-RPC messages, a capability handshake and three server primitives.

Five parts: messages, handshake, tools, resources, prompts.
Figure 2.1 — Messages, handshake, tools, resources and prompts.

A mini JSON-RPC dispatcher, run

I ran this with plain Python 3 (standard library only). The function returns a result for a valid call, None for a notification (no id, no reply), and the standard error objects for an unknown method (-32601), bad params (-32602) and unparseable text (-32700).

import json

def handle(raw, methods):
    """A tiny JSON-RPC 2.0 dispatcher showing request, notification and error shapes."""
    try:
        msg = json.loads(raw)
    except json.JSONDecodeError:
        return {"jsonrpc": "2.0", "id": None, "error": {"code": -32700, "message": "Parse error"}}
    if "id" not in msg:                                   # notification: do the work, send no reply
        methods.get(msg["method"], lambda p: None)(msg.get("params"))
        return None
    if msg["method"] not in methods:
        return {"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32601, "message": "Method not found"}}
    try:
        return {"jsonrpc": "2.0", "id": msg["id"], "result": methods[msg["method"]](msg.get("params"))}
    except (KeyError, TypeError):
        return {"jsonrpc": "2.0", "id": msg["id"], "error": {"code": -32602, "message": "Invalid params"}}

methods = {"add": lambda p: p["a"] + p["b"], "ping": lambda p: {}}
for raw in ('{"jsonrpc":"2.0","id":1,"method":"add","params":{"a":2,"b":3}}',
            '{"jsonrpc":"2.0","method":"ping"}',
            '{"jsonrpc":"2.0","id":2,"method":"mul","params":{}}',
            '{"jsonrpc":"2.0","id":3,"method":"add","params":{"a":2}}',
            '{not json'):
    print(handle(raw, methods))

Output:

{'jsonrpc': '2.0', 'id': 1, 'result': 5}
None
{'jsonrpc': '2.0', 'id': 2, 'error': {'code': -32601, 'message': 'Method not found'}}
{'jsonrpc': '2.0', 'id': 3, 'error': {'code': -32602, 'message': 'Invalid params'}}
{'jsonrpc': '2.0', 'id': None, 'error': {'code': -32700, 'message': 'Parse error'}}

Quick check: What marks a JSON-RPC message as a notification?

  • It has a result field
  • It has no id and expects no reply
  • It uses XML
  • It has error code -32601
Answer

It has no id and expects no reply — Only requests carry an id that a response must echo.