Skip to content

Production integration

registerMcpApps() is the production seam. It adds MCP Apps resources, linked tools, capability negotiation, and text fallback to an existing, unconnected McpServer.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { readMcpAppManifest } from "mountly-mcp/artifact";
import { registerMcpApps } from "mountly-mcp/server";
const server = new McpServer(
{ name: "weather", version: "1.0.0" },
{ capabilities: { tools: {}, resources: {} } },
);
const { artifacts } = await readMcpAppManifest("dist/mountly-mcp.manifest.json");
const apps = await registerMcpApps(server, {
views: artifacts.map((artifact) => ({ artifact })),
tools: [
{
name: "get_weather",
resourceUri: "ui://weather/dashboard",
config: {
description: "Show current weather",
inputSchema: {
type: "object",
properties: { location: { type: "string" } },
required: ["location"],
},
},
handler: async ({ location }: { location: string }) => ({
structuredContent: { location, temperature: 22 },
}),
},
],
});

Call this before server.connect(). You keep auth, transport, prompts, ordinary tools, shutdown, telemetry, scaling, and deploy.

Terminal window
pnpm add @openai/mcp-extensions
import {
enableOpenAiExtensions,
openaiUiToolMeta,
} from "mountly-mcp/openai/server";
enableOpenAiExtensions(server);
// tool config._meta: { ...openaiUiToolMeta({ entrypoints: [{ type: "global" }] }) }

Pass openaiUi on the Vite View. In the View: import { useOpenAIExtensions } from "mountly-mcp/react/openai". Serve HTTPS Streamable HTTP at /mcp for ChatGPT Developer mode.

Keep the server package. Add a View app beside it (or under views/):

  1. npx mountly-mcp create weather-views --framework react (or wire Vite by hand).
  2. Point mountlyMcpViews URIs at your namespace (ui://weather/…).
  3. Build → dist/mountly-mcp.manifest.json.
  4. In the server module: readMcpAppManifest + registerMcpApps before connect.
  5. Leave ordinary server.tool(...) registrations alone.
  6. Develop with npx mountly-mcp dev --server ./path-to-unconnected-factory.mjs.

Default-export an unconnected McpServer (or a factory that returns one). Connected exports fail with mountly-mcp/bad-server-export.

Skill: add-app-to-server.

A View is declared once and tools link to it with resourceUri. This supports:

  • several tools opening the same View;
  • a View with model-visible and app-only tools;
  • a resource with no tool yet;
  • independently replacing tool handlers and View artifacts.

Registration rejects a tool whose URI does not match a declared View, and rejects duplicate View names or URIs before the server connects.

By default, a linked tool is available to both the model and the View. Use a visibility override for UI-only actions:

{
name: "refresh_weather",
resourceUri: "ui://weather/dashboard",
visibility: "app",
handler: async ({ location }) => ({
structuredContent: await refreshWeather(location),
}),
}

visibility accepts one value or an array. App-only tools are callable from the View but are not exposed to the model. On clients without MCP Apps support, Mountly removes app-only tools and UI resources while preserving model-visible tools without their UI metadata.

config.inputSchema and config.outputSchema accept documented JSON Schema as well as schema-library values supported by the MCP SDK. Other official tool configuration is passed through. Existing _meta is preserved; Mountly merges the official _meta.ui.resourceUri and visibility fields it owns.

Handlers may return content, structuredContent, and isError. If structuredContent exists and content does not, Mountly adds a JSON text block for model and text-only compatibility.

The return value owns exactly what that call installed:

apps.remove();

This removes every View resource and tool from the registration, which is useful in tests and controlled reconfiguration.

A Worker has no filesystem, so register the Views from the views module instead of from paths. mountly-mcp/server resolves to a Workers build through the workerd and worker export conditions, so it needs no nodejs_compat. Pair it with the MCP SDK’s web-standard transport:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { registerMcpApps } from "mountly-mcp/server";
import views from "./dist/mountly-mcp.views.js";
export default {
async fetch(request: Request): Promise<Response> {
const server = new McpServer({ name: "tools", version: "1.0.0" });
await registerMcpApps(server, {
views: views.map((artifact) => ({ artifact })),
tools: [{ name: "lookup", resourceUri: "ui://tools/lookup", handler: lookup }],
});
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
return transport.handleRequest(request);
},
};

The Workers build has no serveStdio. Registering a View by path there throws, and the error says to use the views module.

Most hosts block <a download> inside a View. useDownload() (React and Vue) and downloadFile(app, file) from mountly-mcp ask a host that supports ui/download-file to save the file, which the host confirms with the user. Anywhere else they fall back to <a download>:

const download = useDownload();
await download({ name: "matches.csv", data: csv, mimeType: "text/csv" });

data is a string or a Blob. The promise resolves false when the host declined or the user cancelled. mountly-mcp dev implements ui/download-file, so the host path runs locally too.

createMcpAppServer() and serveStdio() declare Views and tools with exactly the same views and tools vocabulary: they only own the McpServer and, for serveStdio(), the transport. Reach for them in examples and small standalone processes; production systems should keep server and transport ownership and install Mountly at the registration seam above.