A stdio MCP server written in PHP is conformant when two things hold at once: stdout carries only valid JSON-RPC messages, and the startup behavior matches the protocol revision the client negotiates. The official PHP MCP SDK (mcp/sdk) is the most direct documented route to both, and most of the work is in keeping the protocol channel clean rather than in the PHP API itself.
What a stdio client expects from your process
In stdio mode the MCP client launches your server as a child process. The client writes requests and notifications to the server’s stdin, and the server writes responses and notifications to its stdout. Nothing else is part of the conversation. The MCP specification’s Transports section, version 2025-11-25, sets the rules that matter for PHP code:
- Messages are JSON-RPC and UTF-8 encoded.
- Each message is newline-delimited and must not contain embedded newlines. Pretty-printed JSON is therefore not an option, even though it is valid JSON.
- The server must not write anything to stdout that is not a valid MCP message. The specification states this as: “The server MUST NOT write anything to its
stdoutthat is not a valid MCP message.” - Stderr is the channel for informational, debug, and error logs. Clients may capture it or ignore it, so output on stderr does not by itself mean the server failed.
Prerequisites and installation
- Confirm that the runtime is PHP 8.1 or newer. This is the requirement listed on the official PHP SDK landing page.
- In the project root, run
composer require mcp/sdk. - Note the SDK’s status before you build anything long-lived. The SDK identifies itself as a collaboration between the PHP Foundation and Symfony, and it describes itself as experimental until version 1.0. Method names and builder calls can change, so check the SDK’s current documentation before you rely on a specific API.
Build the entry point
Put the server script beside Composer’s vendor/ directory, for example server.php. The SDK’s first-server guide follows this pattern, and the steps below mirror it:
- Load Composer’s autoloader with
require __DIR__ . '/vendor/autoload.php';. - Set the server’s name and version in the server builder.
- Register the tools, resources, or prompts you want to expose. Use the builder calls shown in the SDK’s current first-server guide, since those names follow the SDK version you install.
- Build the server object.
- Run it with
McpServerTransportStdioTransport. The transport owns stdin and stdout, so nothing else in the script should write to them.
Keep the script free of any code that runs before the transport starts. A stray echo in a config file or an included bootstrap file will break the first exchange just as thoroughly as one inside a tool handler.
#1 Best Overall
Keep stdout limited to protocol messages
Stdout discipline is where most stdio servers fail, and PHP makes it easy to break. The following sources of stray output are the usual ones:
| Source of output | Where it goes by default in the PHP CLI | Fix |
|---|---|---|
echo, print, print_r, var_dump |
Stdout | Remove them, or write diagnostics with fwrite(STDERR, $message . PHP_EOL); |
Warnings, notices, and deprecations shown because display_errors is enabled |
Stdout | Run the server with php -d display_errors=stderr server.php, or set display_errors to stderr in the PHP configuration used by the client’s launch command |
error_log() |
Stderr when no error_log destination is configured |
Safe to use. Set an explicit error_log file if you want logs to persist |
Leading whitespace, a byte-order mark, or a closing ?> followed by a newline in a PHP file |
Stdout | Save files without a BOM, omit the closing tag in PHP-only files, and check that the first byte of the entry point is <?php |
| Output from a third-party library during construction | Stdout | Find the call with a search for echo or printf in the vendor code, and redirect or suppress it before the transport starts |
Each of these produces output that a client reads as a malformed message, even when the server’s logic is correct. If a client reports a parse error on a response it has never seen, check stdout first.
Rank #2
Match the lifecycle to the client’s protocol revision
Initialization is the second conformance question, and it depends on which revision the client uses. The PHP SDK’s protocol documentation separates the 2026-07-28 modern lifecycle from the earlier handshake family, so an implementation cannot assume a single flow works everywhere.
| Aspect | Handshake lifecycle (MCP specification, Lifecycle section, version 2025-11-25) | Modern lifecycle (PHP SDK protocol documentation, revision 2026-07-28) |
|---|---|---|
| First exchange | The client sends initialize, and the server responds with a negotiated protocol version and its capabilities |
No initialize handshake. Each request carries version and capability information |
| Readiness signal | The client sends notifications/initialized before normal operation |
Not required, because no handshake exists |
| Shutdown behavior | Described in the Lifecycle section for this revision | Not stated in the sources consulted for this article |
For a server that targets clients speaking the 2025-11-25 revision, the legacy flow is:
Recommended Free Tools
- The client starts the server process and sends
initializeon stdin. - The server replies on stdout with the negotiated protocol version and its capabilities.
- The client sends
notifications/initialized. - Normal requests such as tool listing and tool calls begin.
Do not describe this sequence as universal. The 2026-07-28 revision removes the handshake, so a server that waits for initialize will stall against a client that never sends it. Identify the revision your target client uses, and test against that client rather than assuming one exchange covers every version.
Check the server with the MCP Inspector
The SDK documentation describes the MCP Inspector as the interactive way to inspect a server’s exposed elements. From the project root, run:
Rank #4
npx @modelcontextprotocol/inspector php server.php
The Inspector lists the tools, resources, and prompts the server exposes and lets you invoke them. Use it as a manual check of the wire behavior:
- Every tool, resource, or prompt you registered appears in the list.
- Invoking a tool returns a well-formed result rather than a parse error.
- Diagnostic messages you wrote appear as stderr output, not inside the protocol stream.
If the Inspector cannot connect or reports malformed responses, the most likely cause is stray output on stdout, so start with the table in the stdout section above.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →When stdio is the wrong transport
Stdio fits a local server that a desktop or command-line host launches for its own use. The SDK also supports Streamable HTTP, which suits remote or web-hosted integrations. The two differ in deployment model, message channel, and session handling:
| Transport | Deployment model | Message channel | Lifecycle and session notes |
|---|---|---|---|
| stdio | Local child process launched by the client | stdin for client messages, stdout for server messages | Governed by the stdout rules and the revision-specific lifecycle described above |
| Streamable HTTP | Remote or web-hosted service | HTTP | Covered by the SDK’s HTTP documentation; not covered in this article |
If your server runs on the user’s machine and the host starts it, stdio is the right transport. A deployment that other machines reach over a network needs the HTTP route instead, with its own setup.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




