diff --git a/package.json b/package.json index dc1026f0c..851af8886 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "./projects/starters/hugo:ci", "./projects/starters/importmaps:ci", "./projects/starters/lit-library:ci", + "./projects/starters/mcp-app:ci", "./projects/starters/mpa:ci", "./projects/starters/nextjs:ci", "./projects/starters/nuxt:ci", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5f41af95..760370016 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1554,6 +1554,46 @@ importers: specifier: 'catalog:' version: 4.1.6(@opentelemetry/api@1.9.1)(@types/node@25.7.0)(@vitest/browser-playwright@4.1.6)(@vitest/coverage-istanbul@4.1.6)(jsdom@27.1.0)(vite@8.0.13(@types/node@25.7.0)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.97.3)(sugarss@5.0.1(postcss@8.5.14))(terser@5.47.1)(yaml@2.8.3)) + projects/starters/mcp-app: + dependencies: + '@modelcontextprotocol/ext-apps': + specifier: 1.1.2 + version: 1.1.2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3) + '@modelcontextprotocol/sdk': + specifier: 1.29.0 + version: 1.29.0(zod@4.4.3) + '@nvidia-elements/core': + specifier: workspace:* + version: link:../../core + '@nvidia-elements/styles': + specifier: workspace:* + version: link:../../styles + '@nvidia-elements/themes': + specifier: workspace:* + version: link:../../themes + zod: + specifier: 'catalog:' + version: 4.4.3 + devDependencies: + '@nvidia-elements/lint': + specifier: workspace:* + version: link:../../lint + '@types/node': + specifier: 'catalog:' + version: 25.6.2 + eslint: + specifier: 10.3.0 + version: 10.3.0(jiti@2.6.1) + typescript: + specifier: 'catalog:' + version: 6.0.3 + vite: + specifier: 'catalog:' + version: 8.0.13(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.97.3)(sugarss@5.0.1(postcss@8.5.14))(terser@5.47.1)(yaml@2.8.3) + vite-plugin-singlefile: + specifier: 2.3.0 + version: 2.3.0(rollup@4.60.2)(vite@8.0.13(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.97.3)(sugarss@5.0.1(postcss@8.5.14))(terser@5.47.1)(yaml@2.8.3)) + projects/starters/mpa: dependencies: '@nvidia-elements/core': @@ -3841,6 +3881,19 @@ packages: engines: {node: '>=18'} hasBin: true + '@modelcontextprotocol/ext-apps@1.1.2': + resolution: {integrity: sha512-Gx4TEo3/F8yq1Ix6LdgLwMrKqfZqD7++eakZdbMUewrYtHeeJn3nKpeNhgEfO7nYRwonqWYomOAszWZWJS0IbA==} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.24.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@modelcontextprotocol/sdk@1.26.0': resolution: {integrity: sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==} engines: {node: '>=18'} @@ -6189,9 +6242,6 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - '@types/node@25.6.0': - resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} - '@types/node@25.6.2': resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==} @@ -13248,6 +13298,13 @@ packages: '@nuxt/kit': optional: true + vite-plugin-singlefile@2.3.0: + resolution: {integrity: sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A==} + engines: {node: '>18.0.0'} + peerDependencies: + rollup: ^4.44.1 + vite: ^5.4.11 || ^6.0.0 || ^7.0.0 + vite-plugin-solid@2.11.12: resolution: {integrity: sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==} peerDependencies: @@ -15784,6 +15841,31 @@ snapshots: - encoding - supports-color + '@modelcontextprotocol/ext-apps@1.1.2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(zod@4.4.3)': + dependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + zod: 4.4.3 + optionalDependencies: + '@oven/bun-darwin-aarch64': 1.3.13 + '@oven/bun-darwin-x64': 1.3.13 + '@oven/bun-darwin-x64-baseline': 1.3.13 + '@oven/bun-linux-aarch64': 1.3.13 + '@oven/bun-linux-aarch64-musl': 1.3.13 + '@oven/bun-linux-x64': 1.3.13 + '@oven/bun-linux-x64-baseline': 1.3.13 + '@oven/bun-linux-x64-musl': 1.3.13 + '@oven/bun-linux-x64-musl-baseline': 1.3.13 + '@oven/bun-windows-x64': 1.3.13 + '@oven/bun-windows-x64-baseline': 1.3.13 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + '@modelcontextprotocol/sdk@1.26.0(zod@4.3.6)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.14) @@ -17944,11 +18026,11 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.7.0 '@types/conventional-commits-parser@5.0.2': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.7.0 '@types/css-tree@2.3.11': {} @@ -17978,11 +18060,7 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 25.6.0 - - '@types/node@25.6.0': - dependencies: - undici-types: 7.19.2 + '@types/node': 25.7.0 '@types/node@25.6.2': dependencies: @@ -17996,7 +18074,7 @@ snapshots: '@types/parse5@2.2.34': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.7.0 '@types/pg-pool@2.0.6': dependencies: @@ -18004,7 +18082,7 @@ snapshots: '@types/pg@8.6.1': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.7.0 pg-protocol: 1.13.0 pg-types: 2.2.0 @@ -18022,7 +18100,7 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.7.0 '@types/trusted-types@2.0.7': {} @@ -18036,7 +18114,7 @@ snapshots: '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.6.0 + '@types/node': 25.7.0 optional: true '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.3.0(jiti@2.6.1))(typescript@6.0.3)': @@ -19606,7 +19684,7 @@ snapshots: chrome-launcher@1.2.1: dependencies: - '@types/node': 25.6.0 + '@types/node': 25.7.0 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 lighthouse-logger: 2.0.2 @@ -25231,7 +25309,7 @@ snapshots: speedline-core@1.4.3: dependencies: - '@types/node': 25.6.0 + '@types/node': 25.7.0 image-ssim: 0.2.0 jpeg-js: 0.4.4 @@ -26243,6 +26321,12 @@ snapshots: transitivePeerDependencies: - supports-color + vite-plugin-singlefile@2.3.0(rollup@4.60.2)(vite@8.0.13(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.97.3)(sugarss@5.0.1(postcss@8.5.14))(terser@5.47.1)(yaml@2.8.3)): + dependencies: + micromatch: 4.0.8 + rollup: 4.60.2 + vite: 8.0.13(@types/node@25.6.2)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.97.3)(sugarss@5.0.1(postcss@8.5.14))(terser@5.47.1)(yaml@2.8.3) + vite-plugin-solid@2.11.12(solid-js@1.9.12)(vite@8.0.13(@types/node@25.7.0)(esbuild@0.28.0)(jiti@2.6.1)(sass@1.97.3)(sugarss@5.0.1(postcss@8.5.14))(terser@5.47.1)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 337d09d9f..87ac4784f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,6 +9,7 @@ packages: - projects/starters/hugo - projects/starters/importmaps - projects/starters/lit-library + - projects/starters/mcp-app - projects/starters/mpa - projects/starters/nextjs - projects/starters/nuxt @@ -127,6 +128,7 @@ minimumReleaseAgeExclude: - vite allowBuilds: + '@modelcontextprotocol/ext-apps': true '@bundled-es-modules/glob': true '@parcel/watcher': true bun: true diff --git a/projects/internals/tools/src/project/starters.test.ts b/projects/internals/tools/src/project/starters.test.ts index b73f8414e..ccca344d3 100644 --- a/projects/internals/tools/src/project/starters.test.ts +++ b/projects/internals/tools/src/project/starters.test.ts @@ -50,6 +50,7 @@ describe('startersData', () => { expect(startersData.bundles.cli).toBe(true); expect(startersData.importmaps.cli).toBe(false); expect(startersData.lit.cli).toBe(false); + expect(startersData['mcp-app'].cli).toBe(true); expect(startersData.extensions.cli).toBe(false); expect(startersData.preact.cli).toBe(false); }); @@ -65,6 +66,7 @@ describe('startersData', () => { expect(startersData.eleventy.zip).toContain('eleventy.zip'); expect(startersData.importmaps.zip).toContain('importmaps.zip'); expect(startersData.bundles.zip).toContain('bundles.zip'); + expect(startersData['mcp-app'].zip).toContain('mcp-app.zip'); expect(startersData.extensions.zip).toContain('scoped-registry.zip'); expect(startersData.hugo.zip).toContain('hugo.zip'); }); diff --git a/projects/internals/tools/src/project/starters.ts b/projects/internals/tools/src/project/starters.ts index 612f747fa..76ac89a5a 100644 --- a/projects/internals/tools/src/project/starters.ts +++ b/projects/internals/tools/src/project/starters.ts @@ -28,6 +28,7 @@ export type Starter = | 'importmaps' | 'lit-library' | 'lit' + | 'mcp-app' | 'nextjs' | 'nuxt' | 'preact' @@ -76,6 +77,10 @@ export const startersData = { zip: null, cli: false }, + 'mcp-app': { + zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/mcp-app.zip`, + cli: true + }, nextjs: { zip: `${ELEMENTS_PAGES_BASE_URL}/starters/download/nextjs.zip`, cli: true @@ -135,7 +140,7 @@ async function zipProject(outDir: string) { /* istanbul ignore next -- @preserve */ function copyProject(projectDir: string) { - const ignoreDirs = new Set(['dist', 'node_modules', '.wireit']); + const ignoreDirs = new Set(['dist', 'node_modules', '.wireit', '.eslintcache']); cpSync(projectDir, join('dist', projectDir), { recursive: true, filter: src => !ignoreDirs.has(basename(src)) diff --git a/projects/pages/index.js b/projects/pages/index.js index c933996e1..2149aefdb 100644 --- a/projects/pages/index.js +++ b/projects/pages/index.js @@ -33,6 +33,7 @@ cpSync('../starters/angular/dist/angular-starter/browser/', './dist/starters/ang cpSync('../starters/bundles/dist/', './dist/starters/bundles/', { recursive: true }); cpSync('../starters/importmaps/dist/', './dist/starters/importmaps/', { recursive: true }); cpSync('../starters/eleventy/dist/', './dist/starters/eleventy/', { recursive: true }); +cpSync('../starters/mcp-app/dist/', './dist/starters/mcp-app/', { recursive: true }); cpSync('../starters/mpa/dist/', './dist/starters/mpa/', { recursive: true }); cpSync('../starters/nextjs/dist/', './dist/starters/nextjs/', { recursive: true }); cpSync('../starters/react/dist/', './dist/starters/react/', { recursive: true }); diff --git a/projects/site/src/_11ty/layouts/common.js b/projects/site/src/_11ty/layouts/common.js index e14d6dc01..eecfd33fc 100644 --- a/projects/site/src/_11ty/layouts/common.js +++ b/projects/site/src/_11ty/layouts/common.js @@ -146,8 +146,9 @@ export const renderDocsNav = data => /* html */ ` Integrations Installation - MCP CLI + MCP + MCP Apps Lint Angular Bundles diff --git a/projects/site/src/docs/integrations/mcp-apps.md b/projects/site/src/docs/integrations/mcp-apps.md new file mode 100644 index 000000000..16b8f7f4e --- /dev/null +++ b/projects/site/src/docs/integrations/mcp-apps.md @@ -0,0 +1,105 @@ +--- +{ + title: 'MCP Apps', + description: 'Use NVIDIA Elements in MCP Apps and other iframe-based MCP UI hosts with standard Web Components.', + layout: 'docs.11ty.js' +} +--- + +# {{ title }} + +{% integration 'mcp-app' %} + +Elements components are standard [Web Components](docs/integrations/custom-elements/). They run anywhere the host can render HTML and load JavaScript modules. This includes browser apps, framework apps, static HTML pages, and iframe-based MCP UI surfaces. + +[MCP Apps](https://apps.extensions.modelcontextprotocol.io/api/) render tool UI inside an isolated iframe. The host controls the container, fetches a `ui://` HTML resource from the MCP server, and passes tool input and results to the view through the MCP Apps message channel. Because Elements registers native custom elements such as `nve-page`, `nve-alert`, and `nve-button`, the app view does not need a React, Vue, or Svelte adapter to render. + +{% installation 'mcp-app' %} + +## Minimal App Shape + +An MCP App using Elements has three pieces: + +1. An MCP tool that declares `_meta.ui.resourceUri`. +2. A matching `ui://` HTML resource served with `text/html;profile=mcp-app`. +3. A browser view that imports Elements, connects to the host, and renders tool data. + +```typescript +import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; + +const server = new McpServer({ name: 'elements-mcp-app', version: '0.0.0' }); +const resourceUri = 'ui://hello/mcp-app.html'; + +registerAppTool( + server, + 'hello', + { + title: 'Hello', + description: 'Show an Elements MCP App.', + inputSchema: {}, + _meta: { ui: { resourceUri } } + }, + async () => ({ + content: [{ type: 'text', text: 'Hello from Elements.' }], + structuredContent: { greeting: 'Hello from Elements.' } + }) +); + +registerAppResource(server, 'Elements MCP App', resourceUri, { mimeType: RESOURCE_MIME_TYPE }, async () => ({ + contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: appHtml }] +})); +``` + +The HTML resource can use Elements exactly like any other web page: + +```html + + + + + NV +

MCP App

+
+
+

Hello from Elements

+ Waiting for tool result. +
+
+ + + +``` + +Register only the Elements used by the app: + +```typescript +import '@nvidia-elements/core/alert/define.js'; +import '@nvidia-elements/core/logo/define.js'; +import '@nvidia-elements/core/page/define.js'; +import '@nvidia-elements/core/page-header/define.js'; +import { App, applyDocumentTheme } from '@modelcontextprotocol/ext-apps'; + +const app = new App({ name: 'elements-mcp-app', version: '0.0.0' }, {}); +const greeting = document.querySelector('#greeting'); + +app.ontoolresult = result => { + const text = result.structuredContent?.greeting; + if (typeof text === 'string' && greeting) greeting.textContent = text; +}; + +app.onhostcontextchanged = context => { + if (context.theme) { + applyDocumentTheme(context.theme); + document.documentElement.setAttribute('nve-theme', context.theme); + } +}; + +await app.connect(); +``` + +## Layout And Sizing + +MCP hosts own the iframe. The app should treat host dimensions as constraints, not as a canvas it can force to a fixed width. + +Read `app.getHostContext().containerDimensions` after connecting. If the host provides a fixed `width`, let the app fill it. If it provides `maxWidth`, keep the layout responsive up to that maximum. The MCP Apps SDK sends size-change notifications by default, so the host can resize flexible containers as the app content changes. diff --git a/projects/site/src/index.11tydata.js b/projects/site/src/index.11tydata.js index 25822464f..3cf67ada0 100644 --- a/projects/site/src/index.11tydata.js +++ b/projects/site/src/index.11tydata.js @@ -105,6 +105,14 @@ const integrations = { type: 'lit' }) }, + 'mcp-app': { + logo: 'javascript', + starterDemo: `${ELEMENTS_PAGES_BASE_URL}/starters/mcp-app/`, + starterDownload: `${ELEMENTS_PAGES_BASE_URL}/starters/download/mcp-app.zip`, + starterSource: `${ELEMENTS_REPO_BASE_URL}/-/tree/main/projects/starters/mcp-app`, + documentation: 'https://apps.extensions.modelcontextprotocol.io/api/', + playgroundURL: null + }, nextjs: { logo: 'nextjs', starterDemo: null, diff --git a/projects/site/src/starters/index.11ty.js b/projects/site/src/starters/index.11ty.js index d28934ac9..75476cf2a 100644 --- a/projects/site/src/starters/index.11ty.js +++ b/projects/site/src/starters/index.11ty.js @@ -42,6 +42,17 @@ export function render(data) { + + +
+ MCP +
+

MCP App

+

Starter for a minimal MCP App using Elements.

+
+
+
+
diff --git a/projects/starters/README.md b/projects/starters/README.md index b9a7120d4..36abfbae6 100644 --- a/projects/starters/README.md +++ b/projects/starters/README.md @@ -7,6 +7,7 @@ This directory contains a suite of standardized starter apps for kickstarting an - `/bundles` - Minimal tooling example leveraging pre-built JS/CSS bundles. - `/extensions` - Build setup for highly reusable and resilient Web Components. - `/go` - Minimal Go web server leveraging Elements pre-built bundles. +- `/mcp-app` - Minimal MCP App using Elements, TypeScript, and Vite. - `/mpa` - Multi Page App setup for when you just need something simple. - `/nextjs` - Experimental NextJS demo. - `/react` - React app built with [Create React App](https://create-react-app.dev/) diff --git a/projects/starters/mcp-app/.gitignore b/projects/starters/mcp-app/.gitignore new file mode 100644 index 000000000..6dec33a98 --- /dev/null +++ b/projects/starters/mcp-app/.gitignore @@ -0,0 +1,3 @@ +dist +node_modules +.eslintcache diff --git a/projects/starters/mcp-app/AGENTS.md b/projects/starters/mcp-app/AGENTS.md new file mode 100644 index 000000000..e0535a056 --- /dev/null +++ b/projects/starters/mcp-app/AGENTS.md @@ -0,0 +1,23 @@ +# Elements MCP App Starter + +This file only covers how this starter wires Elements into an MCP App. For component APIs, template validation, and project setup commands, use the Elements CLI/MCP documentation instead. + +## Integration Points + +- Register Elements in `src/mcp-app.ts`. +- Keep global Elements CSS in `src/mcp-app.css`. +- Keep the MCP App HTML entry in `mcp-app.html`. +- Keep `server.ts` as the single place that registers MCP tools and app resources. +- Keep `main.ts` limited to stdio transport startup. + +## MCP App Usage + +- Register one MCP tool with `_meta.ui.resourceUri`. +- Register a matching `ui://` app resource with `registerAppResource`. +- Keep the app resource bundled as one HTML file with `vite-plugin-singlefile`. +- Use `App` from `@modelcontextprotocol/ext-apps` in browser code. + +## Verification + +- Run `pnpm run build` in `projects/starters/mcp-app` after HTML, TypeScript, CSS, or Vite changes. +- Run `pnpm run lint` after TypeScript edits. diff --git a/projects/starters/mcp-app/README.md b/projects/starters/mcp-app/README.md new file mode 100644 index 000000000..ac6373fc6 --- /dev/null +++ b/projects/starters/mcp-app/README.md @@ -0,0 +1,33 @@ +# NVIDIA Elements MCP App Starter + +A minimal MCP App using NVIDIA Elements, TypeScript, and Vite. + +## Getting Started + +```shell +npm i +npm run dev +``` + +## MCP Client Configuration + +After building the app, configure a local MCP client with the compiled stdio entrypoint: + +```json +{ + "mcpServers": { + "elements-mcp-app": { + "command": "node", + "args": ["/path/to/mcp-app/dist/main.js"] + } + } +} +``` + +## Tasks + +| Command | Description | +| --------------- | ------------------------------------------------ | +| `npm run build` | Build the single-file app resource and server JS | +| `npm run dev` | Build, then open the MCP Inspector | +| `npm run lint` | Run eslint | diff --git a/projects/starters/mcp-app/eslint.config.js b/projects/starters/mcp-app/eslint.config.js new file mode 100644 index 000000000..49b16f92e --- /dev/null +++ b/projects/starters/mcp-app/eslint.config.js @@ -0,0 +1,4 @@ +import { elementsRecommended } from '@nvidia-elements/lint/eslint'; + +/** @type {import('eslint').Linter.Config[]} */ +export default [...elementsRecommended]; diff --git a/projects/starters/mcp-app/main.ts b/projects/starters/mcp-app/main.ts new file mode 100644 index 000000000..bc3253e1e --- /dev/null +++ b/projects/starters/mcp-app/main.ts @@ -0,0 +1,7 @@ +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; +import { createServer } from './server.js'; + +const server = createServer(); +const transport = new StdioServerTransport(); + +await server.connect(transport); diff --git a/projects/starters/mcp-app/mcp-app.html b/projects/starters/mcp-app/mcp-app.html new file mode 100644 index 000000000..03d5b813a --- /dev/null +++ b/projects/starters/mcp-app/mcp-app.html @@ -0,0 +1,25 @@ + + + + NVIDIA Elements MCP App + + + + + + + + + NV +

MCP App

+
+
+

Hello from Elements

+

This view is rendered from an MCP tool resource.

+ Waiting for the first tool result. + Refresh greeting +
+
+ + + diff --git a/projects/starters/mcp-app/package.json b/projects/starters/mcp-app/package.json new file mode 100644 index 000000000..31cbb4db2 --- /dev/null +++ b/projects/starters/mcp-app/package.json @@ -0,0 +1,90 @@ +{ + "name": "mcp-app-starter", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "ci": "wireit", + "dev": "npm run build && npx @modelcontextprotocol/inspector@0.21.2 node ./dist/main.js", + "build": "wireit", + "lint": "wireit", + "lint:fix": "wireit" + }, + "dependencies": { + "@modelcontextprotocol/ext-apps": "1.1.2", + "@modelcontextprotocol/sdk": "1.29.0", + "@nvidia-elements/core": "workspace:*", + "@nvidia-elements/styles": "workspace:*", + "@nvidia-elements/themes": "workspace:*", + "zod": "catalog:" + }, + "devDependencies": { + "@nvidia-elements/lint": "workspace:*", + "@types/node": "catalog:", + "eslint": "catalog:", + "typescript": "catalog:", + "vite": "catalog:", + "vite-plugin-singlefile": "2.3.0" + }, + "wireit": { + "ci": { + "dependencies": [ + "build", + "lint" + ] + }, + "build": { + "command": "tsc --noEmit && vite build && tsc -p tsconfig.server.json", + "files": [ + "../../core/dist/**/*.js", + "../../styles/dist/**/*.css", + "../../themes/dist/**/*.css", + "src/**", + "mcp-app.html", + "main.ts", + "server.ts", + "package.json", + "tsconfig.json", + "tsconfig.server.json", + "vite.config.ts" + ], + "output": [ + "dist/**" + ], + "dependencies": [ + { + "script": "../../core:build", + "cascade": false + }, + { + "script": "../../styles:build", + "cascade": false + }, + { + "script": "../../themes:build", + "cascade": false + } + ] + }, + "lint": { + "command": "eslint -c ./eslint.config.js --color --cache --cache-location .eslintcache/", + "files": [ + "src/**", + "main.ts", + "server.ts", + "vite.config.ts", + "eslint.config.js" + ], + "output": [], + "dependencies": [ + "../../lint:build" + ] + }, + "lint:fix": { + "command": "eslint -c ./eslint.config.js --fix", + "dependencies": [ + "../../lint:build" + ] + } + } +} diff --git a/projects/starters/mcp-app/server.ts b/projects/starters/mcp-app/server.ts new file mode 100644 index 000000000..0891e27fe --- /dev/null +++ b/projects/starters/mcp-app/server.ts @@ -0,0 +1,65 @@ +import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { CallToolResult, ReadResourceResult } from '@modelcontextprotocol/sdk/types.js'; +import { readFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { z } from 'zod'; + +const APP_RESOURCE_URI = 'ui://hello/mcp-app.html'; +const APP_HTML_PATH = join(dirname(fileURLToPath(import.meta.url)), 'mcp-app.html'); +const GREETING_PREFIX = 'Hello from an Elements MCP App.'; + +let callCount = 0; + +export function createServer(): McpServer { + const server = new McpServer({ + name: 'elements-mcp-app', + version: '0.0.0' + }); + + registerAppTool( + server, + 'hello', + { + title: 'Hello', + description: 'Show a minimal Elements MCP App with an optional custom greeting.', + inputSchema: { + greeting: z.string().trim().optional().describe('Greeting text to display in the app.') + }, + _meta: { ui: { resourceUri: APP_RESOURCE_URI } } + }, + async ({ greeting }): Promise => { + const greetingText = createGreeting(greeting); + return { + content: [{ type: 'text', text: greetingText }], + structuredContent: { greeting: greetingText } + }; + } + ); + + registerAppResource( + server, + 'Elements MCP App', + APP_RESOURCE_URI, + { description: 'Minimal Elements MCP App UI.', mimeType: RESOURCE_MIME_TYPE }, + async (): Promise => ({ + contents: [ + { + uri: APP_RESOURCE_URI, + mimeType: RESOURCE_MIME_TYPE, + text: await readFile(APP_HTML_PATH, 'utf8') + } + ] + }) + ); + + return server; +} + +function createGreeting(greeting?: string) { + callCount += 1; + if (greeting) return greeting; + + return `${GREETING_PREFIX} Server call ${callCount} at ${new Date().toLocaleTimeString()}.`; +} diff --git a/projects/starters/mcp-app/src/mcp-app.css b/projects/starters/mcp-app/src/mcp-app.css new file mode 100644 index 000000000..cea316d92 --- /dev/null +++ b/projects/starters/mcp-app/src/mcp-app.css @@ -0,0 +1,13 @@ +@import '@nvidia-elements/themes/fonts/inter.css'; +@import '@nvidia-elements/themes/index.css'; +@import '@nvidia-elements/themes/dark.css'; +@import '@nvidia-elements/styles/layout.css'; +@import '@nvidia-elements/styles/typography.css'; +@import '@nvidia-elements/styles/view-transitions.css'; + +html, +body { + margin: 0; + min-block-size: 100%; + width: 100vw; +} diff --git a/projects/starters/mcp-app/src/mcp-app.ts b/projects/starters/mcp-app/src/mcp-app.ts new file mode 100644 index 000000000..018c35024 --- /dev/null +++ b/projects/starters/mcp-app/src/mcp-app.ts @@ -0,0 +1,85 @@ +import '@nvidia-elements/core/alert/define.js'; +import '@nvidia-elements/core/button/define.js'; +import '@nvidia-elements/core/logo/define.js'; +import '@nvidia-elements/core/page/define.js'; +import '@nvidia-elements/core/page-header/define.js'; +import { App, applyDocumentTheme, type McpUiHostContext } from '@modelcontextprotocol/ext-apps'; +import './mcp-app.css'; + +interface HelloToolResult { + content?: { + text?: unknown; + type?: string; + }[]; + isError?: boolean; + structuredContent?: { + greeting?: unknown; + }; +} + +const FALLBACK_GREETING = 'Hello from an Elements MCP App.'; +const ERROR_GREETING = 'The MCP tool returned an error.'; + +const app = new App( + { + name: 'elements-mcp-app', + version: '0.0.0' + }, + {} +); + +const greetingElement = document.querySelector('#greeting'); +const refreshButton = document.querySelector('#refresh-greeting'); + +app.ontoolresult = result => setGreeting(getGreeting(result)); +app.onhostcontextchanged = context => applyHostContext(context); + +refreshButton?.addEventListener('click', () => { + void refreshGreeting(); +}); + +void connectApp(); + +async function connectApp() { + try { + await app.connect(); + applyHostContext(app.getHostContext()); + } catch { + setGreeting('Unable to connect to the MCP host.'); + } +} + +async function refreshGreeting() { + refreshButton?.setAttribute('disabled', ''); + + try { + const result = await app.callServerTool({ name: 'hello', arguments: {} }); + setGreeting(getGreeting(result)); + } catch { + setGreeting('Unable to refresh the greeting.'); + } finally { + refreshButton?.removeAttribute('disabled'); + } +} + +function getGreeting(result: HelloToolResult) { + if (result.isError) return ERROR_GREETING; + + const structuredGreeting = result.structuredContent?.greeting; + if (typeof structuredGreeting === 'string') return structuredGreeting; + + const textContent = result.content?.find(item => item.type === 'text' && typeof item.text === 'string')?.text; + return typeof textContent === 'string' ? textContent : FALLBACK_GREETING; +} + +function setGreeting(greeting: string) { + if (!greetingElement) return; + greetingElement.textContent = greeting; +} + +function applyHostContext(context: McpUiHostContext | null | undefined) { + if (context?.theme) { + applyDocumentTheme(context.theme); + document.documentElement.setAttribute('nve-theme', context.theme); + } +} diff --git a/projects/starters/mcp-app/src/vite-env.d.ts b/projects/starters/mcp-app/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/projects/starters/mcp-app/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/projects/starters/mcp-app/tsconfig.json b/projects/starters/mcp-app/tsconfig.json new file mode 100644 index 000000000..a7e714426 --- /dev/null +++ b/projects/starters/mcp-app/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "noUncheckedSideEffectImports": true, + "target": "ESNext", + "experimentalDecorators": true, + "useDefineForClassFields": false, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true + }, + "include": ["src"] +} diff --git a/projects/starters/mcp-app/tsconfig.server.json b/projects/starters/mcp-app/tsconfig.server.json new file mode 100644 index 000000000..8a9610f17 --- /dev/null +++ b/projects/starters/mcp-app/tsconfig.server.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ESNext", "DOM"], + "types": ["node"], + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "outDir": "dist", + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true + }, + "include": ["main.ts", "server.ts"] +} diff --git a/projects/starters/mcp-app/vite.config.ts b/projects/starters/mcp-app/vite.config.ts new file mode 100644 index 000000000..4f15f2510 --- /dev/null +++ b/projects/starters/mcp-app/vite.config.ts @@ -0,0 +1,17 @@ +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import { viteSingleFile } from 'vite-plugin-singlefile'; + +export default defineConfig(() => { + return { + base: './', + plugins: [viteSingleFile()], + build: { + outDir: 'dist', + emptyOutDir: true, + rollupOptions: { + input: resolve(import.meta.dirname, 'mcp-app.html') + } + } + }; +}); diff --git a/projects/starters/package.json b/projects/starters/package.json index 80ab068ec..4edb8d95f 100644 --- a/projects/starters/package.json +++ b/projects/starters/package.json @@ -26,6 +26,7 @@ "./eleventy/dist/**/*.js", "./eleventy-ssr/dist/**/*.js", "./lit-library/dist/**/*.js", + "./mcp-app/dist/**", "./mpa/dist/**/*.js", "./nextjs/dist/**/*.js", "./nuxt/dist/**/*.js", @@ -72,6 +73,10 @@ "script": "./lit-library:build", "cascade": false }, + { + "script": "./mcp-app:build", + "cascade": false + }, { "script": "./mpa:build", "cascade": false