mcp-uplift
MCP 2026-07-28 removed the initialization handshake, sessions, ping,logging configuration, resource subscriptions, and server-initiated requests.Older MCP servers still use those protocol features. mcp-uplift wraps onelegacy stdio server and presents it as a modern stateless server.
Security: wrapping a server executes that server with your operating-systempermissions. This bridge is not a sandbox. Only run packages and commands youtrust.
Demo

The unmodified official @modelcontextprotocol/server-filesystem running behindthe 2026-07-28 protocol: server/discover is synthesized from the legacyhandshake, the server's own roots/list request becomes a keyed input_requiredresult, and answering it resumes the call and returns all 14 tools.
Usage
Run without installing:
npx mcp-uplift <legacy-command> [args...]
For example:
npx mcp-uplift npx -y @modelcontextprotocol/server-filesystem /tmp
The wrapped command receives a minimal environment by default. Forward a neededcredential explicitly, before --:
npx mcp-uplift --env BRAVE_API_KEY -- npx -y @modelcontextprotocol/server-brave-search
--inherit-env is available for compatibility but exposes every environmentvariable to the wrapped process. Run npx mcp-uplift --help for resource andtimeout controls.
Client configuration
Before, a client launches the legacy server directly:
{
"mcpServers": {
"files": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}
After, launch the same command through mcp-uplift:
{
"mcpServers": {
"files": {
"command": "npx",
"args": ["mcp-uplift", "npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
}
}
}
Compatibility
Because 2026-07-28 is stateless, every request must carry its own envelope inparams._meta: the protocol version and the client's capabilities, plusoptional client identity. A request missing them is rejected rather thanguessed at.
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "roots": {} },
"io.modelcontextprotocol/clientInfo": { "name": "my-client", "version": "1.0.0" }
}
}
}
If a wrapped server needs a capability the client did not declare, the bridgereturns -32021 naming what was required, rather than failing deep inside thecall.
| Feature | Behavior |
|---|---|
Legacy initialize |
Performed once and exposed as server/discover |
| Per-request version metadata | Validated as 2026-07-28, else -32022 |
| Tools, prompts, and resources | Forwarded with modern result and cache metadata |
| Legacy resource-not-found errors | Mapped from -32002 to -32602 |
| Sampling, elicitation, and roots requests | Translated into multi-round-trip input_required results |
| Removed methods | Rejected with -32601 method-not-found |
| Upstream failures | Returned as -32603 internal errors |
Answering a server's question
A legacy server can interrupt its own call to ask the client something.Because a modern client cannot receive that push, the bridge returns aninput_required result instead, keyed by request id:
{
"resultType": "input_required",
"inputRequests": {
"ir_1000": {
"method": "elicitation/create",
"params": { "message": "Which environment?" }
}
},
"requestState": "c0a2db7b-62bc-4420-b2e9-31da87f8f999"
}
To resume, send the same request again with the requestState you weregiven and an inputResponses map using those same keys. Each value is theresponse body itself, not wrapped in result:
{
"name": "deploy",
"arguments": {},
"requestState": "c0a2db7b-62bc-4420-b2e9-31da87f8f999",
"inputResponses": {
"ir_1000": { "action": "accept", "content": { "env": "prod" } }
}
}
The parked call then finishes and returns its ordinary "complete" result.Shapes per method: elicitation/create takes { action, content } withaction one of accept, decline, or cancel; roots/list takes{ roots: [...] }; sampling/createMessage takes { model, role, content }.
The resumed request must match the original, every key must be answered, andrequestState is single-use and expires, so an invalid resume is rejectedrather than half-applied.
Known limitations
Real-world validation confirmed discovery and tool listing against 39 distinctlegacy MCP packages. The official filesystem server also completed a realroots/list MRTR round trip and returned all 14 tools. These are testedexamples, not a guarantee that every server or session-dependent behavior canbe translated.
- All legacy calls are serialized. Thelegacy protocol never links a
sampling/createMessage,elicitation/create,orroots/listrequest back to the call that caused it, so the bridgekeeps one call in flight through all of its MRTR rounds. This favors correctattribution over throughput. - Legacy notifications have no home and are dropped.
2026-07-28movedstreamed notifications onto a dedicatedsubscriptions/listenstream; thisbridge is a plain one-request-one-response stdio proxy and does notimplement that stream, sonotifications/progressandnotifications/messagefrom the wrapped server are discarded rather thandelivered. - Real-server checks require downloads and remain outside the offline suite.
- The kill switch terminates the launched process tree on POSIX and Windows,but trusted code can deliberately daemonize into a new OS process session.Use an OS sandbox or container when stronger confinement is required.
Security reports
Do not include credentials or exploit details in a public issue. Report asuspected vulnerability privately through the repository's GitHub securityadvisory page. Ordinary bugs can use the public issue tracker.