You already know the roles and the three primitives. This page answers a different question: how does the plug actually talk? Discovery, capability negotiation, and transports — end to end — for protocol 2026-07-28.
No TypeScript lab here. When you host a remote server later, you will reuse this model; you do not need that post to finish this one.
Before you start
- Refresh JSON-RPC, transport, stdio, Streamable HTTP, and capability negotiation in MCP Without the Hype if those words are fuzzy.
- Optional: a working local server from Part 3 helps intuition, but this article stands alone.
Two layers
MCP has an inner data layer and an outer transport layer:
| Layer | Job |
|---|---|
| Data | JSON-RPC messages: discover, list tools/resources/prompts, call tools, read resources |
| Transport | How those messages move: local stdio pipes, or remote Streamable HTTP |
Same protocol ideas on both pipes. SDKs hide most wire details; you care when debugging “Connected but tools missing,” version mismatches, or choosing local vs remote.
Host (e.g. Cursor)
|
+-- MCP Client ---- transport ---- MCP Server
| |
stdio or HTTP tools / resources / prompts
Stateless by default (2026-07-28)
Older MCP versions leaned on an initialize handshake and session headers. Protocol 2026-07-28 is stateless: each request carries what the server needs to handle that request alone.
Practically:
- Every request includes protocol version (and client capabilities) in
_meta. - Clients should identify themselves in
_metaunless configured not to. - Servers advertise supported versions and capabilities through a mandatory
server/discoverRPC. - Clients may call discover first; they are not required to before doing useful work.
- Cross-call “session state” is not hidden in the transport — if your app needs continuity, mint an explicit handle (e.g. an id returned from a tool) and pass it as a normal argument later.
That design fits load-balanced remote servers: any instance can take any request without shared session storage.
Discovery: server/discover
Discovery answers: who are you, which protocol versions do you speak, and what can you do?
A simplified response shape (field names shortened for reading):
{
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"resources": {},
"prompts": {}
},
"serverInfo": {
"name": "geekmonks-mcp-bookmarks",
"version": "0.4.0"
}
}
| Field | Why it matters |
|---|---|
supportedVersions | Client picks a mutually supported version; mismatch → retry or fail cleanly |
capabilities | Which primitives and related features exist (tools, resources, prompts, …) |
serverInfo | Human/ops identity for logs and UIs |
You should see (in Inspector or host logs after connect): the host knows your server name and which primitives to offer the model. If capabilities omit tools, do not expect tool calls to work — fix the server advertisement, not the Cursor JSON first.
Official overview: Architecture (2026-07-28). Spec: server/discover.
Capability negotiation
Negotiation is not a one-time handshake you forget. On 2026-07-28:
- The client declares what it can do on each request (
clientCapabilitiesin_meta). - The server advertises what it offers via
server/discover(and related result metadata). - Both sides only attempt features the other side claimed.
Examples of what gets negotiated in practice:
| Side | Examples |
|---|---|
| Server | Tools, resources, prompts (and related options your SDK exposes) |
| Client | Features the server may need from the host (sampling is deprecated in this protocol version — do not build new servers around it) |
If a host never lists your tools, check three places: server registration, discover/capabilities output, and host UI support — not only mcp.json.
After discover: list and call
Once the client knows capabilities, the everyday loop looks like this:
(optional) server/discover
|
v
tools/list --> model picks a tool --> tools/call
resources/list --> resources/read
prompts/list --> prompts/get
Light message sketches (not full wire dumps):
List tools (idea):
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28"
}
}
}
Call a tool (idea):
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "list_bookmarks",
"arguments": {},
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28"
}
}
}
Hosts and SDKs fill real _meta fields for you. You care when a proxy strips headers, a version is wrong, or you are reading MCP Logs during a failure.
Transports: same data, different pipes
stdio
The host starts a local process and speaks JSON-RPC on stdin/stdout.
| Trait | Detail |
|---|---|
| Best for | Day-one labs, personal tools, npx / node dist/… |
| Auth | Mostly “who can run this binary on my machine” |
| Gotcha | Never log to stdout — it breaks JSON-RPC. Use stderr. |
Cursor config is a command + args (+ optional env). That is what Part 2 and Part 3 already use.
Streamable HTTP
The client talks to a URL with HTTP POST (optional SSE streaming for that request’s responses).
| Trait | Detail |
|---|---|
| Best for | Shared / remote servers; many clients to one deployment |
| Auth | Standard HTTP patterns (API keys, bearer tokens; OAuth when you need user-delegated access) |
| Shape (2026-07-28) | Stateless requests; method/name often mirrored in headers (Mcp-Method, Mcp-Name) so gateways can route without parsing bodies |
You do not need to host a public URL to understand MCP. Remote hosting is a separate, later post. Here the point is: transport changes the pipe, not the primitives.
Choosing a transport
| Choose | When |
|---|---|
| stdio | Single user, local machine, fastest learning loop |
| Streamable HTTP | Team/shared URL, SaaS-style integration, clients that cannot spawn your process |
Deprecated older HTTP+SSE transport: migrate to Streamable HTTP for new work (changelog).
What you can ignore on day one
Skip full notification catalogs, every error code, and building a custom host. If you can explain discover → list → call, and pick stdio vs HTTP for a use case, you have this post’s promise.
Wrap-up
MCP’s data layer is JSON-RPC: discover capabilities, list primitives, then call or read. Protocol 2026-07-28 keeps that loop stateless per request, with server/discover as the capability advertisement. Transports — stdio or Streamable HTTP — only change how those messages travel.
Series hub: MCP Without the Hype.