mcpjson-rpctoolsagentsai

Build an MCP Server: The JSON-RPC Behind Tools, Resources & Prompts

·10 min read·AI & JSON

MCP is just JSON-RPC with three primitives

The Model Context Protocol standardizes how AI clients connect to external capabilities. By 2026 the ecosystem passed 13,000 servers with OpenAI, Google, and Microsoft on board — but underneath the hype an MCP server is something refreshingly boring: a JSON-RPC 2.0 process that exposes three primitives over stdio or streamable HTTP:

  • Tools — functions the model can call (send an email, query a DB).
  • Resources — data the model can read (files, records, docs).
  • Prompts — reusable templates the user or model can invoke.

If you understand the JSON messages, you understand MCP. SDKs (FastMCP for Python, the TypeScript SDK, Go) hide the wire format, but knowing it is what lets you debug when a client says "no tools found."

The initialize handshake

Every session starts with the client and server exchanging capabilities. It's a plain JSON-RPC request/response:

json
// client -> server
{ "jsonrpc": "2.0", "id": 1, "method": "initialize",
  "params": { "protocolVersion": "2026-03-26", "capabilities": {}, "clientInfo": { "name": "claude", "version": "1.0" } } }
json
// server -> client
{ "jsonrpc": "2.0", "id": 1, "result": {
  "protocolVersion": "2026-03-26",
  "capabilities": { "tools": {}, "resources": {}, "prompts": {} },
  "serverInfo": { "name": "my-server", "version": "0.1.0" } } }

Listing and calling a tool

The client discovers tools with tools/list; the server returns each tool's name, description, and an inputSchema — a JSON Schema the model uses to build valid arguments:

json
// tools/list result
{ "tools": [
  { "name": "get_weather",
    "description": "Get current weather for a city",
    "inputSchema": {
      "type": "object",
      "properties": { "city": { "type": "string" } },
      "required": ["city"]
    } } ] }

To call it, the client sends tools/call with arguments that satisfy that schema, and the server returns a content array:

json
// client -> server
{ "jsonrpc": "2.0", "id": 7, "method": "tools/call",
  "params": { "name": "get_weather", "arguments": { "city": "Ahmedabad" } } }

// server -> client
{ "jsonrpc": "2.0", "id": 7, "result": {
  "content": [{ "type": "text", "text": "34°C, clear skies" }] } }

The inputSchema is the contract that makes tool calls reliable — get it right and the model rarely sends bad arguments.

A minimal server with FastMCP

You don't hand-write JSON-RPC; an SDK maps decorated functions onto it:

python
from fastmcp import FastMCP

mcp = FastMCP("my-server")

@mcp.tool()
def get_weather(city: str) -> str:
    """Get current weather for a city."""
    return f"34°C in {city}"

if __name__ == "__main__":
    mcp.run()   # speaks JSON-RPC over stdio

FastMCP derives the inputSchema from your type hints and the description from the docstring — which is why good hints and docstrings directly improve how well the model uses your tool.

Inspecting the messages while you build

The single best debugging habit is to look at the raw JSON-RPC. The MCP Inspector lets you call tools in a browser without an LLM in the loop; alongside it:

  1. Copy a tools/list result and format it to read each tool's inputSchema clearly.
  2. Validate your inputSchema as real JSON Schema — a malformed schema is the most common reason a client silently shows no tools.
  3. When a tool call fails, diff the arguments the client sent against the schema you expected to find the mismatch.

For the conceptual background see MCP Explained: JSON-RPC for AI and the MCP message JSON example.

Frequently asked questions

JSON-RPC 2.0, transported over stdio (for local servers) or streamable HTTP (for remote ones). Every request, response, and notification is a JSON-RPC message.

Tools (functions the model can call), resources (data the model can read), and prompts (reusable templates). A server can expose any combination and declares which in its initialize capabilities.

The most common cause is an invalid inputSchema — it must be well-formed JSON Schema. Validate it, and confirm your initialize response advertises the tools capability. Inspecting the raw tools/list result usually reveals the problem.

No. SDKs like FastMCP (Python), the TypeScript SDK, and Go libraries generate the wire messages from your functions and type hints. Understanding the JSON is for debugging, not day-to-day coding.

Try MCP Message Example

See the JSON-RPC messages behind tools/list and tools/call.