> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# 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

```bash
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`:

```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:

```ts
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.


```tsx
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.


```ts
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.


```tsx
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.
