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

Two layers

MCP has an inner data layer and an outer transport layer:

LayerJob
DataJSON-RPC messages: discover, list tools/resources/prompts, call tools, read resources
TransportHow 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 _meta unless configured not to.
  • Servers advertise supported versions and capabilities through a mandatory server/discover RPC.
  • 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"
  }
}
FieldWhy it matters
supportedVersionsClient picks a mutually supported version; mismatch → retry or fail cleanly
capabilitiesWhich primitives and related features exist (tools, resources, prompts, …)
serverInfoHuman/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:

  1. The client declares what it can do on each request (clientCapabilities in _meta).
  2. The server advertises what it offers via server/discover (and related result metadata).
  3. Both sides only attempt features the other side claimed.

Examples of what gets negotiated in practice:

SideExamples
ServerTools, resources, prompts (and related options your SDK exposes)
ClientFeatures 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.

TraitDetail
Best forDay-one labs, personal tools, npx / node dist/…
AuthMostly “who can run this binary on my machine”
GotchaNever 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).

TraitDetail
Best forShared / remote servers; many clients to one deployment
AuthStandard 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

ChooseWhen
stdioSingle user, local machine, fastest learning loop
Streamable HTTPTeam/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.

Optional next Harden tool handlers and trust boundaries. Hardening MCP Servers