App Protocol
For developers. Start with the developer manual to choose API work, App publishing, or project changes.
The OpenBot App Protocol is an open way to publish an App on the web. Any Bot host can then find the App, connect to it, and use it. You do not need a developer account, a registry, or a review. An HTTPS address that you control is enough.
This page describes draft V1.1. In this page, “must” means a rule, “should” means a strong advice, and “may” means an option. All domains are examples.
Standards it uses
Section titled “Standards it uses”The protocol adds very little of its own. It picks existing standards and puts them together.
| Part | Question it answers |
|---|---|
| AI Catalog 1.0 | Which services are here? |
| MCP Server Card v1 | How do I connect? |
MCP 2026-07-28 over Streamable HTTP |
What can the App do? |
MCP Apps 2026-01-26 |
How does it show a view? |
MCP (Model Context Protocol) is a standard way for AI hosts to call tools on a server. The protocol has no own RPC methods, fixed tool names, or UI language.
Publish an App
Section titled “Publish an App”AI Catalog
Section titled “AI Catalog”To be found by domain, put an AI Catalog at /.well-known/ai-catalog.json.
Serve it as application/ai-catalog+json.
{ "specVersion": "1.0", "entries": [ { "identifier": "urn:air:apps.example.com:mcp:calendar", "type": "application/mcp-server-card+json", "url": "https://apps.example.com/mcp/calendar/server-card" } ]}A Catalog may point to Cards on other domains. This lets someone else host the service for you.
Server Card
Section titled “Server Card”Each App must have its own stable Card URL. Serve the Card as
application/mcp-server-card+json. It must list at least one HTTPS endpoint
that supports MCP 2026-07-28.
{ "$schema": "https://static.modelcontextprotocol.io/schemas/v1/server-card.schema.json", "name": "com.example/calendar", "title": "Example Calendar", "version": "1.0.0", "description": "Look up schedules and create calendar events.", "websiteUrl": "https://apps.example.com/apps/calendar", "remotes": [ { "type": "streamable-http", "url": "https://apps.example.com/mcp/calendar", "supportedProtocolVersions": ["2026-07-28"] } ]}The Card URL is the App’s identity. The name and the Catalog identifier
only describe the App. Two Apps with the same name are never merged.
A remote may ask for settings, such as headers or URL variables. Mark
secrets with isSecret: true. Hosts keep these values private to each user.
Serve the Catalog and the Card with ETag or Last-Modified. Hosts send
conditional requests and expect 304 Not Modified when nothing changed.
Icons and usage examples
Section titled “Icons and usage examples”Use the standard Card icons list (src, mimeType, sizes, theme). Hosts
must still show your App when an icon is missing. Hosts never run SVG files as
HTML.
You may add usage examples under _meta["openbot/presentation"]:
{ "_meta": { "openbot/presentation": { "version": "1", "examples": [ { "id": "meeting-plan", "title": "Plan the next team meeting", "prompt": "Find a free 30-minute slot next week and draft an agenda.", "description": "A sample week with one proposed meeting.", "preview": { "src": "https://apps.example.com/assets/meeting.webp", "alt": "A sample week with one proposed meeting", "width": 1200, "height": 800, "mimeType": "image/webp" } } ] } }}| Field | Rule |
|---|---|
version |
Must be "1". |
examples |
1 to 12 items. |
id |
Unique. Up to 100 letters, digits, ., _ or -. Starts with a letter or digit. |
title |
Up to 100 characters. |
prompt |
Up to 2000 characters. |
description |
Optional. Up to 500 characters. |
preview |
Optional. HTTPS src, alt up to 500 characters, width and height from 1 to 8192, mimeType one of PNG, JPEG, WebP, AVIF or SVG. |
Examples must only hold public or made-up data. Hosts show them as plain text. Looking at or picking an example never adds an App, signs in, or runs a tool. Hosts ignore a broken presentation block and keep using the Card.
Discovery from a domain or link
Section titled “Discovery from a domain or link”A host must accept four kinds of input: a domain, an App page URL, a Catalog URL, or a Card URL.
-
Domain. The host reads
https://<domain>/.well-known/ai-catalog.json. -
App page. The page points to its Catalog with a link tag or an HTTP
Linkheader:<linkrel="ai-catalog"type="application/ai-catalog+json"href="/apps/calendar/catalog.json"/>The host reads only these static links. It never runs page scripts. It keeps the full path, so a page for one App does not turn into the whole domain.
-
Catalog with many Cards. The host asks the user to choose. It never adds a whole Catalog.
An App Link is a normal HTTPS URL, best an App page. A QR code holds the same URL. There is no special QR format or URI scheme. Links must not hold tokens or private data. Opening a link only starts a review. It grants no access and runs nothing.
Connect and call
Section titled “Connect and call”Adding, signing in, and running are three separate steps. Adding saves the connection. Signing in grants clear access. Running needs the user’s intent, a valid sign-in, and the host’s own rules.
After the user adds the App, the host:
- calls
server/discoverto read the live version and capabilities; - calls
tools/list,resources/listandprompts/list; - does the work with
tools/call,resources/readandprompts/get.
All messages, results, and errors follow MCP. Name your tools by what they do,
such as create_event. Describe input with inputSchema. Return structured
output with outputSchema and structuredContent. Always return a clear text
result too.
Account sign-in
Section titled “Account sign-in”An App that needs a user account must use MCP authorization. This is OAuth 2.1:
- Protected Resource Metadata. The App answers 401 or 403 with a challenge that points to its metadata. The metadata names the authorization servers and scopes.
- Client ID Metadata Document. The host’s client ID is an HTTPS URL to a public JSON document. Authorization servers read it. No one has to register the host by hand. Both sides must support this.
- PKCE with S256. It protects the sign-in code. Requests also send the exact
resource.
Your App may still ask for a login, a plan, or a business permission. This is not a registry step.
MCP Apps views
Section titled “MCP Apps views”An App may show an interactive view. A tool links to a UI resource in its metadata:
{ "_meta": { "ui": { "resourceUri": "ui://calendar/planner.html", "visibility": ["model", "app"] } }}The resource must use a ui:// URI and the type text/html;profile=mcp-app.
The host reads it with resources/read. The host declares
io.modelcontextprotocol/ui in its client capabilities.
| Purpose | Standard method | Direction |
|---|---|---|
| Start and say ready | ui/initialize, ui/notifications/initialized |
View to host |
| Get tool input and result | ui/notifications/tool-input, ui/notifications/tool-result |
Host to view |
| Do work or read data | tools/call, resources/read |
View, through host, to App |
| Update the model context | ui/update-model-context |
View to host |
| Send a chat message | ui/message |
View to host |
| Open a link | ui/open-link |
View to host |
| Share theme and size | ui/notifications/host-context-changed, ui/notifications/size-changed |
Both ways |
| Clean up | ui/resource-teardown |
Host to view |
Rules for views:
- The host waits until the view is ready before it sends tool data.
- A tool with
visibility: ["app"]is only for the view. Visibility is not access control. Your server must still check permissions. - If a view fails to load, the host must not run the tool again. The text result is still there.
App records, Index entries and connections
Section titled “App records, Index entries and connections”A host keeps three separate things:
| Thing | Holds | Shared? |
|---|---|---|
| App record | The Card and where it came from. | Per account |
| Index entry | The public Card and the publisher evidence. Used for search. | Public |
| User connection | The user’s settings, sign-in, and access. | Private to the user |
Many users can connect the same App. Removing one connection never affects another user or the Index. A public Index that is down must not block a known connection.
Public indexing
Section titled “Public indexing”When you list a Card in your public AI Catalog, you allow public indexing of it. The host must confirm this from your own documents. A copy in someone else’s Catalog is only a hint.
The crawler works without user data. It uses the user bot token OpenBot and
follows robots.txt (RFC 9309). It accepts publisher evidence from:
- the Catalog at the Card origin’s
/.well-known/ai-catalog.json; - a Catalog named by an
ai-catalogLinkheader on the Card response; - a Catalog linked from the page in the Card’s
websiteUrl.
To opt out, send X-Robots-Tag: noindex on the Card. noindex is not access
control. If you remove the Card from your Catalog, the host removes the Index
entry at its next check. A Catalog that cannot be reached is not a removal.
No registry is needed to publish, add, or call an App. A host may also read other registries. See how OpenBot does this in App Index.
Safety rules
Section titled “Safety rules”- All connections use HTTPS. Hosts show the real source of each App.
- Names, icons, Index entries, and tool notes are not trust signals. App content is data, never host instructions.
- Public crawlers limit size, time, and redirects. They check each target and never reach private, loopback, or cloud metadata addresses. The library defaults are 1 MiB, 10 seconds, and 5 redirects.
- Hosts also limit the size of each MCP message. The library default is 16 MiB for a JSON response or for one event of an event stream.
- Discovery from one input stops after 64 documents or 60 seconds. It follows at most 64 links from one Catalog or page. A public indexing check reads at most 8 publisher Catalogs and stops after 60 seconds. The library uses these limits.
- Writes need a permission check. Writes that may be retried need a business idempotency key.
- When the result of a call is unknown, the host must not retry it blindly.
- A change of endpoint, provider, or scopes needs a new review. Old tokens never move to a new target.
- Removing a connection stops all calls and deletes its tokens.
Use the library
Section titled “Use the library”@openbot-build/protocol implements this protocol with Effect 4. It is version
0.1.0 and is not yet on npm. Use it inside this repository’s pnpm workspace.
| Entry point | Use |
|---|---|
McpPublisher |
Serve a Card, a Catalog, and an MCP endpoint. |
McpPublisher.layerProtected, Protection |
Protect an endpoint with OAuth. |
Discovery, CatalogDiscovery |
Find Apps from a domain or link. |
AppConnections, McpClient |
Add, call, revalidate, and remove connections. |
OAuthAuthorization |
Run the host side of account sign-in. |
Indexing, PublicIndexing, RobotsPolicy |
Check and index public Apps. |
@openbot-build/protocol/NodeHttpServer |
Node network layers with the public network rules. |
@openbot-build/protocol/McpApps |
Browser host for MCP Apps views. |
McpPublisher.layer and layerProtected take plain publication data and check
it once when the layer starts. registerUi checks plain UI resource data in its
Effect. Bad input fails with a typed error.
McpClient.open opens a connection from its key, endpoint, headers, and client
data. Give it readToken to read the current token and check to check each
action. A host can also give registerScope to track the connection before any
network call. The client needs no App or credential store. Its scope owns the
connection and closes it. For a saved connection, use AppConnections.open.
That adapter reads the current connection and credentials, checks access, and
tracks its scope. Token reads and sends still check the current state.
Opening a client does not read capability lists. Use
client.resolve({ kind, name }, source) to find one tool, resource, template,
or prompt. It reads only that kind of list and stops at the first match. A match
gives the declaration and its invoke and resume operations. They use that
same declaration without reading the list again. Pass request parameters without
the bound name or URI to invoke; a template takes { values }. Keep the
returned action inside its connection scope. A restored phase finds and checks
the declaration again. Direct readResource also supports UI resources that are
not in the resource list.
CatalogDiscovery.sameConnection compares the parts of a remote that affect a
connection. A change to description text or an extension field keeps the
connection and its sign-in. A change to an input format or its secret flag needs
a new check.
OAuth network failures keep NetworkError. They do not mean that the provider
refused a grant. Closing the connection scope stops pending SDK requests,
including requests that still wait for response headers or stored tokens.
For a host with durable execution, use OAuthAuthorization.prepare, redeem,
renew, and revokeCredential. These functions return a flow or grant without
writing the host’s stores. Save a flow before showing its sign-in URL. Record a
code exchange or token refresh before sending it. Seal the returned grant, then
commit it with the connection state. A lost exchange result needs a decision; do
not send the code or refresh token again without one. Revocation uses a fixed
grant and can be repeated. The standalone begin, complete, refresh, and
revoke functions also update the given stores. They do not provide recovery
across process restarts.
Publish a small App:
import { Document, McpPublisher } from "@openbot-build/protocol"import { Effect, Schema } from "effect"import { McpServer, Tool, Toolkit } from "effect/ai"
const tools = Toolkit.make( Tool.make("hello", { parameters: Schema.Struct({ name: Schema.String }), success: Schema.Struct({ greeting: Schema.String }), }),)const handlers = tools.toLayer({ hello: ({ name }) => Effect.succeed({ greeting: `Hello, ${name}` }),})const origin = "https://apps.example.com"
export const publisher = McpPublisher.layer({ publication: { card: { $schema: Document.cardSchemaUrl, name: "com.example/hello", version: "1.0.0", description: "Say hello.", remotes: [ { type: "streamable-http", url: origin + "/mcp", supportedProtocolVersions: [Document.protocolVersion], }, ], }, cardUrl: origin + "/mcp/server-card", catalogUrl: origin + "/.well-known/ai-catalog.json", mcpUrl: origin + "/mcp", allowedOrigins: ["https://host.example.com"], }, register: McpServer.registerToolkit(tools).pipe(Effect.provide(handlers)),})Serve publisher with Effect HttpRouter.serve. Building the layer does no
I/O. The publication also takes etag (a quoted entity tag), lastModified
(an HTTP date, such as Fri, 09 Oct 2026 00:00:00 GMT), cacheControl, and
noindex. Use McpPublisher.registerUi to add a ui:// view resource.
Discover an App:
import { CatalogDiscovery, Discovery } from "@openbot-build/protocol"import * as NodeHttpServer from "@openbot-build/protocol/NodeHttpServer"import { Effect, Layer } from "effect"
const discovery = CatalogDiscovery.layer().pipe( Layer.provide(CatalogDiscovery.layerRules), Layer.provide(NodeHttpServer.layerPublicNetwork()),)const result = await Effect.runPromise( Discovery.resolve("apps.example.com").pipe(Effect.provide(discovery)),)The result is Single or Multiple. To test your own App against a real host,
use the protocol test service and the
developer tools.