Implementing the Model Context Protocol from Scratch

Implementing the Model Context Protocol from Scratch

  ·  9 min read

This post is a Work In Progress.

As you can infer from the title, by the end of this post, you will have a working implementation of the Model Context Protocol (MCP) from scratch.

About MCP #

If you have been following the AI discourse in general, Model Context Protocol (MCP for short) is a star-child of the AI ecosystem. It’s the go-to way to let any LLM reach out to existing APIs and services.

If you are familiar with Remote Procedure Call (RPC) in traditional computer science, MCP is just a rebranded version of RPCs. Any seasoned platform engineer would read the MCP specification and go “oh they just rebranded RPCs for the AI era”.

An oversimplification would be to say MCP is a way to allow an LLM to interact with what we call a “tool provider” (although the protocol offers a lot more than just tools). A tool provider being some service that offers functions your LLM can use and run to get some work done.

But underneath all the hype, it’s just a simple JSON based protocol (preferrably over HTTP/2) to allow LLMs to interact with external tools and services.

And that’s what I want to uncover in this piece, is to implement the protocol itself from scratch and reason about it from first principles.

I’ll try to build all the ideas and code from scratch, the only pre-requisite knowledege I am assuing you possess is that you have interacted with one of the LLMs – ChatGPT, Claude, Grok etc.

Tool Calling and LLMs #

If you just open any of the LLM apps and prompt it with the following

Problem Statement

I need you to come up with a sequence of the following mentioned function calls 
to resolve an issue where a customer has ordered a particular flavour of 
ice-cream and it's not in stock. 

The situation is that Junaid had ordered choco mint icecream and wants it 
delivered in bangalore, 560037. 

Write your sequence as in a text format and add an explanation for why you 
think that sequence is correct. Please only use the functions specified below

You have the following functions at your disposal to use:

- add_inventory(item: str)
- create_icecream(flavour: str)
- deliver_icecream(address: str)

As you can see, your LLM will come with a valid sequence in which you’re supposed to call those functions in order to solve the speficied problem.

LLMs with reasoning capabilities are able to solve this sort of situation very well, where they can look at a set of tools/functions/situations and can come up with a logical sequence of actions that solves the problem.

We have an umbrella term for this capability, it’s called “tool calling”. It’s the ability of your LLM to come up with tool sequences with appropriate parameter values to eventually solve any arbitraty problem statement.

The improvement in reasoning capabilities in LLMs happened kind of at a similar time, and around that very time, Anthropic announced the Model Context Protocol (MCP). (TODO: verify this)

The intention of the protocal was to offer a way to formalise what I just showed you in the above example, a way to have an agreed-upon structure in which you could tell an LLM that “hey these are the tools you have at your disposal” and for the LLM to request the actual execution of those tools.

MCP Overview #

MCP (Model Context Protocol) is an open-source standard for connecting AI applications to external systems.

Using MCP, AI applications like Claude or ChatGPT can connect to data sources (e.g. local files, databases), tools (e.g. search engines, calculators) and workflows (e.g. specialized prompts)—enabling them to access key information and perform tasks.

Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect electronic devices, MCP provides a standardized way to connect AI applications to external systems.

Clients and Servers #

MCP follows a client-server architecture where an MCP host — an AI application like Claude Code or Claude Desktop — establishes connections to one or more MCP servers.

The MCP host accomplishes this by creating one MCP client for each MCP server. Each MCP client maintains a dedicated one-to-one connection with its corresponding MCP server.

The key participants in the MCP architecture are:

  • MCP Host: The AI application that coordinates and manages one or multiple MCP clients
  • MCP Client: A component that maintains a connection to an MCP server and obtains context from an MCP server for the MCP host to use
  • MCP Server: A program that provides context to MCP clients
graph TB
    subgraph "AI Application"
        Host[Claude/ChatGPT]
    end
    
    subgraph "MCP Clients"
        Client1[MCP Client 1]
        Client2[MCP Client 2]
        ClientN[MCP Client N]
    end
    
    subgraph "MCP Servers"
        Server1[MCP Server 1]
        Server2[MCP Server 2]
        ServerN[MCP Server N]
    end
    
    Host --> Client1
    Host --> Client2
    Host --> ClientN
    
    Client1 -.->|"1:1 Connection"| Server1
    Client2 -.->|"1:1 Connection"| Server2
    ClientN -.->|"1:1 Connection"| ServerN
    
    Server1 -->|"Context & Tools"| Client1
    Server2 -->|"Context & Tools"| Client2
    ServerN -->|"Context & Tools"| ClientN
    
    Client1 -->|"Context"| Host
    Client2 -->|"Context"| Host
    ClientN -->|"Context"| Host
  

MCP Server Decomposition #

MCP Clients talk to MCP Servers in a standardized format over a specific protocol.

MCP uses JSON-RPC 2.0 as it’s serialization standard, which is a JSON based standard to perform client-server operations with RPC-like servers.

These JSON-RPC messages can be sent over to a MCP server via the following transport protocols

  • stdio - For local instances and development.
  • Streamable HTTP - For web based interactions.

Just like most client-server protocols, MCP also follows a well-defined lifecycle.

sequenceDiagram
    participant Client
    participant Server

    Note over Client,Server: Initialization Phase
    activate Client
    Client->>+Server: initialize request
    Server-->>Client: initialize response
    Client--)Server: initialized notification

    Note over Client,Server: Operation Phase
    rect rgb(200, 220, 250)
        note over Client,Server: Normal protocol operations
    end

    Note over Client,Server: Shutdown
    Client--)-Server: Disconnect
    deactivate Server
    Note over Client,Server: Connection closed
  

We’ll unwrap this entire lifecycle with specific methods in a while, but let’s understand JSON-RPC and the Transport methods in depth.

JSON-RPC #

JSON RPC is the serialization format used by MCP.

It’s a very simple standard that you can use for RPC like operations. The protocol is so small and simple that it could easily fit on an index card.

The protocol defines two kinds of objects – Request and Response.

  • Request: A rpc call is represented by sending a Request object to a Server
  • Response: When a rpc call is made, the Server MUST reply with a Response, except for in the case of Notifications

Schema of a Request Object

  • jsonrpc
    • A String specifying the version of the JSON-RPC protocol. MUST be exactly “2.0”.
  • method
    • A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else.
  • params
    • A Structured value that holds the parameter values to be used during the invocation of the method. This member MAY be omitted.
  • id
    • An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null and Numbers SHOULD NOT contain fractional parts.

Example:

{"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}

Schema of a Response Object

  • jsonrpc
    • A String specifying the version of the JSON-RPC protocol. MUST be exactly “2.0”.
  • result
    • This member is REQUIRED on success. This member MUST NOT exist if there was an error invoking the method. The value of this member is determined by the method invoked on the Server.
  • error
    • This member is REQUIRED on error. This member MUST NOT exist if there was no error triggered during invocation. The value for this member MUST be an Object as defined in section 5.1.
  • id
    • This member is REQUIRED. It MUST be the same as the value of the id member in the Request Object. If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null. Either the result member or error member MUST be included, but both members MUST NOT be included.

Example:

{"jsonrpc": "2.0", "result": 19, "id": 1}

Transport #

stdio #

The stdio (standard input/output) streams are a core part of Unix systems. Every process gets three default file descriptors:

  • stdin (standard input, usually file descriptor 0): where a process reads input, typically from the keyboard or another program.
  • stdout (standard output, file descriptor 1): where a process writes its output, usually displayed on the terminal or piped to another process.
  • stderr (standard error, file descriptor 2): used for error messages and diagnostics, separate from regular output.

These streams enable simple, composable communication between programs. Tools can be chained together using pipes, allowing the output of one process to become the input of another. This model is foundational for building flexible, scriptable workflows in Unix environments.

Streamable HTTP #

In the Streamable HTTP transport, the server operates as an independent process that can handle multiple client connections. This transport uses HTTP POST and GET requests.

Server can optionally make use of Server-Sent Events (SSE) to stream multiple server messages. This permits basic MCP servers, as well as more feature-rich servers supporting streaming and server-to-client notifications and requests.

The server MUST provide a single HTTP endpoint path (hereafter referred to as the MCP endpoint) that supports both POST and GET methods. For example, this could be a URL like https://example.com/mcp.

If the MCP server is using Server-Sent Events (SSE), then it’s advised to implement it over HTTP/2 for performance reasons.

When not used over HTTP/2, SSE suffers from a limitation to the maximum number of open connections, which can be especially painful when opening multiple tabs, as the limit is per browser and is set to a very low number (6).

When using HTTP/2, the maximum number of simultaneous HTTP streams over a single TCP connection is negotiated between the server and the client (defaults to 100).

sequenceDiagram
    participant Client
    participant Server

    note over Client, Server: initialization

    Client->>+Server: POST InitializeRequest
    Server->>-Client: InitializeResponse
Mcp-Session-Id: 1868a90c... Client->>+Server: POST InitializedNotification
Mcp-Session-Id: 1868a90c... Server->>-Client: 202 Accepted note over Client, Server: client requests Client->>+Server: POST ... request ...
Mcp-Session-Id: 1868a90c... alt single HTTP response Server->>Client: ... response ... else server opens SSE stream loop while connection remains open Server-)Client: ... SSE messages from server ... end Server-)Client: SSE event: ... response ... end deactivate Server note over Client, Server: client notifications/responses Client->>+Server: POST ... notification/response ...
Mcp-Session-Id: 1868a90c... Server->>-Client: 202 Accepted note over Client, Server: server requests Client->>+Server: GET
Mcp-Session-Id: 1868a90c... loop while connection remains open Server-)Client: ... SSE messages from server ... end deactivate Server