prerequisite

HTTP basics

Requests and responses, methods, headers, status codes, and streamed responses: the parts of HTTP a remote MCP server uses.

Before this

Nothing beyond first-year college math. This is a starting page.

Why you need this

A local MCP server talks to its host through pipes. A remote server, one running on another machine, talks over HTTP, the same protocol your browser uses to load web pages. Every remote MCP message is an HTTP request, and every answer is an HTTP response, sometimes one that arrives in pieces. To read a remote server's logs, debug a failed connection, or understand what a load balancer can see, you need to read HTTP at the level of its actual text.

The idea

HTTP is a request and response protocol. A client opens a network connection to a server, sends one request, and gets back one response. That is the whole conversation.

URLs

A URL names where to send a request. Its parts:

https://api.example.com:8443/mcp?units=metric
Part Here Meaning
scheme https Which protocol; https is HTTP over an encrypted connection
host api.example.com Which machine
port 8443 Which program on that machine; defaults to 443 for https, 80 for http
path /mcp Which thing on that server
query units=metric Extra name=value pairs, after ?

Methods

The request's method says what kind of action it is. GET asks for something and carries no body. POST sends data in a body for the server to act on. There are others (PUT, DELETE, PATCH), but MCP uses POST for every message.

Requests and responses as text

An HTTP/1.1 message is text with a fixed layout:

  1. A start line. For a request: method, path, version (POST /mcp HTTP/1.1). For a response: version, status code, reason (HTTP/1.1 200 OK).
  2. Headers, one per line, each Name: value. Header names ignore upper and lower case.
  3. One blank line, which marks the end of the headers.
  4. An optional body: the actual data.

Lines end with two characters, carriage return and line feed, written \r\n in code.

A few headers matter everywhere:

Header Sent by Meaning
Host client Which site, since one machine can host many
Content-Type both The body's format, such as application/json
Content-Length both The body's size in bytes, so the reader knows where it ends
Accept client Which formats the client can read back
Authorization client Credentials, such as Bearer plus a token (see OAuth basics)

Applications may add their own headers. MCP adds MCP-Protocol-Version, Mcp-Method, and Mcp-Name, which Transports explains.

Status codes

The three-digit status code says how it went. The first digit is the family:

Family Meaning Examples
2xx Success 200 OK; 202 Accepted (received, nothing to send back)
3xx Look elsewhere 307 Temporary Redirect
4xx The client's request was wrong 400 Bad Request (malformed or invalid), 401 Unauthorized (no valid credentials: log in), 403 Forbidden (credentials fine, but not allowed), 404 Not Found (nothing at that path), 405 Method Not Allowed (path exists, wrong method)
5xx The server failed 500 Internal Server Error, 503 Service Unavailable

The difference between 401 and 403 trips people up: 401 means "I don't know who you are", 403 means "I know who you are, and no".

Streaming with server-sent events

Usually a response has a Content-Length and arrives whole. Sometimes the server wants to send several things over time on one response: progress updates, then a final answer. Server-sent events (SSE) is a simple format for that. The response has Content-Type: text/event-stream, stays open, and carries events, each a few field: value lines ended by a blank line. The data: field holds the payload. Since the server does not know the total length in advance, HTTP/1.1 sends such a body in chunks, each preceded by its size in hexadecimal, ending with a chunk of size 0.

Here is part of a real stream, from the server in Transports, reporting progress before its answer:

HTTP/1.1 200 OK
date: Fri, 02 Oct 2026 21:35:32 GMT
server: uvicorn
content-type: text/event-stream
cache-control: no-cache, no-transform
x-accel-buffering: no
Transfer-Encoding: chunked
Connection: close

84
event: message
data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"p1","progress":1,"total":2}}

...
0

84 is hexadecimal for 132, the length in bytes of the chunk that follows. The ... stands for a second progress event and the final result, each in its own chunk.

HTTP is stateless

Each request stands alone. The server is not required to remember anything about earlier requests from the same client, and with many copies of a server behind a load balancer (a front door that spreads requests across machines), consecutive requests may reach different copies. Anything that must carry over, such as who you are, travels in every request, usually in a header. The 2026-07-28 revision of MCP works the same way.

Worked example

This script writes one HTTP POST by hand, byte for byte, sends it to an MCP server listening on port 8765 of the same computer (the one built in Transports), and prints both directions.

# post_once.py: send one HTTP POST by hand and print exactly what goes each way.
import json
import socket
import sys

path = sys.argv[1] if len(sys.argv) > 1 else "/mcp"

body = json.dumps({
    "jsonrpc": "2.0", "id": 1, "method": "tools/call",
    "params": {
        "name": "add", "arguments": {"a": 2, "b": 3},
        "_meta": {"io.modelcontextprotocol/protocolVersion": "2026-07-28",
                  "io.modelcontextprotocol/clientCapabilities": {}},
    },
}, separators=(",", ":")).encode("utf-8")

head = (
    f"POST {path} HTTP/1.1\r\n"
    "Host: 127.0.0.1:8765\r\n"
    "Content-Type: application/json\r\n"
    "Accept: application/json, text/event-stream\r\n"
    "MCP-Protocol-Version: 2026-07-28\r\n"
    "Mcp-Method: tools/call\r\n"
    "Mcp-Name: add\r\n"
    f"Content-Length: {len(body)}\r\n"
    "Connection: close\r\n"
    "\r\n"
).encode("ascii")

with socket.create_connection(("127.0.0.1", 8765)) as s:
    s.sendall(head + body)
    reply = b""
    while chunk := s.recv(65536):
        reply += chunk

print((head + body).decode())
print()
print(reply.decode())

python post_once.py printed the request:

POST /mcp HTTP/1.1
Host: 127.0.0.1:8765
Content-Type: application/json
Accept: application/json, text/event-stream
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: add
Content-Length: 209
Connection: close

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"add","arguments":{"a":2,"b":3},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}

and the response:

HTTP/1.1 200 OK
date: Fri, 02 Oct 2026 21:43:32 GMT
server: uvicorn
content-length: 224
content-type: application/json
Connection: close

{"jsonrpc":"2.0","id":1,"result":{"content":[{"text":"5","type":"text"}],"isError":false,"resultType":"complete","structuredContent":{"result":5},"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"adder","version":""}}}}

Read it part by part:

Piece Request Response
Start line POST /mcp HTTP/1.1: send data to path /mcp HTTP/1.1 200 OK: it worked
Body format Content-Type: application/json content-type: application/json
Body size Content-Length: 209 content-length: 224
Blank line ends the headers ends the headers
Body a JSON-RPC request to add 2 and 3 a JSON-RPC result whose answer is 5

The 209 is the byte count of the body exactly as sent: the script computed it with len(body) after encoding. Get it wrong and the server either waits for bytes that never come or cuts the body short.

The same request to a path the server does not serve, python post_once.py /nope, came back:

HTTP/1.1 404 Not Found
date: Fri, 02 Oct 2026 21:43:29 GMT
server: uvicorn
content-length: 9
content-type: text/plain; charset=utf-8
Connection: close

Not Found

Note the HTTP status said 404 before the MCP layer ever saw the body. Errors can come from either layer, and you read the status code first.

In a server's life

  • Build and connect (stage 3). The Streamable HTTP transport is one URL that accepts POST, answers with JSON or an SSE stream, and adds MCP headers; see Transports.
  • Secure it (stage 4). Remote servers check an Authorization header and answer 401 when it is missing; see OAuth basics and Authorization for remote servers.
  • Test and ship (stage 5). Statelessness is what lets a server run as many copies behind a load balancer; see Deploying remote servers.

Common mistakes

  • Reading only the body. A tool returns nothing and you blame your code, when the status line said 401 Unauthorized. Check the status first.
  • Wrong Content-Type. The same JSON body sent to the example server with Content-Type: text/plain came back 400 Bad Request with the body Invalid Content-Type header; the JSON was never parsed.
  • Missing the blank line. A hand-built request with no empty line after the headers makes the server wait for more headers until it times out.
  • Confusing 401 and 403. Logging in again does not fix a 403; the account needs permission.
  • A proxy that buffers streams. Progress events arrive all at once at the end. Streaming servers send X-Accel-Buffering: no (visible in the stream above) to ask common proxies not to hold the response.

Cost

Each HTTP request pays for a network round trip, from under a millisecond on the same machine to a large fraction of a second across the world, plus headers on every request, a few hundred bytes in the example above. Opening a new encrypted connection costs extra round trips, which is why clients keep connections open and reuse them. An SSE stream holds one connection open for as long as the work takes, which matters when a server has thousands of clients at once.

Going further

  • The HTTP Semantics standard, RFC 9110, for the full list of methods, headers, and status codes.
  • HTTP/2, which sends the same requests and responses in a binary, multiplexed form.
  • The server-sent events section of the HTML standard.
  • Using curl -v to watch the start lines and headers of any request.

Leads to

Back to Building and maintaining MCP servers