How MCP Servers Work: the 2026-07-28 Spec Explained
An MCP server is a program that exposes tools, resources and prompts to an AI application over JSON-RPC. Under the current spec revision, 2026-07-28, there is no connection handshake: every request carries its own protocol version and client capabilities in _meta, every result carries a resultType, and the server treats each request on its own.
That last part is the big change, and it’s why most MCP explainers you’ll find now describe what the spec calls Legacy.
Last verified: 26 September 2026 against the 2026-07-28 spec and TypeScript SDK 2.1.0.
The numbers, in one table
| What | Current value | Source |
|---|---|---|
| Current spec revision | 2026-07-28 | Changelog |
| Previous revision | 2025-11-25 (now “Legacy”) | Versioning |
| Connection handshake | None. Removed in 2026-07-28 | Versioning |
| Standard transports | stdio, Streamable HTTP | Transports |
| Required headers on Streamable HTTP POSTs | MCP-Protocol-Version, Mcp-Method, plus Mcp-Name on tools/call, resources/read, prompts/get | Streamable HTTP |
resultType values | "complete", "input_required" | Changelog |
| Unsupported protocol version error | -32022 | Versioning |
| Header mismatch error | HTTP 400, JSON-RPC -32020 | Streamable HTTP |
| Resource not found error | -32602 (was -32002) | Resources |
@modelcontextprotocol/server | 2.1.0 (latest, 23 Sep 2026) | npm |
@modelcontextprotocol/client | 2.1.0 (latest, 23 Sep 2026) | npm |
@modelcontextprotocol/sdk (v1 line) | 1.30.1, not deprecated | npm |
| v1.x support after v2 | Bug fixes and security updates for at least 6 months | SDK README |
Python SDK (mcp on PyPI) | 2.2.0, supports 2026-07-28 | PyPI |
The architecture hasn’t changed
The shape is the same as it’s always been. Three parts:
[AI Tool (Host)] <---> [MCP Client] <---> [MCP Server] <---> [External Service]
The host is the AI application, Claude Code in my case. The client lives inside the host and handles the connection to each server. The server is the small program you write: it exposes tools and data, and it can run locally as a subprocess or remotely over HTTP.
The point of it hasn’t moved either. Instead of every AI tool building its own GitHub integration, its own database integration and so on, a server gets written once and any MCP host can use it.
What changed in 2026-07-28 is everything that happens on the wire between client and server.
How a request works under 2026-07-28
The spec puts it in two sentences: “There is no negotiation handshake. Every request carries its protocol version, and the server accepts or rejects each request independently.”
So there’s no session to set up and nothing to tear down. Each request brings three things in _meta. It sends io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities, and it should also send io.modelcontextprotocol/clientInfo. That one is a SHOULD rather than a MUST. The SDK’s 2.0.0 release notes say it was demoted from required. Servers, for their part, should identify themselves in each result’s _meta under io.modelcontextprotocol/serverInfo.
Here’s the spec’s own example of a tool call over Streamable HTTP, headers and all. I’ve only re-indented the JSON body so it’s readable:
POST /mcp HTTP/1.1
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_weather
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": { "location": "Seattle, WA" },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "ExampleClient", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
The headers are the new part. Mcp-Method and Mcp-Name repeat what’s already in the body, and the spec says outright that “These headers are REQUIRED for compliance.” This is the table it gives:
| Header Name | Source Field | Required For |
|---|---|---|
Mcp-Method | method | All requests |
Mcp-Name | params.name or params.uri | tools/call, resources/read, prompts/get requests |
MCP-Protocol-Version is required on every POST as well.
A tool result from the spec’s tools page looks like this. Note resultType sitting at the top of result:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"resultType": "complete",
"content": [
{
"type": "text",
"text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"
}
],
"isError": false
}
}
resultType is required on every result now. "complete" means a normal answer. "input_required" means the server needs something more before it can finish (more on that below). There’s one piece of backward compatibility: clients “MUST treat results from earlier-protocol servers that omit the field as "complete".”
If a client asks for a version the server doesn’t speak, it gets this back, again straight from the spec:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32022,
"message": "Unsupported protocol version",
"data": { "supported": ["2026-07-28", "2025-11-25"], "requested": "1900-01-01" }
}
}
Discovery is its own call now
With no handshake, a client needs some other way to find out what a server can do. That’s server/discover, and “Servers MUST implement it.” The response lists supported versions, capabilities, server identity in _meta, and optional instructions. It’s also cacheable, so it carries ttlMs and cacheScope. The full example is on the server/discover page. The same caching fields are now required on results from tools/list, prompts/list, resources/list, resources/read and resources/templates/list.
Servers don’t send requests any more
Before this revision, a server could send its own requests back to the client: roots/list, sampling/createMessage, elicitation/create. That’s gone, and the spec calls it “a breaking change.” Its replacement is Multi Round-Trip Requests (MRTR). When a server needs more information to answer a tools/call, resources/read or prompts/get, it returns a result with resultType: "input_required" and puts what it needs in inputRequests. The client then retries the same call with inputResponses filled in. Those three methods are the only ones that can return it. The MRTR page has the full flow.
The legacy handshake, briefly
You’ll still run into it in servers built against 2025-11-25 or earlier. The client opened with an initialize request carrying its protocol version and capabilities. The server replied with its own capabilities, and then the client sent notifications/initialized to finish setting up the session. The spec now calls those versions Legacy, calls 2026-07-28 and later Modern, and calls an implementation that handles both Dual-era.
A dual-era client talking over stdio works out which kind of server it has by sending server/discover first. A DiscoverResult means the server is modern. A recognised modern error like UnsupportedProtocolVersionError means it’s modern but doesn’t support that version. Any other error, or no reply within a reasonable timeout, means it’s legacy, and the client falls back to initialize.
Transports: stdio and Streamable HTTP
The spec defines two. On stdio, the host launches your server as a subprocess, and “The server reads JSON-RPC messages from stdin and writes JSON-RPC messages to stdout.” Messages are separated by newlines “and MUST NOT contain embedded newlines.” That framing didn’t change in 2026-07-28.
On Streamable HTTP, each message is a POST to a single endpoint, and the reply comes back as JSON or as an SSE stream that only lives for that one request. A lot was removed from it. The Mcp-Session-Id header and sessions are gone, and so is the standalone GET stream. Resumability via Last-Event-ID went too. If a response stream breaks, the in-flight request is lost, and the client has to send it again as a new request with a new ID.
The old HTTP+SSE transport from 2024-11-05 has been deprecated since 2025-03-26. As of this revision it’s formally Deprecated under the new lifecycle policy: “New implementations SHOULD NOT adopt it.”
The three primitives
Tools are actions the model can take. A tool has a unique name, a description, an inputSchema, an optional outputSchema and optional annotations. The 2026-07-28 revision loosened inputSchema and outputSchema “to allow any JSON Schema 2020-12 keywords.”
Resources are data the model can read, such as “files, database schemas, or application-specific information.” Each resource is identified by a URI. Change subscriptions now go through subscriptions/listen, which replaced resources/subscribe and resources/unsubscribe.
Prompts are templates meant to be user-controlled, “with the intention of the user being able to explicitly select them for use.” They take optional arguments.
Building a server with TypeScript SDK 2.x
A note on where v2 stands. The README says “v2 is the stable release line”. The 2.0.0 release notes did open with “First beta release of SDK v2 with support for the MCP 2026-07-28 specification revision,” but npm has since moved latest on to 2.1.0 (23 September), whose release notes read as an ordinary minor release, adding request-time OAuth scope challenges among other things. For v2, the SDK split into separate packages. You need @modelcontextprotocol/server to build a server and @modelcontextprotocol/client to connect to one. There are HTTP adapters for Node, Express, Hono and Fastify, plus @modelcontextprotocol/server-legacy (a frozen copy of v1 code that its README says is “planned for removal in v3”) and @modelcontextprotocol/codemod to help with migrating from v1. The README’s install line:
npm install @modelcontextprotocol/server
The quickstart imports zod/v4, and the SDK’s first-server tutorial installs it in the same step, along with tsx to run TypeScript directly:
npm install @modelcontextprotocol/server zod tsx
This is the README quickstart, copied exactly:
import { McpServer } from '@modelcontextprotocol/server';
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';
const server = new McpServer({ name: 'greeting-server', version: '1.0.0' });
server.registerTool(
'greet',
{
description: 'Greet someone by name',
inputSchema: z.object({ name: z.string() })
},
async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }]
})
);
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
}
main();
The SDK’s first-server tutorial starts the server a different way. It uses void serveStdio(createServer);, which takes a factory function that builds the server, and imports it from the same @modelcontextprotocol/server/stdio module. Both are the SDK’s own code. Pick one. Don’t try to combine them.
If you’re coming from v1: the v1 shorthand server.tool(...) took a Zod shape like { name: z.string() }, and v2 removed the shorthand entirely in favour of registerTool.
The schema doesn’t have to be Zod, either. The README says: “Tool and prompt schemas use Standard Schema — bring Zod v4, Valibot, ArkType, or any compatible library.”
Resources and prompts
A static resource, from the SDK’s resources doc:
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
const server = new McpServer({ name: 'workspace', version: '1.0.0' });
server.registerResource(
'config',
'config://app',
{
title: 'Application Config',
description: 'Application configuration data',
mimeType: 'text/plain'
},
async uri => ({
contents: [{ uri: uri.href, text: 'log_level=info\nregion=eu-west-1' }]
})
);
The name comes first and the URI second. The metadata object is required, and the migration guide says to “pass {} if you have none.”
A prompt, from the SDK’s prompts doc:
import { McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';
const server = new McpServer({ name: 'review', version: '1.0.0' });
server.registerPrompt(
'review-code',
{
title: 'Code Review',
description: 'Review code for best practices and potential issues',
argsSchema: z.object({
code: z.string().describe('The code to review')
})
},
({ code }) => ({
messages: [
{
role: 'user' as const,
content: { type: 'text' as const, text: `Review this code:\n\n${code}` }
}
]
})
);
Look at the schema key. Prompts use argsSchema. Tools use inputSchema. If you copy a tool registration and turn it into a prompt, that’s the one line you’ll forget to change.
Testing and registering it
The tutorial’s test command opens your server in the MCP Inspector:
npx @modelcontextprotocol/inspector npx tsx src/index.ts
I’d run it there before touching any host config. That way, if something’s wrong, you know it’s the server and not the host. To hook it up to Claude Code or another host, follow that host’s own MCP setup docs. I’m not reprinting a config block I haven’t re-checked for this update. My Claude Code tips cover how I keep the number of servers under control once they’re connected.
Where people get caught
One console.log breaks a stdio server. On stdio, stdout is the protocol channel. The tutorial puts it plainly: “Log with console.error — one console.log corrupts the JSON-RPC stream.”
The old package installs without any warning. @modelcontextprotocol/sdk is still latest at 1.30.1, with no deprecation flag and no migration notice, though its README does link a V2 API reference. npm install succeeds. If an older tutorial tells you to install it, you get a working v1 server, with v1 import paths like /server/mcp.js, built for a protocol the spec now calls Legacy. v1.x is promised bug fixes and security updates for at least six months after v2’s release, and that’s all.
Your header and body have to agree. If Mcp-Name says one thing and params.name says another, the server has to reject the request with HTTP 400 and error -32020. The spec’s example message reads “Header mismatch: Mcp-Name header value ‘foo’ does not match body value ‘bar’”. The same error covers missing or malformed headers, so an HTTP client written against an older revision fails here before your tool code ever runs.
Error codes moved. Resource-not-found went from -32002 to -32602, though clients should still accept -32002 from older servers. HeaderMismatch, MissingRequiredClientCapability and UnsupportedProtocolVersion were renumbered to -32020, -32021 and -32022. If you match on error codes anywhere, grep for the old numbers.
Logging is deprecated too. Along with Roots and Sampling, the MCP Logging feature is deprecated. logging/setLevel is gone, and log level is now set per request in _meta. The spec’s advice is to log to stderr on stdio, or use OpenTelemetry. For Roots, pass directories through tool parameters, resource URIs or server config. For Sampling, call the model provider’s API directly. All three still work during the deprecation window, but new servers shouldn’t add them.
What already shipped, and when
This timeline was checked against each revision’s changelog on 26 September 2026.
- Streamable HTTP arrived in 2025-03-26, which “Replaced the previous HTTP+SSE transport with a more flexible Streamable HTTP transport (PR #206).” In 2026-07-28, sessions and resumability were stripped back out of it.
- OAuth arrived in 2025-03-26 as an OAuth 2.1-based authorization framework. 2025-06-18 revised it, and so did 2026-07-28, which deprecated Dynamic Client Registration (RFC7591) in favour of Client ID Metadata Documents. It only applies to HTTP transports: “Implementations using an STDIO transport SHOULD NOT follow this specification, and instead retrieve credentials from the environment.”
- Elicitation shipped in 2025-06-18 (PR #382). Since 2026-07-28, it goes through MRTR instead of a server-initiated
elicitation/create.
The deprecation list is the more useful thing to watch now, and the spec keeps it in one place: a registry of deprecated features with an earliest-removal date for each. Roots, Sampling, Logging and Dynamic Client Registration were deprecated in 2026-07-28, with earliest removal in the “First revision released on or after 2027-07-28”. The HTTP+SSE transport is on a shorter clock: “Three months after SEP-2596 reaches Final”. SEP-2596 now carries Final status. The SEP page doesn’t print the date it went Final, but its pull request was merged on 18 May 2026, which would have made HTTP+SSE eligible for removal from about 18 August. It hasn’t gone yet: the registry’s Removed section still reads “No features have been removed under this policy yet”. Earliest removal is when a feature becomes eligible to go. The registry says the actual removal is a Core Maintainer decision that “may happen later”. Separately, the 2026-07-28 revision itself removed ping, logging/setLevel and notifications/roots/list_changed outright, outside the deprecation process, which is why those three don’t appear in the registry at all.
Where to find servers, and where ChatGPT fits
Both checked 26 September 2026.
The reference-servers repo isn’t a catalogue any more. github.com/modelcontextprotocol/servers holds seven reference implementations: Everything, Fetch, Filesystem, Git, Memory, Sequential Thinking and Time. It sends you elsewhere in its own words: “If you are looking for a list of MCP servers, you can browse published servers on the MCP Registry.” The registry is at registry.modelcontextprotocol.io. For the servers I actually run, see the best MCP servers for developers.
ChatGPT plugins aren’t a rival standard. OpenAI’s plugins changelog has a 2026-02-22 entry that says “ChatGPT is now fully compatible with the MCP Apps spec.” If you ship an MCP server, you’re most of the way to shipping into ChatGPT.
That’s it. Two transports, three primitives, and no handshake. If you’re maintaining a server written before this summer, grep for initialize, server.tool( and -32002 before you do anything else.
Related: Best MCP Servers for Developers · 17 Claude Code Tips · Best AI Tools Developers Actually Use
Frequently Asked Questions
What is the current MCP protocol revision?
2026-07-28. The revision before it was 2025-11-25, which the spec now classes as Legacy.
Does MCP still use an initialize handshake?
No. The 2026-07-28 revision removed the initialize/initialized handshake. Every request now carries its own protocol version and client capabilities in _meta, and the server accepts or rejects each request independently.
How does an MCP server work?
It exposes tools, resources and prompts to an AI host over JSON-RPC. On the stdio transport the server reads JSON-RPC messages from stdin and writes them to stdout, one message per line.
What transports does MCP support?
The 2026-07-28 spec defines two: stdio and Streamable HTTP. The older HTTP+SSE transport has been deprecated since 2025-03-26 and should not be used for new servers.
Which npm package do I use to build an MCP server in TypeScript?
@modelcontextprotocol/server, currently version 2.1.0, which targets the 2026-07-28 spec. The older @modelcontextprotocol/sdk package (1.30.1) is the v1 line.
What is resultType in MCP?
A required field on every result since 2026-07-28. It is "complete" for ordinary results and "input_required" when the server needs more input before it can finish.
Comments