App Protocol
The OpenBot App Protocol is an open way to publish an App on the web. Any Agent 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
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
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
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
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
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
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
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
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
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
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 agent 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
- 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.
- 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
@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. |
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, lastModified, 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.