Almost every MCP tutorial starts the same way: install an SDK, run a server process, and keep a long-lived connection open between the client and the server. STDIO does this by design. The remote transports (SSE, and now Streamable HTTP) are usually presented the same way — a stream that stays open so the server can push messages to the client at any time.
That is fine for a local tool or a small integration. But I keep thinking about a different scenario: online stores. Imagine every shop on the web exposing an MCP endpoint so AI tools can browse the catalog and help a user buy. If each of those endpoints expects a persistent connection, we are back to holding thousands of open sockets for clients that send one request every few minutes. That is a lot of unnecessary load on the network infrastructure for almost no benefit.
So I wanted to check a simple question: can an MCP server work as a normal, stateless web app — one request, one response, connection closed — the way websites have worked for decades?
The short answer is yes. And to prove it in the most honest way possible, I implemented one in pure PHP, without any MCP library at all.
The persistent connection is optional
The Streamable HTTP transport actually allows this already. The client sends a JSON-RPC message with a POST, and the server is allowed to answer in one of two ways:
- open an SSE stream and keep talking, or
- reply with a single
application/jsonbody and be done.
Nothing forces the second option to keep the connection alive. So the "simplified" version of MCP is just this:
- The client
POSTs one JSON-RPC message. - The server answers with one JSON body.
- The connection is closed.
- The next request opens a new connection.
No session to keep in memory. No socket to hold open. This is exactly the model PHP was built for — every request starts from nothing and ends completely.
The whole server is three files
There is no framework and no Composer dependency here. Just a front controller that PHP's built-in server can run directly:
php -S localhost:8001 -t public public/index.php
The front controller does three things: authenticate the request, hand the JSON-RPC message to a small handler, and send the answer back. The first interesting detail is that we explicitly ask for the connection to be closed after the response:
// Demonstrate the "simplified transport": close the connection after this
// response. PHP's built-in server is one-request-per-process anyway.
header('Connection: close');
Authentication is just a token in the URL
I did not want to build an OAuth flow for a demo (and, as it turns out, most clients do not need one). The token travels as a query parameter in the connection URL, and the server maps it to a user:
function extract_token(): ?string
{
if (isset($_GET['token']) && $_GET['token'] !== '') {
return (string) $_GET['token'];
}
// ...also accept an "Authorization: Bearer <token>" header
}
$username = $store->validateToken(extract_token());
if ($username === null) {
send_json(401, ['error' => 'Invalid or missing auth token']);
}
Users are stored in a plain text file, one username:token per line. That is enough to show the idea: the store knows who is asking, so it can keep a cart per user.
Speaking MCP by hand
The MCP endpoint reads exactly one JSON-RPC message from the request body, handles it, and replies. That's the entire transport:
$raw = file_get_contents('php://input');
$message = json_decode($raw, true);
$mcp = new McpServer($store, $username);
$response = $mcp->handle($message);
if ($response['body'] === null) {
http_response_code($response['status']); // 202 for notifications
exit;
}
send_json($response['status'], $response['body']);
The handler itself is small. MCP over JSON-RPC really only needs a handful of methods to be useful, and PHP 8's match makes it readable:
public function handle(array $message): array
{
$method = $message['method'] ?? null;
$id = $message['id'] ?? null;
// Notifications have no id — acknowledge with an empty 202.
if ($id === null) {
return ['status' => 202, 'body' => null];
}
return match ($method) {
'initialize' => $this->ok($id, $this->initializeResult($message)),
'ping' => $this->ok($id, new \stdClass()),
'tools/list' => $this->ok($id, ['tools' => $this->toolDefinitions()]),
'tools/call' => $this->ok($id, $this->callTool($message['params'] ?? [])),
default => $this->error($id, -32601, "Method not found: {$method}"),
};
}
And a tool call is just a dispatch to the store, wrapped in the shape MCP expects:
$result = match ($name) {
'list_products' => $this->store->loadProducts(),
'search_products' => $this->store->searchProducts($arguments['query'] ?? ''),
'get_product_details' => $this->getProductDetails($arguments['product_id'] ?? ''),
'add_to_cart' => $this->store->addToCart($this->requireUser(), ...),
'view_cart' => $this->store->cartDetails($this->requireUser()),
// ...
};
return [
'content' => [['type' => 'text', 'text' => json_encode($result)]],
'isError' => false,
];
That is essentially the whole server. No SDK, no event loop, no persistent state.
It really works with a real client
The best part: this is not a toy that only talks to a hand-written client. I connected it straight to VS Code's built-in MCP support with this config:
{
"servers": {
"shop-mcp-server": {
"url": "http://localhost:8001/mcp-server/mcp?token=alice-secret-token",
"type": "http"
}
}
}
VS Code ran the normal MCP handshake — initialize, then tools/list — discovered all six tools, and let the AI call them. Asking the assistant to "add a monitor to my cart" resulted in a real add_to_cart call, authenticated as the user from the token in the URL.

Because there is nothing special about this transport, it should work the same way with most AI tools that support MCP over HTTP. And every one of those calls, on the server side, was a brand-new connection that closed the moment the response was sent — you can literally watch the built-in PHP server log an Accepted and a Closing line for each request.
Where this is heading
I have written before that MCP could change how we use the web, and this experiment makes me more convinced. It is genuinely easy — a weekend, not a quarter — for an online store to expose its catalog through MCP. And the "stateless web app" model is the right fit: shops already serve millions of short HTTP requests; MCP is just a few more.
Here is the shape I expect to see first, and it is deliberately conservative:
- Anonymous MCP servers for browsing and search. No auth, no accounts. An AI tool can call
search_productson a user's behalf while they are chatting, surface a few options, and link out. This is the easy part, and it is almost risk-free for the store. - An authenticated layer for the cart. With a token (like the one in the URL here), the AI can add items to a specific user's cart. Slightly more advanced, still safe, because nothing irreversible happens.
- Payments — not yet. Letting an AI actually pay is a step too far for now. The risk is too high, and the trust and safety story is not ready. The human should still press the final button.
So the near-term picture is not "AI buys everything for you." It is "AI helps you find it, and maybe fills your cart — you check out." That is a modest, achievable goal, and it does not require anything heavier than the little PHP file above.
If a plain PHP script with no dependencies can be a working MCP server that a real AI editor connects to, then the barrier to entry is basically gone. I think a lot of stores are going to notice that.
The full demo — a Python (FastMCP) server, this pure-PHP server, and a Go CLI client, all sharing the same simplified transport — is on GitHub: repository link.