HIROSE PAPER MFG. CO., LTD.

Employees' Blog

Where Does the MCP Client Actually Live?
Part 1 of 6: mapping MCP's host, client, and server before the legacy-to-modern transition

Published on: 2026.08.07 Last updated: 2026.08.07
An illustration of a browser app that takes a natural-language question, has an AI draft SQL, queries a relational database, and returns a table, with the headline 'Where's the Client, Really?' worked into the scene

This is part 1 of a six-part series called “Building an MCP Client in React.” By the end of the series we’ll have built a small browser app together, but this first part is about drawing the map before writing any code. If you’ve never heard of MCP (Model Context Protocol), or you only have a vague sense that “it’s some way of connecting AI to tools,” this part is for you: we’ll sort out who the players are, and what transition the technology is in the middle of right now.

The App: One That Just Returns a Table

Let’s start with the example we’ll use throughout the series. It does one simple thing. A user types a natural-language question into a text box — something like “show me the top 10 products by sales last month” — and the app turns that into SQL, queries a relational database, and renders the result as a table. It’s a minimal browser app with only as much backend as it needs.

At a glance this looks like a typical “let AI write your SQL” app, but break it apart and three distinct roles fall out.

  • The app itself, which takes the user’s question and draws the table
  • The LLM API, which turns natural language into SQL
  • The database gateway, which runs the SQL and hands the table structure and results back to the app

MCP is an attempt to standardize that third piece: how you build the database gateway. And here’s the part that trips people up: the term “MCP client” doesn’t refer to the whole app — it refers to one small piece that talks to that gateway. Where does the host end and the client begin, and where does the server start? Get this wrong and every later post about headers and sessions will feel like it’s talking about something else entirely. So we fix it here, first.

What MCP Actually Is

MCP (Model Context Protocol) is a standardized protocol for connecting LLM applications to external data and tools. The official spec describes its purpose in three parts: sharing contextual information with a model, exposing tools and capabilities to an AI system, and building composable integrations and workflows.

It’s built on top of JSON-RPC 2.0. In short, JSON-RPC is a lightweight convention for remote calls: you send a request with a method name and parameters, and you get back either a result or an error (the exact wire format isn’t the point of this article, so see the primary sources at the end if you want the details). MCP layers its own meaning on top of that JSON-RPC envelope — which methods exist, and who’s allowed to call them.

The official spec points to a precedent for this kind of design: the Language Server Protocol (LSP). Just as LSP standardized how editors add support for programming languages, MCP standardizes how AI applications add context and tools.

Three Parties: Host, Client, Server

The spec splits the parties in a conversation into three roles.

RoleWhat the spec saysWhere it sits in our running example
HostThe LLM application that initiates connectionsThe app that receives the user’s question
ClientA connector living inside the host, responsible for one connection to a serverThe piece that talks to the database gateway
ServerA service that provides context or capabilitiesThe service that returns table definitions and query results

“Client” doesn’t name the whole app — it names a small connector inside the host that owns one connection to one server. Read literally, the series title “Building an MCP Client” really means “embedding, inside the host, a client that talks to a server.”

The spec organizes what a server is allowed to offer into three kinds of features.

  • Resources: context and data for the user or the model to use
  • Prompts: templated messages and workflows for the user
  • Tools: functions the model can execute

In the other direction, the spec lists Elicitation — a server asking the user for more information — as something a client can offer to a server.

Mapped onto our running example, it’s natural for the database server to expose the table definitions as a Resource and the “run this SQL” capability as a Tool. The host fetches the schema via that Resource, hands it to the LLM API with a prompt like “write SQL against this schema,” gets SQL back, calls the Tool with it, and turns the result into a table. What matters here is that calling the LLM API sits outside MCP. What MCP standardizes is how the host gets context and capabilities from the server — not how the host talks to the model. The concrete design of a server’s Resources and Tools (things like inputSchema and outputSchema) is part 3’s topic; the host-side implementation is part 4’s.

A diagram labeled 'Host', 'Client', and 'Server', showing the host application, the MCP client connector inside it, and the MCP server for the database, with a separate 'LLM API' cloud sitting 'Outside MCP'
Figure 1: Host, client, and server — and their roles in our running example

There’s one more constraint worth calling out from the spec’s “message patterns” page. “Servers MUST NOT initiate JSON-RPC requests.” When a server needs something from the client mid-request — a model completion (sampling), a confirmation from the user (elicitation), or a filesystem root — it can’t simply fire off a new request. Instead it answers the in-flight request with an InputRequiredResult, and the client resends the same request with inputResponses attached. The spec calls this pattern Multi Round-Trip Requests (MRTR). Elicitation, mentioned above, is implemented through exactly this MRTR mechanism. The mechanics of an actual MRTR round trip are outside what this series measures (this paragraph is a description of the spec’s text; we haven’t exercised it against a real implementation).

A diagram labeled 'Client' and 'Server' showing that a server cannot issue its own JSON-RPC requests, marked 'No Calls Out', and instead returns an InputRequiredResult that the client answers by resending the same request with inputResponses, labeled 'Resend'
Figure 2: A server can’t speak first — the one-way shape of MRTR

This figure illustrates a pattern the spec defines; we haven’t measured it against a running implementation.

Two Eras: One Handshake, or a Version on Every Request

The MCP spec went through a real fault line at the 2026-07-28 revision. The spec’s “Versioning and Compatibility” page names the two sides of that fault line legacy (2025-11-25 and earlier) and modern (2026-07-28 and later).

A diagram labeled 'Legacy' and 'Modern' contrasting the legacy era, which establishes a session through 'One Handshake', with the modern era, which declares its protocol version on 'Every Request'
Figure 3: The legacy handshake versus the modern per-request ID check

Legacy: One Handshake, Then You’re In

In versions up to 2025-11-25, a client’s first move is to call a method named initialize, negotiating supported versions and capabilities with the server. Once that succeeds, every following request is treated as part of that session. Do the handshake once, and everything after it is just a continuation of that same session.

Modern: State Your Version, Every Time

From 2026-07-28 on, there’s no such thing as a handshake up front. The spec puts it directly:

There is no negotiation handshake. Every request carries its protocol version, and the server accepts or rejects each request independently.

In practice, each request carries its protocol version in the _meta field of the body. If the server doesn’t support the requested version, it’s supposed to reply with an UnsupportedProtocolVersionError (error code -32022), listing the versions it does support.

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": {
      "supported": ["2026-07-28", "2025-11-25"],
      "requested": "1900-01-01"
    }
  }
}

The spec also requires every server to implement server/discover. A client may call it up front to learn what a server supports, but it doesn’t have to — it’s equally valid to just call the method it actually wants and handle an UnsupportedProtocolVersionError if it comes back.

Dropping the session as a unit also reshaped the transport layer. The two standard transports are stdio (newline-delimited messages over the standard streams of a client-launched subprocess) and Streamable HTTP (a single endpoint that takes HTTP POSTs). The Streamable HTTP spec states it plainly:

Removal of the GET stream endpoint. / Removal of protocol-level sessions.

In other words, both the always-open GET stream and the very concept of a protocol-level session were removed from the spec at 2026-07-28. We’ll cover Streamable HTTP’s actual shape — required headers, the _meta mirroring, the HeaderMismatch error — in part 2.

What Happens During the Transition

While legacy and modern coexist, the outcome depends on which combination of client and server you’re running. The spec includes a full compatibility matrix; here’s the part that matters for this transition period.

ClientServerOutcome
modernmodernWorks. A version mismatch surfaces as UnsupportedProtocolVersionError, and the client retries with a supported version
modernlegacyFails. A legacy server doesn’t understand the modern per-request declaration format
legacymodernFails. A modern server doesn’t treat initialize as a required entry point
dual-eraeitherWorks. The client detects the other side’s era and adapts

What this table tells you is that “implement it the way the newest spec reads” doesn’t guarantee it’ll work. The outcome depends on which era the thing you’re actually talking to — in our case, a server library — has caught up to, regardless of how your client code is written. That’s the theoretical picture; the next section is what actually happened when we tried it.

What Actually Happens When You Build This Today

Everything from here on is something we ran, not just something we read. The environment was Node.js v22.12.0, the official TypeScript SDK (@modelcontextprotocol/sdk) at version 1.30.0, run on 2026-08-06. The server was a minimal MCP server with a single echo tool that we built ourselves — it doesn’t depend on any particular product or service.

The Spec Is Ahead of the SDK

First, we checked which protocol version the SDK actually implements.

LATEST_PROTOCOL_VERSION = 2025-11-25
DEFAULT_NEGOTIATED_PROTOCOL_VERSION = 2025-03-26
SUPPORTED_PROTOCOL_VERSIONS = ["2025-11-25","2025-06-18","2025-03-26","2024-11-05","2024-10-07"]

The spec’s current revision is 2026-07-28, but the latest official TypeScript SDK (1.30.0) only implements up to 2025-11-25. That means the SDK is still working entirely within the legacy era. Searching the package contents turned up neither the string 2026-07-28 nor a method named server/discover.

Sure enough, when we announced 2026-07-28, the request was rejected:

--- response status ---
400 Bad Request
--- response body ---
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Unsupported protocol version: 2026-07-28 (supported versions: 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05, 2024-10-07)"},"id":null}

One gap here is worth flagging directly. The spec asks for UnsupportedProtocolVersionError (code -32022) with a data.supported list on an unsupported-version response, but what we actually got was a different code, -32000, and a prose message with the supported versions embedded in the text. Read this with the expectation that the implementation may not have caught up to the spec’s newer error vocabulary yet.

What this means is that “the current state of the spec” and “what you can actually write against the SDK today” have to be treated as two separate things. The hands-on parts of this series (part 2 onward) will prioritize code that actually runs, which means building on the legacy-era approach the SDK supports today, and calling out what changes under the modern era as we go.

An illustration of the specification, marked '2026-07-28', running ahead while the implementation, marked '2025-11-25', follows behind
Figure 4: The spec runs ahead, the implementation catches up

A Plain POST Fails Because of Accept, Not the Handshake

The Streamable HTTP spec requires the client to list both application/json and text/event-stream in its Accept header. Send a plain POST with only Content-Type set, skipping that, and you get a 406 before anything else happens:

--- response status ---
406 Not Acceptable
--- response body ---
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Not Acceptable: Client must accept both application/json and text/event-stream"},"id":null}

This is worth underlining: the real reason a plain POST doesn’t work against an MCP server is the Accept header, not the presence or absence of a handshake. This check happens before the body is even read, so a perfectly well-formed JSON-RPC request still gets the same 406 if Accept is missing. We’ll go through Streamable HTTP’s required headers and how to read its responses in part 2.

Whether You Need initialize Depends on the Server’s Session Mode

There’s one more thing we got wrong ourselves before actually testing it. It is not true that calling a method without going through initialize always fails. With the server built stateless (no session ID issued), tools/list and tools/call both came back 200 with no handshake at all, as long as Accept was satisfied.

--- response status ---
200 OK
--- response body ---
event: message
data: {"result":{"content":[{"type":"text","text":"hello"}]},"jsonrpc":"2.0","id":6}

Rebuild that same SDK’s server as stateful (issuing a session ID), though, and a request that skips initialize gets rejected:

--- response status ---
400 Bad Request
--- response body ---
{"jsonrpc":"2.0","error":{"code":-32000,"message":"Bad Request: Server not initialized"},"id":null}

Every subsequent request also needs the Mcp-Session-Id issued during that handshake attached. So whether you need the handshake is less a rule of the protocol than a property of how the server’s session mode is implemented. The fact that stateless implementations already exist in the wild is a small hint of the direction the protocol eventually took: dropping protocol-level sessions entirely at 2026-07-28.

Mapping the Running Example Back

Let’s put all of this back onto the example we opened with.

The host — the app itself — holds an MCP client connector internally, and uses it to talk to the database’s MCP server. The server exposes a Resource that returns table definitions and a Tool that runs SQL. Calling the LLM API to draft the SQL happens outside MCP, directly in the host. And whether that conversation runs on a legacy handshake or a modern per-request declaration depends entirely on which era the server implementation you’re connecting to has landed on. Build against today’s official SDK, and in practice you’re following legacy-era rules: whether you need initialize depends on the server’s session mode, and Accept must list both content types either way.

What’s Coming in This Series

Part 1 stops at the map. The specifics get their own parts.

An illustration mapping all six parts of the series as trail signposts labeled 'You Are Here', 'HTTP', 'Server', 'React', 'Async', and 'Finish', with the first part marked as the current position
Figure 5: The six-part map — you are here
PartWhat it covers
2Streamable HTTP in detail — required headers, _meta mirroring, HeaderMismatch, SSE, Origin checks
3Server-side design — tools and resources, inputSchema / outputSchema, x-mcp-header
4The React side — can you call it straight from the browser, do you need a BFF, reading SSE, cancellation
5Work that can’t just wait — notifications/progress, subscriptions/listen, the Tasks extension, and cancellation being cooperative
6Finishing and looking back — authorization, dual-era compatibility, whether it’s safe to run generated SQL as-is

The Tasks extension covered in part 5 is one of the opt-in extensions the official spec lists: it uses polling, mid-flight input, and durable handles to run long operations asynchronously (this paragraph is describing what the spec says; we haven’t exercised it).

Wrapping Up

MCP connects a host, a client, and a server over JSON-RPC 2.0. “Client” doesn’t name the whole app — it names the small connector inside the host that owns one connection to one server. The 2026-07-28 revision made explicit a transition from a legacy era that establishes a session through a handshake to a modern era that states its protocol version on every request via _meta, but the latest official TypeScript SDK still only works within the legacy era. Testing it directly, the real reason a plain POST fails is a missing Accept header, and whether you need initialize depends on the server’s session mode. Keeping the spec’s language separate from the language you can actually write today turns out to be the first real step toward getting this running. Next time, we’ll dig into Streamable HTTP’s headers and the shape of its SSE responses, working from actual traffic.

Articles in This Series

Read from Part 1 and the series runs as one line, from mapping the spec to code that runs in a browser. Each part also stands on its own, so jump around from here whenever you want to.

Primary Sources

Pixcel Art of Aki. holdin a cat.

About the Author

Aki Matsumura

Joined HIROSE PAPER MFG. CO., LTD. in November 2024.

Brings a diverse professional background spanning retail, welfare services, and food service before transitioning into system development.

Currently serves as an in-house systems engineer, responsible for internal database development and system improvement initiatives across the company.

View posts by this author