Transport
Serve MCP over streamable HTTP on a singlehttps endpoint, for example https://example.com/mcp. Both MCP eras work: the stateless 2026-07-28 protocol and the older initialize handshake (2025-11-25 and earlier).
The Allternit client is dual-era:
- It POSTs JSON-RPC requests and accepts either a JSON body or an SSE stream in reply (notifications on the stream are skipped).
- It first tries MCP 2026-07-28: a
server/discoverrequest with noinitialize. Every modern request carriesparams._metawithio.modelcontextprotocol/protocolVersion: "2026-07-28",io.modelcontextprotocol/clientCapabilitiesandio.modelcontextprotocol/clientInfo, plus theMCP-Protocol-Version: 2026-07-28andMcp-Methodheaders, andMcp-Nameontools/call,resources/readandprompts/get(in the=?base64?…?=form when the name is not plain ASCII). No session id is used. - If your server answers that request with
-32022and asupportedlist, it retries with a version from the list. Other modern errors (-32020,-32021) are reported, not retried. - If your server answers with any other
4xx(for example400“no session”,404,405), or with a JSON-RPC error that is not a modern one, or with a discover result that has nosupportedVersions, the client falls back toinitialize. It offers2025-11-25, or the newest older version your-32022listed. - After
initializeit sendsMCP-Protocol-Versionwith the negotiated version and echoes anyMcp-Session-Idyour server issues, treats a202reply to a notification as success, sends aDELETEwith the session id when it closes a session, and treats a404on a request that carries a session id as an expired session. 401,403,429and5xxreplies are errors, never an era signal: the client does not fall back on them.- The era it found is remembered per origin (scheme, host and port), so later connections skip the probe. If that guess stops working, the client probes again.
- A result without
resultTypecounts as complete. A result with anotherresultType(such asinput_required) is reported as an error; multi-round-trip input is not supported yet.
2026-07-28 (stateless, server/discover, a _meta envelope on every request) and the older initialize handshake, so it works with every host. The official SDK v2 does this from one server definition (createMcpHandler in @modelcontextprotocol/server; see the Quickstart), and so does the Apps SDK.
Stateless servers (no session id) work in both eras. If both streamable HTTP attempts get 404 or 405, the chat runtime falls back to the older SSE transport, but new servers should use streamable HTTP.
Requests that reach your server are made by Allternit’s servers, not by the user’s browser. Your server does not need to allow cross-origin requests from Allternit’s web origin.
What Allternit sends at initialize
The client advertises MCP Apps support in its capabilities: ininitialize (legacy) or in every request’s _meta["io.modelcontextprotocol/clientCapabilities"] (2026-07-28):
@modelcontextprotocol/ext-apps/server, getUiCapability(clientCapabilities) reads it. A server that always registers its tools works too: a host without UI support ignores _meta.ui and shows the text result.
Tools
Register each tool with a name, a description, an input schema, and annotations.
The submission scan checks all of these.
Annotations
Annotations are hints that tell the host how careful to be. Declare all of them on every tool. They must be booleans.
Allternit uses them to decide when to ask the user. A tool is treated as important when it is destructive, or when it is not read-only and reaches the open world. See permission prompts.
Declare them truthfully. A tool named like a write (
create_, delete_, send_) with readOnlyHint: true, or a tool marked both read-only and destructive, is flagged by the scan.
Results
Returncontent for the model. Return structuredContent when a View or another program needs typed data. Set isError: true for failures the model should see and recover from.
Tool visibility
_meta.ui.visibility controls who can call a tool. It is an array containing "model", "app", or both. Omitting it means both.
Use
["app"] for tools that exist only to serve the View, such as pagination or a refresh action.
Server instructions
Returninstructions in your initialize result. Keep them to guidance about using your tools: which tool finds things, which one shows a UI, what nothing here does.
Resources
A View is an MCP resource with aui:// URI.
- Serve it from
resources/readwith the MIME typetext/html;profile=mcp-app. - List it in
resources/listso the scan can find it. The scan reports an error when a tool points at aui://URI thatresources/listdoes not expose. - Put
cspandpermissionsunder_meta.uion the resource content (or on the read result). The host reads the content-level value first, then the result-level value. See Build: MCP App UI. - Keep the HTML under 2 MiB.
Requests the host makes for a View
While a View is displayed, it can ask the host to forward requests to your server. The host forwards exactly five methods, always with the connecting user’s own credentials:
Any other method, including
prompts/list, prompts/get, and sampling/createMessage, is refused. The host rebuilds each request from validated fields. It does not pass through anything else the View sent. Requests time out after 30 seconds.
Worked example: Allternit’s Agents server
Allternit exposes its own agents as an MCP server, built the same way as a third-party plugin. It is read-only. The default public URL in the code ishttps://mcp.allternit.com/mcp.
It has five data tools and one render tool:
Every tool carries
readOnlyHint: true. Only render_run_status has _meta.ui:
get_run) and render tool (render_run_status) follow a pattern worth copying: the model reads data with the data tool and only calls the render tool when the user wants to see it. The tool description says so.
The View declares csp: { connectDomains: [], resourceDomains: [] }, so it can make no network requests. Its server instructions read:
Read-only access to the caller’s Allternit agents and their runs. Use list_agents to find an agent, list_runs to find its runs (filter by agent_id or status), get_run for status and timestamps, and get_run_result for the output of a finished run. Use render_run_status only when the user wants to see a run’s status displayed. Nothing here starts, changes, or deletes anything.Access requires an OAuth token with the scope
agents:read. See Authentication.
The server answers both MCP eras on one endpoint: stateless 2026-07-28 requests (version in _meta, server/discover, resultType on every result) and initialize for 2025-11-25 and earlier. See protocol versions.