For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /guides/advanced-features/mcp-apps.md.

MCP Apps

Modern.js builds React cards for MCP hosts. UI builds and servers exchange only ui-manifest.json. Servers use the official MCP SDK directly.

Create a project

npx @modern-js/create@latest my-mcp --template mcp-apps
npx @modern-js/create@latest my-mf-mcp --template mcp-apps --mf

Choose a release containing MCP Apps support. Run pnpm dev. The explicit BFF route is /api/mcp.

@modern-js/mcp-apps provides the build plugin, server plugin, server adapters and card runtime through ./cli, ./server-plugin, ./server and ./react. The application must install the peers react, react-dom, @modelcontextprotocol/server and @modelcontextprotocol/ext-apps, so cards use the application's React and tools, resources and HTTP handling use the application's SDK versions. @modern-js/server-runtime and @module-federation/enhanced are optional peers that provide the default request context for BFF handlers and the default MF shell runtime. The project templates already declare these dependencies.

Build cards

Declare local default-export React components or this project's MF exposes in mcp-views.json:

{
  "views": {
    "todos": "./src/components/Todos.tsx",
    "stats": { "expose": "./Stats" }
  },
  "csp": { "resourceDomains": [], "connectDomains": [] },
  "prefersBorder": true
}

Import mcpAppsPlugin from @modern-js/mcp-apps/cli and add mcpAppsPlugin({ views: './mcp-views.json' }) to your Modern.js plugins. Paths resolve relative to the JSON file. MF cards require the Modern.js MF plugin with manifest output enabled. The UI plugin validates exposes against the actual emitted MF manifest and builds the shell with the application's React dependencies.

The plugin finds the MF manifest in the web build output by filename, mf-manifest.json by default, and requires exactly one match. The shell imports createInstance from @module-federation/enhanced/runtime by default. Other MF implementations can replace both through the mf option:

mcpAppsPlugin({
  views: './mcp-views.json',
  mf: {
    // MF manifest filename in the build output
    manifestFile: 'vmok-manifest.json',
    // Resolved from the application. Must export a compatible createInstance
    runtime: '@vmok/kit/runtime',
  },
});

Local cards build as separate entries without the framework page runtime. With source.enableAsyncEntry, each entry adds a dynamic import boundary so MF shared dependencies can initialize. The MF plugin enables this option automatically. Otherwise cards mount directly without an extra request.

import type { McpViewProps } from '@modern-js/mcp-apps/react'

export default function Todos({
  todos = [],
  mcpApp,
}: McpViewProps<{ todos: string[] }>) {
  return (
    <section>
      <ul>
        {todos.map(todo => (
          <li key={todo}>{todo}</li>
        ))}
      </ul>
      <button
        onClick={() =>
          mcpApp.callServerTool({ name: 'refresh', arguments: {} })
        }
      >
        Refresh
      </button>
    </section>
  )
}

Props come from successful structuredContent. The reserved mcpApp prop is injected by the wrapper. Input arguments are not merged into props. New input, cancellation and teardown clear previous output. The wrapper manages connection, host styling and loading/error states. Results from tools called inside a card must be handled by the component itself.

Write the server

Enable bffPlugin() and create api/lambda/mcp.ts. The UI plugin does not register any HTTP route.

import { McpServer } from '@modelcontextprotocol/server'
import { registerAppTool } from '@modelcontextprotocol/ext-apps/server'
import {
  createBffHandlers,
  registerUiResources,
} from '@modern-js/mcp-apps/server'
import { z } from 'zod'

async function createServer(request: Request) {
  const server = new McpServer({ name: 'todos', version: '1.0.0' })
  const ui = await registerUiResources(server, { request })
  registerAppTool(
    server,
    'list_todos',
    {
      inputSchema: z.object({}),
      _meta: { ui: { resourceUri: ui.todos } },
    },
    async () => ({
      content: [{ type: 'text', text: 'Todo list' }],
      structuredContent: { todos: ['Read the MCP Apps guide'] },
    }),
  )
  return server
}

export const { POST, GET, DELETE } = createBffHandlers(createServer)

The adapter reconstructs BFF's consumed JSON body and returns the SDK Response. It reads the raw request through useHonoContext from @modern-js/server-runtime. Frameworks built on Modern.js can pass their own implementation with createBffHandlers(createServer, { useContext }). The official SDK owns protocol serving and tool validation. Serving is stateless, so unsupported session operations such as GET and DELETE are rejected by the SDK. Return a fresh server per request. Put authentication and authorization in surrounding middleware or the factory, and persist business data in an external store.

For independent Hono servers, create handle = createFetchHandler(createServer) and mount app.all('/mcp', c => handle(c.req.raw)). Foreign browser origins are rejected unless listed in allowedOrigins in the adapter's second argument.

Manifest and deployment

Builds emit dist/mcp-apps/ui-manifest.json and dist/mcp-apps/ui/*.html. Static JavaScript, CSS and MF files remain in the application's standard output. MF-only projects also emit a UI manifest. Local HTML filenames use a stable hash of the view name. MF cards share mcp-shell.html.

The version 1 manifest contains assetBase, remotes and views. A local view has { html, ui }. An MF view has { remote, expose, ui }, with its remote declaring { entry, shell }.

  • HTML and shell paths resolve relative to the manifest location
  • MF entry paths resolve relative to assetBase
  • Absolute HTTP(S) sources are supported for independent deployments
  • ui preserves CSP, permissions, domain and border preferences
  • Resource URIs are generated as ui://app/<encoded-view-name> by the server, and multiple tools can share a card
  • MF configuration is injected into resource HTML instead of tool results

createBffHandlers() associates the server plugin’s manifest path and runtime mode with the current request. Call registerUiResources(server, { request }) to resolve them automatically. Development uses the framework internal directory and production uses the runtime output directory. Explicit options override injected configuration. Independent services must provide a manifest file path or HTTP(S) URL. The mode option defaults to production. Relative HTML resolves against the manifest location.

Manifests are cached by source URL and environment. Production caches successful loads for the process lifetime, so updating the same location requires a process restart. Development detects local file changes and caches remote manifests for 60 seconds. Concurrent loads are coalesced and failures can be retried. HTML is read per resource request, and request-specific URLs and metadata remain isolated.

An absolute output.assetPrefix becomes assetBase. Otherwise request-origin resolution is used. Override assetBase in registerUiResources for a separate static origin or deployment path prefix. Request mode honors sanitized x-forwarded-proto headers. Reverse proxies must preserve the public host and sanitize forwarded headers.

Required asset, connection and base URI origins are added to CSP. MF entry origins are also added to connection permissions. The origin of an absolute or protocol-relative output.assetPrefix is added at build time, with //cdn.example/ treated as https://cdn.example. Declare any other API, chunk/CDN or iframe origins in mcp-views.json, or pass domains computed in the config through mcpAppsPlugin({ csp }). Both are merged into every view. Deploy the manifest and HTML together with the application static assets and MF output.

Platforms may rewrite file and MF addresses to absolute URLs and combine cards into another manifest of the same version. Unknown versions are rejected.

Card capabilities

Cards advertise inline and fullscreen by default. Both local component modules and MF exposes can export mcpCapabilities to declare additional capabilities or override the display modes.

import type { McpCapabilities } from '@modern-js/mcp-apps/react'

export const mcpCapabilities = {
  availableDisplayModes: ['inline'],
} satisfies McpCapabilities

export default function Card() {
  return <div>My card</div>
}

You can also pass capabilities as the third argument to createMcpView. Explicit display mode arrays replace the defaults. MF modules are loaded before initialization so the handshake includes their capabilities. Declaring fullscreen support does not add a fullscreen button or switch modes automatically.