Hosting and Compatibility Guide
July 6, 2026 · View on GitHub
One of this SDK's design goals is to run cleanly in typical PHP hosting environments — cPanel, shared Apache, PHP-FPM — in addition to CLI and long-running process managers such as FrankenPHP or RoadRunner. This document describes the principle, enumerates what "works" and "fails gracefully" mean in practice, and lists the environment-specific considerations that have come up during development. The rules here have been validated on a live cPanel account — see the shared-hosting validation report for what was run and the results.
The principle
- Core MCP features must work under cPanel/Apache/PHP-FPM. Tools,
prompts, resources, initialization and capability negotiation (both the
legacy handshake and the
2026-07-28stateless model withserver/discover), stdio transport (where the host permits), and the baseline HTTP transport are all "core." The2026-07-28revision is a particularly natural fit here: every modern request is self-contained, so a fresh PHP process per request needs no persisted protocol state. - Features that are genuinely incompatible with shared hosting still ship. Leaving them out would put the SDK out of spec alignment, which we don't accept. Examples include long-lived SSE streams on hosts with aggressive idle timeouts.
- Incompatible features must fail gracefully. "Graceful" means: throw a typed exception the caller can catch, log a clear message, and leave the rest of the SDK functional. "Not graceful" means: fatal error, hanging request, or corrupted session state.
If you find a core feature that doesn't work on standard cPanel or a non-core feature that crashes rather than failing gracefully, that's a bug worth reporting.
Environment notes
PHP version
- Required: PHP 8.1+.
- Recommended: PHP 8.2+ for performance and improvement to fibers / readonly properties used in internal code paths.
- Tested CI matrix: currently
8.1(the floor). Contributions adding newer versions to CI are welcome — see CONTRIBUTING.md.
PHP extensions
| Extension | Status |
|---|---|
ext-json | Required. MCP is JSON-RPC; this is non-negotiable. |
ext-curl | Required. Used by the HTTP client transport. |
ext-pcntl | Optional. Improves process control for the stdio server runner. Shared hosts often disable it; the SDK degrades cleanly when it's missing. |
ext-mbstring | Recommended. Some content types (e.g. multi-byte text) round-trip more safely with it enabled. |
If your shared host disables ext-pcntl, stdio servers still run but lose
certain signal-handling niceties. HTTP servers are unaffected.
Stdio transport
- Works wherever PHP can be launched as a child process. That includes most
cPanel environments when a cron job or external launcher invokes it, and
the same for
webclient/connecting to a local stdio server. - Does not work from within a typical Apache request handler spawning a
child PHP process mid-request, because shared hosts commonly forbid
proc_openin that context. If that's your situation, use the HTTP transport instead.
HTTP transport
- Streaming HTTP (non-SSE) works under every PHP SAPI we've seen.
- SSE (Server-Sent Events) works under most SAPIs but is sensitive to:
- Apache
mod_deflatecompressing the stream — disable compression for the MCP endpoint if clients see truncated events. output_bufferingbeing on — the SDK flushes explicitly, but some hosts re-wrap output. Checkphp.inior.htaccessoverrides.- FPM's
request_terminate_timeoutbeing too low — long-lived streams can be killed. On legacy sessions the SDK recovers (the client reconnects withLast-Event-Id); on the2026-07-28path there is no stream resumption by design — response streams are request-scoped, and asubscriptions/listenstream cut by a host timeout is simply re-opened by the client (the SDK sends a gracefulSubscriptionsListenResultwhen it ends a stream itself). Throughput suffers on hosts with aggressive timeouts either way. - Aggressive CDNs and proxies that buffer responses — add
X-Accel-Buffering: noor configure the proxy to stream.
- Apache
- When SSE is not viable on a particular host, the SDK's fallback path is to reply with a regular HTTP response and let the client reconnect — the server code paths are the same; only the transport decorator changes.
OAuth and .htaccess
OAuth under cPanel/Apache generally works, but two .htaccess rules are
usually required:
RewriteEngine On
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]
RewriteRule ^\.well-known/oauth-protected-resource(/.*)?$ /server_auth.php [L]
The first forwards the Authorization header to PHP (which shared hosts
strip by default). The second exposes the protected-resource metadata
endpoint. The full walk-through is in
examples/server_auth/README.md.
Session persistence (legacy sessions)
Protocol sessions only exist for legacy-era clients (2024-11-05 …
2025-11-25) — 2026-07-28 requests are sessionless and skip this
machinery entirely. The same file-backed approach also carries the
cross-process state of v2 features that need it (FileSubscriptionBus
for subscriptions/listen events, the Tasks extension's TaskManager
store), with the same hosting characteristics.
- The web client and the HTTP server transport persist session state on disk
via
Mcp\Server\Transport\Http\FileSessionStore. Writes go to a directory you supply; this works anywhere PHP can write files. - On some shared hosts the default temp directory is session-scoped or wiped often. If you see sessions "disappearing" between requests, configure the store to use a directory you control and make sure it's excluded from aggressive cleanup.
Mcp\Server\Transport\Http\InMemorySessionStoreexists for testing and for environments where long-lived processes hold state in memory (FrankenPHP, RoadRunner, Swoole). Don't use it under classic FPM — memory does not survive between requests.
Embedding in frameworks
The HTTP runner uses the HttpIoInterface seam so it can be driven by
something other than native PHP header() / echo. See AGENTS.md for the
quick description, and the source under src/Server/Transport/Http/ for the
details. If you're embedding in Symfony, Slim, Laravel, or a long-running
runtime, pass a custom adapter via
McpServer::httpOptions(['io' => $adapter]) or construct
HttpServerRunner with it directly.
What "graceful degradation" looks like
When a feature can't be supported in the current environment, the SDK aims to:
- Throw a clear, typed exception at the right layer. For transports:
RuntimeExceptionwith a message that says what's missing. For OAuth validation:HttpAuthenticationExceptionso the caller can distinguish from a real transport fault. For protocol errors:Mcp\Shared\McpError. - Not crash the session. A feature failing should surface a JSON-RPC error to the client (where protocol-appropriate) or propagate as an exception to the caller, never as a PHP fatal that tears down the request.
- Log, not leak. Where a feature is genuinely disabled (e.g., SSE not supported under the host), the SDK logs through the PSR-3 logger and continues with whatever fallback path is available.
If you see a failure that doesn't fit this model, it's a bug — please file
an issue with the transport, SAPI, PHP version, and var_dump of
phpversion() / extension_loaded(...) for the relevant extensions.
Checklist for contributors touching transport or environment code
- Does this change assume a feature (
proc_open,pcntl_fork, SSE, long-lived connections) that is commonly disabled on shared hosts? - If yes, is there a documented fallback or a clear exception path?
- Is the failure mode covered by a unit test?
- Does the change preserve behaviour under the SAPI most users are on (Apache + FPM)?