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.

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.

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.