Skip to main content
Glama
dzhunka

mcp-app-boilerplate

by dzhunka

MCP App plugin boilerplate

A deployable starting point for an agent plugin whose core is an MCP App — a remote MCP server that ships an interactive HTML widget the host renders next to the tool result. One repo covers both halves:

Layer

Where

What it is

MCP App

app/mcp/route.ts + widget/

A Next.js route serving a remote MCP server, and the widget it hands the host

Plugin package

plugin.json, mcp.json, skills/

Agent Plugins 1.0.0 package that installs into Cursor, Codex, and Claude Code

This is a GitHub template repository — click Use this template, or copy the directory, to start a project from it. Nothing here is bound to a particular deployment: mcp.json and .mcp.json ship with a replace-me.example.com placeholder so a project that forgets to set its own endpoint fails loudly instead of silently talking to someone else's server.

How the MCP App works

MCP Apps is SEP-1865, stable since 2026-01-26 and identified as io.modelcontextprotocol/ui. Three things wire it together:

  1. A resource with a ui:// URI and MIME type text/html;profile=mcp-app returns the widget's HTML.

  2. A tool points at that resource through _meta.ui.resourceUri.

  3. The host renders the HTML in a sandboxed iframe and speaks MCP JSON-RPC to it over postMessage.

The widget is not served as a web page. Its HTML travels as text inside the JSON-RPC response, and the host injects it into an iframe under the host's own origin. Your deployment is never navigated to — it only answers /mcp.

That is why this repo has two builds and one deployment. Next.js is the server: it terminates Streamable HTTP, runs your tools, and serves resources. The widget is a Vite app compiled into a single self-contained HTML file, which the MCP route reads off disk and returns as the resource.

Inlining everything is deliberate. Host sandboxes enforce a CSP that restricts which origins a widget may load subresources from, and a widget that fetches nothing works on every host regardless of that policy. It also removes the failure mode where a widget renders as readable but unstyled and inert. See DECISIONS.md for the measurements behind this.

app/
  mcp/route.ts          MCP server — tools + the ui:// resource
widget/
  index.html            Vite entry
  main.tsx              React root
  app.tsx               the widget UI
  use-mcp-app.ts        the host bridge: tool input, tool result, callTool
  styles.css            Tailwind
  dist/index.html       built bundle (generated, gitignored)
vite.config.mts         single-file widget build
proxy.ts                CORS headers, so browser-based MCP clients can connect
scripts/dev-codex.mjs   isolated Codex instance with this plugin installed
scripts/test-client.mjs protocol smoke test
scripts/set-endpoint.mjs writes your deployment URL into both manifests
scripts/capture-seed-thread.mjs captures one of your threads as a seed fixture
scripts/fixtures/         seed threads the isolated instance starts with

The two kinds of tool

The demo registers both, because real MCP Apps need both:

  • greet is model-visible. The agent calls it, and the host opens the widget.

  • set_tone is declared visibility: ["app"], so it is hidden from the model and exists only for the widget to call when the user clicks a button. The widget invokes it through callTool from useMcpApp().

Related MCP server: skill-deck

Develop

Requires macOS with the Codex desktop app, and Node 22.12+ (Vite 8).

pnpm install
pnpm dev

pnpm dev is the whole loop. It builds the widget, starts a Vite watcher and the Next.js dev server on port 3100, installs this repo as the only local plugin in a throwaway Codex instance, points that plugin at http://127.0.0.1:3100/mcp, and launches a separate Codex desktop window. Closing the window removes the isolated state. Your everyday Codex install is untouched.

No tunnel is required, because the widget requests no subresources. Because each run gets a fresh Codex instance, its widget cache starts empty, so you see the current bundle without bumping UI_VERSION.

Seed threads

A fresh instance also starts with an empty thread list, which means retyping the same setup before every test. scripts/fixtures/codex-dev-seed-thread-* holds conversations that pnpm dev replays into the isolated instance, so they are waiting in the thread list, ready to continue. The one shipped here establishes that you are Ada and that greetings go through the app, so "greet me" opens the widget on the first turn.

Capture your own from a thread you have already had:

pnpm capture-thread --last                    # or: pnpm capture-thread <thread-id>
pnpm capture-thread <thread-id> --name my-case --description "what it sets up"

Capturing rewrites your home directory, Codex home, and working directory into placeholders that the harness substitutes at seed time. It does not rewrite the conversation, so read a fixture before committing it.

For server-only work, or to check the protocol:

pnpm dev:server               # Vite watcher + Next.js on port 3000
pnpm test:client              # verifies the protocol against localhost:3000

pnpm test:client connects as a UI-capable client and asserts the parts that are easy to get subtly wrong: the extension capability, the ui:// metadata on the tool, the resource MIME type, that the resource is real HTML, and that the bundle is self-contained.

Build your own plugin

The work happens in three places:

  1. app/mcp/route.ts — replace greet/set_tone with your tools. Bump UI_VERSION whenever you ship a widget change, or hosts will serve a cached copy.

  2. widget/app.tsx — your widget. Read state from useMcpApp().

  3. skills/<name>/SKILL.md — teach agents when to call your tools. The description is all an agent sees before loading the file, so it has to name concrete triggers.

Rename it for a new project

The boilerplate name survives in five places:

File

What to change

plugin.json

name, description

.claude-plugin/plugin.json

name, description

mcp.json and .mcp.json

the mcpServers key

skills/mcp-app-boilerplate/

directory name and the name frontmatter — they must match, or the skill is rejected

app/mcp/route.ts, widget/use-mcp-app.ts, package.json

serverInfo.name, the widget's client name, name

The endpoint URL is the one thing you can't set until you've deployed, so leave it and run pnpm set-endpoint afterwards.

Deploy

pnpm build                                           # widget bundle, then Next.js
vercel deploy --prod
pnpm set-endpoint https://your-project.vercel.app    # updates both manifests
pnpm test:client   https://your-project.vercel.app

next.config.ts lists the widget bundle in outputFileTracingIncludes so the serverless function can read it at runtime. If you move the bundle, update that path too, or production will fail to serve the resource.

Install the plugin

The plugin root is the repository root, and both manifests describe the same package; distribute it as a git repo. Run pnpm set-endpoint first — installing while the placeholder URL is still in place gives you a plugin whose MCP server never connects.

Cursor natively supports Agent Plugins. Add the repo through Customize → Plugins, which reads the root plugin.json.

Codex (0.147+) reads the portable root plugin.json and mcp.json:

codex plugin marketplace add <your-org>/<your-repo>
codex plugin add <plugin-name>@<marketplace>
codex plugin list --json

Claude Code does not yet parse the Agent Plugins $schema, so it reads the parallel .claude-plugin/plugin.json and .mcp.json committed here. Its skills/ discovery is the same directory, so the skill is shared:

claude plugin marketplace add <your-org>/<your-repo>

To connect only the MCP server without the plugin wrapper, any host that speaks Streamable HTTP can point at the endpoint directly:

claude mcp add --transport http my-plugin https://your-project.vercel.app/mcp

Stack

  • mcp-handler 2 — framework-agnostic MCP HTTP adapter, serves the 2026-07-28 protocol natively with a fallback for 2025-era Streamable HTTP clients

  • @modelcontextprotocol/server 2 and @modelcontextprotocol/ext-apps 2 — MCP SDK v2 plus the MCP Apps helpers (registerAppTool, registerAppResource)

  • Vite 8 with vite-plugin-singlefile and Tailwind 4 — the widget bundle

  • Next.js 16 on Vercel Fluid Compute — the MCP server

Derived from vercel-labs/mcp-apps-nextjs-starter, upgraded to the v2 MCP stack, packaged as an Agent Plugin, and moved to a self-contained widget bundle.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    A local MCP server that lets AI agents publish markdown/HTML to a browser viewer with session management, revision diffs, and live updates via a single tool call.
    2
    MIT