@omni-ir/mcp
Omni-IR screens inside the AI apps people already use: Claude, ChatGPT, VS Code, Cursor and other hosts of MCP Apps, the official MCP extension for interactive views.
- The host's model calls the
show_screentool with a few lines of Omni-IR. The tool's description teaches it the format, generated from the schema and your tools. - The host draws the screen in its sandboxed frame with Omni-IR's view: the parser and the Trusted Catalog in one HTML file, with no network access. The screen builds line by line while the model writes, in the host's colours.
- Buttons call your tools through the host, as MCP tools only the view can see. Your server checks every call against the tool's schema before your code runs, so the model can trigger only what you allow.
- If lines are rejected, the tool's result tells the model which ones and why, so it can fix the screen.
Try it
npx @omni-ir/mcpThat starts an MCP server over stdio with screens only: no actions and no pictures. Add it to an MCP Apps host (for example Claude desktop's MCP servers) and ask for a screen.
With your tools and pictures
import { createOmniMcpServer } from "@omni-ir/mcp";
import { serveStdio } from "@modelcontextprotocol/server/stdio";
import { z } from "zod";
const tools = {
"orders.requestReturn": z.strictObject({ orderId: z.string().regex(/^[A-Z0-9-]{4,32}$/) }),
};
serveStdio(() =>
createOmniMcpServer({
name: "my-shop",
version: "1.0.0",
tools,
// Pictures by name; `src` must be a data: URI, since the view can't use the network.
assets: { tote: { src: "data:image/svg+xml,…", width: 200, height: 200 } },
// Runs after the params passed the tool's schema: your access rules, idempotency and records.
onAction: async ({ tool, params, idempotencyKey, user }) => {
// …
return { ok: true, result: { returnId: "R-1" }, message: "Return requested." };
},
onEvent: (event) => console.error(JSON.stringify(event)), // screens shown and actions run, for counts
}),
);show_screen's result tells the model which lines were rejected, if any, and gives the screen as plain text (describeScreen from @omni-ir/core), so a host that can't draw the view still shows something useful.
An app's own components (components, with pictures for picture families) are checked and described to the model too. The built-in view has no code for them and shows its fallback; build a view with your components and pass it as viewHtml to draw them.
Over HTTP, use the MCP SDK's createMcpHandler with a factory that passes who is calling (from MCP's authorization) as user, so each action runs as that person.
In the repository
npm run mcpruns the reference server's bridge over stdio, with the demo tools and pictures, acting as the demo visitor. Add it to a local MCP Apps host (Claude desktop's MCP servers, for example) to try screens with real actions. No model is called by the server: the host's model writes the screens.npm run mcp:hostbuilds a local demo host (dist/mcp-host, serve it over HTTP) that streams sample screens into the view through the official host bridge: a way to see the view, in light and dark, without an MCP host.OMNI_MCP=on npm run serverserves the same at/mcpover HTTP. Actions run through the same checks asPOST /api/mutate: the tool's schema, its access rule (signed-in actions needAuthorization: Bearer <token>from the server's sign-in), ownership, idempotency keys and the audit trail. A cookie never signs anyone in here, since any page could make a browser send it.GET /api/healththen counts screens shown, rejected lines and actions per tool.- Full MCP authorization (an OAuth authorization server with discovery) isn't part of the reference server: an app plugs in its identity provider's tokens with the SDK's bearer authentication and passes the person as
user.
Rules
The behaviour is specified in SPEC.md, section 10, "Over MCP Apps" ([10.25]–[10.28]). Apache-2.0.