|
|
||
|---|---|---|
| .. | ||
| src | ||
| .gitignore | ||
| eslint.config.mjs | ||
| package.json | ||
| README.md | ||
| tsconfig.build.json | ||
| tsconfig.json | ||
| vite.config.mts | ||
| vitest.config.mts | ||
@n8n/mcp-apps
UI resources and server helpers that let the n8n MCP server return MCP
Apps — small, sandboxed HTML/Vue experiences rendered inside MCP clients
that support the
@modelcontextprotocol/ext-apps
extension. The package owns both the runtime UI bundles and the small server
helpers used by packages/cli to register them as MCP resources and tools.
What it provides
- Server helpers (
@n8n/mcp-apps/server) for registering MCP App tools and the static HTML resources that back them. - Vue UI apps under
src/apps/*, built with Vite as standalone, fully inlined HTML files (CSS + JS in a single document) so they can be served directly as MCP resources. - i18n plumbing powered by
vue-i18n, with locale negotiation driven by the host context the MCP client provides at runtime.
Today the package ships a single app, workflow-preview, which is rendered
after the create_workflow_from_code MCP tool returns and gives the user a
button to open the freshly created workflow in n8n. New apps can be added
alongside it (see Adding a new app).
Package layout
src/
apps-manifest.ts # single source of truth for the apps registry
apps/ # Vue UI apps, each built into a standalone HTML
workflow-preview/
App.vue # root component
main.ts # mounts App with i18n
index.html # entry HTML (built into dist/apps/<app>.html)
tokens.scss # design tokens / global styles
url.ts # defense-in-depth URL validation
i18n/ # vue-i18n setup + host locale resolution
locales/ # flat-key locale files (en.json, …)
server/ # consumed by packages/cli
apps/ # MCP resource registrations for each UI app
constants.ts # shared URIs, MIME type, _meta keys
register-mcp-app-tool.ts
resource-loader.ts # lazy reads built HTML from dist/apps
index.ts # public entry: @n8n/mcp-apps/server
apps-manifest.ts is the canonical registry of MCP apps. Both the Vite
build (entry directory + output HTML filename per --mode) and the
server-side resource loader (compile-time union + runtime allow-list of
loadable HTML files) derive from it, so the build and runtime stay in
lockstep and there is no separate list to maintain.
The Vite build (pnpm build:ui) emits one inlined HTML file per app into
dist/apps/<app>.html. The TypeScript build (pnpm build:server) emits the
server helpers into dist/server/. Both run as part of pnpm build.
UI runtime
Each app:
- Connects to the host via
@modelcontextprotocol/ext-apps'sAppclass. - Receives a
McpUiHostContext(theme, style variables, host fonts, locale) throughonhostcontextchangedand reflects it on the document. - Reads the originating tool's
structuredContentviaontoolresultto populate its own state. - Calls
app.openLink({ url })to ask the host to navigate — never opens links itself.
URL handling is locked down by isAllowedWorkflowUrl in
src/apps/workflow-preview/url.ts: only http(s):// URLs with a non-empty
host are accepted, both when reading the tool result and right before calling
openLink. This is defense in depth on top of the host's own validation.
Internationalization
Locale files live under src/locales/ and use flat, namespaced keys
(workflowPreview.openButton, workflowPreview.ariaLabel.ready, …). The
host's BCP 47 locale is resolved to a shipped locale via resolveLocale,
applied to the vue-i18n instance, and mirrored to <html lang> for
assistive tech. See src/i18n/index.ts for the full contract.
To add a new locale:
- Drop
<code>.jsonnext toen.jsoninsrc/locales/. - Import it in
src/i18n/index.tsand add the code toSUPPORTED_LOCALES.
The schema is derived from en.json, so other locales are type-checked
against the same key set.
Adding a new app
-
Create
src/apps/<app-name>/withindex.html,main.ts, and anApp.vueroot component. Mount it through the sharedi18ninstance. -
Add an entry to
MCP_APPSinsrc/apps-manifest.ts:'<app-name>': { entry: '<app-name>', // directory under src/apps/ htmlFile: '<app-name>.html', // output under dist/apps/ },This single entry teaches Vite about the new
--mode, expands theMcpAppHtmlFileNametype union, and adds the file to theloadAppHtmlruntime allow-list.pnpm build:ui --mode <app-name>will then producedist/apps/<app-name>.html. -
Add the app's URI constant to
src/server/constants.tsand aregister<App>Apphelper insrc/server/apps/that callsserver.resource(...)withloadAppHtml('<app-name>.html'). -
Re-export the helper and URI constant from
src/server/index.ts. -
Add any UI strings to
src/locales/en.jsonunder a new app-scoped key prefix.
SDK version compatibility
src/server/sdk-version.test.ts asserts that the
@modelcontextprotocol/sdk version installed via the pnpm catalog satisfies
the peer range declared by @modelcontextprotocol/ext-apps. CI fails the
moment those two pins drift, so bumping one without the other is caught
immediately.
Scripts
| Command | Description |
|---|---|
pnpm build |
Build UI apps and server helpers |
pnpm build:ui |
Build the Vue apps to inlined HTML in dist/apps/ |
pnpm build:server |
Build the server entry to dist/server/ |
pnpm typecheck |
Run vue-tsc over the UI and tsc over the server |
pnpm lint |
Lint with the shared ESLint config |
pnpm test |
Run unit tests with Vitest |
pnpm test:dev |
Run Vitest in watch mode |