The server revision documented here implements the modern MCP 2026-07-28
wire contract and retains a separate legacy compatibility lane for clients that
still initialize with 2024-11-05, 2025-03-26, or 2025-06-18.
A healthy /health route or working legacy connector does not prove that the
modern core is deployed. Treat a hosted endpoint as dual-era only when this
read-only probe passes:
python scripts/check-mcp-protocol-eras.py \
--endpoint https://mcp.agentbounties.app/mcp \
--expect dual| Client request | Server behavior |
|---|---|
2026-07-28 per-request metadata |
Strict modern validation and modern result shapes |
Legacy initialize handshake |
Negotiates a supported legacy version and preserves legacy result shapes |
Modern initialize or ping |
404 with JSON-RPC -32601; those methods are not in the modern core |
Modern GET /mcp or DELETE /mcp |
405; modern Streamable HTTP uses one POST per request |
Modern and legacy MCP implementations are not directly interoperable. The
endpoint therefore detects the era from the modern protocol header/request
metadata or server/discover; it does not mix modern fields into legacy
responses.
At first principles, connection state is hidden coupling: if correctness depends on which server handled an earlier handshake, requests need sticky routing or shared session storage. A self-contained request can be retried or sent to any healthy instance. That gives us simpler horizontal scaling and failure recovery on ordinary HTTP infrastructure.
The remaining incentives line up across participants:
- clients gain explicit version errors, safe retry/failover, and cacheable, deterministic catalogs;
- servers remove session-affinity infrastructure and can evolve optional features through extensions instead of expanding the core handshake;
- gateways can route, authorize, rate-limit, and observe a method or tool from
Mcp-MethodandMcp-Namewithout parsing the entire JSON body; - Agent Bounties can keep the existing ChatGPT/legacy lane while deploying the modern lane independently, then measure adoption before retiring anything.
The tradeoff is that each modern request carries more metadata, and the server must validate the header/body pair. The dual-era boundary intentionally pays some temporary implementation complexity to avoid pushing a flag-day migration onto existing users.
Normal app users do not select an era manually. A dual-era client should try
modern discovery first. A recognized modern error must be corrected as a
modern request; an unrecognized error response identifies a legacy server and
can trigger the initialize fallback. For Streamable HTTP, inspect the JSON-RPC
body of a 4xx response before falling back. Existing clients that only
implement the legacy handshake continue to use that lane.
Every modern request is a single JSON-RPC object. It includes:
MCP-Protocol-Version: 2026-07-28;Mcp-Method, equal to the bodymethod;Mcp-Namefortools/call,resources/read, andprompts/get, equal to the body name or URI after decoding the protocol's Base64 sentinel when used;params._meta["io.modelcontextprotocol/protocolVersion"];params._meta["io.modelcontextprotocol/clientCapabilities"];- preferably
params._meta["io.modelcontextprotocol/clientInfo"].
Header/body mismatches return HTTP 400 and JSON-RPC error -32020.
Unsupported modern versions return HTTP 400, error -32022, and the
supported/requested version data. An invalid Origin, when that header is
present, returns HTTP 403.
Use server/discover before calling tools:
POST /mcp
Accept: application/json, text/event-stream
Content-Type: application/json
MCP-Protocol-Version: 2026-07-28
Mcp-Method: server/discover
{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"example-client","version":"1"},"io.modelcontextprotocol/clientCapabilities":{}}}}An older client starts with the legacy handshake and then sends legacy-shaped requests without modern request metadata:
POST /mcp
Accept: application/json, text/event-stream
Content-Type: application/json
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"example-client","version":"1"}}}Successful modern results include resultType: "complete" and server identity
under _meta["io.modelcontextprotocol/serverInfo"]. Discovery, list, and read
results also include ttlMs and cacheScope. Catalogs are deterministic so a
client can safely reuse the cache hint.
The server advertises only tools and resources. It does not advertise Tasks, subscriptions, prompts, or other optional extensions that it does not implement.
Requests without an Origin are allowed for normal server-to-server MCP
clients. Exact first-party, ChatGPT, and loopback development origins are
allowed by default. MCP_BASE_URL adds its exact origin. Deployments with
additional browser clients can set a comma-separated exact allowlist:
MCP_ALLOWED_ORIGINS=https://client.example,https://another.example
Do not use wildcard origins. This check is the endpoint's DNS-rebinding boundary, not an authentication substitute.
cargo test -p mcp-server chatgpt_app::tests
cargo build -p mcp-server
python scripts/check-chatgpt-app-runtime.py
python scripts/check-mcp-protocol-eras.py --endpoint http://127.0.0.1:8080/mcp --expect dualThe runtime check calls modern server/discover, modern catalogs, a resource,
and a tool, plus legacy initialize and a legacy catalog through the real HTTP
endpoint.
The deployed production smoke performs modern discovery and legacy initialize against the published endpoint. A health response alone is not sufficient release evidence.
For choosing between the app, raw MCP, REST API, and CLI, see the interaction guide.
Protocol references: