Your MCP server is the only required part of a plugin. This page covers what Allternit expects from it and what it sends.

Transport

Serve MCP over streamable HTTP on a single https 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/discover request with no initialize. Every modern request carries params._meta with io.modelcontextprotocol/protocolVersion: "2026-07-28", io.modelcontextprotocol/clientCapabilities and io.modelcontextprotocol/clientInfo, plus the MCP-Protocol-Version: 2026-07-28 and Mcp-Method headers, and Mcp-Name on tools/call, resources/read and prompts/get (in the =?base64?…?= form when the name is not plain ASCII). No session id is used.
  • If your server answers that request with -32022 and a supported list, 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 example 400 “no session”, 404, 405), or with a JSON-RPC error that is not a modern one, or with a discover result that has no supportedVersions, the client falls back to initialize. It offers 2025-11-25, or the newest older version your -32022 listed.
  • After initialize it sends MCP-Protocol-Version with the negotiated version and echoes any Mcp-Session-Id your server issues, treats a 202 reply to a notification as success, sends a DELETE with the session id when it closes a session, and treats a 404 on a request that carries a session id as an expired session.
  • 401, 403, 429 and 5xx replies 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 resultType counts as complete. A result with another resultType (such as input_required) is reported as an error; multi-round-trip input is not supported yet.
Build the server dual-era: answer MCP 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: in initialize (legacy) or in every request’s _meta["io.modelcontextprotocol/clientCapabilities"] (2026-07-28):
Use this to decide whether to register UI-bearing tools. With @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

Return content 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.
When a tool has a View, the host passes the tool result to the View. In the chat path, results larger than 256 KB are not kept for the View; the View receives the model-visible text instead. Keep results small and let the View fetch more through a follow-up tool call.

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

Return instructions 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.
Do not use instructions to steer the model toward your app over other apps.

Resources

A View is an MCP resource with a ui:// URI.
  • Serve it from resources/read with the MIME type text/html;profile=mcp-app.
  • List it in resources/list so the scan can find it. The scan reports an error when a tool points at a ui:// URI that resources/list does not expose.
  • Put csp and permissions under _meta.ui on 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 is https://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:
The separate data tool (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.