Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +11 -35
- .github/workflows/ci.yml +154 -0
- .github/workflows/docs.yml +28 -0
- .github/workflows/npm-publish.yml +139 -0
- .github/workflows/publish.yml +41 -0
- .gitignore +13 -0
- .husky/pre-commit +12 -0
- .prettierignore +6 -0
- .prettierrc.json +7 -0
- AGENTS.md +91 -0
- CLAUDE.md +91 -0
- CONTRIBUTING.md +184 -0
- LICENSE +216 -0
- README.md +96 -0
- RELEASES.md +64 -0
- build.bun.ts +54 -0
- docs/index.html +12 -0
- docs/migrate_from_openai_apps.md +351 -0
- docs/quickstart.md +292 -0
- examples/basic-host/.gitignore +2 -0
- examples/basic-host/README.md +43 -0
- examples/basic-host/index.html +14 -0
- examples/basic-host/package.json +35 -0
- examples/basic-host/sandbox.html +35 -0
- examples/basic-host/serve.ts +148 -0
- examples/basic-host/src/global.css +12 -0
- examples/basic-host/src/implementation.ts +312 -0
- examples/basic-host/src/index.module.css +255 -0
- examples/basic-host/src/index.tsx +530 -0
- examples/basic-host/src/sandbox.ts +137 -0
- examples/basic-host/src/vite-env.d.ts +1 -0
- examples/basic-host/tsconfig.json +19 -0
- examples/basic-host/vite.config.ts +24 -0
- examples/basic-server-preact/.gitignore +2 -0
- examples/basic-server-preact/README.md +34 -0
- examples/basic-server-preact/mcp-app.html +14 -0
- examples/basic-server-preact/package.json +45 -0
- examples/basic-server-preact/server-utils.ts +72 -0
- examples/basic-server-preact/server.ts +71 -0
- examples/basic-server-preact/src/global.css +12 -0
- examples/basic-server-preact/src/mcp-app.module.css +65 -0
- examples/basic-server-preact/src/mcp-app.tsx +171 -0
- examples/basic-server-preact/src/vite-env.d.ts +1 -0
- examples/basic-server-preact/tsconfig.json +21 -0
- examples/basic-server-preact/vite.config.ts +25 -0
- examples/basic-server-react/.gitignore +2 -0
- examples/basic-server-react/README.md +36 -0
- examples/basic-server-react/grid-cell.png +0 -0
- examples/basic-server-react/mcp-app.html +14 -0
- examples/basic-server-react/package.json +48 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,11 @@
|
|
| 1 |
-
|
| 2 |
-
*
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
*
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
# Force LF line endings for all text files (for consistent prettier formatting)
|
| 2 |
+
* text=auto eol=lf
|
| 3 |
+
|
| 4 |
+
package-lock.json linguist-generated=true
|
| 5 |
+
src/generated/** linguist-generated=true
|
| 6 |
+
examples/map-server/grid-cell.png filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
examples/map-server/screenshot.png filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
examples/shadertoy-server/screenshot.png filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
examples/video-resource-server/grid-cell.png filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
examples/video-resource-server/screenshot.png filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
tests/e2e/servers.spec.ts-snapshots/map-server.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.github/workflows/ci.yml
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: CI
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches: [main]
|
| 6 |
+
pull_request:
|
| 7 |
+
branches: [main]
|
| 8 |
+
|
| 9 |
+
permissions:
|
| 10 |
+
contents: read
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
build:
|
| 14 |
+
strategy:
|
| 15 |
+
fail-fast: false
|
| 16 |
+
matrix:
|
| 17 |
+
include:
|
| 18 |
+
- os: ubuntu-latest
|
| 19 |
+
name: Linux x64
|
| 20 |
+
- os: ubuntu-24.04-arm
|
| 21 |
+
name: Linux ARM64
|
| 22 |
+
- os: windows-latest
|
| 23 |
+
name: Windows x64
|
| 24 |
+
- os: windows-11-arm
|
| 25 |
+
name: Windows ARM64
|
| 26 |
+
- os: macos-latest
|
| 27 |
+
name: macOS ARM64
|
| 28 |
+
|
| 29 |
+
name: Build (${{ matrix.name }})
|
| 30 |
+
runs-on: ${{ matrix.os }}
|
| 31 |
+
|
| 32 |
+
steps:
|
| 33 |
+
- uses: actions/checkout@v4
|
| 34 |
+
|
| 35 |
+
- name: Verify no private URLs in package-lock.json
|
| 36 |
+
shell: bash
|
| 37 |
+
run: '! grep -E "\"resolved\": \"https?://" package-lock.json | grep -v registry.npmjs.org'
|
| 38 |
+
|
| 39 |
+
- name: Verify example dependency versions
|
| 40 |
+
shell: bash
|
| 41 |
+
run: node scripts/check-versions.mjs
|
| 42 |
+
|
| 43 |
+
- uses: actions/setup-node@v4
|
| 44 |
+
with:
|
| 45 |
+
node-version: "20"
|
| 46 |
+
|
| 47 |
+
- run: npm install
|
| 48 |
+
|
| 49 |
+
- run: npm run build
|
| 50 |
+
|
| 51 |
+
- run: npm run examples:build
|
| 52 |
+
|
| 53 |
+
- name: Verify generated schemas are up-to-date
|
| 54 |
+
shell: bash
|
| 55 |
+
run: |
|
| 56 |
+
npm run generate:schemas
|
| 57 |
+
git diff --exit-code src/generated/ || (echo "Generated schemas are out of date. Run 'npm run generate:schemas' and commit." && exit 1)
|
| 58 |
+
|
| 59 |
+
- run: npm test
|
| 60 |
+
|
| 61 |
+
- run: npm run prettier
|
| 62 |
+
|
| 63 |
+
e2e:
|
| 64 |
+
runs-on: ubuntu-latest
|
| 65 |
+
steps:
|
| 66 |
+
- uses: actions/checkout@v4
|
| 67 |
+
|
| 68 |
+
- uses: oven-sh/setup-bun@v2
|
| 69 |
+
with:
|
| 70 |
+
bun-version: latest
|
| 71 |
+
|
| 72 |
+
- uses: actions/setup-node@v4
|
| 73 |
+
with:
|
| 74 |
+
node-version: "20"
|
| 75 |
+
|
| 76 |
+
- run: npm ci
|
| 77 |
+
|
| 78 |
+
- name: Install Playwright browsers
|
| 79 |
+
run: npx playwright install --with-deps chromium
|
| 80 |
+
|
| 81 |
+
- name: Run E2E tests
|
| 82 |
+
run: npx playwright test --reporter=list
|
| 83 |
+
|
| 84 |
+
- name: Upload test results
|
| 85 |
+
uses: actions/upload-artifact@v4
|
| 86 |
+
if: failure()
|
| 87 |
+
with:
|
| 88 |
+
name: test-results
|
| 89 |
+
path: test-results/
|
| 90 |
+
retention-days: 7
|
| 91 |
+
|
| 92 |
+
# Test build in Windows WSL (Ubuntu)
|
| 93 |
+
build-wsl:
|
| 94 |
+
name: Build (Windows WSL)
|
| 95 |
+
runs-on: windows-latest
|
| 96 |
+
|
| 97 |
+
steps:
|
| 98 |
+
- uses: actions/checkout@v4
|
| 99 |
+
|
| 100 |
+
- uses: Vampire/setup-wsl@v5
|
| 101 |
+
with:
|
| 102 |
+
distribution: Ubuntu-24.04
|
| 103 |
+
|
| 104 |
+
- name: Install Node.js in WSL
|
| 105 |
+
shell: wsl-bash {0}
|
| 106 |
+
run: |
|
| 107 |
+
sudo apt-get update
|
| 108 |
+
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
| 109 |
+
sudo apt-get install -y nodejs
|
| 110 |
+
|
| 111 |
+
- name: Build and test in WSL
|
| 112 |
+
shell: wsl-bash {0}
|
| 113 |
+
run: |
|
| 114 |
+
npm install
|
| 115 |
+
npm run build
|
| 116 |
+
npm run examples:build
|
| 117 |
+
npm test
|
| 118 |
+
npm run prettier
|
| 119 |
+
|
| 120 |
+
# Test that the package can be installed from git (triggers prepare script)
|
| 121 |
+
test-git-install:
|
| 122 |
+
strategy:
|
| 123 |
+
fail-fast: false
|
| 124 |
+
matrix:
|
| 125 |
+
include:
|
| 126 |
+
- os: ubuntu-latest
|
| 127 |
+
name: Linux x64
|
| 128 |
+
- os: ubuntu-24.04-arm
|
| 129 |
+
name: Linux ARM64
|
| 130 |
+
- os: windows-latest
|
| 131 |
+
name: Windows x64
|
| 132 |
+
- os: windows-11-arm
|
| 133 |
+
name: Windows ARM64
|
| 134 |
+
- os: macos-latest
|
| 135 |
+
name: macOS ARM64
|
| 136 |
+
|
| 137 |
+
name: Test git install (${{ matrix.name }})
|
| 138 |
+
runs-on: ${{ matrix.os }}
|
| 139 |
+
|
| 140 |
+
steps:
|
| 141 |
+
- uses: actions/setup-node@v4
|
| 142 |
+
with:
|
| 143 |
+
node-version: "20"
|
| 144 |
+
|
| 145 |
+
- name: Create test project and install from git
|
| 146 |
+
shell: bash
|
| 147 |
+
run: |
|
| 148 |
+
mkdir test-project
|
| 149 |
+
cd test-project
|
| 150 |
+
npm init -y
|
| 151 |
+
# Install from the PR branch (use head repo for fork PRs)
|
| 152 |
+
npm install "git+https://github.com/${{ github.event.pull_request.head.repo.full_name || github.repository }}#${{ github.head_ref || github.ref_name }}"
|
| 153 |
+
# Verify the package is usable (ESM import)
|
| 154 |
+
node --input-type=module -e "import { App } from '@modelcontextprotocol/ext-apps'; console.log('Import successful:', typeof App)"
|
.github/workflows/docs.yml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Deploy Documentation
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
workflow_dispatch:
|
| 5 |
+
release:
|
| 6 |
+
types: [published]
|
| 7 |
+
|
| 8 |
+
permissions:
|
| 9 |
+
contents: write
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
deploy:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
steps:
|
| 15 |
+
- uses: actions/checkout@v4
|
| 16 |
+
- uses: oven-sh/setup-bun@v2
|
| 17 |
+
- uses: actions/setup-node@v4
|
| 18 |
+
with:
|
| 19 |
+
node-version: "20"
|
| 20 |
+
- run: npm install
|
| 21 |
+
- run: npm run build
|
| 22 |
+
- run: npm run docs
|
| 23 |
+
- uses: peaceiris/actions-gh-pages@v4
|
| 24 |
+
with:
|
| 25 |
+
github_token: ${{ secrets.GITHUB_TOKEN }}
|
| 26 |
+
publish_dir: ./docs
|
| 27 |
+
publish_branch: gh-pages
|
| 28 |
+
keep_files: true
|
.github/workflows/npm-publish.yml
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Publish to npm
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
release:
|
| 5 |
+
types: [published]
|
| 6 |
+
|
| 7 |
+
concurrency:
|
| 8 |
+
group: ${{ github.workflow }}-${{ github.ref }}
|
| 9 |
+
cancel-in-progress: true
|
| 10 |
+
|
| 11 |
+
jobs:
|
| 12 |
+
build:
|
| 13 |
+
runs-on: ubuntu-latest
|
| 14 |
+
permissions:
|
| 15 |
+
contents: read
|
| 16 |
+
steps:
|
| 17 |
+
- uses: actions/checkout@v4
|
| 18 |
+
- uses: oven-sh/setup-bun@v2
|
| 19 |
+
with:
|
| 20 |
+
bun-version: latest
|
| 21 |
+
- uses: actions/setup-node@v4
|
| 22 |
+
with:
|
| 23 |
+
node-version: "22"
|
| 24 |
+
cache: npm
|
| 25 |
+
- run: npm ci
|
| 26 |
+
- run: npm run build
|
| 27 |
+
- run: npm run prettier
|
| 28 |
+
|
| 29 |
+
test:
|
| 30 |
+
runs-on: ubuntu-latest
|
| 31 |
+
permissions:
|
| 32 |
+
contents: read
|
| 33 |
+
steps:
|
| 34 |
+
- uses: actions/checkout@v4
|
| 35 |
+
- uses: oven-sh/setup-bun@v2
|
| 36 |
+
with:
|
| 37 |
+
bun-version: latest
|
| 38 |
+
- uses: actions/setup-node@v4
|
| 39 |
+
with:
|
| 40 |
+
node-version: "22"
|
| 41 |
+
cache: npm
|
| 42 |
+
- run: npm ci
|
| 43 |
+
- run: npm test
|
| 44 |
+
|
| 45 |
+
publish:
|
| 46 |
+
runs-on: ubuntu-latest
|
| 47 |
+
if: github.event_name == 'release'
|
| 48 |
+
environment: Release
|
| 49 |
+
needs: [build, test]
|
| 50 |
+
|
| 51 |
+
permissions:
|
| 52 |
+
contents: read
|
| 53 |
+
id-token: write
|
| 54 |
+
|
| 55 |
+
steps:
|
| 56 |
+
- uses: actions/checkout@v4
|
| 57 |
+
- uses: oven-sh/setup-bun@v2
|
| 58 |
+
with:
|
| 59 |
+
bun-version: latest
|
| 60 |
+
- uses: actions/setup-node@v4
|
| 61 |
+
with:
|
| 62 |
+
node-version: "22"
|
| 63 |
+
cache: npm
|
| 64 |
+
registry-url: "https://registry.npmjs.org"
|
| 65 |
+
- run: npm ci
|
| 66 |
+
|
| 67 |
+
- name: Determine npm tag
|
| 68 |
+
id: npm-tag
|
| 69 |
+
run: |
|
| 70 |
+
VERSION=$(node -p "require('./package.json').version")
|
| 71 |
+
# Check if this is a beta release
|
| 72 |
+
if [[ "$VERSION" == *"-beta"* ]]; then
|
| 73 |
+
echo "tag=--tag beta" >> $GITHUB_OUTPUT
|
| 74 |
+
# Check if this release is from a non-main branch (patch/maintenance release)
|
| 75 |
+
elif [[ "${{ github.event.release.target_commitish }}" != "main" ]]; then
|
| 76 |
+
# Use "release-X.Y" as tag for old branch releases
|
| 77 |
+
MAJOR_MINOR=$(echo "$VERSION" | cut -d. -f1,2)
|
| 78 |
+
echo "tag=--tag release-${MAJOR_MINOR}" >> $GITHUB_OUTPUT
|
| 79 |
+
else
|
| 80 |
+
echo "tag=" >> $GITHUB_OUTPUT
|
| 81 |
+
fi
|
| 82 |
+
|
| 83 |
+
- run: npm publish --provenance --access public ${{ steps.npm-tag.outputs.tag }}
|
| 84 |
+
env:
|
| 85 |
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }}
|
| 86 |
+
|
| 87 |
+
publish-examples:
|
| 88 |
+
runs-on: ubuntu-latest
|
| 89 |
+
if: github.event_name == 'release'
|
| 90 |
+
environment: Release
|
| 91 |
+
needs: [publish]
|
| 92 |
+
|
| 93 |
+
permissions:
|
| 94 |
+
contents: read
|
| 95 |
+
id-token: write
|
| 96 |
+
|
| 97 |
+
strategy:
|
| 98 |
+
fail-fast: false
|
| 99 |
+
matrix:
|
| 100 |
+
example:
|
| 101 |
+
- basic-server-preact
|
| 102 |
+
- basic-server-react
|
| 103 |
+
- basic-server-solid
|
| 104 |
+
- basic-server-svelte
|
| 105 |
+
- basic-server-vanillajs
|
| 106 |
+
- basic-server-vue
|
| 107 |
+
- budget-allocator-server
|
| 108 |
+
- cohort-heatmap-server
|
| 109 |
+
- customer-segmentation-server
|
| 110 |
+
- map-server
|
| 111 |
+
- pdf-server
|
| 112 |
+
- scenario-modeler-server
|
| 113 |
+
- shadertoy-server
|
| 114 |
+
- sheet-music-server
|
| 115 |
+
- system-monitor-server
|
| 116 |
+
- threejs-server
|
| 117 |
+
- transcript-server
|
| 118 |
+
- video-resource-server
|
| 119 |
+
- wiki-explorer-server
|
| 120 |
+
|
| 121 |
+
steps:
|
| 122 |
+
- uses: actions/checkout@v4
|
| 123 |
+
- uses: oven-sh/setup-bun@v2
|
| 124 |
+
with:
|
| 125 |
+
bun-version: latest
|
| 126 |
+
- uses: actions/setup-node@v4
|
| 127 |
+
with:
|
| 128 |
+
node-version: "22"
|
| 129 |
+
cache: npm
|
| 130 |
+
registry-url: "https://registry.npmjs.org"
|
| 131 |
+
- run: npm ci
|
| 132 |
+
|
| 133 |
+
- name: Build example
|
| 134 |
+
run: npm run build --workspace examples/${{ matrix.example }}
|
| 135 |
+
|
| 136 |
+
- name: Publish example
|
| 137 |
+
run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public
|
| 138 |
+
env:
|
| 139 |
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }}
|
.github/workflows/publish.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Publish Any Commit
|
| 2 |
+
permissions:
|
| 3 |
+
contents: read
|
| 4 |
+
on:
|
| 5 |
+
pull_request:
|
| 6 |
+
push:
|
| 7 |
+
branches:
|
| 8 |
+
- "**"
|
| 9 |
+
tags:
|
| 10 |
+
- "!**"
|
| 11 |
+
|
| 12 |
+
jobs:
|
| 13 |
+
pkg-publish:
|
| 14 |
+
runs-on: ubuntu-latest
|
| 15 |
+
steps:
|
| 16 |
+
- uses: actions/checkout@v4
|
| 17 |
+
- uses: actions/setup-node@v4
|
| 18 |
+
with:
|
| 19 |
+
node-version: 22
|
| 20 |
+
cache: npm
|
| 21 |
+
- run: npm ci
|
| 22 |
+
- run: npm run build
|
| 23 |
+
- run: npm run examples:build
|
| 24 |
+
- run: |
|
| 25 |
+
npx pkg-pr-new publish \
|
| 26 |
+
. \
|
| 27 |
+
./examples/basic-server-react \
|
| 28 |
+
./examples/basic-server-vanillajs \
|
| 29 |
+
./examples/budget-allocator-server \
|
| 30 |
+
./examples/cohort-heatmap-server \
|
| 31 |
+
./examples/customer-segmentation-server \
|
| 32 |
+
./examples/map-server \
|
| 33 |
+
./examples/pdf-server \
|
| 34 |
+
./examples/scenario-modeler-server \
|
| 35 |
+
./examples/shadertoy-server \
|
| 36 |
+
./examples/sheet-music-server \
|
| 37 |
+
./examples/system-monitor-server \
|
| 38 |
+
./examples/threejs-server \
|
| 39 |
+
./examples/transcript-server \
|
| 40 |
+
./examples/video-resource-server \
|
| 41 |
+
./examples/wiki-explorer-server
|
.gitignore
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.DS_Store
|
| 2 |
+
dist/
|
| 3 |
+
node_modules/
|
| 4 |
+
yarn.lock
|
| 5 |
+
bun.lockb
|
| 6 |
+
.vscode/
|
| 7 |
+
docs/api/
|
| 8 |
+
tmp/
|
| 9 |
+
intermediate-findings/
|
| 10 |
+
|
| 11 |
+
# Playwright
|
| 12 |
+
playwright-report/
|
| 13 |
+
test-results/
|
.husky/pre-commit
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Verify no private registry URLs in package-lock.json
|
| 2 |
+
if grep -E '"resolved": "https?://' package-lock.json | grep -v registry.npmjs.org > /dev/null; then
|
| 3 |
+
echo "ERROR: package-lock.json contains non-npmjs.org URLs"
|
| 4 |
+
echo "Run: docker run --rm -i -v \$PWD:/src -w /src node:latest npm i --registry=https://registry.npmjs.org/"
|
| 5 |
+
exit 1
|
| 6 |
+
fi
|
| 7 |
+
|
| 8 |
+
npm run build:all
|
| 9 |
+
npm run prettier:fix
|
| 10 |
+
|
| 11 |
+
# Stage any changes to generated files (they may have been reformatted by prettier)
|
| 12 |
+
git add src/generated/
|
.prettierignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
examples/basic-host/**/*.ts
|
| 2 |
+
examples/basic-host/**/*.tsx
|
| 3 |
+
examples/basic-server-*/**/*.ts
|
| 4 |
+
examples/basic-server-*/**/*.tsx
|
| 5 |
+
**/vendor/**
|
| 6 |
+
SKILL.md
|
.prettierrc.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"semi": true,
|
| 3 |
+
"trailingComma": "all",
|
| 4 |
+
"singleQuote": false,
|
| 5 |
+
"printWidth": 80,
|
| 6 |
+
"tabWidth": 2
|
| 7 |
+
}
|
AGENTS.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MCP Apps SDK
|
| 2 |
+
|
| 3 |
+
## Project Overview
|
| 4 |
+
|
| 5 |
+
MCP Apps SDK (`@modelcontextprotocol/ext-apps`) enables MCP servers to display interactive UIs in conversational clients.
|
| 6 |
+
|
| 7 |
+
Key abstractions:
|
| 8 |
+
|
| 9 |
+
- **Guest** - UI running in an iframe, uses `App` class with `PostMessageTransport` to communicate with host
|
| 10 |
+
- **Host** - Chat client embedding the iframe, uses `AppBridge` class to proxy MCP requests
|
| 11 |
+
- **Server** - MCP server that registers tools/resources with UI metadata
|
| 12 |
+
|
| 13 |
+
Specification (draft): `specification/draft/apps.mdx`
|
| 14 |
+
|
| 15 |
+
## Commands
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
# Install dependencies
|
| 19 |
+
npm install
|
| 20 |
+
|
| 21 |
+
# Build the SDK only (generates schemas + bundles, does not build examples)
|
| 22 |
+
npm run build
|
| 23 |
+
|
| 24 |
+
# Build everything (SDK + all examples)
|
| 25 |
+
npm run build:all
|
| 26 |
+
|
| 27 |
+
# Type check + build a single example
|
| 28 |
+
npm run --workspace examples/<example-name> build
|
| 29 |
+
|
| 30 |
+
# Run all examples (starts server at http://localhost:8080)
|
| 31 |
+
npm start
|
| 32 |
+
|
| 33 |
+
# Run E2E tests (primary testing mechanism - starts examples server automatically)
|
| 34 |
+
npm run test:e2e
|
| 35 |
+
|
| 36 |
+
# Run unit tests (E2E tests have broader coverage; unit tests cover specific modules)
|
| 37 |
+
npm test
|
| 38 |
+
|
| 39 |
+
# Check JSDoc comment syntax and `{@link}` references
|
| 40 |
+
npm exec typedoc -- --treatValidationWarningsAsErrors --emit none
|
| 41 |
+
|
| 42 |
+
# Regenerate package-lock.json (especially on setups w/ custom npm registry)
|
| 43 |
+
rm -fR package-lock.json node_modules && \
|
| 44 |
+
docker run --rm -it --platform linux/amd64 -v $PWD:/src:rw -w /src node:latest npm i && \
|
| 45 |
+
rm -fR node_modules && \
|
| 46 |
+
npm i --cache=~/.npm-mcp-apps --registry=https://registry.npmjs.org/
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Architecture
|
| 50 |
+
|
| 51 |
+
### SDK Entry Points
|
| 52 |
+
|
| 53 |
+
- `@modelcontextprotocol/ext-apps` - Main SDK for Apps (`App` class, `PostMessageTransport`)
|
| 54 |
+
- `@modelcontextprotocol/ext-apps/react` - React hooks (`useApp`, `useHostStyleVariables`, etc.)
|
| 55 |
+
- `@modelcontextprotocol/ext-apps/app-bridge` - SDK for hosts (`AppBridge` class)
|
| 56 |
+
- `@modelcontextprotocol/ext-apps/server` - Server helpers (`registerAppTool`, `registerAppResource`)
|
| 57 |
+
|
| 58 |
+
### Key Source Files
|
| 59 |
+
|
| 60 |
+
- `src/app.ts` - `App` class extends MCP Protocol, handles guest initialization, tool calls, messaging
|
| 61 |
+
- `src/app-bridge.ts` - `AppBridge` class for hosts, proxies MCP requests, sends tool input/results to guests
|
| 62 |
+
- `src/server/index.ts` - Helpers for MCP servers to register tools/resources with UI metadata
|
| 63 |
+
- `src/types.ts` - Protocol types re-exported from `spec.types.ts` and Zod schemas from `generated/schema.ts` (auto-generated during build)
|
| 64 |
+
- `src/message-transport.ts` - `PostMessageTransport` for iframe communication
|
| 65 |
+
- `src/react/` - React hooks: `useApp`, `useHostStyles`, `useAutoResize`, `useDocumentTheme`
|
| 66 |
+
|
| 67 |
+
### Protocol Flow
|
| 68 |
+
|
| 69 |
+
```
|
| 70 |
+
Guest UI (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
1. Host creates iframe with Guest UI HTML
|
| 74 |
+
2. Guest UI creates `App` instance and calls `connect()` with `PostMessageTransport`
|
| 75 |
+
3. App sends `ui/initialize` request, receives host capabilities and context
|
| 76 |
+
4. Host sends `sendToolInput()` with tool arguments after initialization
|
| 77 |
+
5. Guest UI can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()`
|
| 78 |
+
6. Host sends `sendToolResult()` when tool execution completes
|
| 79 |
+
7. Host calls `teardownResource()` before unmounting iframe
|
| 80 |
+
|
| 81 |
+
## Examples
|
| 82 |
+
|
| 83 |
+
Uses npm workspaces. Examples in `examples/` are separate packages:
|
| 84 |
+
|
| 85 |
+
- `basic-server-*` - Starter templates (vanillajs, react, vue, svelte, preact, solid). Use these as the basis for new examples.
|
| 86 |
+
- `basic-host` - Reference host implementation
|
| 87 |
+
- Other examples showcase specific features (charts, 3D, video, etc.)
|
| 88 |
+
|
| 89 |
+
## Claude Code Plugin
|
| 90 |
+
|
| 91 |
+
The `plugins/mcp-apps/` directory contains a Claude Code plugin distributed via the plugin marketplace. It provides the "Create MCP App" skill (`plugins/mcp-apps/skills/create-mcp-app/SKILL.md`) that guides users through building MCP Apps with interactive UIs.
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# MCP Apps SDK
|
| 2 |
+
|
| 3 |
+
## Project Overview
|
| 4 |
+
|
| 5 |
+
MCP Apps SDK (`@modelcontextprotocol/ext-apps`) enables MCP servers to display interactive UIs in conversational clients.
|
| 6 |
+
|
| 7 |
+
Key abstractions:
|
| 8 |
+
|
| 9 |
+
- **Guest** - UI running in an iframe, uses `App` class with `PostMessageTransport` to communicate with host
|
| 10 |
+
- **Host** - Chat client embedding the iframe, uses `AppBridge` class to proxy MCP requests
|
| 11 |
+
- **Server** - MCP server that registers tools/resources with UI metadata
|
| 12 |
+
|
| 13 |
+
Specification (draft): `specification/draft/apps.mdx`
|
| 14 |
+
|
| 15 |
+
## Commands
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
# Install dependencies
|
| 19 |
+
npm install
|
| 20 |
+
|
| 21 |
+
# Build the SDK only (generates schemas + bundles, does not build examples)
|
| 22 |
+
npm run build
|
| 23 |
+
|
| 24 |
+
# Build everything (SDK + all examples)
|
| 25 |
+
npm run build:all
|
| 26 |
+
|
| 27 |
+
# Type check + build a single example
|
| 28 |
+
npm run --workspace examples/<example-name> build
|
| 29 |
+
|
| 30 |
+
# Run all examples (starts server at http://localhost:8080)
|
| 31 |
+
npm start
|
| 32 |
+
|
| 33 |
+
# Run E2E tests (primary testing mechanism - starts examples server automatically)
|
| 34 |
+
npm run test:e2e
|
| 35 |
+
|
| 36 |
+
# Run unit tests (E2E tests have broader coverage; unit tests cover specific modules)
|
| 37 |
+
npm test
|
| 38 |
+
|
| 39 |
+
# Check JSDoc comment syntax and `{@link}` references
|
| 40 |
+
npm exec typedoc -- --treatValidationWarningsAsErrors --emit none
|
| 41 |
+
|
| 42 |
+
# Regenerate package-lock.json (especially on setups w/ custom npm registry)
|
| 43 |
+
rm -fR package-lock.json node_modules && \
|
| 44 |
+
docker run --rm -it --platform linux/amd64 -v $PWD:/src:rw -w /src node:latest npm i && \
|
| 45 |
+
rm -fR node_modules && \
|
| 46 |
+
npm i --cache=~/.npm-mcp-apps --registry=https://registry.npmjs.org/
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
## Architecture
|
| 50 |
+
|
| 51 |
+
### SDK Entry Points
|
| 52 |
+
|
| 53 |
+
- `@modelcontextprotocol/ext-apps` - Main SDK for Apps (`App` class, `PostMessageTransport`)
|
| 54 |
+
- `@modelcontextprotocol/ext-apps/react` - React hooks (`useApp`, `useHostStyleVariables`, etc.)
|
| 55 |
+
- `@modelcontextprotocol/ext-apps/app-bridge` - SDK for hosts (`AppBridge` class)
|
| 56 |
+
- `@modelcontextprotocol/ext-apps/server` - Server helpers (`registerAppTool`, `registerAppResource`)
|
| 57 |
+
|
| 58 |
+
### Key Source Files
|
| 59 |
+
|
| 60 |
+
- `src/app.ts` - `App` class extends MCP Protocol, handles guest initialization, tool calls, messaging
|
| 61 |
+
- `src/app-bridge.ts` - `AppBridge` class for hosts, proxies MCP requests, sends tool input/results to guests
|
| 62 |
+
- `src/server/index.ts` - Helpers for MCP servers to register tools/resources with UI metadata
|
| 63 |
+
- `src/types.ts` - Protocol types re-exported from `spec.types.ts` and Zod schemas from `generated/schema.ts` (auto-generated during build)
|
| 64 |
+
- `src/message-transport.ts` - `PostMessageTransport` for iframe communication
|
| 65 |
+
- `src/react/` - React hooks: `useApp`, `useHostStyles`, `useAutoResize`, `useDocumentTheme`
|
| 66 |
+
|
| 67 |
+
### Protocol Flow
|
| 68 |
+
|
| 69 |
+
```
|
| 70 |
+
Guest UI (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
1. Host creates iframe with Guest UI HTML
|
| 74 |
+
2. Guest UI creates `App` instance and calls `connect()` with `PostMessageTransport`
|
| 75 |
+
3. App sends `ui/initialize` request, receives host capabilities and context
|
| 76 |
+
4. Host sends `sendToolInput()` with tool arguments after initialization
|
| 77 |
+
5. Guest UI can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()`
|
| 78 |
+
6. Host sends `sendToolResult()` when tool execution completes
|
| 79 |
+
7. Host calls `teardownResource()` before unmounting iframe
|
| 80 |
+
|
| 81 |
+
## Examples
|
| 82 |
+
|
| 83 |
+
Uses npm workspaces. Examples in `examples/` are separate packages:
|
| 84 |
+
|
| 85 |
+
- `basic-server-*` - Starter templates (vanillajs, react, vue, svelte, preact, solid). Use these as the basis for new examples.
|
| 86 |
+
- `basic-host` - Reference host implementation
|
| 87 |
+
- Other examples showcase specific features (charts, 3D, video, etc.)
|
| 88 |
+
|
| 89 |
+
## Claude Code Plugin
|
| 90 |
+
|
| 91 |
+
The `plugins/mcp-apps/` directory contains a Claude Code plugin distributed via the plugin marketplace. It provides the "Create MCP App" skill (`plugins/mcp-apps/skills/create-mcp-app/SKILL.md`) that guides users through building MCP Apps with interactive UIs.
|
CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributing to MCP Apps SDK
|
| 2 |
+
|
| 3 |
+
We welcome contributions to the MCP Apps SDK! This document outlines the process for contributing to the project.
|
| 4 |
+
|
| 5 |
+
## Getting Started
|
| 6 |
+
|
| 7 |
+
1. Fork the repository
|
| 8 |
+
2. Clone your fork: `git clone https://github.com/YOUR-USERNAME/ext-apps.git`
|
| 9 |
+
3. Install dependencies: `npm install`
|
| 10 |
+
4. Build the project: `npm run build`
|
| 11 |
+
5. Run tests: `npm test`
|
| 12 |
+
|
| 13 |
+
## Development Process
|
| 14 |
+
|
| 15 |
+
1. Create a new branch for your changes
|
| 16 |
+
2. Make your changes
|
| 17 |
+
3. Run `npm run prettier` to ensure code style compliance
|
| 18 |
+
4. Run `npm test` to verify all tests pass
|
| 19 |
+
5. Submit a pull request
|
| 20 |
+
|
| 21 |
+
## Pull Request Guidelines
|
| 22 |
+
|
| 23 |
+
- Follow the existing code style
|
| 24 |
+
- Include tests for new functionality
|
| 25 |
+
- Update documentation as needed
|
| 26 |
+
- Keep changes focused and atomic
|
| 27 |
+
- Provide a clear description of changes
|
| 28 |
+
|
| 29 |
+
## Running Examples
|
| 30 |
+
|
| 31 |
+
Start the development environment with hot reloading:
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
npm run examples:dev
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
Or build and run examples:
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
npm run examples:start
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
## Testing
|
| 44 |
+
|
| 45 |
+
### Unit Tests
|
| 46 |
+
|
| 47 |
+
Run unit tests with Bun:
|
| 48 |
+
|
| 49 |
+
```bash
|
| 50 |
+
npm test
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### E2E Tests
|
| 54 |
+
|
| 55 |
+
E2E tests use Playwright to verify all example servers work correctly with screenshot comparisons.
|
| 56 |
+
|
| 57 |
+
```bash
|
| 58 |
+
# Run all E2E tests
|
| 59 |
+
npm run test:e2e
|
| 60 |
+
|
| 61 |
+
# Run a specific server's tests
|
| 62 |
+
npm run test:e2e -- --grep "Budget Allocator"
|
| 63 |
+
|
| 64 |
+
# Run tests in interactive UI mode
|
| 65 |
+
npm run test:e2e:ui
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### Updating Golden Screenshots
|
| 69 |
+
|
| 70 |
+
When UI changes are intentional, update the golden screenshots:
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Update all screenshots
|
| 74 |
+
npm run test:e2e:update
|
| 75 |
+
|
| 76 |
+
# Update screenshots for a specific server
|
| 77 |
+
npm run test:e2e:update -- --grep "Three.js"
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**Note**: Golden screenshots are platform-agnostic. Tests use canvas masking and tolerance thresholds to handle minor cross-platform rendering differences.
|
| 81 |
+
|
| 82 |
+
## Code of Conduct
|
| 83 |
+
|
| 84 |
+
This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please review it before contributing.
|
| 85 |
+
|
| 86 |
+
## Reporting Issues
|
| 87 |
+
|
| 88 |
+
- Use the [GitHub issue tracker](https://github.com/modelcontextprotocol/ext-apps/issues)
|
| 89 |
+
- Search existing issues before creating a new one
|
| 90 |
+
- Provide clear reproduction steps
|
| 91 |
+
|
| 92 |
+
## Security Issues
|
| 93 |
+
|
| 94 |
+
Please review our [Security Policy](SECURITY.md) for reporting security vulnerabilities.
|
| 95 |
+
|
| 96 |
+
---
|
| 97 |
+
|
| 98 |
+
## For Maintainers
|
| 99 |
+
|
| 100 |
+
### Repository Setup
|
| 101 |
+
|
| 102 |
+
This repository uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/) with OIDC - no secrets required.
|
| 103 |
+
|
| 104 |
+
Before publishing releases, ensure the following are configured:
|
| 105 |
+
|
| 106 |
+
1. **Trusted publisher on npm**: Configure the package to trust this GitHub repository
|
| 107 |
+
- Go to https://www.npmjs.com/package/@modelcontextprotocol/ext-apps/access
|
| 108 |
+
- Under "Trusted Publishers", click "Add trusted publisher"
|
| 109 |
+
- Select "GitHub Actions"
|
| 110 |
+
- Repository: `modelcontextprotocol/ext-apps`
|
| 111 |
+
- Workflow filename: `npm-publish.yml`
|
| 112 |
+
- Environment: `Release` (optional, for additional protection)
|
| 113 |
+
|
| 114 |
+
2. **`Release` environment** (optional): Create a protected environment for additional safeguards
|
| 115 |
+
- Go to Settings > Environments > New environment
|
| 116 |
+
- Name it `Release`
|
| 117 |
+
- Add required reviewers or other protection rules as needed
|
| 118 |
+
|
| 119 |
+
### Publishing a Release
|
| 120 |
+
|
| 121 |
+
Releases are published automatically via GitHub Actions when a GitHub Release is created.
|
| 122 |
+
|
| 123 |
+
#### Steps to publish:
|
| 124 |
+
|
| 125 |
+
1. **Update the version** in `package.json`:
|
| 126 |
+
|
| 127 |
+
```bash
|
| 128 |
+
# For a regular release
|
| 129 |
+
npm version patch # or minor, or major
|
| 130 |
+
|
| 131 |
+
# For a beta release
|
| 132 |
+
npm version prerelease --preid=beta
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
2. **Commit the version bump** (if not done by `npm version`):
|
| 136 |
+
|
| 137 |
+
```bash
|
| 138 |
+
git add package.json
|
| 139 |
+
git commit -m "Bump version to X.Y.Z"
|
| 140 |
+
git push origin main
|
| 141 |
+
```
|
| 142 |
+
|
| 143 |
+
3. **Create a GitHub Release**:
|
| 144 |
+
- Go to [Releases](https://github.com/modelcontextprotocol/ext-apps/releases)
|
| 145 |
+
- Click "Draft a new release"
|
| 146 |
+
- Create a new tag matching the version (e.g., `v0.1.0`)
|
| 147 |
+
- Set the target branch (usually `main`)
|
| 148 |
+
- Write release notes describing the changes
|
| 149 |
+
- Click "Publish release"
|
| 150 |
+
|
| 151 |
+
4. **Monitor the workflow**:
|
| 152 |
+
- The [npm-publish workflow](https://github.com/modelcontextprotocol/ext-apps/actions/workflows/npm-publish.yml) will trigger automatically
|
| 153 |
+
- It runs build and test jobs before publishing
|
| 154 |
+
- On success, the package is published to npm with provenance
|
| 155 |
+
|
| 156 |
+
#### npm Tags
|
| 157 |
+
|
| 158 |
+
The workflow automatically determines the npm dist-tag:
|
| 159 |
+
|
| 160 |
+
| Version Pattern | npm Tag | Install Command |
|
| 161 |
+
| ----------------------------- | ------------- | -------------------------------------------------------- |
|
| 162 |
+
| `X.Y.Z` (from main) | `latest` | `npm install @modelcontextprotocol/ext-apps` |
|
| 163 |
+
| `X.Y.Z-beta.N` | `beta` | `npm install @modelcontextprotocol/ext-apps@beta` |
|
| 164 |
+
| `X.Y.Z` (from release branch) | `release-X.Y` | `npm install @modelcontextprotocol/ext-apps@release-X.Y` |
|
| 165 |
+
|
| 166 |
+
#### Maintenance Releases
|
| 167 |
+
|
| 168 |
+
To release a patch for an older version:
|
| 169 |
+
|
| 170 |
+
1. Create a release branch from the tag: `git checkout -b release-0.1 v0.1.0`
|
| 171 |
+
2. Cherry-pick or apply fixes
|
| 172 |
+
3. Bump the patch version
|
| 173 |
+
4. Create a GitHub Release targeting the release branch
|
| 174 |
+
5. The package will be published with tag `release-0.1`
|
| 175 |
+
|
| 176 |
+
### Testing Pre-releases
|
| 177 |
+
|
| 178 |
+
Every commit and PR automatically publishes a preview package via [pkg-pr-new](https://github.com/pkg-pr-new/pkg-pr-new). Check the PR comments or workflow logs for the install command.
|
| 179 |
+
|
| 180 |
+
---
|
| 181 |
+
|
| 182 |
+
## License
|
| 183 |
+
|
| 184 |
+
By contributing, you agree that your contributions will be licensed under the MIT License.
|
LICENSE
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0.
|
| 2 |
+
|
| 3 |
+
Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License.
|
| 4 |
+
|
| 5 |
+
No rights beyond those granted by the applicable original license are conveyed for such contributions.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
Apache License
|
| 10 |
+
Version 2.0, January 2004
|
| 11 |
+
http://www.apache.org/licenses/
|
| 12 |
+
|
| 13 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 14 |
+
|
| 15 |
+
1. Definitions.
|
| 16 |
+
|
| 17 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 18 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 19 |
+
|
| 20 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 21 |
+
the copyright owner that is granting the License.
|
| 22 |
+
|
| 23 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 24 |
+
other entities that control, are controlled by, or are under common
|
| 25 |
+
control with that entity. For the purposes of this definition,
|
| 26 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 27 |
+
direction or management of such entity, whether by contract or
|
| 28 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 29 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 30 |
+
|
| 31 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 32 |
+
exercising permissions granted by this License.
|
| 33 |
+
|
| 34 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 35 |
+
including but not limited to software source code, documentation
|
| 36 |
+
source, and configuration files.
|
| 37 |
+
|
| 38 |
+
"Object" form shall mean any form resulting from mechanical
|
| 39 |
+
transformation or translation of a Source form, including but
|
| 40 |
+
not limited to compiled object code, generated documentation,
|
| 41 |
+
and conversions to other media types.
|
| 42 |
+
|
| 43 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 44 |
+
Object form, made available under the License, as indicated by a
|
| 45 |
+
copyright notice that is included in or attached to the work
|
| 46 |
+
(an example is provided in the Appendix below).
|
| 47 |
+
|
| 48 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 49 |
+
form, that is based on (or derived from) the Work and for which the
|
| 50 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 51 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 52 |
+
of this License, Derivative Works shall not include works that remain
|
| 53 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 54 |
+
the Work and Derivative Works thereof.
|
| 55 |
+
|
| 56 |
+
"Contribution" shall mean any work of authorship, including
|
| 57 |
+
the original version of the Work and any modifications or additions
|
| 58 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 59 |
+
submitted to the Licensor for inclusion in the Work by the copyright
|
| 60 |
+
owner or by an individual or Legal Entity authorized to submit on behalf
|
| 61 |
+
of the copyright owner. For the purposes of this definition, "submitted"
|
| 62 |
+
means any form of electronic, verbal, or written communication sent
|
| 63 |
+
to the Licensor or its representatives, including but not limited to
|
| 64 |
+
communication on electronic mailing lists, source code control systems,
|
| 65 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 66 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 67 |
+
excluding communication that is conspicuously marked or otherwise
|
| 68 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 69 |
+
|
| 70 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 71 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 72 |
+
subsequently incorporated within the Work.
|
| 73 |
+
|
| 74 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 78 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 79 |
+
Work and such Derivative Works in Source or Object form.
|
| 80 |
+
|
| 81 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 82 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 83 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 84 |
+
(except as stated in this section) patent license to make, have made,
|
| 85 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 86 |
+
where such license applies only to those patent claims licensable
|
| 87 |
+
by such Contributor that are necessarily infringed by their
|
| 88 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 89 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 90 |
+
institute patent litigation against any entity (including a
|
| 91 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 92 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 93 |
+
or contributory patent infringement, then any patent licenses
|
| 94 |
+
granted to You under this License for that Work shall terminate
|
| 95 |
+
as of the date such litigation is filed.
|
| 96 |
+
|
| 97 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 98 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 99 |
+
modifications, and in Source or Object form, provided that You
|
| 100 |
+
meet the following conditions:
|
| 101 |
+
|
| 102 |
+
(a) You must give any other recipients of the Work or
|
| 103 |
+
Derivative Works a copy of this License; and
|
| 104 |
+
|
| 105 |
+
(b) You must cause any modified files to carry prominent notices
|
| 106 |
+
stating that You changed the files; and
|
| 107 |
+
|
| 108 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 109 |
+
that You distribute, all copyright, patent, trademark, and
|
| 110 |
+
attribution notices from the Source form of the Work,
|
| 111 |
+
excluding those notices that do not pertain to any part of
|
| 112 |
+
the Derivative Works; and
|
| 113 |
+
|
| 114 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 115 |
+
distribution, then any Derivative Works that You distribute must
|
| 116 |
+
include a readable copy of the attribution notices contained
|
| 117 |
+
within such NOTICE file, excluding those notices that do not
|
| 118 |
+
pertain to any part of the Derivative Works, in at least one
|
| 119 |
+
of the following places: within a NOTICE text file distributed
|
| 120 |
+
as part of the Derivative Works; within the Source form or
|
| 121 |
+
documentation, if provided along with the Derivative Works; or,
|
| 122 |
+
within a display generated by the Derivative Works, if and
|
| 123 |
+
wherever such third-party notices normally appear. The contents
|
| 124 |
+
of the NOTICE file are for informational purposes only and
|
| 125 |
+
do not modify the License. You may add Your own attribution
|
| 126 |
+
notices within Derivative Works that You distribute, alongside
|
| 127 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 128 |
+
that such additional attribution notices cannot be construed
|
| 129 |
+
as modifying the License.
|
| 130 |
+
|
| 131 |
+
You may add Your own copyright statement to Your modifications and
|
| 132 |
+
may provide additional or different license terms and conditions
|
| 133 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 134 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 135 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 136 |
+
the conditions stated in this License.
|
| 137 |
+
|
| 138 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 139 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 140 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 141 |
+
this License, without any additional terms or conditions.
|
| 142 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 143 |
+
the terms of any separate license agreement you may have executed
|
| 144 |
+
with Licensor regarding such Contributions.
|
| 145 |
+
|
| 146 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 147 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 148 |
+
except as required for reasonable and customary use in describing the
|
| 149 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 150 |
+
|
| 151 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 152 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 153 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 154 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 155 |
+
implied, including, without limitation, any warranties or conditions
|
| 156 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 157 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 158 |
+
appropriateness of using or redistributing the Work and assume any
|
| 159 |
+
risks associated with Your exercise of permissions under this License.
|
| 160 |
+
|
| 161 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 162 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 163 |
+
unless required by applicable law (such as deliberate and grossly
|
| 164 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 165 |
+
liable to You for damages, including any direct, indirect, special,
|
| 166 |
+
incidental, or consequential damages of any character arising as a
|
| 167 |
+
result of this License or out of the use or inability to use the
|
| 168 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 169 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 170 |
+
other commercial damages or losses), even if such Contributor
|
| 171 |
+
has been advised of the possibility of such damages.
|
| 172 |
+
|
| 173 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 174 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 175 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 176 |
+
or other liability obligations and/or rights consistent with this
|
| 177 |
+
License. However, in accepting such obligations, You may act only
|
| 178 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 179 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 180 |
+
defend, and hold each Contributor harmless for any liability
|
| 181 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 182 |
+
of your accepting any such warranty or additional liability.
|
| 183 |
+
|
| 184 |
+
END OF TERMS AND CONDITIONS
|
| 185 |
+
|
| 186 |
+
---
|
| 187 |
+
|
| 188 |
+
MIT License
|
| 189 |
+
|
| 190 |
+
Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC.
|
| 191 |
+
|
| 192 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 193 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 194 |
+
in the Software without restriction, including without limitation the rights
|
| 195 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 196 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 197 |
+
furnished to do so, subject to the following conditions:
|
| 198 |
+
|
| 199 |
+
The above copyright notice and this permission notice shall be included in all
|
| 200 |
+
copies or substantial portions of the Software.
|
| 201 |
+
|
| 202 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 203 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 204 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 205 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 206 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 207 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 208 |
+
SOFTWARE.
|
| 209 |
+
|
| 210 |
+
---
|
| 211 |
+
|
| 212 |
+
Creative Commons Attribution 4.0 International (CC-BY-4.0)
|
| 213 |
+
|
| 214 |
+
Documentation in this project (excluding specifications) is licensed under
|
| 215 |
+
CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for
|
| 216 |
+
the full license text.
|
README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# @modelcontextprotocol/ext-apps
|
| 2 |
+
|
| 3 |
+
[](https://www.npmjs.com/package/@modelcontextprotocol/ext-apps) [](https://modelcontextprotocol.github.io/ext-apps/api/)
|
| 4 |
+
|
| 5 |
+
This repo contains the SDK and [specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) for MCP Apps Extension ([SEP-1865](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)).
|
| 6 |
+
|
| 7 |
+
MCP Apps are a proposed standard inspired by [MCP-UI](https://mcpui.dev/) and [OpenAI's Apps SDK](https://developers.openai.com/apps-sdk/) to allow MCP Servers to display interactive UI elements in conversational MCP clients / chatbots.
|
| 8 |
+
|
| 9 |
+
## How It Works
|
| 10 |
+
|
| 11 |
+
MCP Apps extend the Model Context Protocol to let servers deliver **interactive UIs** to MCP hosts. Here's how it works:
|
| 12 |
+
|
| 13 |
+
1. **Tool call** — The LLM calls a tool on your server
|
| 14 |
+
2. **UI Resource** — The tool's definition links to a predeclared `ui://` resource containing its HTML interface
|
| 15 |
+
3. **Host renders** — The host fetches the resource and displays it in a sandboxed iframe
|
| 16 |
+
4. **Bidirectional communication** — The host passes tool data to the UI via notifications, and the UI can call other tools through the host
|
| 17 |
+
|
| 18 |
+
This enables dashboards, forms, visualizations, and other rich experiences inside chat interfaces.
|
| 19 |
+
|
| 20 |
+
## Overview
|
| 21 |
+
|
| 22 |
+
This SDK serves two audiences:
|
| 23 |
+
|
| 24 |
+
### App Developers
|
| 25 |
+
|
| 26 |
+
Build interactive UIs that run inside MCP-enabled chat clients.
|
| 27 |
+
|
| 28 |
+
- **SDK for Apps**: `@modelcontextprotocol/ext-apps` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/api/modules/app.html)
|
| 29 |
+
- **React hooks**: `@modelcontextprotocol/ext-apps/react` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/api/modules/_modelcontextprotocol_ext-apps_react.html)
|
| 30 |
+
|
| 31 |
+
### Host Developers
|
| 32 |
+
|
| 33 |
+
Embed and communicate with MCP Apps in your chat application.
|
| 34 |
+
|
| 35 |
+
- **SDK for Hosts**: `@modelcontextprotocol/ext-apps/app-bridge` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/api/modules/app-bridge.html)
|
| 36 |
+
|
| 37 |
+
There's no _supported_ host implementation in this repo (beyond the [examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host) example).
|
| 38 |
+
|
| 39 |
+
We have [contributed a tentative implementation](https://github.com/MCP-UI-Org/mcp-ui/pull/147) of hosting / iframing / sandboxing logic to the [MCP-UI](https://github.com/idosal/mcp-ui) repository, and expect OSS clients may use it, while other clients might roll their own hosting logic.
|
| 40 |
+
|
| 41 |
+
## Installation
|
| 42 |
+
|
| 43 |
+
```bash
|
| 44 |
+
npm install -S @modelcontextprotocol/ext-apps
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
Or edit your `package.json` manually:
|
| 48 |
+
|
| 49 |
+
```json
|
| 50 |
+
{
|
| 51 |
+
"dependencies": {
|
| 52 |
+
"@modelcontextprotocol/ext-apps": "^0.0.1"
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
## Examples
|
| 58 |
+
|
| 59 |
+
<!-- prettier-ignore-start -->
|
| 60 |
+
| | | |
|
| 61 |
+
|:---:|:---:|:---:|
|
| 62 |
+
| [](examples/map-server) | [](examples/threejs-server) | [](examples/shadertoy-server) |
|
| 63 |
+
| [**Map**](examples/map-server) | [**Three.js**](examples/threejs-server) | [**ShaderToy**](examples/shadertoy-server) |
|
| 64 |
+
| [](examples/sheet-music-server) | [](examples/wiki-explorer-server) | [](examples/cohort-heatmap-server) |
|
| 65 |
+
| [**Sheet Music**](examples/sheet-music-server) | [**Wiki Explorer**](examples/wiki-explorer-server) | [**Cohort Heatmap**](examples/cohort-heatmap-server) |
|
| 66 |
+
| [](examples/scenario-modeler-server) | [](examples/budget-allocator-server) | [](examples/customer-segmentation-server) |
|
| 67 |
+
| [**Scenario Modeler**](examples/scenario-modeler-server) | [**Budget Allocator**](examples/budget-allocator-server) | [**Customer Segmentation**](examples/customer-segmentation-server) |
|
| 68 |
+
| [](examples/system-monitor-server) | [](examples/transcript-server) | [](examples/video-resource-server) |
|
| 69 |
+
| [**System Monitor**](examples/system-monitor-server) | [**Transcript**](examples/transcript-server) | [**Video Resource**](examples/video-resource-server) |
|
| 70 |
+
| [](examples/pdf-server) | | |
|
| 71 |
+
| [**PDF Server**](examples/pdf-server) | | |
|
| 72 |
+
|
| 73 |
+
### Starter Templates
|
| 74 |
+
|
| 75 |
+
| | |
|
| 76 |
+
|:---:|:---|
|
| 77 |
+
| [](examples/basic-server-react) | The same app built with different frameworks — pick your favorite!<br><br>[React](examples/basic-server-react) · [Vue](examples/basic-server-vue) · [Svelte](examples/basic-server-svelte) · [Preact](examples/basic-server-preact) · [Solid](examples/basic-server-solid) · [Vanilla JS](examples/basic-server-vanillajs) |
|
| 78 |
+
<!-- prettier-ignore-end -->
|
| 79 |
+
|
| 80 |
+
The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains additional demo apps showcasing real-world use cases.
|
| 81 |
+
|
| 82 |
+
To run all examples:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
npm install
|
| 86 |
+
npm start
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
Then open http://localhost:8080/.
|
| 90 |
+
|
| 91 |
+
## Resources
|
| 92 |
+
|
| 93 |
+
- [Quickstart Guide](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html)
|
| 94 |
+
- [API Documentation](https://modelcontextprotocol.github.io/ext-apps/api/)
|
| 95 |
+
- [Draft Specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)
|
| 96 |
+
- [SEP-1865 Discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)
|
RELEASES.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Release Notes
|
| 2 |
+
|
| 3 |
+
## 0.3.0
|
| 4 |
+
|
| 5 |
+
### Breaking Changes
|
| 6 |
+
|
| 7 |
+
- **`viewport` replaced with `containerDimensions`** — The `viewport` property in host context has been replaced with `containerDimensions`, which provides a clearer distinction between fixed dimensions and maximum constraints. The new type allows specifying either `height` or `maxHeight`, and either `width` or `maxWidth` by @martinalong in [#153](https://github.com/modelcontextprotocol/ext-apps/pull/153)
|
| 8 |
+
|
| 9 |
+
### New Examples
|
| 10 |
+
|
| 11 |
+
- **Video resource server** — Demonstrates video resource handling with proper mimeType declarations by @antonpk1 in [#175](https://github.com/modelcontextprotocol/ext-apps/pull/175)
|
| 12 |
+
- **Sheet music server** — Interactive sheet music notation example by @jonathanhefner in [#196](https://github.com/modelcontextprotocol/ext-apps/pull/196)
|
| 13 |
+
|
| 14 |
+
### Developer Experience
|
| 15 |
+
|
| 16 |
+
- **`npm start` alias** — Added `npm start` as alias for `npm run examples:start` by @jonathanhefner in [#183](https://github.com/modelcontextprotocol/ext-apps/pull/183)
|
| 17 |
+
- **Examples cleanup** — Improved consistency across example servers by @jonathanhefner in [#182](https://github.com/modelcontextprotocol/ext-apps/pull/182)
|
| 18 |
+
- **Documentation fixes** — Fixed tsc command in docs to use tsconfig.json by @blackgirlbytes in [#188](https://github.com/modelcontextprotocol/ext-apps/pull/188)
|
| 19 |
+
|
| 20 |
+
### Bug Fixes
|
| 21 |
+
|
| 22 |
+
- **Move prettier to dev dependency** — Fixed incorrect dependency classification by @niclim in [#179](https://github.com/modelcontextprotocol/ext-apps/pull/179)
|
| 23 |
+
- **Fix build errors in examples** — Resolved build issues across example servers by @jonathanhefner in [#180](https://github.com/modelcontextprotocol/ext-apps/pull/180)
|
| 24 |
+
|
| 25 |
+
**Full Changelog**: https://github.com/modelcontextprotocol/ext-apps/compare/v0.2.2...v0.3.0
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## 0.2.2
|
| 30 |
+
|
| 31 |
+
Changes from 0.1.x to 0.2.2.
|
| 32 |
+
|
| 33 |
+
### Highlights
|
| 34 |
+
|
| 35 |
+
- **Server helpers** — New `registerAppTool()` and `registerAppResource()` simplify server setup with proper type safety. `connect()` now defaults to `PostMessageTransport(window.parent)`, enabling simpler initialization with just `await app.connect()` by @ochafik in [#165](https://github.com/modelcontextprotocol/ext-apps/pull/165)
|
| 36 |
+
- **Tool visibility control** — New `visibility` array field controls whether tools are visible to the agent, apps, or both. Restructured `_meta` format from flat `"ui/resourceUri"` to nested `_meta.ui.resourceUri` by @jonathanhefner in [#131](https://github.com/modelcontextprotocol/ext-apps/pull/131)
|
| 37 |
+
- **Host-provided theming** — Apps receive 36 standardized CSS variables for colors, typography, and spacing via `styles.variables`, enabling visual consistency with the host by @martinalong in [#127](https://github.com/modelcontextprotocol/ext-apps/pull/127)
|
| 38 |
+
- **Display mode requests** — Apps can request display mode changes (e.g., fullscreen) via `requestDisplayMode`, with hosts able to accept or reject by @martinalong in [#152](https://github.com/modelcontextprotocol/ext-apps/pull/152)
|
| 39 |
+
- **Custom fonts support** — Apps can receive custom fonts via `styles.css.fonts` and apply them using `applyHostFonts()` or `useHostFonts()` helpers by @martinalong in [#159](https://github.com/modelcontextprotocol/ext-apps/pull/159)
|
| 40 |
+
|
| 41 |
+
### API Changes
|
| 42 |
+
|
| 43 |
+
- **MCP SDK as peer dependency** — Consumers control their SDK version, reducing duplication by @ochafik in [#168](https://github.com/modelcontextprotocol/ext-apps/pull/168)
|
| 44 |
+
- **React as peer dependency** — Supports React 17, 18, and 19 by @ochafik in [#164](https://github.com/modelcontextprotocol/ext-apps/pull/164)
|
| 45 |
+
- **Renamed request methods** — Removed `send` prefix: `openLink()` and `teardownResource()`. Deprecated aliases maintained by @ochafik in [#161](https://github.com/modelcontextprotocol/ext-apps/pull/161)
|
| 46 |
+
- **Optional Client in AppBridge** — Enables custom forwarding scenarios without direct MCP client access by @ochafik in [#146](https://github.com/modelcontextprotocol/ext-apps/pull/146)
|
| 47 |
+
- **Zod v3 and v4 support** — Bring your own Zod by @alpic-ai in [#49](https://github.com/modelcontextprotocol/ext-apps/pull/49)
|
| 48 |
+
- **Zod schemas now version-agnostic** — Generated schemas work with both Zod 3.25+ and v4, using `z.object().passthrough()` instead of v4-only `z.looseObject()` by @ochafik in [#178](https://github.com/modelcontextprotocol/ext-apps/pull/178)
|
| 49 |
+
|
| 50 |
+
### Platform & DX
|
| 51 |
+
|
| 52 |
+
- **Windows compatibility** — Bun as optional dependency with automatic setup, `cross-env` for examples. Just run `npm install` by @ochafik in [#145](https://github.com/modelcontextprotocol/ext-apps/pull/145)
|
| 53 |
+
- **Widened `@oven/bun-*` version range** — Lowered the minimum from `^1.3.4` to `^1.2.21`, helping contributors whose registries may not have the latest packages by @ochafik in [#176](https://github.com/modelcontextprotocol/ext-apps/pull/176)
|
| 54 |
+
- **SSE transport support** — All examples now support both stdio and HTTP transports with SSE endpoints for older clients by @ochafik in [#136](https://github.com/modelcontextprotocol/ext-apps/pull/136)
|
| 55 |
+
- **Playwright E2E tests** — Screenshot golden testing across all 9 example servers with parallel execution (~28s) by @ochafik in [#115](https://github.com/modelcontextprotocol/ext-apps/pull/115)
|
| 56 |
+
- **hostContext exposure** — Access `hostContext` directly from the `App` class by @ochafik in [#139](https://github.com/modelcontextprotocol/ext-apps/pull/139)
|
| 57 |
+
|
| 58 |
+
### Bug Fixes
|
| 59 |
+
|
| 60 |
+
- **Responsive UIs** — Fixed narrow viewport handling for mobile and sidebar experiences by @ochafik in [#135](https://github.com/modelcontextprotocol/ext-apps/pull/135)
|
| 61 |
+
- **Sandbox notification fix** — Fixed `sandbox-ready` notification name to match implementation by @ochafik in [#160](https://github.com/modelcontextprotocol/ext-apps/pull/160)
|
| 62 |
+
- **Fixed non-UI tool registration** — Use `server.registerTool` for tools without UI and fix missing imports across examples by @ochafik in [#173](https://github.com/modelcontextprotocol/ext-apps/pull/173)
|
| 63 |
+
|
| 64 |
+
**Full Changelog**: https://github.com/modelcontextprotocol/ext-apps/compare/v0.1.1...v0.2.2
|
build.bun.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bun
|
| 2 |
+
import { $ } from "bun";
|
| 3 |
+
|
| 4 |
+
// Run TypeScript compiler for type declarations
|
| 5 |
+
await $`tsc`;
|
| 6 |
+
|
| 7 |
+
const isDevelopment = Bun.env.NODE_ENV === "development";
|
| 8 |
+
|
| 9 |
+
// Build all JavaScript/TypeScript files
|
| 10 |
+
function buildJs(
|
| 11 |
+
entrypoint: string,
|
| 12 |
+
opts: Partial<Parameters<(typeof Bun)["build"]>[0]> = {},
|
| 13 |
+
) {
|
| 14 |
+
return Bun.build({
|
| 15 |
+
entrypoints: [entrypoint],
|
| 16 |
+
outdir: "dist",
|
| 17 |
+
target: "browser",
|
| 18 |
+
minify: !isDevelopment,
|
| 19 |
+
...(isDevelopment
|
| 20 |
+
? {
|
| 21 |
+
sourcemap: "inline",
|
| 22 |
+
}
|
| 23 |
+
: {}),
|
| 24 |
+
...opts,
|
| 25 |
+
});
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
await Promise.all([
|
| 29 |
+
buildJs("src/app.ts", {
|
| 30 |
+
outdir: "dist/src",
|
| 31 |
+
external: ["@modelcontextprotocol/sdk"],
|
| 32 |
+
}),
|
| 33 |
+
buildJs("src/app.ts", {
|
| 34 |
+
outdir: "dist/src",
|
| 35 |
+
naming: { entry: "app-with-deps.js" },
|
| 36 |
+
}),
|
| 37 |
+
buildJs("src/app-bridge.ts", {
|
| 38 |
+
outdir: "dist/src",
|
| 39 |
+
external: ["@modelcontextprotocol/sdk"],
|
| 40 |
+
}),
|
| 41 |
+
buildJs("src/react/index.tsx", {
|
| 42 |
+
outdir: "dist/src/react",
|
| 43 |
+
external: ["react", "react-dom", "@modelcontextprotocol/sdk"],
|
| 44 |
+
}),
|
| 45 |
+
buildJs("src/react/index.tsx", {
|
| 46 |
+
outdir: "dist/src/react",
|
| 47 |
+
external: ["react", "react-dom", "@modelcontextprotocol/sdk"],
|
| 48 |
+
naming: { entry: "react-with-deps.js" },
|
| 49 |
+
}),
|
| 50 |
+
buildJs("src/server/index.ts", {
|
| 51 |
+
outdir: "dist/src/server",
|
| 52 |
+
external: ["@modelcontextprotocol/sdk"],
|
| 53 |
+
}),
|
| 54 |
+
]);
|
docs/index.html
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta http-equiv="refresh" content="0; url=api/">
|
| 6 |
+
<title>Redirecting...</title>
|
| 7 |
+
</head>
|
| 8 |
+
<body>
|
| 9 |
+
<p>Redirecting to <a href="api/">API Documentation</a>...</p>
|
| 10 |
+
<script>window.location.href = 'api/';</script>
|
| 11 |
+
</body>
|
| 12 |
+
</html>
|
docs/migrate_from_openai_apps.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Migrating from OpenAI Apps SDK to MCP Apps SDK
|
| 2 |
+
|
| 3 |
+
This guide helps you migrate from the OpenAI Apps SDK to the MCP Apps SDK (`@modelcontextprotocol/ext-apps`).
|
| 4 |
+
|
| 5 |
+
## Server-Side
|
| 6 |
+
|
| 7 |
+
### Quick Start Comparison
|
| 8 |
+
|
| 9 |
+
| OpenAI Apps SDK | MCP Apps SDK |
|
| 10 |
+
| ---------------------------------------------------------- | -------------------------------------------------------------- |
|
| 11 |
+
| Flat metadata keys (`_meta["openai/..."]`) | Nested metadata structure (`_meta.ui.*`) |
|
| 12 |
+
| Direct `server.registerTool()`/`server.registerResource()` | Helper functions: `registerAppTool()`, `registerAppResource()` |
|
| 13 |
+
| UI Resource MIME type: `text/html+skybridge` | UI Resource MIME type: `text/html;profile=mcp-app` |
|
| 14 |
+
|
| 15 |
+
### Tool Metadata
|
| 16 |
+
|
| 17 |
+
| OpenAI | MCP Apps | Notes |
|
| 18 |
+
| ---------------------------------------------- | ---------------------------------- | ----------------------------------------------------------- |
|
| 19 |
+
| `_meta["openai/outputTemplate"]` | `_meta.ui.resourceUri` | URI of UI resource |
|
| 20 |
+
| `_meta["openai/toolInvocation/invoking"]` | — | Not yet implemented |
|
| 21 |
+
| `_meta["openai/toolInvocation/invoked"]` | — | Not yet implemented |
|
| 22 |
+
| `_meta["openai/widgetAccessible"]` (`boolean`) | `_meta.ui.visibility` (`string[]`) | `true`/`false` → include/exclude `"app"` in array |
|
| 23 |
+
| `_meta["openai/visibility"]` (`string`) | `_meta.ui.visibility` (`string[]`) | `"public"`/`"private"` → include/exclude `"model"` in array |
|
| 24 |
+
|
| 25 |
+
### Resource Metadata
|
| 26 |
+
|
| 27 |
+
| OpenAI | MCP Apps | Notes |
|
| 28 |
+
| ------------------------------------- | ------------------------ | ---------------------------------------------------------------------------------- |
|
| 29 |
+
| `_meta["openai/widgetCSP"]` | `_meta.ui.csp` | `connect_domains` → `connectDomains`, `resource_domains` → `resourceDomains`, etc. |
|
| 30 |
+
| — | `_meta.ui.permissions` | MCP adds: permissions for camera, microphone, geolocation, clipboard |
|
| 31 |
+
| `_meta["openai/widgetDomain"]` | `_meta.ui.domain` | Dedicated sandbox origin |
|
| 32 |
+
| `_meta["openai/widgetPrefersBorder"]` | `_meta.ui.prefersBorder` | Visual boundary preference |
|
| 33 |
+
| `_meta["openai/widgetDescription"]` | — | Not yet implemented; use `app.updateModelContext()` for dynamic context |
|
| 34 |
+
|
| 35 |
+
### Resource MIME Type
|
| 36 |
+
|
| 37 |
+
| OpenAI | MCP Apps | Notes |
|
| 38 |
+
| --------------------- | --------------------------- | -------------------------------------------------------------------------------- |
|
| 39 |
+
| `text/html+skybridge` | `text/html;profile=mcp-app` | Auto-set by `registerAppResource()`; use `RESOURCE_MIME_TYPE` constant if manual |
|
| 40 |
+
|
| 41 |
+
### Server-Side Migration Example
|
| 42 |
+
|
| 43 |
+
### Before (OpenAI)
|
| 44 |
+
|
| 45 |
+
```typescript
|
| 46 |
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
| 47 |
+
import { z } from "zod";
|
| 48 |
+
|
| 49 |
+
function createServer() {
|
| 50 |
+
const server = new McpServer({ name: "shop", version: "1.0.0" });
|
| 51 |
+
|
| 52 |
+
// Register tool with OpenAI metadata
|
| 53 |
+
server.registerTool(
|
| 54 |
+
"shopping-cart",
|
| 55 |
+
{
|
| 56 |
+
title: "Shopping Cart",
|
| 57 |
+
description: "Display the user's shopping cart",
|
| 58 |
+
inputSchema: { userId: z.string() },
|
| 59 |
+
annotations: { readOnlyHint: true },
|
| 60 |
+
_meta: {
|
| 61 |
+
"openai/outputTemplate": "ui://widget/cart.html",
|
| 62 |
+
"openai/toolInvocation/invoking": "Loading cart...",
|
| 63 |
+
"openai/toolInvocation/invoked": "Cart ready",
|
| 64 |
+
"openai/widgetAccessible": true,
|
| 65 |
+
},
|
| 66 |
+
},
|
| 67 |
+
async (args) => {
|
| 68 |
+
const cart = await getCart(args.userId);
|
| 69 |
+
return {
|
| 70 |
+
content: [{ type: "text", text: JSON.stringify(cart) }],
|
| 71 |
+
structuredContent: { cart },
|
| 72 |
+
};
|
| 73 |
+
},
|
| 74 |
+
);
|
| 75 |
+
|
| 76 |
+
// Register UI resource
|
| 77 |
+
server.registerResource(
|
| 78 |
+
"Cart Widget",
|
| 79 |
+
"ui://widget/cart.html",
|
| 80 |
+
{ mimeType: "text/html+skybridge" },
|
| 81 |
+
async () => ({
|
| 82 |
+
contents: [
|
| 83 |
+
{
|
| 84 |
+
uri: "ui://widget/cart.html",
|
| 85 |
+
mimeType: "text/html+skybridge",
|
| 86 |
+
text: getCartHtml(),
|
| 87 |
+
},
|
| 88 |
+
],
|
| 89 |
+
}),
|
| 90 |
+
);
|
| 91 |
+
|
| 92 |
+
return server;
|
| 93 |
+
}
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
### After (MCP Apps)
|
| 97 |
+
|
| 98 |
+
```typescript
|
| 99 |
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
| 100 |
+
import {
|
| 101 |
+
registerAppTool,
|
| 102 |
+
registerAppResource,
|
| 103 |
+
RESOURCE_MIME_TYPE,
|
| 104 |
+
} from "@modelcontextprotocol/ext-apps/server";
|
| 105 |
+
import { z } from "zod";
|
| 106 |
+
|
| 107 |
+
function createServer() {
|
| 108 |
+
const server = new McpServer({ name: "shop", version: "1.0.0" });
|
| 109 |
+
|
| 110 |
+
// Register tool with MCP Apps metadata
|
| 111 |
+
registerAppTool(
|
| 112 |
+
server,
|
| 113 |
+
"shopping-cart",
|
| 114 |
+
{
|
| 115 |
+
title: "Shopping Cart",
|
| 116 |
+
description: "Display the user's shopping cart",
|
| 117 |
+
inputSchema: { userId: z.string() },
|
| 118 |
+
annotations: { readOnlyHint: true },
|
| 119 |
+
_meta: { ui: { resourceUri: "ui://widget/cart.html" } },
|
| 120 |
+
},
|
| 121 |
+
async (args) => {
|
| 122 |
+
const cart = await getCart(args.userId);
|
| 123 |
+
return {
|
| 124 |
+
content: [{ type: "text", text: JSON.stringify(cart) }],
|
| 125 |
+
structuredContent: { cart },
|
| 126 |
+
};
|
| 127 |
+
},
|
| 128 |
+
);
|
| 129 |
+
|
| 130 |
+
// Register UI resource
|
| 131 |
+
registerAppResource(
|
| 132 |
+
server,
|
| 133 |
+
"Cart Widget",
|
| 134 |
+
"ui://widget/cart.html",
|
| 135 |
+
{ description: "Shopping cart UI" },
|
| 136 |
+
async () => ({
|
| 137 |
+
contents: [
|
| 138 |
+
{
|
| 139 |
+
uri: "ui://widget/cart.html",
|
| 140 |
+
mimeType: RESOURCE_MIME_TYPE,
|
| 141 |
+
text: getCartHtml(),
|
| 142 |
+
},
|
| 143 |
+
],
|
| 144 |
+
}),
|
| 145 |
+
);
|
| 146 |
+
|
| 147 |
+
return server;
|
| 148 |
+
}
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### Key Differences Summary
|
| 152 |
+
|
| 153 |
+
1. **Metadata Structure**: OpenAI uses flat `_meta["openai/..."]` properties; MCP uses nested `_meta.ui.*` structure
|
| 154 |
+
2. **Tool Visibility**: OpenAI uses boolean/string (`true`/`"public"`); MCP uses string arrays (`["app", "model"]`)
|
| 155 |
+
3. **CSP Property Names**: snake_case → camelCase (`connect_domains` → `connectDomains`)
|
| 156 |
+
4. **App Permissions**: MCP adds `_meta.ui.permissions` for camera, microphone, geolocation, clipboard (not in OpenAI)
|
| 157 |
+
5. **Resource MIME Type**: `text/html+skybridge` → `text/html;profile=mcp-app` (use `RESOURCE_MIME_TYPE` constant)
|
| 158 |
+
6. **Helper Functions**: MCP provides `registerAppTool()` and `registerAppResource()` helpers
|
| 159 |
+
7. **Not Yet Implemented**: `_meta["openai/toolInvocation/invoking"]`, `_meta["openai/toolInvocation/invoked"]`, and `_meta["openai/widgetDescription"]` don't have MCP equivalents yet
|
| 160 |
+
|
| 161 |
+
## Client-side
|
| 162 |
+
|
| 163 |
+
### Quick Start Comparison
|
| 164 |
+
|
| 165 |
+
| OpenAI Apps SDK | MCP Apps SDK |
|
| 166 |
+
| --------------------------------- | ---------------------------------- |
|
| 167 |
+
| Implicit global (`window.openai`) | Explicit instance (`new App(...)`) |
|
| 168 |
+
| Properties pre-populated on load | Async connection + notifications |
|
| 169 |
+
| Sync property access | Getters + event handlers |
|
| 170 |
+
|
| 171 |
+
### Setup & Connection
|
| 172 |
+
|
| 173 |
+
| OpenAI | MCP Apps | Notes |
|
| 174 |
+
| -------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
| 175 |
+
| `window.openai` (auto-available) | `const app = new App({name, version}, {})` | MCP requires explicit instantiation |
|
| 176 |
+
| (implicit) | Vanilla: `await app.connect()` / React: `useApp()` | MCP requires async connection; auto-detects OpenAI env |
|
| 177 |
+
| — | `await app.connect(new OpenAITransport())` | Force OpenAI mode (not yet available, see [PR #172](https://github.com/modelcontextprotocol/ext-apps/pull/172)) |
|
| 178 |
+
| — | `await app.connect(new PostMessageTransport(...))` | Force MCP mode explicitly |
|
| 179 |
+
|
| 180 |
+
### Host Context Properties
|
| 181 |
+
|
| 182 |
+
| OpenAI | MCP Apps | Notes |
|
| 183 |
+
| --------------------------- | --------------------------------------------- | --------------------------------------- |
|
| 184 |
+
| `window.openai.theme` | `app.getHostContext()?.theme` | `"light"` \| `"dark"` |
|
| 185 |
+
| `window.openai.locale` | `app.getHostContext()?.locale` | BCP 47 language tag (e.g., `"en-US"`) |
|
| 186 |
+
| `window.openai.displayMode` | `app.getHostContext()?.displayMode` | `"inline"` \| `"pip"` \| `"fullscreen"` |
|
| 187 |
+
| `window.openai.maxHeight` | `app.getHostContext()?.viewport?.maxHeight` | Max container height in px |
|
| 188 |
+
| `window.openai.safeArea` | `app.getHostContext()?.safeAreaInsets` | `{ top, right, bottom, left }` |
|
| 189 |
+
| `window.openai.userAgent` | `app.getHostContext()?.userAgent` | Host user agent string |
|
| 190 |
+
| — | `app.getHostContext()?.availableDisplayModes` | MCP adds: which modes host supports |
|
| 191 |
+
| — | `app.getHostContext()?.toolInfo` | MCP adds: tool metadata during call |
|
| 192 |
+
|
| 193 |
+
### Tool Data (Input/Output)
|
| 194 |
+
|
| 195 |
+
| OpenAI | MCP Apps | Notes |
|
| 196 |
+
| ------------------------------------ | ------------------------------------------------------------- | ----------------------------------- |
|
| 197 |
+
| `window.openai.toolInput` | `app.ontoolinput = (params) => { params.arguments }` | Tool arguments; MCP uses callback |
|
| 198 |
+
| `window.openai.toolOutput` | `app.ontoolresult = (params) => { params.structuredContent }` | Tool result; MCP uses callback |
|
| 199 |
+
| `window.openai.toolResponseMetadata` | `app.ontoolresult` → `params._meta` | Widget-only metadata from server |
|
| 200 |
+
| — | `app.ontoolinputpartial = (params) => {...}` | MCP adds: streaming partial args |
|
| 201 |
+
| — | `app.ontoolcancelled = (params) => {...}` | MCP adds: cancellation notification |
|
| 202 |
+
|
| 203 |
+
### Calling Tools
|
| 204 |
+
|
| 205 |
+
| OpenAI | MCP Apps | Notes |
|
| 206 |
+
| ------------------------------------------ | ----------------------------------------------------- | ---------------------------- |
|
| 207 |
+
| `await window.openai.callTool(name, args)` | `await app.callServerTool({ name, arguments: args })` | Call another MCP server tool |
|
| 208 |
+
|
| 209 |
+
### Sending Messages
|
| 210 |
+
|
| 211 |
+
| OpenAI | MCP Apps | Notes |
|
| 212 |
+
| ----------------------------------------------------- | ------------------------------------------------------------------------------------ | --------------------------------- |
|
| 213 |
+
| `await window.openai.sendFollowUpMessage({ prompt })` | `await app.sendMessage({ role: "user", content: [{ type: "text", text: prompt }] })` | MCP uses structured content array |
|
| 214 |
+
|
| 215 |
+
### External Links
|
| 216 |
+
|
| 217 |
+
| OpenAI | MCP Apps | Notes |
|
| 218 |
+
| -------------------------------------------- | ----------------------------------- | ------------------------------------ |
|
| 219 |
+
| `await window.openai.openExternal({ href })` | `await app.openLink({ url: href })` | Different param name: `href` → `url` |
|
| 220 |
+
|
| 221 |
+
### Display Mode
|
| 222 |
+
|
| 223 |
+
| OpenAI | MCP Apps | Notes |
|
| 224 |
+
| -------------------------------------------------- | --------------------------------------------------------- | ----------------------------------- |
|
| 225 |
+
| `await window.openai.requestDisplayMode({ mode })` | `await app.requestDisplayMode({ mode })` | Same API |
|
| 226 |
+
| — | Check `app.getHostContext()?.availableDisplayModes` first | MCP lets you check what's available |
|
| 227 |
+
|
| 228 |
+
### Size Reporting
|
| 229 |
+
|
| 230 |
+
| OpenAI | MCP Apps | Notes |
|
| 231 |
+
| --------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------- |
|
| 232 |
+
| `window.openai.notifyIntrinsicHeight(height)` | `app.sendSizeChanged({ width, height })` | MCP includes width |
|
| 233 |
+
| Manual only | `new App(appInfo, capabilities, { autoResize: true /* default */ })` | MCP auto-reports via `ResizeObserver` |
|
| 234 |
+
|
| 235 |
+
### State Persistence
|
| 236 |
+
|
| 237 |
+
| OpenAI | MCP Apps | Notes |
|
| 238 |
+
| ------------------------------------- | -------- | -------------------------------------------------------------------- |
|
| 239 |
+
| `window.openai.widgetState` | — | Not directly available in MCP |
|
| 240 |
+
| `window.openai.setWidgetState(state)` | — | Use alternative mechanisms (`localStorage`, server-side state, etc.) |
|
| 241 |
+
|
| 242 |
+
### File Operations (Not Yet in MCP Apps)
|
| 243 |
+
|
| 244 |
+
| OpenAI | MCP Apps | Notes |
|
| 245 |
+
| ---------------------------------------------------- | -------- | ------------------- |
|
| 246 |
+
| `await window.openai.uploadFile(file)` | — | Not yet implemented |
|
| 247 |
+
| `await window.openai.getFileDownloadUrl({ fileId })` | — | Not yet implemented |
|
| 248 |
+
|
| 249 |
+
### Other (Not Yet in MCP Apps)
|
| 250 |
+
|
| 251 |
+
| OpenAI | MCP Apps | Notes |
|
| 252 |
+
| ------------------------------------------- | -------- | ------------------- |
|
| 253 |
+
| `await window.openai.requestModal(options)` | — | Not yet implemented |
|
| 254 |
+
| `window.openai.requestClose()` | — | Not yet implemented |
|
| 255 |
+
| `window.openai.view` | — | Not yet mapped |
|
| 256 |
+
|
| 257 |
+
### Event Handling
|
| 258 |
+
|
| 259 |
+
| OpenAI | MCP Apps | Notes |
|
| 260 |
+
| ------------------------------ | ------------------------------------------- | -------------------------------- |
|
| 261 |
+
| Read `window.openai.*` on load | `app.ontoolinput = (params) => {...}` | Register before `connect()` |
|
| 262 |
+
| Read `window.openai.*` on load | `app.ontoolresult = (params) => {...}` | Register before `connect()` |
|
| 263 |
+
| Poll or re-read properties | `app.onhostcontextchanged = (ctx) => {...}` | MCP pushes context changes |
|
| 264 |
+
| — | `app.onteardown = async () => {...}` | MCP adds: cleanup before unmount |
|
| 265 |
+
|
| 266 |
+
### Logging
|
| 267 |
+
|
| 268 |
+
| OpenAI | MCP Apps | Notes |
|
| 269 |
+
| ------------------ | --------------------------------------------- | ------------------------------- |
|
| 270 |
+
| `console.log(...)` | `app.sendLog({ level: "info", data: "..." })` | MCP provides structured logging |
|
| 271 |
+
|
| 272 |
+
### Host Info
|
| 273 |
+
|
| 274 |
+
| OpenAI | MCP Apps | Notes |
|
| 275 |
+
| ------ | --------------------------- | ------------------------------------------------- |
|
| 276 |
+
| — | `app.getHostVersion()` | Returns `{ name, version }` of host |
|
| 277 |
+
| — | `app.getHostCapabilities()` | Check `serverTools`, `openLinks`, `logging`, etc. |
|
| 278 |
+
|
| 279 |
+
### Full Migration Example
|
| 280 |
+
|
| 281 |
+
#### Before (OpenAI)
|
| 282 |
+
|
| 283 |
+
```typescript
|
| 284 |
+
// OpenAI Apps SDK
|
| 285 |
+
applyTheme(window.openai.theme);
|
| 286 |
+
console.log("Tool args:", window.openai.toolInput);
|
| 287 |
+
console.log("Tool result:", window.openai.toolOutput);
|
| 288 |
+
|
| 289 |
+
// Call a tool
|
| 290 |
+
const result = await window.openai.callTool("get_weather", { city: "Tokyo" });
|
| 291 |
+
|
| 292 |
+
// Send a message
|
| 293 |
+
await window.openai.sendFollowUpMessage({ prompt: "Weather updated!" });
|
| 294 |
+
|
| 295 |
+
// Report height
|
| 296 |
+
window.openai.notifyIntrinsicHeight(400);
|
| 297 |
+
|
| 298 |
+
// Open link
|
| 299 |
+
await window.openai.openExternal({ href: "https://example.com" });
|
| 300 |
+
```
|
| 301 |
+
|
| 302 |
+
#### After (MCP Apps)
|
| 303 |
+
|
| 304 |
+
```typescript
|
| 305 |
+
import { App } from "@modelcontextprotocol/ext-apps";
|
| 306 |
+
|
| 307 |
+
const app = new App({ name: "MyApp", version: "1.0.0" });
|
| 308 |
+
|
| 309 |
+
// Register handlers BEFORE connect (events may occur immediately after connect)
|
| 310 |
+
app.ontoolinput = (params) => {
|
| 311 |
+
console.log("Tool args:", params.arguments);
|
| 312 |
+
};
|
| 313 |
+
|
| 314 |
+
app.ontoolresult = (params) => {
|
| 315 |
+
console.log("Tool result:", params.structuredContent);
|
| 316 |
+
};
|
| 317 |
+
|
| 318 |
+
app.onhostcontextchanged = (ctx) => {
|
| 319 |
+
if (ctx.theme) applyTheme(ctx.theme);
|
| 320 |
+
};
|
| 321 |
+
|
| 322 |
+
// Connect (auto-detects OpenAI vs MCP)
|
| 323 |
+
await app.connect();
|
| 324 |
+
|
| 325 |
+
// Access context
|
| 326 |
+
applyTheme(app.getHostContext()?.theme);
|
| 327 |
+
|
| 328 |
+
// Call a tool
|
| 329 |
+
const result = await app.callServerTool({
|
| 330 |
+
name: "get_weather",
|
| 331 |
+
arguments: { city: "Tokyo" },
|
| 332 |
+
});
|
| 333 |
+
|
| 334 |
+
// Send a message
|
| 335 |
+
await app.sendMessage({
|
| 336 |
+
role: "user",
|
| 337 |
+
content: [{ type: "text", text: "Weather updated!" }],
|
| 338 |
+
});
|
| 339 |
+
|
| 340 |
+
// Open link (note: url not href)
|
| 341 |
+
await app.openLink({ url: "https://example.com" });
|
| 342 |
+
```
|
| 343 |
+
|
| 344 |
+
### Key Differences Summary
|
| 345 |
+
|
| 346 |
+
1. **Initialization**: OpenAI is implicit; MCP requires `new App()` + `await app.connect()`
|
| 347 |
+
2. **Data Flow**: OpenAI pre-populates; MCP uses async notifications (register handlers before `connect()`)
|
| 348 |
+
3. **Auto-resize**: MCP has built-in ResizeObserver support via `autoResize` option
|
| 349 |
+
4. **Structured Content**: MCP uses `{ type: "text", text: "..." }` arrays for messages
|
| 350 |
+
5. **Context Changes**: MCP pushes updates via `onhostcontextchanged`; no polling needed
|
| 351 |
+
6. **Capabilities**: MCP lets you check what the host supports before calling methods
|
docs/quickstart.md
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Quickstart
|
| 3 |
+
---
|
| 4 |
+
|
| 5 |
+
# Build Your First MCP App
|
| 6 |
+
|
| 7 |
+
This tutorial walks you through building an MCP App—a tool with an interactive UI that renders inside MCP hosts like Claude Desktop.
|
| 8 |
+
|
| 9 |
+
## What You'll Build
|
| 10 |
+
|
| 11 |
+
A simple app that fetches the current server time and displays it in a clickable UI. You'll learn the core pattern: **MCP Apps = Tool + UI Resource**.
|
| 12 |
+
|
| 13 |
+
> [!NOTE]
|
| 14 |
+
> The complete example is available at [`examples/basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs).
|
| 15 |
+
|
| 16 |
+
## Prerequisites
|
| 17 |
+
|
| 18 |
+
- Familiarity with MCP concepts, especially [Tools](https://modelcontextprotocol.io/docs/learn/server-concepts#tools) and [Resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources)
|
| 19 |
+
- Familiarity with the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk)
|
| 20 |
+
- Node.js 18+
|
| 21 |
+
|
| 22 |
+
> [!TIP]
|
| 23 |
+
> New to building MCP servers? Start with the [official MCP quickstart guide](https://modelcontextprotocol.io/docs/develop/build-server) to learn the core concepts first.
|
| 24 |
+
|
| 25 |
+
## 1. Project Setup
|
| 26 |
+
|
| 27 |
+
Create a new directory and initialize:
|
| 28 |
+
|
| 29 |
+
```bash
|
| 30 |
+
mkdir my-mcp-app && cd my-mcp-app
|
| 31 |
+
npm init -y
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
Install dependencies:
|
| 35 |
+
|
| 36 |
+
```bash
|
| 37 |
+
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk
|
| 38 |
+
npm install -D typescript vite vite-plugin-singlefile express cors @types/express @types/cors tsx
|
| 39 |
+
```
|
| 40 |
+
|
| 41 |
+
Create `tsconfig.json`:
|
| 42 |
+
|
| 43 |
+
```json
|
| 44 |
+
{
|
| 45 |
+
"compilerOptions": {
|
| 46 |
+
"target": "ES2022",
|
| 47 |
+
"module": "ESNext",
|
| 48 |
+
"moduleResolution": "bundler",
|
| 49 |
+
"strict": true,
|
| 50 |
+
"esModuleInterop": true,
|
| 51 |
+
"skipLibCheck": true,
|
| 52 |
+
"outDir": "dist"
|
| 53 |
+
},
|
| 54 |
+
"include": ["*.ts", "src/**/*.ts"]
|
| 55 |
+
}
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
Create `vite.config.ts` — this bundles your UI into a single HTML file:
|
| 59 |
+
|
| 60 |
+
```typescript
|
| 61 |
+
import { defineConfig } from "vite";
|
| 62 |
+
import { viteSingleFile } from "vite-plugin-singlefile";
|
| 63 |
+
|
| 64 |
+
export default defineConfig({
|
| 65 |
+
plugins: [viteSingleFile()],
|
| 66 |
+
build: {
|
| 67 |
+
outDir: "dist",
|
| 68 |
+
rollupOptions: {
|
| 69 |
+
input: process.env.INPUT,
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
});
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
Add to your `package.json`:
|
| 76 |
+
|
| 77 |
+
```json
|
| 78 |
+
{
|
| 79 |
+
"type": "module",
|
| 80 |
+
"scripts": {
|
| 81 |
+
"build": "INPUT=mcp-app.html vite build",
|
| 82 |
+
"serve": "npx tsx server.ts"
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
```
|
| 86 |
+
|
| 87 |
+
> [!NOTE]
|
| 88 |
+
> **Full files:** [`package.json`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/package.json), [`tsconfig.json`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/tsconfig.json), [`vite.config.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/vite.config.ts)
|
| 89 |
+
|
| 90 |
+
## 2. Create the Server
|
| 91 |
+
|
| 92 |
+
MCP Apps use a **two-part registration**:
|
| 93 |
+
|
| 94 |
+
1. A **tool** that the LLM/host calls
|
| 95 |
+
2. A **resource** that serves the UI HTML
|
| 96 |
+
|
| 97 |
+
The tool's `_meta` field links them together.
|
| 98 |
+
|
| 99 |
+
Create `server.ts`:
|
| 100 |
+
|
| 101 |
+
```typescript
|
| 102 |
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
| 103 |
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
| 104 |
+
import {
|
| 105 |
+
registerAppTool,
|
| 106 |
+
registerAppResource,
|
| 107 |
+
RESOURCE_MIME_TYPE,
|
| 108 |
+
} from "@modelcontextprotocol/ext-apps/server";
|
| 109 |
+
import cors from "cors";
|
| 110 |
+
import express from "express";
|
| 111 |
+
import fs from "node:fs/promises";
|
| 112 |
+
import path from "node:path";
|
| 113 |
+
|
| 114 |
+
const server = new McpServer({
|
| 115 |
+
name: "My MCP App Server",
|
| 116 |
+
version: "1.0.0",
|
| 117 |
+
});
|
| 118 |
+
|
| 119 |
+
// Two-part registration: tool + resource, tied together by the resource URI.
|
| 120 |
+
const resourceUri = "ui://get-time/mcp-app.html";
|
| 121 |
+
|
| 122 |
+
// Register a tool with UI metadata. When the host calls this tool, it reads
|
| 123 |
+
// `_meta.ui.resourceUri` to know which resource to fetch and render as an
|
| 124 |
+
// interactive UI.
|
| 125 |
+
registerAppTool(
|
| 126 |
+
server,
|
| 127 |
+
"get-time",
|
| 128 |
+
{
|
| 129 |
+
title: "Get Time",
|
| 130 |
+
description: "Returns the current server time.",
|
| 131 |
+
inputSchema: {},
|
| 132 |
+
_meta: { ui: { resourceUri } },
|
| 133 |
+
},
|
| 134 |
+
async () => {
|
| 135 |
+
const time = new Date().toISOString();
|
| 136 |
+
return {
|
| 137 |
+
content: [{ type: "text", text: time }],
|
| 138 |
+
};
|
| 139 |
+
},
|
| 140 |
+
);
|
| 141 |
+
|
| 142 |
+
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
|
| 143 |
+
registerAppResource(
|
| 144 |
+
server,
|
| 145 |
+
resourceUri,
|
| 146 |
+
resourceUri,
|
| 147 |
+
{ mimeType: RESOURCE_MIME_TYPE },
|
| 148 |
+
async () => {
|
| 149 |
+
const html = await fs.readFile(
|
| 150 |
+
path.join(import.meta.dirname, "dist", "mcp-app.html"),
|
| 151 |
+
"utf-8",
|
| 152 |
+
);
|
| 153 |
+
return {
|
| 154 |
+
contents: [
|
| 155 |
+
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
|
| 156 |
+
],
|
| 157 |
+
};
|
| 158 |
+
},
|
| 159 |
+
);
|
| 160 |
+
|
| 161 |
+
// Start an Express server that exposes the MCP endpoint.
|
| 162 |
+
const expressApp = express();
|
| 163 |
+
expressApp.use(cors());
|
| 164 |
+
expressApp.use(express.json());
|
| 165 |
+
|
| 166 |
+
expressApp.post("/mcp", async (req, res) => {
|
| 167 |
+
const transport = new StreamableHTTPServerTransport({
|
| 168 |
+
sessionIdGenerator: undefined,
|
| 169 |
+
enableJsonResponse: true,
|
| 170 |
+
});
|
| 171 |
+
res.on("close", () => transport.close());
|
| 172 |
+
await server.connect(transport);
|
| 173 |
+
await transport.handleRequest(req, res, req.body);
|
| 174 |
+
});
|
| 175 |
+
|
| 176 |
+
expressApp.listen(3001, (err) => {
|
| 177 |
+
if (err) {
|
| 178 |
+
console.error("Error starting server:", err);
|
| 179 |
+
process.exit(1);
|
| 180 |
+
}
|
| 181 |
+
console.log("Server listening on http://localhost:3001/mcp");
|
| 182 |
+
});
|
| 183 |
+
```
|
| 184 |
+
|
| 185 |
+
> [!NOTE]
|
| 186 |
+
> **Full file:** [`server.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/server.ts)
|
| 187 |
+
|
| 188 |
+
Then, verify your server compiles:
|
| 189 |
+
|
| 190 |
+
```bash
|
| 191 |
+
npx tsc --noEmit
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
No output means success. If you see errors, check for typos in `server.ts`.
|
| 195 |
+
|
| 196 |
+
## 3. Build the UI
|
| 197 |
+
|
| 198 |
+
Create `mcp-app.html`:
|
| 199 |
+
|
| 200 |
+
```html
|
| 201 |
+
<!DOCTYPE html>
|
| 202 |
+
<html lang="en">
|
| 203 |
+
<head>
|
| 204 |
+
<meta charset="UTF-8" />
|
| 205 |
+
<title>Get Time App</title>
|
| 206 |
+
</head>
|
| 207 |
+
<body>
|
| 208 |
+
<p>
|
| 209 |
+
<strong>Server Time:</strong> <code id="server-time">Loading...</code>
|
| 210 |
+
</p>
|
| 211 |
+
<button id="get-time-btn">Get Server Time</button>
|
| 212 |
+
<script type="module" src="/src/mcp-app.ts"></script>
|
| 213 |
+
</body>
|
| 214 |
+
</html>
|
| 215 |
+
```
|
| 216 |
+
|
| 217 |
+
Create `src/mcp-app.ts`:
|
| 218 |
+
|
| 219 |
+
```typescript
|
| 220 |
+
import { App } from "@modelcontextprotocol/ext-apps";
|
| 221 |
+
|
| 222 |
+
// Get element references
|
| 223 |
+
const serverTimeEl = document.getElementById("server-time")!;
|
| 224 |
+
const getTimeBtn = document.getElementById("get-time-btn")!;
|
| 225 |
+
|
| 226 |
+
// Create app instance
|
| 227 |
+
const app = new App({ name: "Get Time App", version: "1.0.0" });
|
| 228 |
+
|
| 229 |
+
// Register handlers BEFORE connecting
|
| 230 |
+
app.ontoolresult = (result) => {
|
| 231 |
+
const time = result.content?.find((c) => c.type === "text")?.text;
|
| 232 |
+
serverTimeEl.textContent = time ?? "[ERROR]";
|
| 233 |
+
};
|
| 234 |
+
|
| 235 |
+
// Wire up button click
|
| 236 |
+
getTimeBtn.addEventListener("click", async () => {
|
| 237 |
+
const result = await app.callServerTool({ name: "get-time", arguments: {} });
|
| 238 |
+
const time = result.content?.find((c) => c.type === "text")?.text;
|
| 239 |
+
serverTimeEl.textContent = time ?? "[ERROR]";
|
| 240 |
+
});
|
| 241 |
+
|
| 242 |
+
// Connect to host
|
| 243 |
+
app.connect();
|
| 244 |
+
```
|
| 245 |
+
|
| 246 |
+
> [!NOTE]
|
| 247 |
+
> **Full files:** [`mcp-app.html`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/mcp-app.html), [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/src/mcp-app.ts)
|
| 248 |
+
|
| 249 |
+
Build the UI:
|
| 250 |
+
|
| 251 |
+
```bash
|
| 252 |
+
npm run build
|
| 253 |
+
```
|
| 254 |
+
|
| 255 |
+
This produces `dist/mcp-app.html` which contains your bundled app:
|
| 256 |
+
|
| 257 |
+
```console
|
| 258 |
+
$ ls dist/mcp-app.html
|
| 259 |
+
dist/mcp-app.html
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
## 4. Test It
|
| 263 |
+
|
| 264 |
+
You'll need two terminals.
|
| 265 |
+
|
| 266 |
+
**Terminal 1** — Build and start your server:
|
| 267 |
+
|
| 268 |
+
```bash
|
| 269 |
+
npm run build && npm run serve
|
| 270 |
+
```
|
| 271 |
+
|
| 272 |
+
**Terminal 2** — Run the test host (from the [ext-apps repo](https://github.com/modelcontextprotocol/ext-apps)):
|
| 273 |
+
|
| 274 |
+
```bash
|
| 275 |
+
git clone https://github.com/modelcontextprotocol/ext-apps.git
|
| 276 |
+
cd ext-apps/examples/basic-host
|
| 277 |
+
npm install
|
| 278 |
+
npm run start
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
Open http://localhost:8080 in your browser:
|
| 282 |
+
|
| 283 |
+
1. Select **get-time** from the "Tool Name" dropdown
|
| 284 |
+
2. Click **Call Tool**
|
| 285 |
+
3. Your UI renders in the sandbox below
|
| 286 |
+
4. Click **Get Server Time** — the current time appears!
|
| 287 |
+
|
| 288 |
+
## Next Steps
|
| 289 |
+
|
| 290 |
+
- **Host communication**: Add [`sendMessage()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), and [`sendOpenLink()`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendopenlink) to interact with the host — see [`src/mcp-app.ts`](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/basic-server-vanillajs/src/mcp-app.ts)
|
| 291 |
+
- **React version**: Compare with [`basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for a React-based UI
|
| 292 |
+
- **API reference**: See the full [API documentation](https://modelcontextprotocol.github.io/ext-apps/api/)
|
examples/basic-host/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
examples/basic-host/README.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Example: Basic Host
|
| 2 |
+
|
| 3 |
+
A reference implementation showing how to build an MCP host application that connects to MCP servers and renders tool UIs in a secure sandbox.
|
| 4 |
+
|
| 5 |
+
This basic host can also be used to test MCP Apps during local development.
|
| 6 |
+
|
| 7 |
+
## Key Files
|
| 8 |
+
|
| 9 |
+
- [`index.html`](index.html) / [`src/index.tsx`](src/index.tsx) - React UI host with tool selection, parameter input, and iframe management
|
| 10 |
+
- [`sandbox.html`](sandbox.html) / [`src/sandbox.ts`](src/sandbox.ts) - Outer iframe proxy with security validation and bidirectional message relay
|
| 11 |
+
- [`src/implementation.ts`](src/implementation.ts) - Core logic: server connection, tool calling, and AppBridge setup
|
| 12 |
+
|
| 13 |
+
## Getting Started
|
| 14 |
+
|
| 15 |
+
```bash
|
| 16 |
+
npm install
|
| 17 |
+
npm run start
|
| 18 |
+
# Open http://localhost:8080
|
| 19 |
+
```
|
| 20 |
+
|
| 21 |
+
By default, the host application will try to connect to an MCP server at `http://localhost:3001/mcp`. You can configure this behavior by setting the `SERVERS` environment variable with a JSON array of server URLs:
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
SERVERS='["http://localhost:1234/mcp", "http://localhost:5678/mcp"]' npm run start
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
## Architecture
|
| 28 |
+
|
| 29 |
+
This example uses a double-iframe sandbox pattern for secure UI isolation:
|
| 30 |
+
|
| 31 |
+
```
|
| 32 |
+
Host (port 8080)
|
| 33 |
+
└── Outer iframe (port 8081) - sandbox proxy
|
| 34 |
+
└── Inner iframe (srcdoc) - untrusted tool UI
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
**Why two iframes?**
|
| 38 |
+
|
| 39 |
+
- The outer iframe runs on a separate origin (port 8081) preventing direct access to the host
|
| 40 |
+
- The inner iframe receives HTML via `srcdoc` and is restricted by sandbox attributes
|
| 41 |
+
- Messages flow through the outer iframe which validates and relays them bidirectionally
|
| 42 |
+
|
| 43 |
+
This architecture ensures that even if tool UI code is malicious, it cannot access the host application's DOM, cookies, or JavaScript context.
|
examples/basic-host/index.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<meta name="color-scheme" content="light dark">
|
| 7 |
+
<title>MCP Apps Host</title>
|
| 8 |
+
<link rel="stylesheet" href="/src/global.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="root"></div>
|
| 12 |
+
<script src="/src/index.tsx" type="module"></script>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
examples/basic-host/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host",
|
| 3 |
+
"name": "@modelcontextprotocol/ext-apps-basic-host",
|
| 4 |
+
"version": "0.4.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"",
|
| 8 |
+
"watch": "concurrently \"cross-env INPUT=index.html vite build --watch\" \"cross-env INPUT=sandbox.html vite build --watch\"",
|
| 9 |
+
"serve": "bun --watch serve.ts",
|
| 10 |
+
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
|
| 11 |
+
"dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\""
|
| 12 |
+
},
|
| 13 |
+
"dependencies": {
|
| 14 |
+
"@modelcontextprotocol/ext-apps": "../..",
|
| 15 |
+
"@modelcontextprotocol/sdk": "^1.24.0",
|
| 16 |
+
"react": "^19.2.0",
|
| 17 |
+
"react-dom": "^19.2.0",
|
| 18 |
+
"zod": "^4.1.13"
|
| 19 |
+
},
|
| 20 |
+
"devDependencies": {
|
| 21 |
+
"@types/express": "^5.0.0",
|
| 22 |
+
"@types/node": "^22.0.0",
|
| 23 |
+
"@types/react": "^19.2.2",
|
| 24 |
+
"@types/react-dom": "^19.2.2",
|
| 25 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 26 |
+
"concurrently": "^9.2.1",
|
| 27 |
+
"cors": "^2.8.5",
|
| 28 |
+
"express": "^5.1.0",
|
| 29 |
+
"prettier": "^3.6.2",
|
| 30 |
+
"vite": "^6.0.0",
|
| 31 |
+
"vite-plugin-singlefile": "^2.3.0",
|
| 32 |
+
"typescript": "^5.9.3",
|
| 33 |
+
"vitest": "^3.2.4"
|
| 34 |
+
}
|
| 35 |
+
}
|
examples/basic-host/sandbox.html
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html>
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8" />
|
| 5 |
+
<meta name="color-scheme" content="light dark">
|
| 6 |
+
<!-- CSP is set via HTTP headers by serve.ts (based on ?csp= query param).
|
| 7 |
+
The inner iframe inherits this CSP since we use document.write(). -->
|
| 8 |
+
<title>MCP-UI Proxy</title>
|
| 9 |
+
<style>
|
| 10 |
+
html,
|
| 11 |
+
body {
|
| 12 |
+
margin: 0;
|
| 13 |
+
height: 100vh;
|
| 14 |
+
width: 100vw;
|
| 15 |
+
}
|
| 16 |
+
body {
|
| 17 |
+
display: flex;
|
| 18 |
+
flex-direction: column;
|
| 19 |
+
}
|
| 20 |
+
* {
|
| 21 |
+
box-sizing: border-box;
|
| 22 |
+
}
|
| 23 |
+
iframe {
|
| 24 |
+
background-color: transparent;
|
| 25 |
+
border: 0px none transparent;
|
| 26 |
+
padding: 0px;
|
| 27 |
+
overflow: hidden;
|
| 28 |
+
flex-grow: 1;
|
| 29 |
+
}
|
| 30 |
+
</style>
|
| 31 |
+
</head>
|
| 32 |
+
<body>
|
| 33 |
+
<script type="module" src="/src/sandbox.ts"></script>
|
| 34 |
+
</body>
|
| 35 |
+
</html>
|
examples/basic-host/serve.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env npx tsx
|
| 2 |
+
/**
|
| 3 |
+
* HTTP servers for the MCP UI example:
|
| 4 |
+
* - Host server (port 8080): serves host HTML files (React and Vanilla examples)
|
| 5 |
+
* - Sandbox server (port 8081): serves sandbox.html with CSP headers
|
| 6 |
+
*
|
| 7 |
+
* Running on separate ports ensures proper origin isolation for security.
|
| 8 |
+
*
|
| 9 |
+
* Security: CSP is set via HTTP headers based on ?csp= query param.
|
| 10 |
+
* This ensures content cannot tamper with CSP (unlike meta tags).
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
import express from "express";
|
| 14 |
+
import cors from "cors";
|
| 15 |
+
import { fileURLToPath } from "url";
|
| 16 |
+
import { dirname, join } from "path";
|
| 17 |
+
import type { McpUiResourceCsp } from "@modelcontextprotocol/ext-apps";
|
| 18 |
+
|
| 19 |
+
const __filename = fileURLToPath(import.meta.url);
|
| 20 |
+
const __dirname = dirname(__filename);
|
| 21 |
+
|
| 22 |
+
const HOST_PORT = parseInt(process.env.HOST_PORT || "8080", 10);
|
| 23 |
+
const SANDBOX_PORT = parseInt(process.env.SANDBOX_PORT || "8081", 10);
|
| 24 |
+
const DIRECTORY = join(__dirname, "dist");
|
| 25 |
+
const SERVERS: string[] = process.env.SERVERS
|
| 26 |
+
? JSON.parse(process.env.SERVERS)
|
| 27 |
+
: ["http://localhost:3001/mcp"];
|
| 28 |
+
|
| 29 |
+
// ============ Host Server (port 8080) ============
|
| 30 |
+
const hostApp = express();
|
| 31 |
+
hostApp.use(cors());
|
| 32 |
+
|
| 33 |
+
// Exclude sandbox.html from host server
|
| 34 |
+
hostApp.use((req, res, next) => {
|
| 35 |
+
if (req.path === "/sandbox.html") {
|
| 36 |
+
res.status(404).send("Sandbox is served on a different port");
|
| 37 |
+
return;
|
| 38 |
+
}
|
| 39 |
+
next();
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
hostApp.use(express.static(DIRECTORY));
|
| 43 |
+
|
| 44 |
+
// API endpoint to get configured server URLs
|
| 45 |
+
hostApp.get("/api/servers", (_req, res) => {
|
| 46 |
+
res.json(SERVERS);
|
| 47 |
+
});
|
| 48 |
+
|
| 49 |
+
hostApp.get("/", (_req, res) => {
|
| 50 |
+
res.redirect("/index.html");
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
// ============ Sandbox Server (port 8081) ============
|
| 54 |
+
const sandboxApp = express();
|
| 55 |
+
sandboxApp.use(cors());
|
| 56 |
+
|
| 57 |
+
// Validate CSP domain entries to prevent injection attacks.
|
| 58 |
+
// Rejects entries containing characters that could:
|
| 59 |
+
// - `;` or newlines: break out to new CSP directive
|
| 60 |
+
// - quotes: inject CSP keywords like 'unsafe-eval'
|
| 61 |
+
// - space: inject multiple sources in one entry
|
| 62 |
+
function sanitizeCspDomains(domains?: string[]): string[] {
|
| 63 |
+
if (!domains) return [];
|
| 64 |
+
return domains.filter((d) => typeof d === "string" && !/[;\r\n'" ]/.test(d));
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
function buildCspHeader(csp?: McpUiResourceCsp): string {
|
| 68 |
+
const resourceDomains = sanitizeCspDomains(csp?.resourceDomains).join(" ");
|
| 69 |
+
const connectDomains = sanitizeCspDomains(csp?.connectDomains).join(" ");
|
| 70 |
+
const frameDomains = sanitizeCspDomains(csp?.frameDomains).join(" ") || null;
|
| 71 |
+
const baseUriDomains =
|
| 72 |
+
sanitizeCspDomains(csp?.baseUriDomains).join(" ") || null;
|
| 73 |
+
|
| 74 |
+
const directives = [
|
| 75 |
+
// Default: allow same-origin + inline styles/scripts (needed for bundled apps)
|
| 76 |
+
"default-src 'self' 'unsafe-inline'",
|
| 77 |
+
// Scripts: same-origin + inline + eval (some libs need eval) + blob (workers) + specified domains
|
| 78 |
+
`script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: ${resourceDomains}`.trim(),
|
| 79 |
+
// Styles: same-origin + inline + specified domains
|
| 80 |
+
`style-src 'self' 'unsafe-inline' blob: data: ${resourceDomains}`.trim(),
|
| 81 |
+
// Images: same-origin + data/blob URIs + specified domains
|
| 82 |
+
`img-src 'self' data: blob: ${resourceDomains}`.trim(),
|
| 83 |
+
// Fonts: same-origin + data/blob URIs + specified domains
|
| 84 |
+
`font-src 'self' data: blob: ${resourceDomains}`.trim(),
|
| 85 |
+
// Network requests: same-origin + specified API/tile domains
|
| 86 |
+
`connect-src 'self' ${connectDomains}`.trim(),
|
| 87 |
+
// Workers: same-origin + blob (dynamic workers) + specified domains
|
| 88 |
+
// This is critical for WebGL apps (CesiumJS, Three.js) that use workers for:
|
| 89 |
+
// - Tile decoding and terrain processing
|
| 90 |
+
// - Image processing and texture loading
|
| 91 |
+
// - Physics and geometry calculations
|
| 92 |
+
`worker-src 'self' blob: ${resourceDomains}`.trim(),
|
| 93 |
+
// Nested iframes: use frameDomains if provided, otherwise block all
|
| 94 |
+
frameDomains ? `frame-src ${frameDomains}` : "frame-src 'none'",
|
| 95 |
+
// Plugins: always blocked (defense in depth)
|
| 96 |
+
"object-src 'none'",
|
| 97 |
+
// Base URI: use baseUriDomains if provided, otherwise block all
|
| 98 |
+
baseUriDomains ? `base-uri ${baseUriDomains}` : "base-uri 'none'",
|
| 99 |
+
];
|
| 100 |
+
|
| 101 |
+
return directives.join("; ");
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
// Serve sandbox.html with CSP from query params
|
| 105 |
+
sandboxApp.get(["/", "/sandbox.html"], (req, res) => {
|
| 106 |
+
// Parse CSP config from query param: ?csp=<url-encoded-json>
|
| 107 |
+
let cspConfig: McpUiResourceCsp | undefined;
|
| 108 |
+
if (typeof req.query.csp === "string") {
|
| 109 |
+
try {
|
| 110 |
+
cspConfig = JSON.parse(req.query.csp);
|
| 111 |
+
} catch (e) {
|
| 112 |
+
console.warn("[Sandbox] Invalid CSP query param:", e);
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
// Set CSP via HTTP header - tamper-proof unlike meta tags
|
| 117 |
+
const cspHeader = buildCspHeader(cspConfig);
|
| 118 |
+
res.setHeader("Content-Security-Policy", cspHeader);
|
| 119 |
+
|
| 120 |
+
// Prevent caching to ensure fresh CSP on each load
|
| 121 |
+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
| 122 |
+
res.setHeader("Pragma", "no-cache");
|
| 123 |
+
res.setHeader("Expires", "0");
|
| 124 |
+
|
| 125 |
+
res.sendFile(join(DIRECTORY, "sandbox.html"));
|
| 126 |
+
});
|
| 127 |
+
|
| 128 |
+
sandboxApp.use((_req, res) => {
|
| 129 |
+
res.status(404).send("Only sandbox.html is served on this port");
|
| 130 |
+
});
|
| 131 |
+
|
| 132 |
+
// ============ Start both servers ============
|
| 133 |
+
hostApp.listen(HOST_PORT, (err) => {
|
| 134 |
+
if (err) {
|
| 135 |
+
console.error("Error starting server:", err);
|
| 136 |
+
process.exit(1);
|
| 137 |
+
}
|
| 138 |
+
console.log(`Host server: http://localhost:${HOST_PORT}`);
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
sandboxApp.listen(SANDBOX_PORT, (err) => {
|
| 142 |
+
if (err) {
|
| 143 |
+
console.error("Error starting server:", err);
|
| 144 |
+
process.exit(1);
|
| 145 |
+
}
|
| 146 |
+
console.log(`Sandbox server: http://localhost:${SANDBOX_PORT}`);
|
| 147 |
+
console.log("\nPress Ctrl+C to stop\n");
|
| 148 |
+
});
|
examples/basic-host/src/global.css
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
box-sizing: border-box;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
html, body {
|
| 6 |
+
font-family: system-ui, -apple-system, sans-serif;
|
| 7 |
+
font-size: 1rem;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
code {
|
| 11 |
+
font-size: 1em;
|
| 12 |
+
}
|
examples/basic-host/src/implementation.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
|
| 2 |
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
| 3 |
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
| 4 |
+
import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
const SANDBOX_PROXY_BASE_URL = "http://localhost:8081/sandbox.html";
|
| 8 |
+
const IMPLEMENTATION = { name: "MCP Apps Host", version: "1.0.0" };
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
export const log = {
|
| 12 |
+
info: console.log.bind(console, "[HOST]"),
|
| 13 |
+
warn: console.warn.bind(console, "[HOST]"),
|
| 14 |
+
error: console.error.bind(console, "[HOST]"),
|
| 15 |
+
};
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
export interface ServerInfo {
|
| 19 |
+
name: string;
|
| 20 |
+
client: Client;
|
| 21 |
+
tools: Map<string, Tool>;
|
| 22 |
+
appHtmlCache: Map<string, string>;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
export async function connectToServer(serverUrl: URL): Promise<ServerInfo> {
|
| 27 |
+
const client = new Client(IMPLEMENTATION);
|
| 28 |
+
|
| 29 |
+
log.info("Connecting to server:", serverUrl.href);
|
| 30 |
+
await client.connect(new StreamableHTTPClientTransport(serverUrl));
|
| 31 |
+
log.info("Connection successful");
|
| 32 |
+
|
| 33 |
+
const name = client.getServerVersion()?.name ?? serverUrl.href;
|
| 34 |
+
|
| 35 |
+
const toolsList = await client.listTools();
|
| 36 |
+
const tools = new Map(toolsList.tools.map((tool) => [tool.name, tool]));
|
| 37 |
+
log.info("Server tools:", Array.from(tools.keys()));
|
| 38 |
+
|
| 39 |
+
return { name, client, tools, appHtmlCache: new Map() };
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
interface UiResourceData {
|
| 44 |
+
html: string;
|
| 45 |
+
csp?: McpUiResourceCsp;
|
| 46 |
+
permissions?: McpUiResourcePermissions;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
export interface ToolCallInfo {
|
| 50 |
+
serverInfo: ServerInfo;
|
| 51 |
+
tool: Tool;
|
| 52 |
+
input: Record<string, unknown>;
|
| 53 |
+
resultPromise: Promise<CallToolResult>;
|
| 54 |
+
appResourcePromise?: Promise<UiResourceData>;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
export function hasAppHtml(toolCallInfo: ToolCallInfo): toolCallInfo is Required<ToolCallInfo> {
|
| 59 |
+
return !!toolCallInfo.appResourcePromise;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
export function callTool(
|
| 64 |
+
serverInfo: ServerInfo,
|
| 65 |
+
name: string,
|
| 66 |
+
input: Record<string, unknown>,
|
| 67 |
+
): ToolCallInfo {
|
| 68 |
+
log.info("Calling tool", name, "with input", input);
|
| 69 |
+
const resultPromise = serverInfo.client.callTool({ name, arguments: input }) as Promise<CallToolResult>;
|
| 70 |
+
|
| 71 |
+
const tool = serverInfo.tools.get(name);
|
| 72 |
+
if (!tool) {
|
| 73 |
+
throw new Error(`Unknown tool: ${name}`);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
const toolCallInfo: ToolCallInfo = { serverInfo, tool, input, resultPromise };
|
| 77 |
+
|
| 78 |
+
const uiResourceUri = getToolUiResourceUri(tool);
|
| 79 |
+
if (uiResourceUri) {
|
| 80 |
+
toolCallInfo.appResourcePromise = getUiResource(serverInfo, uiResourceUri);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
return toolCallInfo;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
async function getUiResource(serverInfo: ServerInfo, uri: string): Promise<UiResourceData> {
|
| 88 |
+
log.info("Reading UI resource:", uri);
|
| 89 |
+
const resource = await serverInfo.client.readResource({ uri });
|
| 90 |
+
|
| 91 |
+
if (!resource) {
|
| 92 |
+
throw new Error(`Resource not found: ${uri}`);
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
if (resource.contents.length !== 1) {
|
| 96 |
+
throw new Error(`Unexpected contents count: ${resource.contents.length}`);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
const content = resource.contents[0];
|
| 100 |
+
|
| 101 |
+
// Per the MCP App specification, "text/html;profile=mcp-app" signals this
|
| 102 |
+
// resource is indeed for an MCP App UI.
|
| 103 |
+
if (content.mimeType !== RESOURCE_MIME_TYPE) {
|
| 104 |
+
throw new Error(`Unsupported MIME type: ${content.mimeType}`);
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
const html = "blob" in content ? atob(content.blob) : content.text;
|
| 108 |
+
|
| 109 |
+
// Extract CSP and permissions metadata from resource content._meta.ui (or content.meta for Python SDK)
|
| 110 |
+
log.info("Resource content keys:", Object.keys(content));
|
| 111 |
+
log.info("Resource content._meta:", (content as any)._meta);
|
| 112 |
+
|
| 113 |
+
// Try both _meta (spec) and meta (Python SDK quirk)
|
| 114 |
+
const contentMeta = (content as any)._meta || (content as any).meta;
|
| 115 |
+
const csp = contentMeta?.ui?.csp;
|
| 116 |
+
const permissions = contentMeta?.ui?.permissions;
|
| 117 |
+
|
| 118 |
+
return { html, csp, permissions };
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
export function loadSandboxProxy(
|
| 123 |
+
iframe: HTMLIFrameElement,
|
| 124 |
+
csp?: McpUiResourceCsp,
|
| 125 |
+
permissions?: McpUiResourcePermissions,
|
| 126 |
+
): Promise<boolean> {
|
| 127 |
+
// Prevent reload
|
| 128 |
+
if (iframe.src) return Promise.resolve(false);
|
| 129 |
+
|
| 130 |
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
| 131 |
+
|
| 132 |
+
// Set Permission Policy allow attribute based on requested permissions
|
| 133 |
+
const allowAttribute = buildAllowAttribute(permissions);
|
| 134 |
+
if (allowAttribute) {
|
| 135 |
+
iframe.setAttribute("allow", allowAttribute);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
const readyNotification: McpUiSandboxProxyReadyNotification["method"] =
|
| 139 |
+
"ui/notifications/sandbox-proxy-ready";
|
| 140 |
+
|
| 141 |
+
const readyPromise = new Promise<boolean>((resolve) => {
|
| 142 |
+
const listener = ({ source, data }: MessageEvent) => {
|
| 143 |
+
if (source === iframe.contentWindow && data?.method === readyNotification) {
|
| 144 |
+
log.info("Sandbox proxy loaded")
|
| 145 |
+
window.removeEventListener("message", listener);
|
| 146 |
+
resolve(true);
|
| 147 |
+
}
|
| 148 |
+
};
|
| 149 |
+
window.addEventListener("message", listener);
|
| 150 |
+
});
|
| 151 |
+
|
| 152 |
+
// Build sandbox URL with CSP query param for HTTP header-based CSP
|
| 153 |
+
const sandboxUrl = new URL(SANDBOX_PROXY_BASE_URL);
|
| 154 |
+
if (csp) {
|
| 155 |
+
sandboxUrl.searchParams.set("csp", JSON.stringify(csp));
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
log.info("Loading sandbox proxy...", csp ? `(CSP: ${JSON.stringify(csp)})` : "");
|
| 159 |
+
iframe.src = sandboxUrl.href;
|
| 160 |
+
|
| 161 |
+
return readyPromise;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
export async function initializeApp(
|
| 166 |
+
iframe: HTMLIFrameElement,
|
| 167 |
+
appBridge: AppBridge,
|
| 168 |
+
{ input, resultPromise, appResourcePromise }: Required<ToolCallInfo>,
|
| 169 |
+
): Promise<void> {
|
| 170 |
+
const appInitializedPromise = hookInitializedCallback(appBridge);
|
| 171 |
+
|
| 172 |
+
// Connect app bridge (triggers MCP initialization handshake)
|
| 173 |
+
//
|
| 174 |
+
// IMPORTANT: Pass `iframe.contentWindow` as BOTH target and source to ensure
|
| 175 |
+
// this proxy only responds to messages from its specific iframe.
|
| 176 |
+
await appBridge.connect(
|
| 177 |
+
new PostMessageTransport(iframe.contentWindow!, iframe.contentWindow!),
|
| 178 |
+
);
|
| 179 |
+
|
| 180 |
+
// Load inner iframe HTML with CSP and permissions metadata
|
| 181 |
+
const { html, csp, permissions } = await appResourcePromise;
|
| 182 |
+
log.info("Sending UI resource HTML to MCP App", csp ? `(CSP: ${JSON.stringify(csp)})` : "", permissions ? `(Permissions: ${JSON.stringify(permissions)})` : "");
|
| 183 |
+
await appBridge.sendSandboxResourceReady({ html, csp, permissions });
|
| 184 |
+
|
| 185 |
+
// Wait for inner iframe to be ready
|
| 186 |
+
log.info("Waiting for MCP App to initialize...");
|
| 187 |
+
await appInitializedPromise;
|
| 188 |
+
log.info("MCP App initialized");
|
| 189 |
+
|
| 190 |
+
// Send tool call input to iframe
|
| 191 |
+
log.info("Sending tool call input to MCP App:", input);
|
| 192 |
+
appBridge.sendToolInput({ arguments: input });
|
| 193 |
+
|
| 194 |
+
// Schedule tool call result (or cancellation) to be sent to MCP App
|
| 195 |
+
resultPromise.then(
|
| 196 |
+
(result) => {
|
| 197 |
+
log.info("Sending tool call result to MCP App:", result);
|
| 198 |
+
appBridge.sendToolResult(result);
|
| 199 |
+
},
|
| 200 |
+
(error) => {
|
| 201 |
+
log.error("Tool call failed, sending cancellation to MCP App:", error);
|
| 202 |
+
appBridge.sendToolCancelled({
|
| 203 |
+
reason: error instanceof Error ? error.message : String(error),
|
| 204 |
+
});
|
| 205 |
+
},
|
| 206 |
+
);
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
/**
|
| 210 |
+
* Hooks into `AppBridge.oninitialized` and returns a Promise that resolves when
|
| 211 |
+
* the MCP App is initialized (i.e., when the inner iframe is ready).
|
| 212 |
+
*/
|
| 213 |
+
function hookInitializedCallback(appBridge: AppBridge): Promise<void> {
|
| 214 |
+
const oninitialized = appBridge.oninitialized;
|
| 215 |
+
return new Promise<void>((resolve) => {
|
| 216 |
+
appBridge.oninitialized = (...args) => {
|
| 217 |
+
resolve();
|
| 218 |
+
appBridge.oninitialized = oninitialized;
|
| 219 |
+
appBridge.oninitialized?.(...args);
|
| 220 |
+
};
|
| 221 |
+
});
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
export type ModelContext = McpUiUpdateModelContextRequest["params"];
|
| 226 |
+
export type AppMessage = McpUiMessageRequest["params"];
|
| 227 |
+
|
| 228 |
+
export interface AppBridgeCallbacks {
|
| 229 |
+
onContextUpdate?: (context: ModelContext | null) => void;
|
| 230 |
+
onMessage?: (message: AppMessage) => void;
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
export function newAppBridge(
|
| 234 |
+
serverInfo: ServerInfo,
|
| 235 |
+
iframe: HTMLIFrameElement,
|
| 236 |
+
callbacks?: AppBridgeCallbacks,
|
| 237 |
+
): AppBridge {
|
| 238 |
+
const serverCapabilities = serverInfo.client.getServerCapabilities();
|
| 239 |
+
const appBridge = new AppBridge(serverInfo.client, IMPLEMENTATION, {
|
| 240 |
+
openLinks: {},
|
| 241 |
+
serverTools: serverCapabilities?.tools,
|
| 242 |
+
serverResources: serverCapabilities?.resources,
|
| 243 |
+
// Declare support for model context updates
|
| 244 |
+
updateModelContext: { text: {} },
|
| 245 |
+
});
|
| 246 |
+
|
| 247 |
+
// Register all handlers before calling connect(). The Guest UI can start
|
| 248 |
+
// sending requests immediately after the initialization handshake, so any
|
| 249 |
+
// handlers registered after connect() might miss early requests.
|
| 250 |
+
|
| 251 |
+
appBridge.onmessage = async (params, _extra) => {
|
| 252 |
+
log.info("Message from MCP App:", params);
|
| 253 |
+
callbacks?.onMessage?.(params);
|
| 254 |
+
return {};
|
| 255 |
+
};
|
| 256 |
+
|
| 257 |
+
appBridge.onopenlink = async (params, _extra) => {
|
| 258 |
+
log.info("Open link request:", params);
|
| 259 |
+
window.open(params.url, "_blank", "noopener,noreferrer");
|
| 260 |
+
return {};
|
| 261 |
+
};
|
| 262 |
+
|
| 263 |
+
appBridge.onloggingmessage = (params) => {
|
| 264 |
+
log.info("Log message from MCP App:", params);
|
| 265 |
+
};
|
| 266 |
+
|
| 267 |
+
appBridge.onupdatemodelcontext = async (params) => {
|
| 268 |
+
log.info("Model context update from MCP App:", params);
|
| 269 |
+
// Normalize: empty content array means clear context
|
| 270 |
+
const hasContent = params.content && params.content.length > 0;
|
| 271 |
+
const hasStructured = params.structuredContent && Object.keys(params.structuredContent).length > 0;
|
| 272 |
+
callbacks?.onContextUpdate?.(hasContent || hasStructured ? params : null);
|
| 273 |
+
return {};
|
| 274 |
+
};
|
| 275 |
+
|
| 276 |
+
appBridge.onsizechange = async ({ width, height }) => {
|
| 277 |
+
// The MCP App has requested a `width` and `height`, but if
|
| 278 |
+
// `box-sizing: border-box` is applied to the outer iframe element, then we
|
| 279 |
+
// must add border thickness to `width` and `height` to compute the actual
|
| 280 |
+
// necessary width and height (in order to prevent a resize feedback loop).
|
| 281 |
+
const style = getComputedStyle(iframe);
|
| 282 |
+
const isBorderBox = style.boxSizing === "border-box";
|
| 283 |
+
|
| 284 |
+
// Animate the change for a smooth transition.
|
| 285 |
+
const from: Keyframe = {};
|
| 286 |
+
const to: Keyframe = {};
|
| 287 |
+
|
| 288 |
+
if (width !== undefined) {
|
| 289 |
+
if (isBorderBox) {
|
| 290 |
+
width += parseFloat(style.borderLeftWidth) + parseFloat(style.borderRightWidth);
|
| 291 |
+
}
|
| 292 |
+
// Use min-width instead of width to allow responsive growing.
|
| 293 |
+
// With auto-resize (the default), the app reports its minimum content
|
| 294 |
+
// width; we honor that as a floor but allow the iframe to expand when
|
| 295 |
+
// the host layout allows. And we use `min(..., 100%)` so that the iframe
|
| 296 |
+
// shrinks with its container.
|
| 297 |
+
from.minWidth = `${iframe.offsetWidth}px`;
|
| 298 |
+
iframe.style.minWidth = to.minWidth = `min(${width}px, 100%)`;
|
| 299 |
+
}
|
| 300 |
+
if (height !== undefined) {
|
| 301 |
+
if (isBorderBox) {
|
| 302 |
+
height += parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
|
| 303 |
+
}
|
| 304 |
+
from.height = `${iframe.offsetHeight}px`;
|
| 305 |
+
iframe.style.height = to.height = `${height}px`;
|
| 306 |
+
}
|
| 307 |
+
|
| 308 |
+
iframe.animate([from, to], { duration: 300, easing: "ease-out" });
|
| 309 |
+
};
|
| 310 |
+
|
| 311 |
+
return appBridge;
|
| 312 |
+
}
|
examples/basic-host/src/index.module.css
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.callToolPanel, .toolCallInfoPanel {
|
| 2 |
+
margin: 0 auto;
|
| 3 |
+
padding: 1rem;
|
| 4 |
+
border: 1px solid #ddd;
|
| 5 |
+
border-radius: 4px;
|
| 6 |
+
|
| 7 |
+
* + & {
|
| 8 |
+
margin-top: 1rem;
|
| 9 |
+
}
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
.callToolPanel {
|
| 13 |
+
max-width: 480px;
|
| 14 |
+
|
| 15 |
+
form {
|
| 16 |
+
display: flex;
|
| 17 |
+
flex-direction: column;
|
| 18 |
+
gap: 1rem;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
label {
|
| 22 |
+
display: flex;
|
| 23 |
+
flex-direction: column;
|
| 24 |
+
gap: 0.25rem;
|
| 25 |
+
font-weight: 600;
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
select,
|
| 29 |
+
textarea {
|
| 30 |
+
padding: 0.5rem;
|
| 31 |
+
border: 1px solid #ccc;
|
| 32 |
+
border-radius: 4px;
|
| 33 |
+
font-size: inherit;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
.toolSelect {
|
| 37 |
+
font-family: monospace;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.toolInput {
|
| 41 |
+
min-height: 6rem;
|
| 42 |
+
font-family: monospace;
|
| 43 |
+
resize: vertical;
|
| 44 |
+
|
| 45 |
+
&[aria-invalid="true"] {
|
| 46 |
+
background-color: #fdd;
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
button {
|
| 51 |
+
align-self: center;
|
| 52 |
+
min-width: 200px;
|
| 53 |
+
padding: 0.75rem 1.5rem;
|
| 54 |
+
border: none;
|
| 55 |
+
border-radius: 4px;
|
| 56 |
+
background-color: #1e3a5f;
|
| 57 |
+
font-size: inherit;
|
| 58 |
+
font-weight: 600;
|
| 59 |
+
color: white;
|
| 60 |
+
cursor: pointer;
|
| 61 |
+
|
| 62 |
+
&:hover:not(:disabled) {
|
| 63 |
+
background-color: #2d4a7c;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
&:disabled {
|
| 67 |
+
opacity: 0.5;
|
| 68 |
+
cursor: not-allowed;
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
.toolCallInfoPanel {
|
| 74 |
+
display: flex;
|
| 75 |
+
gap: 1rem;
|
| 76 |
+
animation: slideDown 0.3s ease-out;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
@keyframes slideDown {
|
| 80 |
+
from { opacity: 0; transform: translateY(-12px); }
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.inputInfoPanel {
|
| 84 |
+
display: flex;
|
| 85 |
+
flex: 3;
|
| 86 |
+
flex-direction: column;
|
| 87 |
+
gap: 0.5rem;
|
| 88 |
+
min-width: 0;
|
| 89 |
+
|
| 90 |
+
h2 {
|
| 91 |
+
display: flex;
|
| 92 |
+
flex-direction: column;
|
| 93 |
+
margin: 0;
|
| 94 |
+
font-size: 1.5rem;
|
| 95 |
+
position: relative;
|
| 96 |
+
|
| 97 |
+
.toolName {
|
| 98 |
+
font-family: monospace;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.closeButton {
|
| 102 |
+
position: absolute;
|
| 103 |
+
top: 0;
|
| 104 |
+
right: 0;
|
| 105 |
+
width: 1.5rem;
|
| 106 |
+
height: 1.5rem;
|
| 107 |
+
padding: 0;
|
| 108 |
+
border: none;
|
| 109 |
+
border-radius: 4px;
|
| 110 |
+
background: #e0e0e0;
|
| 111 |
+
font-size: 1.25rem;
|
| 112 |
+
line-height: 1;
|
| 113 |
+
color: #666;
|
| 114 |
+
cursor: pointer;
|
| 115 |
+
|
| 116 |
+
&:hover {
|
| 117 |
+
background: #d0d0d0;
|
| 118 |
+
color: #333;
|
| 119 |
+
}
|
| 120 |
+
}
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.outputInfoPanel {
|
| 125 |
+
flex: 4;
|
| 126 |
+
min-width: 0;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.appOutputPanel {
|
| 130 |
+
flex: 1;
|
| 131 |
+
min-width: 0;
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
.appHeader {
|
| 135 |
+
display: flex;
|
| 136 |
+
align-items: center;
|
| 137 |
+
gap: 0.5rem;
|
| 138 |
+
margin-bottom: 0.5rem;
|
| 139 |
+
font-size: 1.25rem;
|
| 140 |
+
font-weight: 600;
|
| 141 |
+
position: relative;
|
| 142 |
+
|
| 143 |
+
.toolName {
|
| 144 |
+
font-family: monospace;
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
.closeButton {
|
| 148 |
+
position: absolute;
|
| 149 |
+
top: 0;
|
| 150 |
+
right: 0;
|
| 151 |
+
width: 1.5rem;
|
| 152 |
+
height: 1.5rem;
|
| 153 |
+
padding: 0;
|
| 154 |
+
border: none;
|
| 155 |
+
border-radius: 4px;
|
| 156 |
+
background: #e0e0e0;
|
| 157 |
+
font-size: 1.25rem;
|
| 158 |
+
line-height: 1;
|
| 159 |
+
color: #666;
|
| 160 |
+
cursor: pointer;
|
| 161 |
+
|
| 162 |
+
&:hover {
|
| 163 |
+
background: #d0d0d0;
|
| 164 |
+
color: #333;
|
| 165 |
+
}
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
.jsonBlock {
|
| 170 |
+
flex-grow: 1;
|
| 171 |
+
min-height: 0;
|
| 172 |
+
margin: 0;
|
| 173 |
+
padding: 1rem;
|
| 174 |
+
border-radius: 4px;
|
| 175 |
+
background-color: #f5f5f5;
|
| 176 |
+
overflow: auto;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.appIframePanel {
|
| 180 |
+
min-height: 200px;
|
| 181 |
+
|
| 182 |
+
iframe {
|
| 183 |
+
width: 100%;
|
| 184 |
+
height: 600px;
|
| 185 |
+
box-sizing: border-box;
|
| 186 |
+
border: 3px dashed #888;
|
| 187 |
+
border-radius: 4px;
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.collapsiblePanel {
|
| 192 |
+
margin: 0.5rem 0;
|
| 193 |
+
padding: 0.5rem 0.75rem;
|
| 194 |
+
border: 1px solid #e0e0e0;
|
| 195 |
+
border-radius: 4px;
|
| 196 |
+
background-color: #fafafa;
|
| 197 |
+
font-size: 0.875rem;
|
| 198 |
+
cursor: pointer;
|
| 199 |
+
transition: background-color 0.15s;
|
| 200 |
+
|
| 201 |
+
&:hover {
|
| 202 |
+
background-color: #f0f0f0;
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.collapsibleHeader {
|
| 207 |
+
display: flex;
|
| 208 |
+
align-items: center;
|
| 209 |
+
gap: 0.5rem;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.collapsibleLabel {
|
| 213 |
+
font-weight: 600;
|
| 214 |
+
color: #555;
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
.collapsibleSize {
|
| 218 |
+
color: #888;
|
| 219 |
+
font-size: 0.75rem;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.collapsibleToggle {
|
| 223 |
+
margin-left: auto;
|
| 224 |
+
color: #888;
|
| 225 |
+
font-size: 0.75rem;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.collapsiblePreview {
|
| 229 |
+
margin-top: 0.25rem;
|
| 230 |
+
color: #666;
|
| 231 |
+
font-family: monospace;
|
| 232 |
+
font-size: 0.8rem;
|
| 233 |
+
white-space: nowrap;
|
| 234 |
+
overflow: hidden;
|
| 235 |
+
text-overflow: ellipsis;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.collapsibleFull {
|
| 239 |
+
margin: 0.5rem 0 0;
|
| 240 |
+
padding: 0.5rem;
|
| 241 |
+
border-radius: 4px;
|
| 242 |
+
background-color: #f5f5f5;
|
| 243 |
+
font-family: monospace;
|
| 244 |
+
font-size: 0.8rem;
|
| 245 |
+
white-space: pre-wrap;
|
| 246 |
+
word-break: break-word;
|
| 247 |
+
max-height: 200px;
|
| 248 |
+
overflow: auto;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.error {
|
| 252 |
+
padding: 1.5rem;
|
| 253 |
+
background-color: #ddd;
|
| 254 |
+
color: #d00;
|
| 255 |
+
}
|
examples/basic-host/src/index.tsx
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge";
|
| 2 |
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
| 3 |
+
import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react";
|
| 4 |
+
import { createRoot } from "react-dom/client";
|
| 5 |
+
import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation";
|
| 6 |
+
import styles from "./index.module.css";
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Check if a tool is visible to the model (not app-only).
|
| 10 |
+
* Tools with `visibility: ["app"]` should not be shown in tool lists.
|
| 11 |
+
*/
|
| 12 |
+
function isToolVisibleToModel(tool: { _meta?: Record<string, unknown> }): boolean {
|
| 13 |
+
const result = McpUiToolMetaSchema.safeParse(tool._meta?.ui);
|
| 14 |
+
if (!result.success) return true; // default: visible to model
|
| 15 |
+
const visibility = result.data.visibility;
|
| 16 |
+
if (!visibility) return true; // default: visible to model
|
| 17 |
+
return visibility.includes("model");
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
/** Compare tools: UI-enabled first, then alphabetically by name. */
|
| 21 |
+
function compareTools(a: Tool, b: Tool): number {
|
| 22 |
+
const aHasUi = !!getToolUiResourceUri(a);
|
| 23 |
+
const bHasUi = !!getToolUiResourceUri(b);
|
| 24 |
+
if (aHasUi && !bHasUi) return -1;
|
| 25 |
+
if (!aHasUi && bHasUi) return 1;
|
| 26 |
+
return a.name.localeCompare(b.name);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
/**
|
| 30 |
+
* Extract default values from a tool's JSON Schema inputSchema.
|
| 31 |
+
* Returns a formatted JSON string with defaults, or "{}" if none found.
|
| 32 |
+
*/
|
| 33 |
+
function getToolDefaults(tool: Tool | undefined): string {
|
| 34 |
+
if (!tool?.inputSchema?.properties) return "{}";
|
| 35 |
+
|
| 36 |
+
const defaults: Record<string, unknown> = {};
|
| 37 |
+
for (const [key, prop] of Object.entries(tool.inputSchema.properties)) {
|
| 38 |
+
if (prop && typeof prop === "object" && "default" in prop) {
|
| 39 |
+
defaults[key] = prop.default;
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
return Object.keys(defaults).length > 0
|
| 44 |
+
? JSON.stringify(defaults, null, 2)
|
| 45 |
+
: "{}";
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
// Host passes serversPromise to CallToolPanel
|
| 50 |
+
interface HostProps {
|
| 51 |
+
serversPromise: Promise<ServerInfo[]>;
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
type ToolCallEntry = ToolCallInfo & { id: number };
|
| 55 |
+
let nextToolCallId = 0;
|
| 56 |
+
|
| 57 |
+
function Host({ serversPromise }: HostProps) {
|
| 58 |
+
const [toolCalls, setToolCalls] = useState<ToolCallEntry[]>([]);
|
| 59 |
+
const [destroyingIds, setDestroyingIds] = useState<Set<number>>(new Set());
|
| 60 |
+
|
| 61 |
+
const requestClose = (id: number) => {
|
| 62 |
+
setDestroyingIds((s) => new Set(s).add(id));
|
| 63 |
+
};
|
| 64 |
+
|
| 65 |
+
const completeClose = (id: number) => {
|
| 66 |
+
setDestroyingIds((s) => {
|
| 67 |
+
const next = new Set(s);
|
| 68 |
+
next.delete(id);
|
| 69 |
+
return next;
|
| 70 |
+
});
|
| 71 |
+
setToolCalls((calls) => calls.filter((c) => c.id !== id));
|
| 72 |
+
};
|
| 73 |
+
|
| 74 |
+
return (
|
| 75 |
+
<>
|
| 76 |
+
{toolCalls.map((info) => (
|
| 77 |
+
<ToolCallInfoPanel
|
| 78 |
+
key={info.id}
|
| 79 |
+
toolCallInfo={info}
|
| 80 |
+
isDestroying={destroyingIds.has(info.id)}
|
| 81 |
+
onRequestClose={() => requestClose(info.id)}
|
| 82 |
+
onCloseComplete={() => completeClose(info.id)}
|
| 83 |
+
/>
|
| 84 |
+
))}
|
| 85 |
+
<CallToolPanel
|
| 86 |
+
serversPromise={serversPromise}
|
| 87 |
+
addToolCall={(info) => setToolCalls([...toolCalls, { ...info, id: nextToolCallId++ }])}
|
| 88 |
+
/>
|
| 89 |
+
</>
|
| 90 |
+
);
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
// CallToolPanel renders the unified form with Suspense around ServerSelect
|
| 95 |
+
interface CallToolPanelProps {
|
| 96 |
+
serversPromise: Promise<ServerInfo[]>;
|
| 97 |
+
addToolCall: (info: ToolCallInfo) => void;
|
| 98 |
+
}
|
| 99 |
+
function CallToolPanel({ serversPromise, addToolCall }: CallToolPanelProps) {
|
| 100 |
+
const [selectedServer, setSelectedServer] = useState<ServerInfo | null>(null);
|
| 101 |
+
const [selectedTool, setSelectedTool] = useState("");
|
| 102 |
+
const [inputJson, setInputJson] = useState("{}");
|
| 103 |
+
|
| 104 |
+
// Filter out app-only tools, prioritize tools with UIs
|
| 105 |
+
const toolNames = selectedServer
|
| 106 |
+
? Array.from(selectedServer.tools.values())
|
| 107 |
+
.filter((tool) => isToolVisibleToModel(tool))
|
| 108 |
+
.sort(compareTools)
|
| 109 |
+
.map((tool) => tool.name)
|
| 110 |
+
: [];
|
| 111 |
+
|
| 112 |
+
const isValidJson = useMemo(() => {
|
| 113 |
+
try {
|
| 114 |
+
JSON.parse(inputJson);
|
| 115 |
+
return true;
|
| 116 |
+
} catch {
|
| 117 |
+
return false;
|
| 118 |
+
}
|
| 119 |
+
}, [inputJson]);
|
| 120 |
+
|
| 121 |
+
const handleServerSelect = (server: ServerInfo) => {
|
| 122 |
+
setSelectedServer(server);
|
| 123 |
+
// Filter out app-only tools, prioritize tools with UIs
|
| 124 |
+
const visibleTools = Array.from(server.tools.values())
|
| 125 |
+
.filter((tool) => isToolVisibleToModel(tool))
|
| 126 |
+
.sort(compareTools);
|
| 127 |
+
const firstTool = visibleTools[0]?.name ?? "";
|
| 128 |
+
setSelectedTool(firstTool);
|
| 129 |
+
// Set input JSON to tool defaults (if any)
|
| 130 |
+
setInputJson(getToolDefaults(server.tools.get(firstTool)));
|
| 131 |
+
};
|
| 132 |
+
|
| 133 |
+
const handleToolSelect = (toolName: string) => {
|
| 134 |
+
setSelectedTool(toolName);
|
| 135 |
+
// Set input JSON to tool defaults (if any)
|
| 136 |
+
setInputJson(getToolDefaults(selectedServer?.tools.get(toolName)));
|
| 137 |
+
};
|
| 138 |
+
|
| 139 |
+
const handleSubmit = () => {
|
| 140 |
+
if (!selectedServer) return;
|
| 141 |
+
const toolCallInfo = callTool(selectedServer, selectedTool, JSON.parse(inputJson));
|
| 142 |
+
addToolCall(toolCallInfo);
|
| 143 |
+
};
|
| 144 |
+
|
| 145 |
+
return (
|
| 146 |
+
<div className={styles.callToolPanel}>
|
| 147 |
+
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(); }}>
|
| 148 |
+
<label>
|
| 149 |
+
Server
|
| 150 |
+
<Suspense fallback={<select disabled><option>Loading...</option></select>}>
|
| 151 |
+
<ServerSelect serversPromise={serversPromise} onSelect={handleServerSelect} />
|
| 152 |
+
</Suspense>
|
| 153 |
+
</label>
|
| 154 |
+
<label>
|
| 155 |
+
Tool
|
| 156 |
+
<select
|
| 157 |
+
className={styles.toolSelect}
|
| 158 |
+
value={selectedTool}
|
| 159 |
+
onChange={(e) => handleToolSelect(e.target.value)}
|
| 160 |
+
>
|
| 161 |
+
{selectedServer && toolNames.map((name) => (
|
| 162 |
+
<option key={name} value={name}>{name}</option>
|
| 163 |
+
))}
|
| 164 |
+
</select>
|
| 165 |
+
</label>
|
| 166 |
+
<label>
|
| 167 |
+
Input
|
| 168 |
+
<textarea
|
| 169 |
+
className={styles.toolInput}
|
| 170 |
+
aria-invalid={!isValidJson}
|
| 171 |
+
value={inputJson}
|
| 172 |
+
onChange={(e) => setInputJson(e.target.value)}
|
| 173 |
+
/>
|
| 174 |
+
</label>
|
| 175 |
+
<button type="submit" disabled={!selectedTool || !isValidJson}>
|
| 176 |
+
Call Tool
|
| 177 |
+
</button>
|
| 178 |
+
</form>
|
| 179 |
+
</div>
|
| 180 |
+
);
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
// ServerSelect calls use() and renders the server <select>
|
| 185 |
+
interface ServerSelectProps {
|
| 186 |
+
serversPromise: Promise<ServerInfo[]>;
|
| 187 |
+
onSelect: (server: ServerInfo) => void;
|
| 188 |
+
}
|
| 189 |
+
function ServerSelect({ serversPromise, onSelect }: ServerSelectProps) {
|
| 190 |
+
const servers = use(serversPromise);
|
| 191 |
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
| 192 |
+
|
| 193 |
+
useEffect(() => {
|
| 194 |
+
if (servers.length > selectedIndex) {
|
| 195 |
+
onSelect(servers[selectedIndex]);
|
| 196 |
+
}
|
| 197 |
+
}, [servers]);
|
| 198 |
+
|
| 199 |
+
if (servers.length === 0) {
|
| 200 |
+
return <select disabled><option>No servers configured</option></select>;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
return (
|
| 204 |
+
<select
|
| 205 |
+
value={selectedIndex}
|
| 206 |
+
onChange={(e) => {
|
| 207 |
+
const newIndex = Number(e.target.value);
|
| 208 |
+
setSelectedIndex(newIndex);
|
| 209 |
+
onSelect(servers[newIndex]);
|
| 210 |
+
}}
|
| 211 |
+
>
|
| 212 |
+
{servers.map((server, i) => (
|
| 213 |
+
<option key={i} value={i}>{server.name}</option>
|
| 214 |
+
))}
|
| 215 |
+
</select>
|
| 216 |
+
);
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
interface ToolCallInfoPanelProps {
|
| 221 |
+
toolCallInfo: ToolCallInfo;
|
| 222 |
+
isDestroying?: boolean;
|
| 223 |
+
onRequestClose?: () => void;
|
| 224 |
+
onCloseComplete?: () => void;
|
| 225 |
+
}
|
| 226 |
+
function ToolCallInfoPanel({ toolCallInfo, isDestroying, onRequestClose, onCloseComplete }: ToolCallInfoPanelProps) {
|
| 227 |
+
const isApp = hasAppHtml(toolCallInfo);
|
| 228 |
+
|
| 229 |
+
// For non-app tool calls, close immediately when isDestroying becomes true
|
| 230 |
+
useEffect(() => {
|
| 231 |
+
if (isDestroying && !isApp) {
|
| 232 |
+
onCloseComplete?.();
|
| 233 |
+
}
|
| 234 |
+
}, [isDestroying, isApp, onCloseComplete]);
|
| 235 |
+
|
| 236 |
+
return (
|
| 237 |
+
<div
|
| 238 |
+
className={styles.toolCallInfoPanel}
|
| 239 |
+
style={isDestroying ? { opacity: 0.5, pointerEvents: "none" } : undefined}
|
| 240 |
+
>
|
| 241 |
+
{/* For non-app tools, show input/output side by side */}
|
| 242 |
+
{!isApp && (
|
| 243 |
+
<div className={styles.inputInfoPanel}>
|
| 244 |
+
<h2>
|
| 245 |
+
<span>{toolCallInfo.serverInfo.name}</span>
|
| 246 |
+
<span className={styles.toolName}>{toolCallInfo.tool.name}</span>
|
| 247 |
+
{onRequestClose && !isDestroying && (
|
| 248 |
+
<button
|
| 249 |
+
className={styles.closeButton}
|
| 250 |
+
onClick={onRequestClose}
|
| 251 |
+
title="Close"
|
| 252 |
+
>
|
| 253 |
+
×
|
| 254 |
+
</button>
|
| 255 |
+
)}
|
| 256 |
+
</h2>
|
| 257 |
+
<JsonBlock value={toolCallInfo.input} />
|
| 258 |
+
</div>
|
| 259 |
+
)}
|
| 260 |
+
<div className={isApp ? styles.appOutputPanel : styles.outputInfoPanel}>
|
| 261 |
+
{/* For apps, show header above the app: ServerName:tool_name */}
|
| 262 |
+
{isApp && (
|
| 263 |
+
<div className={styles.appHeader}>
|
| 264 |
+
<span>{toolCallInfo.serverInfo.name}:<span className={styles.toolName}>{toolCallInfo.tool.name}</span></span>
|
| 265 |
+
{onRequestClose && !isDestroying && (
|
| 266 |
+
<button
|
| 267 |
+
className={styles.closeButton}
|
| 268 |
+
onClick={onRequestClose}
|
| 269 |
+
title="Close"
|
| 270 |
+
>
|
| 271 |
+
×
|
| 272 |
+
</button>
|
| 273 |
+
)}
|
| 274 |
+
</div>
|
| 275 |
+
)}
|
| 276 |
+
<ErrorBoundary>
|
| 277 |
+
<Suspense fallback="Loading...">
|
| 278 |
+
{
|
| 279 |
+
isApp
|
| 280 |
+
? <AppIFramePanel
|
| 281 |
+
toolCallInfo={toolCallInfo}
|
| 282 |
+
isDestroying={isDestroying}
|
| 283 |
+
onTeardownComplete={onCloseComplete}
|
| 284 |
+
/>
|
| 285 |
+
: <ToolResultPanel toolCallInfo={toolCallInfo} />
|
| 286 |
+
}
|
| 287 |
+
</Suspense>
|
| 288 |
+
</ErrorBoundary>
|
| 289 |
+
</div>
|
| 290 |
+
</div>
|
| 291 |
+
);
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
function JsonBlock({ value }: { value: object }) {
|
| 296 |
+
return (
|
| 297 |
+
<pre className={styles.jsonBlock}>
|
| 298 |
+
<code>{JSON.stringify(value, null, 2)}</code>
|
| 299 |
+
</pre>
|
| 300 |
+
);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
|
| 304 |
+
interface CollapsiblePanelProps {
|
| 305 |
+
icon: string;
|
| 306 |
+
label: string;
|
| 307 |
+
content: string;
|
| 308 |
+
badge?: string;
|
| 309 |
+
defaultExpanded?: boolean;
|
| 310 |
+
}
|
| 311 |
+
function CollapsiblePanel({ icon, label, content, badge, defaultExpanded = false }: CollapsiblePanelProps) {
|
| 312 |
+
const [expanded, setExpanded] = useState(defaultExpanded);
|
| 313 |
+
|
| 314 |
+
return (
|
| 315 |
+
<div
|
| 316 |
+
className={styles.collapsiblePanel}
|
| 317 |
+
onClick={() => setExpanded(!expanded)}
|
| 318 |
+
title={expanded ? "Click to collapse" : "Click to expand"}
|
| 319 |
+
>
|
| 320 |
+
<div className={styles.collapsibleHeader}>
|
| 321 |
+
<span className={styles.collapsibleLabel}>{icon} {label}</span>
|
| 322 |
+
<span className={styles.collapsibleSize}>
|
| 323 |
+
{badge ?? `${content.length} chars`}
|
| 324 |
+
</span>
|
| 325 |
+
<span className={styles.collapsibleToggle}>
|
| 326 |
+
{expanded ? "▼" : "▶"}
|
| 327 |
+
</span>
|
| 328 |
+
</div>
|
| 329 |
+
{expanded ? (
|
| 330 |
+
<pre className={styles.collapsibleFull}>{content}</pre>
|
| 331 |
+
) : (
|
| 332 |
+
<div className={styles.collapsiblePreview}>
|
| 333 |
+
{content.slice(0, 100)}{content.length > 100 ? "…" : ""}
|
| 334 |
+
</div>
|
| 335 |
+
)}
|
| 336 |
+
</div>
|
| 337 |
+
);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
|
| 341 |
+
interface AppIFramePanelProps {
|
| 342 |
+
toolCallInfo: Required<ToolCallInfo>;
|
| 343 |
+
isDestroying?: boolean;
|
| 344 |
+
onTeardownComplete?: () => void;
|
| 345 |
+
}
|
| 346 |
+
function AppIFramePanel({ toolCallInfo, isDestroying, onTeardownComplete }: AppIFramePanelProps) {
|
| 347 |
+
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
| 348 |
+
const appBridgeRef = useRef<ReturnType<typeof newAppBridge> | null>(null);
|
| 349 |
+
const [modelContext, setModelContext] = useState<ModelContext | null>(null);
|
| 350 |
+
const [toolResult, setToolResult] = useState<object | null>(null);
|
| 351 |
+
const [messages, setMessages] = useState<AppMessage[]>([]);
|
| 352 |
+
|
| 353 |
+
useEffect(() => {
|
| 354 |
+
const iframe = iframeRef.current!;
|
| 355 |
+
|
| 356 |
+
// First get CSP and permissions from resource, then load sandbox
|
| 357 |
+
// CSP is set via HTTP headers (tamper-proof), permissions via iframe allow attribute
|
| 358 |
+
toolCallInfo.appResourcePromise.then(({ csp, permissions }) => {
|
| 359 |
+
loadSandboxProxy(iframe, csp, permissions).then((firstTime) => {
|
| 360 |
+
// The `firstTime` check guards against React Strict Mode's double
|
| 361 |
+
// invocation (mount → unmount → remount simulation in development).
|
| 362 |
+
// Outside of Strict Mode, this `useEffect` runs only once per
|
| 363 |
+
// `toolCallInfo`.
|
| 364 |
+
if (firstTime) {
|
| 365 |
+
const appBridge = newAppBridge(toolCallInfo.serverInfo, iframe, {
|
| 366 |
+
onContextUpdate: setModelContext,
|
| 367 |
+
onMessage: (msg) => setMessages((prev) => [...prev, msg]),
|
| 368 |
+
});
|
| 369 |
+
appBridgeRef.current = appBridge;
|
| 370 |
+
initializeApp(iframe, appBridge, toolCallInfo);
|
| 371 |
+
}
|
| 372 |
+
});
|
| 373 |
+
});
|
| 374 |
+
|
| 375 |
+
// Track tool result for display
|
| 376 |
+
toolCallInfo.resultPromise.then(setToolResult).catch(() => {});
|
| 377 |
+
}, [toolCallInfo]);
|
| 378 |
+
|
| 379 |
+
// Graceful teardown: wait for guest to respond before unmounting
|
| 380 |
+
// This follows the spec: "Host SHOULD wait for a response before tearing
|
| 381 |
+
// down the resource (to prevent data loss)."
|
| 382 |
+
useEffect(() => {
|
| 383 |
+
if (!isDestroying) return;
|
| 384 |
+
|
| 385 |
+
if (!appBridgeRef.current) {
|
| 386 |
+
// Bridge not ready yet (e.g., user closed before iframe loaded)
|
| 387 |
+
onTeardownComplete?.();
|
| 388 |
+
return;
|
| 389 |
+
}
|
| 390 |
+
|
| 391 |
+
log.info("Sending teardown notification to MCP App");
|
| 392 |
+
appBridgeRef.current.teardownResource({})
|
| 393 |
+
.catch((err) => {
|
| 394 |
+
log.warn("Teardown request failed (app may have already closed):", err);
|
| 395 |
+
})
|
| 396 |
+
.finally(() => {
|
| 397 |
+
onTeardownComplete?.();
|
| 398 |
+
});
|
| 399 |
+
}, [isDestroying, onTeardownComplete]);
|
| 400 |
+
|
| 401 |
+
// Format content blocks - handle text, images, resources, etc.
|
| 402 |
+
const formatContentBlock = (c: { type: string; [key: string]: unknown }) => {
|
| 403 |
+
switch (c.type) {
|
| 404 |
+
case "text":
|
| 405 |
+
return (c as { type: "text"; text: string }).text;
|
| 406 |
+
case "image":
|
| 407 |
+
return `<image: ${(c as { mimeType?: string }).mimeType ?? "unknown"}>`;
|
| 408 |
+
case "audio":
|
| 409 |
+
return `<audio: ${(c as { mimeType?: string }).mimeType ?? "unknown"}>`;
|
| 410 |
+
case "resource":
|
| 411 |
+
return `<resource: ${(c as { resource?: { uri?: string } }).resource?.uri ?? "unknown"}>`;
|
| 412 |
+
default:
|
| 413 |
+
return `<${c.type}>`;
|
| 414 |
+
}
|
| 415 |
+
};
|
| 416 |
+
|
| 417 |
+
// Format context for display
|
| 418 |
+
const contextText = modelContext?.content?.map(formatContentBlock).join("\n") ?? "";
|
| 419 |
+
const contextJson = modelContext?.structuredContent
|
| 420 |
+
? JSON.stringify(modelContext.structuredContent, null, 2)
|
| 421 |
+
: "";
|
| 422 |
+
const fullContext = [contextText, contextJson].filter(Boolean).join("\n\n");
|
| 423 |
+
|
| 424 |
+
const inputJson = JSON.stringify(toolCallInfo.input, null, 2);
|
| 425 |
+
const resultJson = toolResult ? JSON.stringify(toolResult, null, 2) : null;
|
| 426 |
+
|
| 427 |
+
// Format messages
|
| 428 |
+
const formatMessage = (m: AppMessage) => {
|
| 429 |
+
const content = m.content.map(formatContentBlock).join("\n");
|
| 430 |
+
return `[${m.role}] ${content}`;
|
| 431 |
+
};
|
| 432 |
+
const messagesText = messages.map(formatMessage).join("\n\n");
|
| 433 |
+
|
| 434 |
+
return (
|
| 435 |
+
<div className={styles.appIframePanel}>
|
| 436 |
+
<CollapsiblePanel icon="📥" label="Tool Input" content={inputJson} />
|
| 437 |
+
<iframe ref={iframeRef} />
|
| 438 |
+
{resultJson && (
|
| 439 |
+
<CollapsiblePanel icon="📤" label="Tool Result" content={resultJson} />
|
| 440 |
+
)}
|
| 441 |
+
{messages.length > 0 && (
|
| 442 |
+
<CollapsiblePanel
|
| 443 |
+
icon="💬"
|
| 444 |
+
label="Messages"
|
| 445 |
+
content={messagesText}
|
| 446 |
+
badge={`${messages.length} message${messages.length > 1 ? "s" : ""}`}
|
| 447 |
+
/>
|
| 448 |
+
)}
|
| 449 |
+
{modelContext && (
|
| 450 |
+
<CollapsiblePanel icon="📋" label="Model Context" content={fullContext} />
|
| 451 |
+
)}
|
| 452 |
+
</div>
|
| 453 |
+
);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
|
| 457 |
+
interface ToolResultPanelProps {
|
| 458 |
+
toolCallInfo: ToolCallInfo;
|
| 459 |
+
}
|
| 460 |
+
function ToolResultPanel({ toolCallInfo }: ToolResultPanelProps) {
|
| 461 |
+
const result = use(toolCallInfo.resultPromise);
|
| 462 |
+
return <JsonBlock value={result} />;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
interface ErrorBoundaryProps {
|
| 467 |
+
children: ReactNode;
|
| 468 |
+
}
|
| 469 |
+
interface ErrorBoundaryState {
|
| 470 |
+
hasError: boolean;
|
| 471 |
+
error: unknown;
|
| 472 |
+
}
|
| 473 |
+
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
|
| 474 |
+
state: ErrorBoundaryState = { hasError: false, error: undefined };
|
| 475 |
+
|
| 476 |
+
// Called during render phase - must be pure (no side effects)
|
| 477 |
+
// Note: error is `unknown` because JS allows throwing any value
|
| 478 |
+
static getDerivedStateFromError(error: unknown): ErrorBoundaryState {
|
| 479 |
+
return { hasError: true, error };
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
// Called during commit phase - can have side effects (logging, etc.)
|
| 483 |
+
componentDidCatch(error: unknown, errorInfo: ErrorInfo): void {
|
| 484 |
+
log.error("Caught:", error, errorInfo.componentStack);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
render(): ReactNode {
|
| 488 |
+
if (this.state.hasError) {
|
| 489 |
+
const { error } = this.state;
|
| 490 |
+
const message = error instanceof Error ? error.message : String(error);
|
| 491 |
+
return <div className={styles.error}><strong>ERROR:</strong> {message}</div>;
|
| 492 |
+
}
|
| 493 |
+
return this.props.children;
|
| 494 |
+
}
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
async function connectToAllServers(): Promise<ServerInfo[]> {
|
| 499 |
+
const serverUrlsResponse = await fetch("/api/servers");
|
| 500 |
+
const serverUrls = (await serverUrlsResponse.json()) as string[];
|
| 501 |
+
|
| 502 |
+
// Use allSettled to be resilient to individual server failures
|
| 503 |
+
const results = await Promise.allSettled(
|
| 504 |
+
serverUrls.map((url) => connectToServer(new URL(url)))
|
| 505 |
+
);
|
| 506 |
+
|
| 507 |
+
const servers: ServerInfo[] = [];
|
| 508 |
+
for (let i = 0; i < results.length; i++) {
|
| 509 |
+
const result = results[i];
|
| 510 |
+
if (result.status === "fulfilled") {
|
| 511 |
+
servers.push(result.value);
|
| 512 |
+
} else {
|
| 513 |
+
console.warn(`[HOST] Failed to connect to ${serverUrls[i]}:`, result.reason);
|
| 514 |
+
}
|
| 515 |
+
}
|
| 516 |
+
|
| 517 |
+
if (servers.length === 0 && serverUrls.length > 0) {
|
| 518 |
+
throw new Error(`Failed to connect to any servers (${serverUrls.length} attempted)`);
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
return servers;
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
createRoot(document.getElementById("root")!).render(
|
| 525 |
+
<StrictMode>
|
| 526 |
+
<ErrorBoundary>
|
| 527 |
+
<Host serversPromise={connectToAllServers()} />
|
| 528 |
+
</ErrorBoundary>
|
| 529 |
+
</StrictMode>,
|
| 530 |
+
);
|
examples/basic-host/src/sandbox.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { McpUiSandboxProxyReadyNotification, McpUiSandboxResourceReadyNotification } from "../../../dist/src/types";
|
| 2 |
+
import { buildAllowAttribute } from "../../../dist/src/app-bridge";
|
| 3 |
+
|
| 4 |
+
const ALLOWED_REFERRER_PATTERN = /^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/;
|
| 5 |
+
|
| 6 |
+
if (window.self === window.top) {
|
| 7 |
+
throw new Error("This file is only to be used in an iframe sandbox.");
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
if (!document.referrer) {
|
| 11 |
+
throw new Error("No referrer, cannot validate embedding site.");
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) {
|
| 15 |
+
throw new Error(
|
| 16 |
+
`Embedding domain not allowed in referrer ${document.referrer}. (Consider updating the validation logic to allow your domain.)`,
|
| 17 |
+
);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// Extract the expected host origin from the referrer for origin validation.
|
| 21 |
+
// This is the origin we expect all parent messages to come from.
|
| 22 |
+
const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin;
|
| 23 |
+
|
| 24 |
+
const OWN_ORIGIN = new URL(window.location.href).origin;
|
| 25 |
+
|
| 26 |
+
// Security self-test: verify iframe isolation is working correctly.
|
| 27 |
+
// This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox
|
| 28 |
+
// configuration is dangerously broken and untrusted content could escape.
|
| 29 |
+
try {
|
| 30 |
+
window.top!.alert("If you see this, the sandbox is not setup securely.");
|
| 31 |
+
throw "FAIL";
|
| 32 |
+
} catch (e) {
|
| 33 |
+
if (e === "FAIL") {
|
| 34 |
+
throw new Error("The sandbox is not setup securely.");
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
// Expected: SecurityError confirms proper sandboxing.
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Double-iframe sandbox architecture: THIS file is the outer sandbox proxy
|
| 41 |
+
// iframe on a separate origin. It creates an inner iframe for untrusted HTML
|
| 42 |
+
// content. Per the specification, the Host and the Sandbox MUST have different
|
| 43 |
+
// origins.
|
| 44 |
+
const inner = document.createElement("iframe");
|
| 45 |
+
inner.style = "width:100%; height:100%; border:none;";
|
| 46 |
+
inner.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
|
| 47 |
+
// Note: allow attribute is set later when receiving sandbox-resource-ready notification
|
| 48 |
+
// based on the permissions requested by the app
|
| 49 |
+
document.body.appendChild(inner);
|
| 50 |
+
|
| 51 |
+
const RESOURCE_READY_NOTIFICATION: McpUiSandboxResourceReadyNotification["method"] =
|
| 52 |
+
"ui/notifications/sandbox-resource-ready";
|
| 53 |
+
const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification["method"] =
|
| 54 |
+
"ui/notifications/sandbox-proxy-ready";
|
| 55 |
+
|
| 56 |
+
// Message relay: This Sandbox (outer iframe) acts as a bidirectional bridge,
|
| 57 |
+
// forwarding messages between:
|
| 58 |
+
//
|
| 59 |
+
// Host (parent window) ↔ Sandbox (outer frame) ↔ Guest UI (inner iframe)
|
| 60 |
+
//
|
| 61 |
+
// Reason: the parent window and inner iframe have different origins and can't
|
| 62 |
+
// communicate directly, so the outer iframe forwards messages in both
|
| 63 |
+
// directions to connect them.
|
| 64 |
+
//
|
| 65 |
+
// Special case: The "ui/notifications/sandbox-proxy-ready" message is
|
| 66 |
+
// intercepted here (not relayed) because the Sandbox uses it to configure and
|
| 67 |
+
// load the inner iframe with the Guest UI HTML content.
|
| 68 |
+
//
|
| 69 |
+
// Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts
|
| 70 |
+
// based on ?csp= query param). This is tamper-proof unlike meta tags.
|
| 71 |
+
|
| 72 |
+
window.addEventListener("message", async (event) => {
|
| 73 |
+
if (event.source === window.parent) {
|
| 74 |
+
// Validate that messages from parent come from the expected host origin.
|
| 75 |
+
// This prevents malicious pages from sending messages to this sandbox.
|
| 76 |
+
if (event.origin !== EXPECTED_HOST_ORIGIN) {
|
| 77 |
+
console.error(
|
| 78 |
+
"[Sandbox] Rejecting message from unexpected origin:",
|
| 79 |
+
event.origin,
|
| 80 |
+
"expected:",
|
| 81 |
+
EXPECTED_HOST_ORIGIN
|
| 82 |
+
);
|
| 83 |
+
return;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
| 87 |
+
const { html, sandbox, permissions } = event.data.params;
|
| 88 |
+
if (typeof sandbox === "string") {
|
| 89 |
+
inner.setAttribute("sandbox", sandbox);
|
| 90 |
+
}
|
| 91 |
+
// Set Permission Policy allow attribute if permissions are requested
|
| 92 |
+
const allowAttribute = buildAllowAttribute(permissions);
|
| 93 |
+
if (allowAttribute) {
|
| 94 |
+
console.log("[Sandbox] Setting allow attribute:", allowAttribute);
|
| 95 |
+
inner.setAttribute("allow", allowAttribute);
|
| 96 |
+
}
|
| 97 |
+
if (typeof html === "string") {
|
| 98 |
+
// Use document.write instead of srcdoc (which the CesiumJS Map won't work with)
|
| 99 |
+
const doc = inner.contentDocument || inner.contentWindow?.document;
|
| 100 |
+
if (doc) {
|
| 101 |
+
doc.open();
|
| 102 |
+
doc.write(html);
|
| 103 |
+
doc.close();
|
| 104 |
+
} else {
|
| 105 |
+
// Fallback to srcdoc if document is not accessible
|
| 106 |
+
console.warn("[Sandbox] document.write not available, falling back to srcdoc");
|
| 107 |
+
inner.srcdoc = html;
|
| 108 |
+
}
|
| 109 |
+
}
|
| 110 |
+
} else {
|
| 111 |
+
if (inner && inner.contentWindow) {
|
| 112 |
+
inner.contentWindow.postMessage(event.data, "*");
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
} else if (event.source === inner.contentWindow) {
|
| 116 |
+
if (event.origin !== OWN_ORIGIN) {
|
| 117 |
+
console.error(
|
| 118 |
+
"[Sandbox] Rejecting message from inner iframe with unexpected origin:",
|
| 119 |
+
event.origin,
|
| 120 |
+
"expected:",
|
| 121 |
+
OWN_ORIGIN
|
| 122 |
+
);
|
| 123 |
+
return;
|
| 124 |
+
}
|
| 125 |
+
// Relay messages from inner frame to parent window.
|
| 126 |
+
// Use specific origin instead of "*" to prevent message interception.
|
| 127 |
+
window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN);
|
| 128 |
+
}
|
| 129 |
+
});
|
| 130 |
+
|
| 131 |
+
// Notify the Host that the Sandbox is ready to receive Guest UI HTML.
|
| 132 |
+
// Use specific origin instead of "*" to ensure only the expected host receives this.
|
| 133 |
+
window.parent.postMessage({
|
| 134 |
+
jsonrpc: "2.0",
|
| 135 |
+
method: PROXY_READY_NOTIFICATION,
|
| 136 |
+
params: {},
|
| 137 |
+
}, EXPECTED_HOST_ORIGIN);
|
examples/basic-host/src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
examples/basic-host/tsconfig.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ESNext",
|
| 4 |
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"moduleResolution": "bundler",
|
| 7 |
+
"resolveJsonModule": true,
|
| 8 |
+
"isolatedModules": true,
|
| 9 |
+
"verbatimModuleSyntax": true,
|
| 10 |
+
"noEmit": true,
|
| 11 |
+
"jsx": "react-jsx",
|
| 12 |
+
"strict": true,
|
| 13 |
+
"skipLibCheck": true,
|
| 14 |
+
"noUnusedLocals": true,
|
| 15 |
+
"noUnusedParameters": true,
|
| 16 |
+
"noFallthroughCasesInSwitch": true
|
| 17 |
+
},
|
| 18 |
+
"include": ["src", "*.ts"]
|
| 19 |
+
}
|
examples/basic-host/vite.config.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "vite";
|
| 2 |
+
import react from "@vitejs/plugin-react";
|
| 3 |
+
import { viteSingleFile } from "vite-plugin-singlefile";
|
| 4 |
+
|
| 5 |
+
const INPUT = process.env.INPUT;
|
| 6 |
+
if (!INPUT) {
|
| 7 |
+
throw new Error("INPUT environment variable is not set");
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const isDevelopment = process.env.NODE_ENV === "development";
|
| 11 |
+
|
| 12 |
+
export default defineConfig({
|
| 13 |
+
plugins: [react(), viteSingleFile()],
|
| 14 |
+
build: {
|
| 15 |
+
sourcemap: isDevelopment ? "inline" : undefined,
|
| 16 |
+
cssMinify: !isDevelopment,
|
| 17 |
+
minify: !isDevelopment,
|
| 18 |
+
rollupOptions: {
|
| 19 |
+
input: INPUT,
|
| 20 |
+
},
|
| 21 |
+
outDir: `dist`,
|
| 22 |
+
emptyOutDir: false,
|
| 23 |
+
},
|
| 24 |
+
});
|
examples/basic-server-preact/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
examples/basic-server-preact/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Example: Basic Server (Preact)
|
| 2 |
+
|
| 3 |
+
An MCP App example with a Preact UI.
|
| 4 |
+
|
| 5 |
+
> [!TIP]
|
| 6 |
+
> Looking for a vanilla JavaScript example? See [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs)!
|
| 7 |
+
|
| 8 |
+
## Overview
|
| 9 |
+
|
| 10 |
+
- Tool registration with a linked UI resource
|
| 11 |
+
- Preact UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class
|
| 12 |
+
- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink)
|
| 13 |
+
|
| 14 |
+
## Key Files
|
| 15 |
+
|
| 16 |
+
- [`server.ts`](server.ts) - MCP server with tool and resource registration
|
| 17 |
+
- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.tsx`](src/mcp-app.tsx) - Preact UI using `App` class
|
| 18 |
+
|
| 19 |
+
## Getting Started
|
| 20 |
+
|
| 21 |
+
```bash
|
| 22 |
+
npm install
|
| 23 |
+
npm run dev
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
## How It Works
|
| 27 |
+
|
| 28 |
+
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
|
| 29 |
+
2. When the tool is invoked, the Host renders the UI from the resource.
|
| 30 |
+
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
|
| 31 |
+
|
| 32 |
+
## Build System
|
| 33 |
+
|
| 34 |
+
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.
|
examples/basic-server-preact/mcp-app.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<meta name="color-scheme" content="light dark">
|
| 7 |
+
<title>Get Time App</title>
|
| 8 |
+
<link rel="stylesheet" href="/src/global.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="root"></div>
|
| 12 |
+
<script type="module" src="/src/mcp-app.tsx"></script>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
examples/basic-server-preact/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@modelcontextprotocol/server-basic-preact",
|
| 3 |
+
"version": "0.4.0",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"description": "Basic MCP App Server example using Preact",
|
| 6 |
+
"repository": {
|
| 7 |
+
"type": "git",
|
| 8 |
+
"url": "https://github.com/modelcontextprotocol/ext-apps",
|
| 9 |
+
"directory": "examples/basic-server-preact"
|
| 10 |
+
},
|
| 11 |
+
"license": "MIT",
|
| 12 |
+
"main": "server.ts",
|
| 13 |
+
"files": [
|
| 14 |
+
"server.ts",
|
| 15 |
+
"server-utils.ts",
|
| 16 |
+
"dist"
|
| 17 |
+
],
|
| 18 |
+
"scripts": {
|
| 19 |
+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
|
| 20 |
+
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
|
| 21 |
+
"serve": "bun --watch server.ts",
|
| 22 |
+
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
|
| 23 |
+
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
|
| 24 |
+
"prepublishOnly": "npm run build"
|
| 25 |
+
},
|
| 26 |
+
"dependencies": {
|
| 27 |
+
"@modelcontextprotocol/ext-apps": "^0.4.0",
|
| 28 |
+
"@modelcontextprotocol/sdk": "^1.24.0",
|
| 29 |
+
"preact": "^10.0.0",
|
| 30 |
+
"zod": "^4.1.13"
|
| 31 |
+
},
|
| 32 |
+
"devDependencies": {
|
| 33 |
+
"@preact/preset-vite": "^2.0.0",
|
| 34 |
+
"@types/cors": "^2.8.19",
|
| 35 |
+
"@types/express": "^5.0.0",
|
| 36 |
+
"@types/node": "^22.0.0",
|
| 37 |
+
"concurrently": "^9.2.1",
|
| 38 |
+
"cors": "^2.8.5",
|
| 39 |
+
"cross-env": "^10.1.0",
|
| 40 |
+
"express": "^5.1.0",
|
| 41 |
+
"typescript": "^5.9.3",
|
| 42 |
+
"vite": "^6.0.0",
|
| 43 |
+
"vite-plugin-singlefile": "^2.3.0"
|
| 44 |
+
}
|
| 45 |
+
}
|
examples/basic-server-preact/server-utils.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Shared utilities for running MCP servers with Streamable HTTP transport.
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
| 6 |
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
| 7 |
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
| 8 |
+
import cors from "cors";
|
| 9 |
+
import type { Request, Response } from "express";
|
| 10 |
+
|
| 11 |
+
export interface ServerOptions {
|
| 12 |
+
port: number;
|
| 13 |
+
name?: string;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
/**
|
| 17 |
+
* Starts an MCP server with Streamable HTTP transport in stateless mode.
|
| 18 |
+
*
|
| 19 |
+
* @param createServer - Factory function that creates a new McpServer instance per request.
|
| 20 |
+
* @param options - Server configuration options.
|
| 21 |
+
*/
|
| 22 |
+
export async function startServer(
|
| 23 |
+
createServer: () => McpServer,
|
| 24 |
+
options: ServerOptions,
|
| 25 |
+
): Promise<void> {
|
| 26 |
+
const { port, name = "MCP Server" } = options;
|
| 27 |
+
|
| 28 |
+
const app = createMcpExpressApp({ host: "0.0.0.0" });
|
| 29 |
+
app.use(cors());
|
| 30 |
+
|
| 31 |
+
app.all("/mcp", async (req: Request, res: Response) => {
|
| 32 |
+
const server = createServer();
|
| 33 |
+
const transport = new StreamableHTTPServerTransport({
|
| 34 |
+
sessionIdGenerator: undefined,
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
res.on("close", () => {
|
| 38 |
+
transport.close().catch(() => {});
|
| 39 |
+
server.close().catch(() => {});
|
| 40 |
+
});
|
| 41 |
+
|
| 42 |
+
try {
|
| 43 |
+
await server.connect(transport);
|
| 44 |
+
await transport.handleRequest(req, res, req.body);
|
| 45 |
+
} catch (error) {
|
| 46 |
+
console.error("MCP error:", error);
|
| 47 |
+
if (!res.headersSent) {
|
| 48 |
+
res.status(500).json({
|
| 49 |
+
jsonrpc: "2.0",
|
| 50 |
+
error: { code: -32603, message: "Internal server error" },
|
| 51 |
+
id: null,
|
| 52 |
+
});
|
| 53 |
+
}
|
| 54 |
+
}
|
| 55 |
+
});
|
| 56 |
+
|
| 57 |
+
const httpServer = app.listen(port, (err) => {
|
| 58 |
+
if (err) {
|
| 59 |
+
console.error("Failed to start server:", err);
|
| 60 |
+
process.exit(1);
|
| 61 |
+
}
|
| 62 |
+
console.log(`${name} listening on http://localhost:${port}/mcp`);
|
| 63 |
+
});
|
| 64 |
+
|
| 65 |
+
const shutdown = () => {
|
| 66 |
+
console.log("\nShutting down...");
|
| 67 |
+
httpServer.close(() => process.exit(0));
|
| 68 |
+
};
|
| 69 |
+
|
| 70 |
+
process.on("SIGINT", shutdown);
|
| 71 |
+
process.on("SIGTERM", shutdown);
|
| 72 |
+
}
|
examples/basic-server-preact/server.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
| 2 |
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
| 3 |
+
import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
|
| 4 |
+
import fs from "node:fs/promises";
|
| 5 |
+
import path from "node:path";
|
| 6 |
+
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
|
| 7 |
+
import { startServer } from "./server-utils.js";
|
| 8 |
+
|
| 9 |
+
const DIST_DIR = path.join(import.meta.dirname, "dist");
|
| 10 |
+
|
| 11 |
+
/**
|
| 12 |
+
* Creates a new MCP server instance with tools and resources registered.
|
| 13 |
+
*/
|
| 14 |
+
function createServer(): McpServer {
|
| 15 |
+
const server = new McpServer({
|
| 16 |
+
name: "Basic MCP App Server (Preact)",
|
| 17 |
+
version: "1.0.0",
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
// Two-part registration: tool + resource, tied together by the resource URI.
|
| 21 |
+
const resourceUri = "ui://get-time/mcp-app.html";
|
| 22 |
+
|
| 23 |
+
// Register a tool with UI metadata. When the host calls this tool, it reads
|
| 24 |
+
// `_meta.ui.resourceUri` to know which resource to fetch and render as an
|
| 25 |
+
// interactive UI.
|
| 26 |
+
registerAppTool(server,
|
| 27 |
+
"get-time",
|
| 28 |
+
{
|
| 29 |
+
title: "Get Time",
|
| 30 |
+
description: "Returns the current server time as an ISO 8601 string.",
|
| 31 |
+
inputSchema: {},
|
| 32 |
+
_meta: { ui: { resourceUri } },
|
| 33 |
+
},
|
| 34 |
+
async (): Promise<CallToolResult> => {
|
| 35 |
+
const time = new Date().toISOString();
|
| 36 |
+
return { content: [{ type: "text", text: time }] };
|
| 37 |
+
},
|
| 38 |
+
);
|
| 39 |
+
|
| 40 |
+
// Register the resource, which returns the bundled HTML/JavaScript for the UI.
|
| 41 |
+
registerAppResource(server,
|
| 42 |
+
resourceUri,
|
| 43 |
+
resourceUri,
|
| 44 |
+
{ mimeType: RESOURCE_MIME_TYPE },
|
| 45 |
+
async (): Promise<ReadResourceResult> => {
|
| 46 |
+
const html = await fs.readFile(path.join(DIST_DIR, "mcp-app.html"), "utf-8");
|
| 47 |
+
|
| 48 |
+
return {
|
| 49 |
+
contents: [
|
| 50 |
+
{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html },
|
| 51 |
+
],
|
| 52 |
+
};
|
| 53 |
+
},
|
| 54 |
+
);
|
| 55 |
+
|
| 56 |
+
return server;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
async function main() {
|
| 60 |
+
if (process.argv.includes("--stdio")) {
|
| 61 |
+
await createServer().connect(new StdioServerTransport());
|
| 62 |
+
} else {
|
| 63 |
+
const port = parseInt(process.env.PORT ?? "3001", 10);
|
| 64 |
+
await startServer(createServer, { port, name: "Basic MCP App Server (Preact)" });
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
main().catch((e) => {
|
| 69 |
+
console.error(e);
|
| 70 |
+
process.exit(1);
|
| 71 |
+
});
|
examples/basic-server-preact/src/global.css
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
box-sizing: border-box;
|
| 3 |
+
}
|
| 4 |
+
|
| 5 |
+
html, body {
|
| 6 |
+
font-family: system-ui, -apple-system, sans-serif;
|
| 7 |
+
font-size: 1rem;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
code {
|
| 11 |
+
font-size: 1em;
|
| 12 |
+
}
|
examples/basic-server-preact/src/mcp-app.module.css
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.main {
|
| 2 |
+
--color-primary: #2563eb;
|
| 3 |
+
--color-primary-hover: #1d4ed8;
|
| 4 |
+
--color-notice-bg: #eff6ff;
|
| 5 |
+
|
| 6 |
+
width: 100%;
|
| 7 |
+
max-width: 425px;
|
| 8 |
+
box-sizing: border-box;
|
| 9 |
+
|
| 10 |
+
> * {
|
| 11 |
+
margin-top: 0;
|
| 12 |
+
margin-bottom: 0;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
> * + * {
|
| 16 |
+
margin-top: 1.5rem;
|
| 17 |
+
}
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
.action {
|
| 21 |
+
> * {
|
| 22 |
+
margin-top: 0;
|
| 23 |
+
margin-bottom: 0;
|
| 24 |
+
width: 100%;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
> * + * {
|
| 28 |
+
margin-top: 0.5rem;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
/* Consistent font for form inputs (inherits from global.css) */
|
| 32 |
+
textarea,
|
| 33 |
+
input {
|
| 34 |
+
font-family: inherit;
|
| 35 |
+
font-size: inherit;
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
button {
|
| 39 |
+
padding: 0.5rem 1rem;
|
| 40 |
+
border: none;
|
| 41 |
+
border-radius: 6px;
|
| 42 |
+
color: white;
|
| 43 |
+
font-weight: bold;
|
| 44 |
+
background-color: var(--color-primary);
|
| 45 |
+
cursor: pointer;
|
| 46 |
+
|
| 47 |
+
&:hover,
|
| 48 |
+
&:focus-visible {
|
| 49 |
+
background-color: var(--color-primary-hover);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
.notice {
|
| 55 |
+
padding: 0.5rem 0.75rem;
|
| 56 |
+
color: var(--color-primary);
|
| 57 |
+
text-align: center;
|
| 58 |
+
font-style: italic;
|
| 59 |
+
background-color: var(--color-notice-bg);
|
| 60 |
+
|
| 61 |
+
&::before {
|
| 62 |
+
content: "ℹ️ ";
|
| 63 |
+
font-style: normal;
|
| 64 |
+
}
|
| 65 |
+
}
|
examples/basic-server-preact/src/mcp-app.tsx
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* @file App that demonstrates a few features using MCP Apps SDK + Preact.
|
| 3 |
+
*/
|
| 4 |
+
import {
|
| 5 |
+
App,
|
| 6 |
+
applyDocumentTheme,
|
| 7 |
+
applyHostFonts,
|
| 8 |
+
applyHostStyleVariables,
|
| 9 |
+
type McpUiHostContext,
|
| 10 |
+
} from "@modelcontextprotocol/ext-apps";
|
| 11 |
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
| 12 |
+
import { useCallback, useEffect, useState } from "preact/hooks";
|
| 13 |
+
import { render } from "preact";
|
| 14 |
+
import styles from "./mcp-app.module.css";
|
| 15 |
+
|
| 16 |
+
function extractTime(callToolResult: CallToolResult): string {
|
| 17 |
+
const { text } = callToolResult.content?.find((c) => c.type === "text")!;
|
| 18 |
+
return text;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
function GetTimeApp() {
|
| 23 |
+
const [app, setApp] = useState<App | null>(null);
|
| 24 |
+
const [error, setError] = useState<Error | null>(null);
|
| 25 |
+
const [toolResult, setToolResult] = useState<CallToolResult | null>(null);
|
| 26 |
+
const [hostContext, setHostContext] = useState<McpUiHostContext | undefined>();
|
| 27 |
+
|
| 28 |
+
// Apply host styles reactively when hostContext changes
|
| 29 |
+
useEffect(() => {
|
| 30 |
+
if (hostContext?.theme) {
|
| 31 |
+
applyDocumentTheme(hostContext.theme);
|
| 32 |
+
}
|
| 33 |
+
if (hostContext?.styles?.variables) {
|
| 34 |
+
applyHostStyleVariables(hostContext.styles.variables);
|
| 35 |
+
}
|
| 36 |
+
if (hostContext?.styles?.css?.fonts) {
|
| 37 |
+
applyHostFonts(hostContext.styles.css.fonts);
|
| 38 |
+
}
|
| 39 |
+
}, [hostContext]);
|
| 40 |
+
|
| 41 |
+
useEffect(() => {
|
| 42 |
+
const instance = new App({ name: "Get Time App", version: "1.0.0" });
|
| 43 |
+
|
| 44 |
+
instance.ontoolinput = async (input) => {
|
| 45 |
+
console.info("Received tool call input:", input);
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
instance.ontoolresult = async (result) => {
|
| 49 |
+
console.info("Received tool call result:", result);
|
| 50 |
+
setToolResult(result);
|
| 51 |
+
};
|
| 52 |
+
|
| 53 |
+
instance.ontoolcancelled = (params) => {
|
| 54 |
+
console.info("Tool call cancelled:", params.reason);
|
| 55 |
+
};
|
| 56 |
+
|
| 57 |
+
instance.onerror = console.error;
|
| 58 |
+
|
| 59 |
+
instance.onhostcontextchanged = (params) => {
|
| 60 |
+
setHostContext((prev) => ({ ...prev, ...params }));
|
| 61 |
+
};
|
| 62 |
+
|
| 63 |
+
instance
|
| 64 |
+
.connect()
|
| 65 |
+
.then(() => {
|
| 66 |
+
setApp(instance);
|
| 67 |
+
setHostContext(instance.getHostContext());
|
| 68 |
+
})
|
| 69 |
+
.catch(setError);
|
| 70 |
+
}, []);
|
| 71 |
+
|
| 72 |
+
if (error) return <div><strong>ERROR:</strong> {error.message}</div>;
|
| 73 |
+
if (!app) return <div>Connecting...</div>;
|
| 74 |
+
|
| 75 |
+
return <GetTimeAppInner app={app} toolResult={toolResult} hostContext={hostContext} />;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
interface GetTimeAppInnerProps {
|
| 80 |
+
app: App;
|
| 81 |
+
toolResult: CallToolResult | null;
|
| 82 |
+
hostContext?: McpUiHostContext;
|
| 83 |
+
}
|
| 84 |
+
function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps) {
|
| 85 |
+
const [serverTime, setServerTime] = useState("Loading...");
|
| 86 |
+
const [messageText, setMessageText] = useState("This is message text.");
|
| 87 |
+
const [logText, setLogText] = useState("This is log text.");
|
| 88 |
+
const [linkUrl, setLinkUrl] = useState("https://modelcontextprotocol.io/");
|
| 89 |
+
|
| 90 |
+
useEffect(() => {
|
| 91 |
+
if (toolResult) {
|
| 92 |
+
setServerTime(extractTime(toolResult));
|
| 93 |
+
}
|
| 94 |
+
}, [toolResult]);
|
| 95 |
+
|
| 96 |
+
const handleGetTime = useCallback(async () => {
|
| 97 |
+
try {
|
| 98 |
+
console.info("Calling get-time tool...");
|
| 99 |
+
const result = await app.callServerTool({ name: "get-time", arguments: {} });
|
| 100 |
+
console.info("get-time result:", result);
|
| 101 |
+
setServerTime(extractTime(result));
|
| 102 |
+
} catch (e) {
|
| 103 |
+
console.error(e);
|
| 104 |
+
setServerTime("[ERROR]");
|
| 105 |
+
}
|
| 106 |
+
}, [app]);
|
| 107 |
+
|
| 108 |
+
const handleSendMessage = useCallback(async () => {
|
| 109 |
+
const signal = AbortSignal.timeout(5000);
|
| 110 |
+
try {
|
| 111 |
+
console.info("Sending message text to Host:", messageText);
|
| 112 |
+
const { isError } = await app.sendMessage(
|
| 113 |
+
{ role: "user", content: [{ type: "text", text: messageText }] },
|
| 114 |
+
{ signal },
|
| 115 |
+
);
|
| 116 |
+
console.info("Message", isError ? "rejected" : "accepted");
|
| 117 |
+
} catch (e) {
|
| 118 |
+
console.error("Message send error:", signal.aborted ? "timed out" : e);
|
| 119 |
+
}
|
| 120 |
+
}, [app, messageText]);
|
| 121 |
+
|
| 122 |
+
const handleSendLog = useCallback(async () => {
|
| 123 |
+
console.info("Sending log text to Host:", logText);
|
| 124 |
+
await app.sendLog({ level: "info", data: logText });
|
| 125 |
+
}, [app, logText]);
|
| 126 |
+
|
| 127 |
+
const handleOpenLink = useCallback(async () => {
|
| 128 |
+
console.info("Sending open link request to Host:", linkUrl);
|
| 129 |
+
const { isError } = await app.openLink({ url: linkUrl });
|
| 130 |
+
console.info("Open link request", isError ? "rejected" : "accepted");
|
| 131 |
+
}, [app, linkUrl]);
|
| 132 |
+
|
| 133 |
+
return (
|
| 134 |
+
<main
|
| 135 |
+
className={styles.main}
|
| 136 |
+
style={{
|
| 137 |
+
paddingTop: hostContext?.safeAreaInsets?.top,
|
| 138 |
+
paddingRight: hostContext?.safeAreaInsets?.right,
|
| 139 |
+
paddingBottom: hostContext?.safeAreaInsets?.bottom,
|
| 140 |
+
paddingLeft: hostContext?.safeAreaInsets?.left,
|
| 141 |
+
}}
|
| 142 |
+
>
|
| 143 |
+
<p className={styles.notice}>Watch activity in the DevTools console!</p>
|
| 144 |
+
|
| 145 |
+
<div className={styles.action}>
|
| 146 |
+
<p>
|
| 147 |
+
<strong>Server Time:</strong> <code id="server-time">{serverTime}</code>
|
| 148 |
+
</p>
|
| 149 |
+
<button onClick={handleGetTime}>Get Server Time</button>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<div className={styles.action}>
|
| 153 |
+
<textarea value={messageText} onChange={(e) => setMessageText(e.currentTarget.value)} />
|
| 154 |
+
<button onClick={handleSendMessage}>Send Message</button>
|
| 155 |
+
</div>
|
| 156 |
+
|
| 157 |
+
<div className={styles.action}>
|
| 158 |
+
<input type="text" value={logText} onChange={(e) => setLogText(e.currentTarget.value)} />
|
| 159 |
+
<button onClick={handleSendLog}>Send Log</button>
|
| 160 |
+
</div>
|
| 161 |
+
|
| 162 |
+
<div className={styles.action}>
|
| 163 |
+
<input type="url" value={linkUrl} onChange={(e) => setLinkUrl(e.currentTarget.value)} />
|
| 164 |
+
<button onClick={handleOpenLink}>Open Link</button>
|
| 165 |
+
</div>
|
| 166 |
+
</main>
|
| 167 |
+
);
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
render(<GetTimeApp />, document.getElementById("root")!);
|
examples/basic-server-preact/src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
examples/basic-server-preact/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"target": "ESNext",
|
| 4 |
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
| 5 |
+
"module": "ESNext",
|
| 6 |
+
"moduleResolution": "bundler",
|
| 7 |
+
"allowImportingTsExtensions": true,
|
| 8 |
+
"resolveJsonModule": true,
|
| 9 |
+
"isolatedModules": true,
|
| 10 |
+
"verbatimModuleSyntax": true,
|
| 11 |
+
"noEmit": true,
|
| 12 |
+
"jsx": "react-jsx",
|
| 13 |
+
"jsxImportSource": "preact",
|
| 14 |
+
"strict": true,
|
| 15 |
+
"skipLibCheck": true,
|
| 16 |
+
"noUnusedLocals": true,
|
| 17 |
+
"noUnusedParameters": true,
|
| 18 |
+
"noFallthroughCasesInSwitch": true
|
| 19 |
+
},
|
| 20 |
+
"include": ["src", "server.ts"]
|
| 21 |
+
}
|
examples/basic-server-preact/vite.config.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from "vite";
|
| 2 |
+
import preact from "@preact/preset-vite";
|
| 3 |
+
import { viteSingleFile } from "vite-plugin-singlefile";
|
| 4 |
+
|
| 5 |
+
const INPUT = process.env.INPUT;
|
| 6 |
+
if (!INPUT) {
|
| 7 |
+
throw new Error("INPUT environment variable is not set");
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const isDevelopment = process.env.NODE_ENV === "development";
|
| 11 |
+
|
| 12 |
+
export default defineConfig({
|
| 13 |
+
plugins: [preact(), viteSingleFile()],
|
| 14 |
+
build: {
|
| 15 |
+
sourcemap: isDevelopment ? "inline" : undefined,
|
| 16 |
+
cssMinify: !isDevelopment,
|
| 17 |
+
minify: !isDevelopment,
|
| 18 |
+
|
| 19 |
+
rollupOptions: {
|
| 20 |
+
input: INPUT,
|
| 21 |
+
},
|
| 22 |
+
outDir: "dist",
|
| 23 |
+
emptyOutDir: false,
|
| 24 |
+
},
|
| 25 |
+
});
|
examples/basic-server-react/.gitignore
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules/
|
| 2 |
+
dist/
|
examples/basic-server-react/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Example: Basic Server (React)
|
| 2 |
+
|
| 3 |
+

|
| 4 |
+
|
| 5 |
+
An MCP App example with a React UI.
|
| 6 |
+
|
| 7 |
+
> [!TIP]
|
| 8 |
+
> Looking for a vanilla JavaScript example? See [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs)!
|
| 9 |
+
|
| 10 |
+
## Overview
|
| 11 |
+
|
| 12 |
+
- Tool registration with a linked UI resource
|
| 13 |
+
- React UI using the [`useApp()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook
|
| 14 |
+
- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink)
|
| 15 |
+
|
| 16 |
+
## Key Files
|
| 17 |
+
|
| 18 |
+
- [`server.ts`](server.ts) - MCP server with tool and resource registration
|
| 19 |
+
- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.tsx`](src/mcp-app.tsx) - React UI using `useApp()` hook
|
| 20 |
+
|
| 21 |
+
## Getting Started
|
| 22 |
+
|
| 23 |
+
```bash
|
| 24 |
+
npm install
|
| 25 |
+
npm run dev
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## How It Works
|
| 29 |
+
|
| 30 |
+
1. The server registers a `get-time` tool with metadata linking it to a UI HTML resource (`ui://get-time/mcp-app.html`).
|
| 31 |
+
2. When the tool is invoked, the Host renders the UI from the resource.
|
| 32 |
+
3. The UI uses the MCP App SDK API to communicate with the host and call server tools.
|
| 33 |
+
|
| 34 |
+
## Build System
|
| 35 |
+
|
| 36 |
+
This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.
|
examples/basic-server-react/grid-cell.png
ADDED
|
examples/basic-server-react/mcp-app.html
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<meta name="color-scheme" content="light dark">
|
| 7 |
+
<title>Get Time App</title>
|
| 8 |
+
<link rel="stylesheet" href="/src/global.css">
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div id="root"></div>
|
| 12 |
+
<script type="module" src="/src/mcp-app.tsx"></script>
|
| 13 |
+
</body>
|
| 14 |
+
</html>
|
examples/basic-server-react/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "@modelcontextprotocol/server-basic-react",
|
| 3 |
+
"version": "0.4.0",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"description": "Basic MCP App Server example using React",
|
| 6 |
+
"repository": {
|
| 7 |
+
"type": "git",
|
| 8 |
+
"url": "https://github.com/modelcontextprotocol/ext-apps",
|
| 9 |
+
"directory": "examples/basic-server-react"
|
| 10 |
+
},
|
| 11 |
+
"license": "MIT",
|
| 12 |
+
"main": "server.ts",
|
| 13 |
+
"files": [
|
| 14 |
+
"server.ts",
|
| 15 |
+
"server-utils.ts",
|
| 16 |
+
"dist"
|
| 17 |
+
],
|
| 18 |
+
"scripts": {
|
| 19 |
+
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build",
|
| 20 |
+
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
|
| 21 |
+
"serve": "bun --watch server.ts",
|
| 22 |
+
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
|
| 23 |
+
"dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
|
| 24 |
+
"prepublishOnly": "npm run build"
|
| 25 |
+
},
|
| 26 |
+
"dependencies": {
|
| 27 |
+
"@modelcontextprotocol/ext-apps": "^0.4.0",
|
| 28 |
+
"@modelcontextprotocol/sdk": "^1.24.0",
|
| 29 |
+
"react": "^19.2.0",
|
| 30 |
+
"react-dom": "^19.2.0",
|
| 31 |
+
"zod": "^4.1.13"
|
| 32 |
+
},
|
| 33 |
+
"devDependencies": {
|
| 34 |
+
"@types/cors": "^2.8.19",
|
| 35 |
+
"@types/express": "^5.0.0",
|
| 36 |
+
"@types/node": "^22.0.0",
|
| 37 |
+
"@types/react": "^19.2.2",
|
| 38 |
+
"@types/react-dom": "^19.2.2",
|
| 39 |
+
"@vitejs/plugin-react": "^4.3.4",
|
| 40 |
+
"concurrently": "^9.2.1",
|
| 41 |
+
"cors": "^2.8.5",
|
| 42 |
+
"cross-env": "^10.1.0",
|
| 43 |
+
"express": "^5.1.0",
|
| 44 |
+
"typescript": "^5.9.3",
|
| 45 |
+
"vite": "^6.0.0",
|
| 46 |
+
"vite-plugin-singlefile": "^2.3.0"
|
| 47 |
+
}
|
| 48 |
+
}
|