AbdulElahGwaith commited on
Commit
e1cc3bc
·
verified ·
1 Parent(s): 5655e55

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +11 -35
  2. .github/workflows/ci.yml +154 -0
  3. .github/workflows/docs.yml +28 -0
  4. .github/workflows/npm-publish.yml +139 -0
  5. .github/workflows/publish.yml +41 -0
  6. .gitignore +13 -0
  7. .husky/pre-commit +12 -0
  8. .prettierignore +6 -0
  9. .prettierrc.json +7 -0
  10. AGENTS.md +91 -0
  11. CLAUDE.md +91 -0
  12. CONTRIBUTING.md +184 -0
  13. LICENSE +216 -0
  14. README.md +96 -0
  15. RELEASES.md +64 -0
  16. build.bun.ts +54 -0
  17. docs/index.html +12 -0
  18. docs/migrate_from_openai_apps.md +351 -0
  19. docs/quickstart.md +292 -0
  20. examples/basic-host/.gitignore +2 -0
  21. examples/basic-host/README.md +43 -0
  22. examples/basic-host/index.html +14 -0
  23. examples/basic-host/package.json +35 -0
  24. examples/basic-host/sandbox.html +35 -0
  25. examples/basic-host/serve.ts +148 -0
  26. examples/basic-host/src/global.css +12 -0
  27. examples/basic-host/src/implementation.ts +312 -0
  28. examples/basic-host/src/index.module.css +255 -0
  29. examples/basic-host/src/index.tsx +530 -0
  30. examples/basic-host/src/sandbox.ts +137 -0
  31. examples/basic-host/src/vite-env.d.ts +1 -0
  32. examples/basic-host/tsconfig.json +19 -0
  33. examples/basic-host/vite.config.ts +24 -0
  34. examples/basic-server-preact/.gitignore +2 -0
  35. examples/basic-server-preact/README.md +34 -0
  36. examples/basic-server-preact/mcp-app.html +14 -0
  37. examples/basic-server-preact/package.json +45 -0
  38. examples/basic-server-preact/server-utils.ts +72 -0
  39. examples/basic-server-preact/server.ts +71 -0
  40. examples/basic-server-preact/src/global.css +12 -0
  41. examples/basic-server-preact/src/mcp-app.module.css +65 -0
  42. examples/basic-server-preact/src/mcp-app.tsx +171 -0
  43. examples/basic-server-preact/src/vite-env.d.ts +1 -0
  44. examples/basic-server-preact/tsconfig.json +21 -0
  45. examples/basic-server-preact/vite.config.ts +25 -0
  46. examples/basic-server-react/.gitignore +2 -0
  47. examples/basic-server-react/README.md +36 -0
  48. examples/basic-server-react/grid-cell.png +0 -0
  49. examples/basic-server-react/mcp-app.html +14 -0
  50. examples/basic-server-react/package.json +48 -0
.gitattributes CHANGED
@@ -1,35 +1,11 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
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
+ [![npm version](https://img.shields.io/npm/v/@modelcontextprotocol/ext-apps.svg)](https://www.npmjs.com/package/@modelcontextprotocol/ext-apps) [![API Documentation](https://img.shields.io/badge/docs-API%20Reference-blue)](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
+ | [![Map](examples/map-server/grid-cell.png "Interactive 3D globe viewer using CesiumJS")](examples/map-server) | [![Three.js](examples/threejs-server/grid-cell.png "Interactive 3D scene renderer")](examples/threejs-server) | [![ShaderToy](examples/shadertoy-server/grid-cell.png "Real-time GLSL shader renderer")](examples/shadertoy-server) |
63
+ | [**Map**](examples/map-server) | [**Three.js**](examples/threejs-server) | [**ShaderToy**](examples/shadertoy-server) |
64
+ | [![Sheet Music](examples/sheet-music-server/grid-cell.png "ABC notation to sheet music")](examples/sheet-music-server) | [![Wiki Explorer](examples/wiki-explorer-server/grid-cell.png "Wikipedia link graph visualization")](examples/wiki-explorer-server) | [![Cohort Heatmap](examples/cohort-heatmap-server/grid-cell.png "Customer retention heatmap")](examples/cohort-heatmap-server) |
65
+ | [**Sheet Music**](examples/sheet-music-server) | [**Wiki Explorer**](examples/wiki-explorer-server) | [**Cohort Heatmap**](examples/cohort-heatmap-server) |
66
+ | [![Scenario Modeler](examples/scenario-modeler-server/grid-cell.png "SaaS business projections")](examples/scenario-modeler-server) | [![Budget Allocator](examples/budget-allocator-server/grid-cell.png "Interactive budget allocation")](examples/budget-allocator-server) | [![Customer Segmentation](examples/customer-segmentation-server/grid-cell.png "Scatter chart with clustering")](examples/customer-segmentation-server) |
67
+ | [**Scenario Modeler**](examples/scenario-modeler-server) | [**Budget Allocator**](examples/budget-allocator-server) | [**Customer Segmentation**](examples/customer-segmentation-server) |
68
+ | [![System Monitor](examples/system-monitor-server/grid-cell.png "Real-time OS metrics")](examples/system-monitor-server) | [![Transcript](examples/transcript-server/grid-cell.png "Live speech transcription")](examples/transcript-server) | [![Video Resource](examples/video-resource-server/grid-cell.png "Binary video via MCP resources")](examples/video-resource-server) |
69
+ | [**System Monitor**](examples/system-monitor-server) | [**Transcript**](examples/transcript-server) | [**Video Resource**](examples/video-resource-server) |
70
+ | [![PDF Server](examples/pdf-server/grid-cell.png "Interactive PDF viewer with chunked loading")](examples/pdf-server) | | |
71
+ | [**PDF Server**](examples/pdf-server) | | |
72
+
73
+ ### Starter Templates
74
+
75
+ | | |
76
+ |:---:|:---|
77
+ | [![Basic](examples/basic-server-react/grid-cell.png "Starter template")](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
+ ![Screenshot](screenshot.png)
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
+ }