MCP MVP PLAN
MCP MVP Plan for vhttpd
This document proposes how vhttpd should support MCP as a transport/runtime layer, without turning vhttpd into an MCP application framework.
Scope
Target the current official MCP transport model:
- latest transport spec observed during design:
2025-11-25 - official transports:
stdioStreamable HTTP
For vhttpd, the correct first target is:
Streamable HTTPonly
Not in scope for the first MVP:
- stdio MCP server mode
- provider-specific MCP application logic
- OAuth authorization
- MCP Registry support
- backwards compatibility with deprecated
HTTP+SSEMCP transport
stdio should not be implemented inside vhttpd.
sampling is also not part of the first MCP transport MVP implementation.
It should be added later as a relay/runtime feature on top of the same Streamable HTTP session model.
Recommended boundary:
vhttpd- Streamable HTTP MCP
vphpx(or another local CLI runtime)- stdio MCP
Why vhttpd fits MCP
vhttpd already has the right building blocks:
- short-lived worker dispatch
- SSE transport ownership in
vhttpd - session-like runtime state for websocket/upstream flows
- admin/runtime visibility
- PHP worker contract already separated from connection ownership
That means MCP should be implemented as:
vhttpdowns MCP transport- PHP worker owns MCP method handling
Not as:
vhttpdhardcoding MCP tools/resources/prompts behavior
Vlib Reuse Matrix
Before implementing MCP, the relevant vlib pieces were re-checked against the local vlib source and current module docs.
Reuse directly
-
net.http- use for MCP HTTP request/response semantics
- header parsing, status codes, request helpers, upstream helpers
- already central in
vhttpd
-
veb- keep using
vebas the HTTP server/runtime source of truth - MCP should remain just another runtime transport on top of
veb
- keep using
-
veb.sse- useful for SSE formatting and transport behavior
- especially valuable for
GET /mcpstream handling - good fit because MCP Streamable HTTP still uses
text/event-stream
-
sync- channels, mutexes, rwmutexes, waitgroups
- this should remain the primary primitive set for session registries, queues, and cleanup loops
Reuse partially / with caution
-
net.jsonrpc- useful as a reference for JSON-RPC data model and handler shape
- not a good transport fit for MCP-over-HTTP in
vhttpd - current
net.jsonrpc.Serveris stream-oriented and assumes Content-Length framing over anio.ReaderWriter - that is much closer to MCP
stdiothan MCP Streamable HTTP - recommendation:
- reuse concepts, not the server as-is
-
eventbus- acceptable for internal notifications or low-priority hooks
- not a good fit as the primary MCP session/message store
- it is synchronous pub/sub, not a session runtime
-
pool.ConnectionPool- potentially useful later for outbound upstream optimization
- not needed for MCP transport MVP
- should not be the foundation of session handling
Do not use as MCP transport core
-
net.websocket- excellent for websocket support, but MCP MVP should start with Streamable HTTP
- may become relevant later only if websocket-based MCP transport is explored
-
sync.pool- useful for batch/parallel task execution
- not suitable as the core abstraction for MCP sessions or message queues
Practical Conclusion
For MCP MVP, the correct base is:
vebnet.httpveb.ssesync
while avoiding the temptation to force-fit:
net.jsonrpc.Servereventbuspool.ConnectionPoolsync.pool
This keeps the implementation aligned with the rest of vhttpd:
- transport/session ownership in
vhttpd - business semantics in PHP worker
Design Principle
vhttpd should become an MCP transport adapter, not an MCP business framework.
Layer split:
vhttpd- HTTP transport
- SSE stream handling
- session registry
- version/header validation
- origin/auth hooks
- observability
- PHP worker
- JSON-RPC request handling
- MCP method routing
- server feature implementation
- VSlim / package helpers
- userland API for tools/resources/prompts
MCP Facts That Shape The Design
Based on the current official MCP transport specification:
- MCP uses JSON-RPC messages.
- Streamable HTTP uses a single MCP endpoint path.
- client messages are sent via
HTTP POST - the server may answer with either:
application/jsontext/event-stream
- clients may open a separate
HTTP GETSSE stream to receive server-to-client messages - sessions may be established with
Mcp-Session-Id - clients should send
MCP-Protocol-Version - servers must validate
Originon HTTP transports
This strongly matches vhttpd’s existing phase-2/phase-3 runtime direction.
Proposed Endpoint Shape
Use a single endpoint, for example:
POST /mcpGET /mcp- optional
DELETE /mcp
Semantics:
POST /mcp- accepts one JSON-RPC request / notification / response
- returns JSON or SSE
GET /mcp- opens server-to-client SSE stream
DELETE /mcp- terminates
Mcp-Session-Idif supported
- terminates
Runtime Model
Introduce a new worker surface:
mode = mcp
High-level flow:
MCP client
-> POST /mcp
-> vhttpd validates headers/session/version/origin
-> vhttpd dispatches one short MCP envelope to php-worker
-> php-worker returns one of:
- JSON-RPC response
- SSE command list / deferred messages
- session commands
-> vhttpd writes JSON or SSE response
For server-initiated messages:
MCP client
-> GET /mcp
-> vhttpd binds SSE stream to session
-> later worker requests/notifications are queued to that session stream
Session Ownership
Sessions should live in vhttpd, not in PHP worker memory.
Session state stored in vhttpd:
session_id- negotiated protocol version
- active GET SSE stream, if any
- pending outbound JSON-RPC messages
- created_at / last_seen
- optional auth principal summary
This follows the same principle as websocket phase 2:
- connection/session ownership in
vhttpd - business handling in worker
Worker Contract Draft
Incoming request from vhttpd to PHP worker:
{
"mode": "mcp",
"event": "message",
"session_id": "mcp_sess_123",
"transport": "streamable_http",
"http_method": "POST",
"path": "/mcp",
"protocol_version": "2025-11-25",
"headers": {
"accept": "application/json, text/event-stream",
"mcp-protocol-version": "2025-11-25"
},
"jsonrpc": {
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {}
}
}
Worker response draft:
{
"mode": "mcp",
"handled": true,
"response_mode": "json",
"session": {
"create": true,
"id": "mcp_sess_123"
},
"jsonrpc": {
"jsonrpc": "2.0",
"id": 1,
"result": {}
}
}
Or SSE-style response:
{
"mode": "mcp",
"handled": true,
"response_mode": "sse",
"session": {
"id": "mcp_sess_123"
},
"messages": [
{
"type": "jsonrpc",
"data": {
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {}
}
},
{
"type": "jsonrpc",
"data": {
"jsonrpc": "2.0",
"id": 1,
"result": {}
}
}
]
}
Phase Breakdown
Phase A: Transport MVP
Goal:
- accept
POST /mcp - validate
MCP-Protocol-Version - dispatch a single JSON-RPC request to PHP worker
- return
application/json
No SSE GET session stream yet. No server-initiated notifications yet.
Current implementation target:
POST /mcponlyGET /mcpreturns405- PHP worker mode:
mcp - helper:
VPhp\VSlim\Mcp\App
This is the smallest useful MCP baseline.
Phase B: Streamable HTTP MVP
Goal:
- add
GET /mcp - add
Mcp-Session-Id - allow
POST /mcpto respond withtext/event-stream - support queued server-to-client JSON-RPC messages
This is the first version that really feels like modern MCP Streamable HTTP.
Current implementation target:
POST /mcp- JSON-only request/response still supported
- response sets
Mcp-Session-Idwhen session is created
GET /mcp- requires
Mcp-Session-Id - upgrades to
text/event-stream - keeps the session open with keepalive comments
- requires
- queued server notifications
- PHP worker may return
messages[] vhttpdqueues them per session- active
GET /mcpstream flushes them as SSEdata:JSON frames
- PHP worker may return
- helper extension:
VPhp\VSlim\Mcp\Appsupports response arrays withmessagesApp::tool(...)can provide builtintools/listandtools/callApp::resource(...)can provide builtinresources/listandresources/readApp::prompt(...)can provide builtinprompts/listandprompts/get
Current limitations:
- no resumability
initializesession creation is still inferred from current request flow- POST does not yet return
text/event-stream
Phase C: Hardening
Goal:
- strict
Originvalidation - auth hooks
- admin/runtime visibility for MCP sessions
- expiry / cleanup / pending queue limits
- resumability discussion
Current implementation target:
/admin/runtime/mcp- summary by default
details=1limit/offsetsession_id/protocol_versionfilters
- bounded session memory
mcp.max_sessionsmcp.max_pending_messagesmcp.session_ttl_seconds
- automatic pruning
- stale sessions expire by TTL
- pending queues are truncated to configured max
- transport guardrails
- optional
allowed_originsallowlist DELETE /mcpsession termination
- optional
Still pending:
- auth hook integration beyond existing admin token pattern
- resumability / reconnect semantics
Why Not Start With Full MCP Features
MCP includes a lot more than transport:
- initialization/lifecycle
- tools
- resources
- prompts
- completions
- sampling / elicitation
- authorization
Those should mostly live above vhttpd.
vhttpd should care first about:
- transport correctness
- session correctness
- SSE correctness
- worker contract correctness
Security Requirements For MVP
Even the MVP should enforce:
Originvalidation for HTTP transport- bind local deployments to
127.0.0.1by default - configurable auth hook/token support
- protocol-version validation
- bounded session and pending message memory
Observability Requirements
Runtime visibility now includes:
/admin/runtimemcp_enabledactive_mcp_sessions
/admin/runtime/mcp- session summaries
- pending outbound counts
- negotiated protocol versions
- configured limits and allowlist
Relevant runtime counters now include:
mcp_sessions_expired_totalmcp_sessions_evicted_totalmcp_pending_dropped_total
This follows the same philosophy as websocket/upstream admin visibility:
- default summary
- details only when requested
Current config surface:
[mcp].max_sessions[mcp].max_pending_messages[mcp].session_ttl_seconds[mcp].allowed_origins
When allowed_origins is non-empty:
POST /mcprequires a matchingOriginGET /mcprequires a matchingOriginDELETE /mcprequires a matchingOrigin
Recommendation
Implement MCP in this order:
POST /mcpJSON-only MVPGET /mcp+Mcp-Session-Id- SSE response mode on POST
- admin/runtime MCP visibility
- auth/origin hardening
This keeps vhttpd focused on what it already does well:
- protocol transport
- session/runtime ownership
- worker orchestration
while leaving MCP application semantics in PHP userland.