diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6848e4c..c444585 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,36 +10,6 @@ permissions:
contents: read
jobs:
- prettier-fix:
- name: Auto-fix formatting
- if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
- runs-on: ubuntu-latest
- permissions:
- contents: write
- steps:
- - uses: actions/checkout@v6
- with:
- ref: ${{ github.head_ref }}
- token: ${{ github.token }}
-
- - uses: actions/setup-node@v6
- with:
- node-version: "22"
- cache: npm
-
- - run: npm ci
-
- - run: npm run prettier:fix
-
- - name: Commit formatting changes
- run: |
- git diff --quiet && exit 0
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
- git add -A
- git commit -m "style: auto-fix prettier formatting"
- git push
-
build:
strategy:
fail-fast: false
@@ -60,7 +30,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- name: Verify no private URLs in package-lock.json
shell: bash
@@ -70,19 +40,14 @@ jobs:
shell: bash
run: node scripts/check-versions.mjs
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
- node-version: "22"
- cache: npm
+ node-version: "20"
- - run: npm ci
+ - run: npm install
- run: npm run build
- - name: Validate TypeDoc links
- if: runner.os == 'Linux' && matrix.name == 'Linux x64'
- run: npm exec typedoc -- --treatValidationWarningsAsErrors --emit none
-
- run: npm run examples:build
- name: Verify generated schemas are up-to-date
@@ -101,62 +66,31 @@ jobs:
- run: npm run prettier
- - name: Build MCPB bundle (pdf-server)
- if: runner.os == 'Linux' && matrix.name == 'Linux x64'
- run: node build-mcpb.mjs
- working-directory: examples/pdf-server
-
- - name: Smoke-test MCPB bundle starts
- if: runner.os == 'Linux' && matrix.name == 'Linux x64'
- run: |
- unzip -q pdf-server.mcpb -d .mcpb-smoke
- node .mcpb-smoke/dist/index.js --stdio < /dev/null 2>&1 | tee /tmp/mcpb-smoke.log
- grep -q "Ready" /tmp/mcpb-smoke.log
- working-directory: examples/pdf-server
-
- dependency-isolation:
- name: Dependency isolation
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v6
-
- - uses: actions/setup-node@v6
- with:
- node-version: "22"
- cache: npm
-
- - run: npm ci
-
- - run: npm run test:dependency-isolation
-
e2e:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
- node-version: "22"
- cache: npm
+ node-version: "20"
- - uses: astral-sh/setup-uv@v7
+ - uses: astral-sh/setup-uv@v5
- run: npm ci
- # Headless runs without a channel use chromium-headless-shell, so the
- # full Chromium build (about 165 MB) would only be downloaded, never run.
- name: Install Playwright browsers
- run: npx playwright install --with-deps --only-shell chromium
+ run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npx playwright test --reporter=list
- name: Upload test results
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v4
if: failure()
with:
name: test-results
@@ -169,9 +103,9 @@ jobs:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- - uses: Vampire/setup-wsl@v6
+ - uses: Vampire/setup-wsl@v5
with:
distribution: Ubuntu-24.04
@@ -179,18 +113,13 @@ jobs:
shell: wsl-bash {0}
run: |
sudo apt-get update
- curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
+ curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs
- # The checkout lives on the Windows drive, which WSL reaches over 9P;
- # writing node_modules there is what makes this job slow (npm ci alone
- # took about 3 minutes). Copy the tree onto the distro's ext4 filesystem
- # first, as Microsoft's WSL guidance recommends for anything I/O heavy.
- name: Build and test in WSL
shell: wsl-bash {0}
run: |
- cp -r "$(pwd)" ~/ext-apps && cd ~/ext-apps
- npm ci
+ npm install
npm run build
npm run examples:build
npm test
@@ -217,20 +146,17 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
- node-version: "22"
+ node-version: "20"
- name: Create test project and install from git
shell: bash
- env:
- HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
- HEAD_REF: ${{ github.head_ref || github.ref_name }}
run: |
mkdir test-project
cd test-project
npm init -y
# Install from the PR branch (use head repo for fork PRs)
- npm install "git+https://github.com/${HEAD_REPO}#${HEAD_REF}"
+ npm install "git+https://github.com/${{ github.event.pull_request.head.repo.full_name || github.repository }}#${{ github.head_ref || github.ref_name }}"
# Verify the package is usable (ESM import)
node --input-type=module -e "import { App } from '@modelcontextprotocol/ext-apps'; console.log('Import successful:', typeof App)"
diff --git a/.github/workflows/docs-preview.yml b/.github/workflows/docs-preview.yml
deleted file mode 100644
index 3a170cd..0000000
--- a/.github/workflows/docs-preview.yml
+++ /dev/null
@@ -1,65 +0,0 @@
-name: Docs Preview
-
-on:
- pull_request:
- types: [opened, synchronize, reopened, closed]
- paths:
- - "docs/**"
- - "src/**"
- - "README.md"
- - "typedoc.config.mjs"
- - "scripts/typedoc-*.mjs"
- - ".github/workflows/docs-preview.yml"
-
-permissions:
- contents: read
- pull-requests: write
-
-concurrency:
- group: docs-preview-${{ github.event.pull_request.number }}
- cancel-in-progress: true
-
-jobs:
- build-and-deploy:
- # Refuse to run for fork PRs โ forks do not have access to the Cloudflare
- # secrets, so deploys would fail anyway.
- if: >-
- github.event.action != 'closed' &&
- github.event.pull_request.head.repo.full_name == github.repository
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v6
-
- - uses: actions/setup-node@v6
- with:
- node-version: "22"
- cache: npm
-
- - run: npm ci
-
- - run: npm run build
-
- - run: npm run docs
-
- - name: Deploy preview
- uses: modelcontextprotocol/actions/cloudflare-pages-preview/deploy@main
- with:
- directory: docs
- project-name: mcp-ext-apps-docs-preview
- api-token: ${{ secrets.CF_PAGES_PREVIEW_API_TOKEN }}
- account-id: ${{ secrets.CF_PAGES_PREVIEW_ACCOUNT_ID }}
- comment-title: "๐ Docs Preview Deployed"
- comment-marker: ""
-
- cleanup:
- if: >-
- github.event.action == 'closed' &&
- github.event.pull_request.head.repo.full_name == github.repository
- runs-on: ubuntu-latest
- steps:
- - uses: modelcontextprotocol/actions/cloudflare-pages-preview/cleanup@main
- with:
- project-name: mcp-ext-apps-docs-preview
- api-token: ${{ secrets.CF_PAGES_PREVIEW_API_TOKEN }}
- account-id: ${{ secrets.CF_PAGES_PREVIEW_ACCOUNT_ID }}
- comment-marker: ""
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index a046c29..0fbb170 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -12,13 +12,12 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
- node-version: "22"
- cache: npm
- - run: npm ci
+ node-version: "20"
+ - run: npm install
- run: npm run build
- run: npm run docs
- uses: peaceiris/actions-gh-pages@v4
@@ -27,4 +26,3 @@ jobs:
publish_dir: ./docs
publish_branch: gh-pages
keep_files: true
- cname: apps.extensions.modelcontextprotocol.io
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index ed36c15..3b237cb 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -14,11 +14,11 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
@@ -31,21 +31,17 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
- run: npm ci
- run: npm test
- # All publish jobs depend directly on [build, test] (not on each other) so
- # they all enter "waiting for approval" together and can be approved in a
- # single "Review deployments" click.
-
publish:
runs-on: ubuntu-latest
if: github.event_name == 'release'
@@ -57,11 +53,11 @@ jobs:
id-token: write
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
@@ -92,7 +88,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment: Release
- needs: [build, test]
+ needs: [publish]
permissions:
contents: read
@@ -111,8 +107,6 @@ jobs:
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
- - debug-server
- - lazy-auth-server
- map-server
- pdf-server
- scenario-modeler-server
@@ -125,11 +119,11 @@ jobs:
- wiki-explorer-server
steps:
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
@@ -139,45 +133,7 @@ jobs:
- name: Build example
run: npm run build --workspace examples/${{ matrix.example }}
- - name: Determine npm tag
- id: npm-tag
- run: |
- VERSION=$(node -p "require('./package.json').version")
- if [[ "$VERSION" == *"-"* ]]; then
- echo "tag=--tag beta" >> $GITHUB_OUTPUT
- else
- echo "tag=" >> $GITHUB_OUTPUT
- fi
-
- name: Publish example
- run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public ${{ steps.npm-tag.outputs.tag }}
+ run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }}
-
- publish-mcpb:
- runs-on: ubuntu-latest
- if: github.event_name == 'release'
- environment: Release
- needs: [build, test]
-
- permissions:
- contents: write
-
- steps:
- - uses: actions/checkout@v6
- - uses: oven-sh/setup-bun@v2
- with:
- bun-version: latest
- - uses: actions/setup-node@v6
- with:
- node-version: "22"
- cache: npm
- - run: npm ci
-
- - name: Build pdf-server + MCPB bundle
- run: npm run build:mcpb --workspace examples/pdf-server
-
- - name: Upload MCPB to release
- run: gh release upload "${{ github.event.release.tag_name }}" examples/pdf-server/*.mcpb --clobber
- env:
- GH_TOKEN: ${{ github.token }}
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 478c5b8..283577b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -13,8 +13,8 @@ jobs:
pkg-publish:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
@@ -24,17 +24,11 @@ jobs:
- run: |
npx pkg-pr-new publish \
. \
- ./examples/basic-server-preact \
./examples/basic-server-react \
- ./examples/basic-server-solid \
- ./examples/basic-server-svelte \
./examples/basic-server-vanillajs \
- ./examples/basic-server-vue \
./examples/budget-allocator-server \
./examples/cohort-heatmap-server \
./examples/customer-segmentation-server \
- ./examples/debug-server \
- ./examples/lazy-auth-server \
./examples/map-server \
./examples/pdf-server \
./examples/scenario-modeler-server \
diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml
index 182f01a..52795d6 100644
--- a/.github/workflows/update-snapshots.yml
+++ b/.github/workflows/update-snapshots.yml
@@ -16,20 +16,16 @@ permissions:
jobs:
update-snapshots:
- # Run on workflow_dispatch OR when a maintainer comments "/update-snapshots" on a PR.
- # author_association check prevents arbitrary commenters from triggering a job with
- # contents:write that pushes a [skip ci] commit to the PR branch.
+ # Run on workflow_dispatch OR when someone comments "/update-snapshots" on a PR
if: >
github.event_name == 'workflow_dispatch' ||
- (github.event.issue.pull_request &&
- contains(github.event.comment.body, '/update-snapshots') &&
- contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
+ (github.event.issue.pull_request && contains(github.event.comment.body, '/update-snapshots'))
runs-on: ubuntu-latest
steps:
- name: Get PR branch
if: github.event_name == 'issue_comment'
id: pr
- uses: actions/github-script@v8
+ uses: actions/github-script@v7
with:
script: |
const pr = await github.rest.pulls.get({
@@ -42,7 +38,7 @@ jobs:
- name: Add reaction to comment
if: github.event_name == 'issue_comment'
- uses: actions/github-script@v8
+ uses: actions/github-script@v7
with:
script: |
await github.rest.reactions.createForIssueComment({
@@ -52,7 +48,7 @@ jobs:
content: 'rocket'
});
- - uses: actions/checkout@v6
+ - uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch || steps.pr.outputs.ref || github.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
@@ -61,12 +57,11 @@ jobs:
with:
bun-version: latest
- - uses: actions/setup-node@v6
+ - uses: actions/setup-node@v4
with:
- node-version: "22"
- cache: npm
+ node-version: "20"
- - uses: astral-sh/setup-uv@v7
+ - uses: astral-sh/setup-uv@v5
- run: npm ci
@@ -93,7 +88,7 @@ jobs:
- name: Comment on PR
if: github.event_name == 'issue_comment'
- uses: actions/github-script@v8
+ uses: actions/github-script@v7
with:
script: |
const changed = '${{ steps.commit.outputs.changed }}' === 'true';
diff --git a/.gitignore b/.gitignore
index 5b5486b..9c6a137 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,9 +12,4 @@ intermediate-findings/
playwright-report/
test-results/
__pycache__/
-*.pyc
-
-# MCPB bundles (built artifacts)
-*.mcpb
-.mcpb-stage/
-.mcpb-smoke/
\ No newline at end of file
+*.pyc
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
index ae5b3c7..6fd6718 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -14,16 +14,8 @@ if grep -E '"resolved": "https?://' package-lock.json | grep -v registry.npmjs.o
exit 1
fi
-# Capture staged files so we only re-stage what the user intended to commit
-# (avoids sweeping unrelated WIP into the commit). --diff-filter=d skips
-# deletions so the xargs git add below doesn't fail on removed paths.
-STAGED=$(git diff --name-only --cached --diff-filter=d)
-
npm run build:all
npm run prettier:fix
-# Re-stage originally-staged files (prettier may have reformatted them)
-echo "$STAGED" | xargs -r git add
-
-# Also stage generated files (regenerated by build:all from spec.types.ts)
+# Stage any changes to generated files (they may have been reformatted by prettier)
git add src/generated/
diff --git a/.npmrc b/.npmrc
deleted file mode 100644
index 214c29d..0000000
--- a/.npmrc
+++ /dev/null
@@ -1 +0,0 @@
-registry=https://registry.npmjs.org/
diff --git a/.prettierignore b/.prettierignore
index c51df7b..f5edcf9 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,6 +4,4 @@ examples/basic-server-*/**/*.ts
examples/basic-server-*/**/*.tsx
examples/quickstart/**/*.ts
**/vendor/**
-**/.build/**
-**/.claude/**
SKILL.md
diff --git a/AGENTS.md b/AGENTS.md
index 219b947..9bbd2f0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -10,7 +10,7 @@ Key abstractions:
- **Host** - Chat client embedding the iframe, uses `AppBridge` class to proxy MCP requests
- **Server** - MCP server that registers tools/resources with UI metadata
-Specification (stable): `specification/2026-01-26/apps.mdx`
+Specification (draft): `specification/draft/apps.mdx`
## Commands
@@ -39,10 +39,7 @@ npm test
# Check JSDoc comment syntax and `{@link}` references
npm exec typedoc -- --treatValidationWarningsAsErrors --emit none
-# Regenerate package-lock.json
-# Note: repo .npmrc pins registry to npmjs.org, so a plain `npm i` is safe even
-# if your global npm config points elsewhere. The Docker step below is optional
-# โ it locks linux-amd64 optionalDependencies (sharp, rollup, bun) for CI.
+# Regenerate package-lock.json (especially on setups w/ custom npm registry)
rm -fR package-lock.json node_modules && \
docker run --rm -it --platform linux/amd64 -v $PWD:/src:rw -w /src node:latest npm i && \
rm -fR node_modules && \
@@ -60,8 +57,8 @@ rm -fR package-lock.json node_modules && \
### Key Source Files
-- `src/app.ts` - `App` subclasses `Protocol` from `@modelcontextprotocol/client`, handles View initialization, tool calls, and messaging
-- `src/app-bridge.ts` - `AppBridge` subclasses `Protocol` from `@modelcontextprotocol/client` for the iframe channel and proxies through a separate outer `Client`
+- `src/app.ts` - `App` class extends MCP Protocol, handles guest initialization, tool calls, messaging
+- `src/app-bridge.ts` - `AppBridge` class for hosts, proxies MCP requests, sends tool input/results to guests
- `src/server/index.ts` - Helpers for MCP servers to register tools/resources with UI metadata
- `src/types.ts` - Protocol types re-exported from `spec.types.ts` and Zod schemas from `generated/schema.ts` (auto-generated during build)
- `src/message-transport.ts` - `PostMessageTransport` for iframe communication
@@ -70,12 +67,12 @@ rm -fR package-lock.json node_modules && \
### Protocol Flow
```
-View (App/Protocol) <--PostMessageTransport--> Host (AppBridge/Protocol) <--separate MCP Client--> MCP Server
+View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
```
1. Host creates iframe with view HTML
2. View creates `App` instance and calls `connect()` with `PostMessageTransport`
-3. View sends `ui/initialize` (the only handshake on the iframe channel) and receives authoritative Apps capabilities and context
+3. View sends `ui/initialize` request, receives host capabilities and context
4. Host sends `sendToolInput()` with tool arguments after initialization
5. View can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()`
6. Host sends `sendToolResult()` when tool execution completes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e28dcd1..3793622 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -25,15 +25,6 @@ We welcome contributions to the MCP Apps SDK! This document outlines the process
- Update documentation as needed
- Keep changes focused and atomic
- Provide a clear description of changes
-- **Keep "Allow edits by maintainers" checked** when opening your PR โ this lets maintainers rebase your branch and lets the [`/update-snapshots` workflow](#updating-snapshots-in-ci) push updated screenshots to it
-
-## Examples Policy
-
-The examples in `examples/` are maintained by the core team. **We do not accept pull requests that add new example servers from outside contributors.** Each example carries an ongoing maintenance cost โ keeping it building, tested, and up to date with SDK changes โ so we are deliberate about what we add.
-
-If you have an idea for an example that would provide significant educational value, please [open an issue](https://github.com/modelcontextprotocol/ext-apps/issues) describing what the example would demonstrate and why it would be useful, and maintainers will evaluate the request.
-
-PRs that add new examples without a prior approved issue may be closed.
## Running Examples
@@ -49,408 +40,6 @@ Or build and run examples:
npm run examples:start
```
-### With MCP Clients
-
-To use these examples with MCP clients that support the stdio transport (such as Claude Desktop or VS Code), add this MCP server configuration to your client's settings:
-
-MCP client configuration for all examples (using stdio)
-
-```json
-{
- "mcpServers": {
- "basic-react": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-react",
- "--stdio"
- ]
- },
- "basic-vanillajs": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-vanillajs",
- "--stdio"
- ]
- },
- "basic-vue": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-vue",
- "--stdio"
- ]
- },
- "basic-svelte": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-svelte",
- "--stdio"
- ]
- },
- "basic-preact": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-preact",
- "--stdio"
- ]
- },
- "basic-solid": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-basic-solid",
- "--stdio"
- ]
- },
- "budget-allocator": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-budget-allocator",
- "--stdio"
- ]
- },
- "cohort-heatmap": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-cohort-heatmap",
- "--stdio"
- ]
- },
- "customer-segmentation": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-customer-segmentation",
- "--stdio"
- ]
- },
- "map": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-map",
- "--stdio"
- ]
- },
- "pdf": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-pdf",
- "--stdio"
- ]
- },
- "scenario-modeler": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-scenario-modeler",
- "--stdio"
- ]
- },
- "shadertoy": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-shadertoy",
- "--stdio"
- ]
- },
- "sheet-music": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-sheet-music",
- "--stdio"
- ]
- },
- "system-monitor": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-system-monitor",
- "--stdio"
- ]
- },
- "threejs": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-threejs",
- "--stdio"
- ]
- },
- "transcript": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-transcript",
- "--stdio"
- ]
- },
- "video-resource": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-video-resource",
- "--stdio"
- ]
- },
- "wiki-explorer": {
- "command": "npx",
- "args": [
- "-y",
- "--silent",
- "--registry=https://registry.npmjs.org/",
- "@modelcontextprotocol/server-wiki-explorer",
- "--stdio"
- ]
- },
- "qr": {
- "command": "uv",
- "args": [
- "run",
- "/path/to/ext-apps/examples/qr-server/server.py",
- "--stdio"
- ]
- },
- "say": {
- "command": "uv",
- "args": [
- "run",
- "--default-index",
- "https://pypi.org/simple",
- "https://raw.githubusercontent.com/modelcontextprotocol/ext-apps/refs/heads/main/examples/say-server/server.py",
- "--stdio"
- ]
- }
- }
-}
-```
-
-MCP client configuration for local development (all examples)
-
-```json
-{
- "mcpServers": {
- "basic-react": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/basic-server-react && npm --silent run start:stdio"
- ]
- },
- "basic-vanillajs": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/basic-server-vanillajs && npm --silent run start:stdio"
- ]
- },
- "basic-vue": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/basic-server-vue && npm --silent run start:stdio"
- ]
- },
- "basic-svelte": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/basic-server-svelte && npm --silent run start:stdio"
- ]
- },
- "basic-preact": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/basic-server-preact && npm --silent run start:stdio"
- ]
- },
- "basic-solid": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/basic-server-solid && npm --silent run start:stdio"
- ]
- },
- "budget-allocator": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/budget-allocator-server && npm --silent run start:stdio"
- ]
- },
- "cohort-heatmap": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/cohort-heatmap-server && npm --silent run start:stdio"
- ]
- },
- "customer-segmentation": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/customer-segmentation-server && npm --silent run start:stdio"
- ]
- },
- "map": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/map-server && npm --silent run start:stdio"
- ]
- },
- "pdf": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/pdf-server && npm --silent run start:stdio"
- ]
- },
- "scenario-modeler": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/scenario-modeler-server && npm --silent run start:stdio"
- ]
- },
- "shadertoy": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/shadertoy-server && npm --silent run start:stdio"
- ]
- },
- "sheet-music": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/sheet-music-server && npm --silent run start:stdio"
- ]
- },
- "system-monitor": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/system-monitor-server && npm --silent run start:stdio"
- ]
- },
- "threejs": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/threejs-server && npm --silent run start:stdio"
- ]
- },
- "transcript": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/transcript-server && npm --silent run start:stdio"
- ]
- },
- "video-resource": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/video-resource-server && npm --silent run start:stdio"
- ]
- },
- "wiki-explorer": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/src/ext-apps/examples/wiki-explorer-server && npm --silent run start:stdio"
- ]
- },
- "qr": {
- "command": "bash",
- "args": [
- "-c",
- "uv run ~/src/ext-apps/examples/qr-server/server.py --stdio"
- ]
- },
- "say": {
- "command": "bash",
- "args": [
- "-c",
- "uv run --index https://pypi.org/simple ~/src/ext-apps/examples/say-server/server.py --stdio"
- ]
- }
- }
-}
-```
-
-
- Build interactive UIs for MCP tools โ charts, forms, dashboards โ that render inline in Claude, ChatGPT and any other compliant chat client.
-
- Why
- ยท
- Quickstart
- ยท
- API Docs
- ยท
- Spec
- ยท
- Contributing
-
-
-
Excalidraw built with MCP Apps, running in Claude
-
Styled with host CSS variables and fonts
+Uses [data-theme] selectors
` tag before executing it, partially rendering a table as data arrives, or incrementally populating a chart.
+Use {@link app!App.ontoolinputpartial `ontoolinputpartial`} to receive streaming tool arguments as they arrive, allowing you to show a loading preview before the complete input is available.
```ts source="../src/app.examples.ts#App_ontoolinputpartial_progressiveRendering"
-const codePreview = document.querySelector("#code-preview")!;
-const canvas = document.querySelector("#canvas")!;
+let toolInputs: Record | null = null;
+let toolInputsPartial: Record | null = null;
app.ontoolinputpartial = (params) => {
- codePreview.textContent = (params.arguments?.code as string) ?? "";
- codePreview.style.display = "block";
- canvas.style.display = "none";
+ toolInputsPartial = params.arguments as Record;
+ render();
};
app.ontoolinput = (params) => {
- codePreview.style.display = "none";
- canvas.style.display = "block";
- render(params.arguments?.code as string);
+ toolInputs = params.arguments as Record;
+ toolInputsPartial = null;
+ render();
};
+
+function render() {
+ if (toolInputs) {
+ renderFinalUI(toolInputs);
+ } else {
+ renderLoadingUI(toolInputsPartial); // e.g., shimmer with partial preview
+ }
+}
```
> [!IMPORTANT]
> Partial arguments are "healed" JSON โ the host closes unclosed brackets/braces to produce valid JSON. This means objects may be incomplete (e.g., the last item in an array may be truncated). Don't rely on partial data for critical operations; use it only for preview UI.
-> [!NOTE]
-> For full examples that implement this pattern, see: [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) and [`examples/threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server).
+_See [`examples/threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server) for a full implementation of this pattern._
diff --git a/docs/patterns.tsx b/docs/patterns.tsx
index bb9eaa4..f13268f 100644
--- a/docs/patterns.tsx
+++ b/docs/patterns.tsx
@@ -14,83 +14,13 @@ import {
applyHostStyleVariables,
} from "../src/styles.js";
import { randomUUID } from "node:crypto";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import type { McpUiHostContext } from "../src/types.js";
-import { useEffect, useState } from "react";
-import { useApp } from "../src/react/index.js";
+import { useApp, useHostStyles } from "../src/react/index.js";
import { registerAppTool } from "../src/server/index.js";
-import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
-/**
- * Example: Polling for live data (Vanilla JS)
- */
-function pollingVanillaJs(app: App, updateUI: (data: unknown) => void) {
- //#region pollingVanillaJs
- let intervalId: number | null = null;
-
- async function poll() {
- const result = await app.callServerTool({
- name: "poll-data",
- arguments: {},
- });
- updateUI(result.structuredContent);
- }
-
- function startPolling() {
- if (intervalId !== null) return;
- poll();
- intervalId = window.setInterval(poll, 2000);
- }
-
- function stopPolling() {
- if (intervalId === null) return;
- clearInterval(intervalId);
- intervalId = null;
- }
-
- // Clean up when host tears down the view
- app.onteardown = async () => {
- stopPolling();
- return {};
- };
- //#endregion pollingVanillaJs
-}
-
-/**
- * Example: Polling for live data (React)
- */
-function pollingReact(
- app: App | null, // via useApp()
-) {
- const [data, setData] = useState();
-
- //#region pollingReact
- useEffect(() => {
- if (!app) return;
- let cancelled = false;
-
- async function poll() {
- const result = await app!.callServerTool({
- name: "poll-data",
- arguments: {},
- });
- if (!cancelled) setData(result.structuredContent);
- }
-
- poll();
- const id = setInterval(poll, 2000);
- return () => {
- cancelled = true;
- clearInterval(id);
- };
- }, [app]);
- //#endregion pollingReact
-}
-
/**
* Example: Server-side chunked data tool (app-only)
*/
@@ -113,14 +43,14 @@ function chunkedDataServer(server: McpServer) {
{
title: "Read Data Bytes",
description: "Load binary data in chunks",
- inputSchema: z.object({
+ inputSchema: {
id: z.string().describe("Resource identifier"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
.number()
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
- }),
+ },
outputSchema: DataChunkSchema,
// Hidden from model - only callable by the App
_meta: { ui: { visibility: ["app"] } },
@@ -217,54 +147,10 @@ function chunkedDataClient(app: App, resourceId: string) {
}
/**
- * Example: Serving binary blobs via resources (server-side)
+ * Example: Unified host styling (theme, CSS variables, fonts)
*/
-function binaryBlobResourceServer(
- server: McpServer,
- getVideoData: (id: string) => Promise,
-) {
- //#region binaryBlobResourceServer
- server.registerResource(
- "Video",
- new ResourceTemplate("video://{id}", { list: undefined }),
- {
- description: "Video data served as base64 blob",
- mimeType: "video/mp4",
- },
- async (uri, { id }): Promise => {
- // Fetch or load your binary data
- const idString = Array.isArray(id) ? id[0] : id;
- const buffer = await getVideoData(idString);
- const blob = Buffer.from(buffer).toString("base64");
-
- return { contents: [{ uri: uri.href, mimeType: "video/mp4", blob }] };
- },
- );
- //#endregion binaryBlobResourceServer
-}
-
-/**
- * Example: Serving binary blobs via resources (client-side)
- */
-async function binaryBlobResourceClient(app: App, videoId: string) {
- //#region binaryBlobResourceClient
- const result = await app.readServerResource({ uri: `video://${videoId}` });
-
- const content = result.contents[0];
- if (!content || !("blob" in content)) {
- throw new Error("Resource did not contain blob data");
- }
-
- const videoEl = document.querySelector("video")!;
- videoEl.src = `data:${content.mimeType!};base64,${content.blob}`;
- //#endregion binaryBlobResourceClient
-}
-
-/**
- * Example: Adapting to host context (theme, CSS variables, fonts, safe areas)
- */
-function hostContextVanillaJs(app: App, mainEl: HTMLElement) {
- //#region hostContextVanillaJs
+function hostStylingVanillaJs(app: App) {
+ //#region hostStylingVanillaJs
function applyHostContext(ctx: McpUiHostContext) {
if (ctx.theme) {
applyDocumentTheme(ctx.theme);
@@ -275,81 +161,48 @@ function hostContextVanillaJs(app: App, mainEl: HTMLElement) {
if (ctx.styles?.css?.fonts) {
applyHostFonts(ctx.styles.css.fonts);
}
- if (ctx.safeAreaInsets) {
- mainEl.style.paddingTop = `${ctx.safeAreaInsets.top}px`;
- mainEl.style.paddingRight = `${ctx.safeAreaInsets.right}px`;
- mainEl.style.paddingBottom = `${ctx.safeAreaInsets.bottom}px`;
- mainEl.style.paddingLeft = `${ctx.safeAreaInsets.left}px`;
- }
}
// Apply when host context changes
app.onhostcontextchanged = applyHostContext;
- // Apply initial context after connecting
+ // Apply initial styles after connecting
app.connect().then(() => {
const ctx = app.getHostContext();
if (ctx) {
applyHostContext(ctx);
}
});
- //#endregion hostContextVanillaJs
+ //#endregion hostStylingVanillaJs
}
/**
- * Example: Adapting to host context with React (CSS variables, theme, fonts, safe areas)
+ * Example: Host styling with React (CSS variables, theme, fonts)
*/
-function hostContextReact() {
- //#region hostContextReact
+function hostStylingReact() {
+ //#region hostStylingReact
function MyApp() {
- const [hostContext, setHostContext] = useState();
-
const { app } = useApp({
appInfo: { name: "MyApp", version: "1.0.0" },
capabilities: {},
- onAppCreated: (app) => {
- app.onhostcontextchanged = (ctx) => {
- setHostContext((prev) => ({ ...prev, ...ctx }));
- };
- },
});
- // Set initial host context after connection
- useEffect(() => {
- if (app) {
- setHostContext(app.getHostContext());
- }
- }, [app]);
-
- // Apply styles when host context changes
- useEffect(() => {
- if (hostContext?.theme) {
- applyDocumentTheme(hostContext.theme);
- }
- if (hostContext?.styles?.variables) {
- applyHostStyleVariables(hostContext.styles.variables);
- }
- if (hostContext?.styles?.css?.fonts) {
- applyHostFonts(hostContext.styles.css.fonts);
- }
- }, [hostContext]);
+ // Apply all host styles (variables, theme, fonts)
+ useHostStyles(app, app?.getHostContext());
return (
- Styled with host CSS variables, fonts, and safe area insets
+ Styled with host CSS variables and fonts
+ Uses [data-theme] selectors
);
}
- //#endregion hostContextReact
+ //#endregion hostStylingReact
}
/**
@@ -431,9 +284,9 @@ function visibilityBasedPause(
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
- animation.play(); // or startPolling(), etc
+ animation.play();
} else {
- animation.pause(); // or stopPolling(), etc
+ animation.pause();
}
});
});
@@ -449,14 +302,10 @@ function visibilityBasedPause(
}
// Suppress unused variable warnings
-void pollingVanillaJs;
-void pollingReact;
void chunkedDataServer;
void chunkedDataClient;
-void binaryBlobResourceServer;
-void binaryBlobResourceClient;
-void hostContextVanillaJs;
-void hostContextReact;
+void hostStylingVanillaJs;
+void hostStylingReact;
void persistViewStateServer;
void persistViewState;
void visibilityBasedPause;
diff --git a/docs/quickstart.md b/docs/quickstart.md
index a021912..8146824 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -1,7 +1,5 @@
---
title: Quickstart
-group: Getting Started
-description: Build your first MCP App step by step โ create an MCP server with an interactive View that renders inside Claude Desktop and other MCP hosts.
---
# Build Your First MCP App
@@ -21,7 +19,7 @@ This tutorial assumes you've built an MCP server before and are comfortable with
We'll use the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) to build the server.
-You'll also need Node.js 20+.
+You'll also need Node.js 18+.
## 1. Set up the project
@@ -37,7 +35,7 @@ Install the dependencies you'll need:
```bash
npm init -y
-npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 @modelcontextprotocol/node@^2.0.0 @modelcontextprotocol/express@^2.0.0 zod@^4.2.0 express cors
+npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors
npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env
```
@@ -46,12 +44,9 @@ Configure your [`package.json`](https://github.com/modelcontextprotocol/ext-apps
```bash
npm pkg set type=module
npm pkg set scripts.build="tsc --noEmit && tsc -p tsconfig.server.json && cross-env INPUT=mcp-app.html vite build"
-npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch" "tsx watch main.ts"'
+npm pkg set scripts.start="concurrently 'cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch' 'tsx watch main.ts'"
```
-> [!NOTE]
-> Windows `cmd.exe` users will need to convert quotes in the above command: `npm pkg set scripts.start="concurrently --raw ""cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch"" ""tsx watch main.ts"""`.
-
Create tsconfig.json:
@@ -59,7 +54,6 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN
```json source="../examples/quickstart/tsconfig.json"
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
@@ -88,7 +82,6 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN
```json source="../examples/quickstart/tsconfig.server.json"
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
@@ -115,7 +108,7 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN
```ts source="../examples/quickstart/vite.config.ts"
-import { createLogger, defineConfig } from "vite";
+import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";
const INPUT = process.env.INPUT;
@@ -125,14 +118,7 @@ if (!INPUT) {
const isDevelopment = process.env.NODE_ENV === "development";
-const prefixedLogger = createLogger();
-for (const level of ["info", "warn", "error"] as const) {
- const fn = prefixedLogger[level];
- prefixedLogger[level] = (msg, opts) => fn(msg.replace(/^/mg, "[vite] "), opts);
-}
-
export default defineConfig({
- customLogger: prefixedLogger,
plugins: [viteSingleFile()],
build: {
sourcemap: isDevelopment ? "inline" : undefined,
@@ -180,10 +166,9 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import fs from "node:fs/promises";
import path from "node:path";
-import { z } from "zod";
const DIST_DIR = path.join(import.meta.dirname, "dist");
@@ -208,7 +193,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async () => {
@@ -243,10 +228,10 @@ export function createServer(): McpServer {
```ts source="../examples/quickstart/main.ts"
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -266,7 +251,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
@@ -462,9 +447,8 @@ MCP server listening on http://localhost:3001/mcp
```bash
git clone https://github.com/modelcontextprotocol/ext-apps.git
-cd ext-apps
+cd ext-apps/examples/basic-host
npm install
-cd examples/basic-host
npm start
```
@@ -484,4 +468,4 @@ You've built your first MCP App!
- **Continue learning**: The [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) example builds on this quickstart with host communication, theming, and lifecycle handlers
- **React version**: Compare with [`basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for a React-based UI
- **Other frameworks**: See also [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue), [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte), [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact), and [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) examples
-- **API reference**: See the full [API documentation](https://apps.extensions.modelcontextprotocol.io/api/)
+- **API reference**: See the full [API documentation](https://modelcontextprotocol.github.io/ext-apps/api/)
diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md
index a8eb333..06873da 100644
--- a/docs/testing-mcp-apps.md
+++ b/docs/testing-mcp-apps.md
@@ -1,7 +1,5 @@
---
title: Testing MCP Apps
-group: Getting Started
-description: Test MCP Apps locally with the basic-host reference implementation or in production hosts like Claude.ai and VS Code.
---
# Test Your MCP App
@@ -23,15 +21,10 @@ The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/ex
```bash
git clone https://github.com/modelcontextprotocol/ext-apps.git
- cd ext-apps
+ cd ext-apps/examples/basic-host
npm install
- cd examples/basic-host
```
- The root install runs the package build, including type-checking the
- documentation snippets against the pinned base MCP SDK 2.x packages. Do
- not bypass a failed build before testing the host.
-
2. Start basic-host, pointing it to your MCP server:
```bash
@@ -66,8 +59,8 @@ For additional observability, open your browser's developer console. Basic-host
To test your App in a real conversational environment, install your MCP server in a host that supports MCP Apps:
- Claude\.ai
- - [Remote MCP servers (over HTTP)](https://claude.com/docs/connectors/custom/remote-mcp)
- - [Local MCP servers (over stdio)](https://claude.com/docs/connectors/custom/desktop-extensions)
+ - [Remote MCP servers (over HTTP)](https://claude.ai/docs/connectors/custom/remote-mcp)
+ - [Local MCP servers (over stdio)](https://claude.ai/docs/connectors/custom/desktop-extensions)
- [VS Code (Insiders)](https://code.visualstudio.com/docs/copilot/customization/mcp-servers)
- [Goose](https://block.github.io/goose/docs/getting-started/using-extensions/)
diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json
index cefe9d5..d3ce4f9 100644
--- a/examples/basic-host/package.json
+++ b/examples/basic-host/package.json
@@ -1,7 +1,7 @@
{
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host",
"name": "@modelcontextprotocol/ext-apps-basic-host",
- "version": "2.0.0",
+ "version": "0.4.2",
"type": "module",
"scripts": {
"build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"",
@@ -11,11 +11,11 @@
"dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\""
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
+ "@modelcontextprotocol/ext-apps": "../..",
+ "@modelcontextprotocol/sdk": "^1.24.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/express": "^5.0.0",
@@ -29,6 +29,7 @@
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"vite": "^6.0.0",
- "vite-plugin-singlefile": "^2.3.0"
+ "vite-plugin-singlefile": "^2.3.0",
+ "vitest": "^3.2.4"
}
}
diff --git a/examples/basic-host/serve.ts b/examples/basic-host/serve.ts
index bc3fbff..03c3a0b 100644
--- a/examples/basic-host/serve.ts
+++ b/examples/basic-host/serve.ts
@@ -82,8 +82,6 @@ function buildCspHeader(csp?: McpUiResourceCsp): string {
`img-src 'self' data: blob: ${resourceDomains}`.trim(),
// Fonts: same-origin + data/blob URIs + specified domains
`font-src 'self' data: blob: ${resourceDomains}`.trim(),
- // Media (audio/video): same-origin + data/blob URIs + specified domains
- `media-src 'self' data: blob: ${resourceDomains}`.trim(),
// Network requests: same-origin + specified API/tile domains
`connect-src 'self' ${connectDomains}`.trim(),
// Workers: same-origin + blob (dynamic workers) + specified domains
diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts
index 5f680ff..434169a 100644
--- a/examples/basic-host/src/implementation.ts
+++ b/examples/basic-host/src/implementation.ts
@@ -1,12 +1,8 @@
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
-import {
- Client,
- SSEClientTransport,
- StreamableHTTPClientTransport,
- type CallToolResult,
- type Resource,
- type Tool,
-} from "@modelcontextprotocol/client";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
+import type { CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
import { getTheme, onThemeChange } from "./theme";
import { HOST_STYLE_VARIABLES } from "./host-styles";
@@ -26,7 +22,6 @@ export interface ServerInfo {
name: string;
client: Client;
tools: Map;
- resources: Map;
appHtmlCache: Map;
}
@@ -41,12 +36,7 @@ export async function connectToServer(serverUrl: URL): Promise {
const tools = new Map(toolsList.tools.map((tool) => [tool.name, tool]));
log.info("Server tools:", Array.from(tools.keys()));
- // Fetch resources for listing-level _meta.ui (fallback for content-level)
- const resourcesList = await client.listResources();
- const resources = new Map(resourcesList.resources.map((r) => [r.uri, r]));
- log.info("Server resources:", Array.from(resources.keys()));
-
- return { name, client, tools, resources, appHtmlCache: new Map() };
+ return { name, client, tools, appHtmlCache: new Map() };
}
async function connectWithFallback(serverUrl: URL): Promise {
@@ -138,23 +128,14 @@ async function getUiResource(serverInfo: ServerInfo, uri: string): Promise {
- const width = Math.round(entry.contentRect.width);
- if (width > 0) {
- appBridge.sendHostContextChange({
- containerDimensions: { width, maxHeight: 6000 },
- });
- }
- });
- iframeResizeObserver.observe(iframe);
- // AppBridge inherits Protocol's onclose hook โ chain disposal there.
- const prevOnclose = appBridge.onclose;
- appBridge.onclose = () => {
- iframeResizeObserver.disconnect();
- prevOnclose?.();
- };
-
// Register all handlers before calling connect(). The view can start
// sending requests immediately after the initialization handshake, so any
// handlers registered after connect() might miss early requests.
diff --git a/examples/basic-host/src/index.module.css b/examples/basic-host/src/index.module.css
index 0bd09dd..ec18e2b 100644
--- a/examples/basic-host/src/index.module.css
+++ b/examples/basic-host/src/index.module.css
@@ -1,7 +1,7 @@
/* Theme toggle button */
.themeToggle {
position: fixed;
- bottom: 1rem;
+ top: 1rem;
right: 1rem;
width: 2.5rem;
height: 2.5rem;
@@ -165,7 +165,7 @@
margin: 0;
padding: 1rem;
max-width: none;
- background: var(--color-bg);
+ background: white;
border: none;
border-radius: 0;
display: flex;
@@ -201,30 +201,29 @@
width: 32px;
height: 32px;
padding: 0;
- border: 1px solid var(--color-border);
+ border: 1px solid #ccc;
border-radius: 4px;
- background: var(--color-bg-secondary);
- color: var(--color-text);
+ background: #f5f5f5;
font-size: 1.25rem;
cursor: pointer;
&:hover {
- background: var(--color-bg);
+ background: #e0e0e0;
}
}
.collapsiblePanel {
margin: 0.5rem 0;
padding: 0.5rem 0.75rem;
- border: 1px solid var(--color-border);
+ border: 1px solid #e0e0e0;
border-radius: 4px;
- background-color: var(--color-bg-secondary);
+ background-color: #fafafa;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.15s;
&:hover {
- background-color: var(--color-bg);
+ background-color: #f0f0f0;
}
}
@@ -236,23 +235,23 @@
.collapsibleLabel {
font-weight: 600;
- color: var(--color-text);
+ color: #555;
}
.collapsibleSize {
- color: var(--color-text-secondary);
+ color: #888;
font-size: 0.75rem;
}
.collapsibleToggle {
margin-left: auto;
- color: var(--color-text-secondary);
+ color: #888;
font-size: 0.75rem;
}
.collapsiblePreview {
margin-top: 0.25rem;
- color: var(--color-text-secondary);
+ color: #666;
font-family: monospace;
font-size: 0.8rem;
white-space: nowrap;
@@ -264,8 +263,7 @@
margin: 0.5rem 0 0;
padding: 0.5rem;
border-radius: 4px;
- background-color: var(--color-bg);
- color: var(--color-text);
+ background-color: #f5f5f5;
font-family: monospace;
font-size: 0.8rem;
white-space: pre-wrap;
diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx
index 00c0d52..3d488a7 100644
--- a/examples/basic-host/src/index.tsx
+++ b/examples/basic-host/src/index.tsx
@@ -1,5 +1,5 @@
import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge";
-import type { Tool } from "@modelcontextprotocol/client";
+import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation";
diff --git a/examples/basic-host/src/sandbox.ts b/examples/basic-host/src/sandbox.ts
index 6f1dd93..1caf1c5 100644
--- a/examples/basic-host/src/sandbox.ts
+++ b/examples/basic-host/src/sandbox.ts
@@ -1,5 +1,5 @@
-import type { McpUiSandboxProxyReadyNotification, McpUiSandboxResourceReadyNotification } from "@modelcontextprotocol/ext-apps/app-bridge";
-import { buildAllowAttribute } from "@modelcontextprotocol/ext-apps/app-bridge";
+import type { McpUiSandboxProxyReadyNotification, McpUiSandboxResourceReadyNotification } from "../../../dist/src/types";
+import { buildAllowAttribute } from "../../../dist/src/app-bridge";
const ALLOWED_REFERRER_PATTERN = /^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/;
diff --git a/examples/basic-host/tsconfig.json b/examples/basic-host/tsconfig.json
index 3c143ca..9d7c3bc 100644
--- a/examples/basic-host/tsconfig.json
+++ b/examples/basic-host/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-preact/README.md b/examples/basic-server-preact/README.md
index 6a75ddb..b85fb90 100644
--- a/examples/basic-server-preact/README.md
+++ b/examples/basic-server-preact/README.md
@@ -26,29 +26,11 @@ Add to your MCP client configuration (stdio transport):
}
```
-### Local Development
-
-To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):
-
-```json
-{
- "mcpServers": {
- "basic-preact": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-preact && npm run build >&2 && node dist/index.js --stdio"
- ]
- }
- }
-}
-```
-
## Overview
- Tool registration with a linked UI resource
-- Preact UI using the [`App`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) class
-- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink)
+- Preact UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class
+- 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)
## Key Files
@@ -70,4 +52,4 @@ npm run dev
## Build System
-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://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.
+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.
diff --git a/examples/basic-server-preact/main.ts b/examples/basic-server-preact/main.ts
index ffdff52..7642632 100644
--- a/examples/basic-server-preact/main.ts
+++ b/examples/basic-server-preact/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json
index 60aa40d..d58b2f4 100644
--- a/examples/basic-server-preact/package.json
+++ b/examples/basic-server-preact/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "2.0.0",
+ "version": "0.4.2",
"type": "module",
"description": "Basic MCP App Server example using Preact",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-preact"
},
"license": "MIT",
@@ -17,22 +17,17 @@
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun --watch main.ts",
- "serve:stdio": "bun main.ts --stdio",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
- "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio",
- "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"",
+ "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "../..",
+ "@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"preact": "^10.0.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@preact/preset-vite": "^2.0.0",
diff --git a/examples/basic-server-preact/server.ts b/examples/basic-server-preact/server.ts
index dfec94a..bf14ea5 100644
--- a/examples/basic-server-preact/server.ts
+++ b/examples/basic-server-preact/server.ts
@@ -1,12 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -32,7 +28,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-preact/src/global.css b/examples/basic-server-preact/src/global.css
index 801291f..08d4da1 100644
--- a/examples/basic-server-preact/src/global.css
+++ b/examples/basic-server-preact/src/global.css
@@ -1,92 +1,18 @@
-:root {
- color-scheme: light dark;
-
- /*
- * Fallbacks for host style variables used by this app.
- * The host may provide these (and many more) via the host context.
- */
- --color-text-primary: light-dark(#1f2937, #f3f4f6);
- --color-text-inverse: light-dark(#f3f4f6, #1f2937);
- --color-text-info: light-dark(#1d4ed8, #60a5fa);
- --color-background-primary: light-dark(#ffffff, #1a1a1a);
- --color-background-inverse: light-dark(#1a1a1a, #ffffff);
- --color-background-info: light-dark(#eff6ff, #1e3a5f);
- --color-ring-primary: light-dark(#3b82f6, #60a5fa);
- --border-radius-md: 6px;
- --border-width-regular: 1px;
- --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
- --font-weight-normal: 400;
- --font-weight-bold: 700;
- --font-text-md-size: 1rem;
- --font-text-md-line-height: 1.5;
- --font-heading-3xl-size: 2.25rem;
- --font-heading-3xl-line-height: 1.1;
- --font-heading-2xl-size: 1.875rem;
- --font-heading-2xl-line-height: 1.2;
- --font-heading-xl-size: 1.5rem;
- --font-heading-xl-line-height: 1.25;
- --font-heading-lg-size: 1.25rem;
- --font-heading-lg-line-height: 1.3;
- --font-heading-md-size: 1rem;
- --font-heading-md-line-height: 1.4;
- --font-heading-sm-size: 0.875rem;
- --font-heading-sm-line-height: 1.4;
-
- /* Spacing derived from host typography */
- --spacing-unit: var(--font-text-md-size);
- --spacing-xs: calc(var(--spacing-unit) * 0.25);
- --spacing-sm: calc(var(--spacing-unit) * 0.5);
- --spacing-md: var(--spacing-unit);
- --spacing-lg: calc(var(--spacing-unit) * 1.5);
-
- /* App accent color (customize for your brand) */
- --color-accent: #2563eb;
- --color-text-on-accent: #ffffff;
-}
-
* {
box-sizing: border-box;
}
html, body {
- font-family: var(--font-sans);
- font-size: var(--font-text-md-size);
- font-weight: var(--font-weight-normal);
- line-height: var(--font-text-md-line-height);
- color: var(--color-text-primary);
+ font-family: system-ui, -apple-system, sans-serif;
+ font-size: 1rem;
}
-h1 {
- font-size: var(--font-heading-3xl-size);
- line-height: var(--font-heading-3xl-line-height);
-}
-h2 {
- font-size: var(--font-heading-2xl-size);
- line-height: var(--font-heading-2xl-line-height);
-}
-h3 {
- font-size: var(--font-heading-xl-size);
- line-height: var(--font-heading-xl-line-height);
-}
-h4 {
- font-size: var(--font-heading-lg-size);
- line-height: var(--font-heading-lg-line-height);
-}
-h5 {
- font-size: var(--font-heading-md-size);
- line-height: var(--font-heading-md-line-height);
-}
-h6 {
- font-size: var(--font-heading-sm-size);
- line-height: var(--font-heading-sm-line-height);
-}
-
-code, pre, kbd {
- font-family: var(--font-mono);
+code {
font-size: 1em;
}
-b, strong {
- font-weight: var(--font-weight-bold);
+/* Server time fills remaining width for consistent E2E screenshot masking */
+#server-time {
+ flex: 1;
+ min-width: 0;
}
diff --git a/examples/basic-server-preact/src/mcp-app.module.css b/examples/basic-server-preact/src/mcp-app.module.css
index ae08a8c..41a0456 100644
--- a/examples/basic-server-preact/src/mcp-app.module.css
+++ b/examples/basic-server-preact/src/mcp-app.module.css
@@ -1,4 +1,8 @@
.main {
+ --color-primary: #2563eb;
+ --color-primary-hover: #1d4ed8;
+ --color-notice-bg: #eff6ff;
+
width: 100%;
max-width: 425px;
box-sizing: border-box;
@@ -9,7 +13,7 @@
}
> * + * {
- margin-top: var(--spacing-lg);
+ margin-top: 1.5rem;
}
}
@@ -21,59 +25,48 @@
}
> * + * {
- margin-top: var(--spacing-sm);
+ margin-top: 0.5rem;
}
/* Server time row: flex layout for consistent mask width in E2E tests */
> p {
display: flex;
align-items: baseline;
- gap: var(--spacing-xs);
+ gap: 0.25em;
}
/* Consistent font for form inputs (inherits from global.css) */
textarea,
input {
- display: block;
font-family: inherit;
font-size: inherit;
}
button {
- padding: var(--spacing-sm) var(--spacing-md);
+ padding: 0.5rem 1rem;
border: none;
- border-radius: var(--border-radius-md);
- color: var(--color-text-on-accent);
- font-weight: var(--font-weight-bold);
- background-color: var(--color-accent);
+ border-radius: 6px;
+ color: white;
+ font-weight: bold;
+ background-color: var(--color-primary);
cursor: pointer;
- &:hover {
- background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse));
- }
-
+ &:hover,
&:focus-visible {
- outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary);
- outline-offset: var(--border-width-regular);
+ background-color: var(--color-primary-hover);
}
}
}
.notice {
- padding: var(--spacing-sm) var(--spacing-md);
- color: var(--color-text-info);
+ padding: 0.5rem 0.75rem;
+ color: var(--color-primary);
text-align: center;
font-style: italic;
- background-color: var(--color-background-info);
+ background-color: var(--color-notice-bg);
&::before {
content: "โน๏ธ ";
font-style: normal;
}
}
-
-/* Server time fills remaining width for consistent E2E screenshot masking */
-.serverTime {
- flex: 1;
- min-width: 0;
-}
diff --git a/examples/basic-server-preact/src/mcp-app.tsx b/examples/basic-server-preact/src/mcp-app.tsx
index 351a8ec..d5b6b60 100644
--- a/examples/basic-server-preact/src/mcp-app.tsx
+++ b/examples/basic-server-preact/src/mcp-app.tsx
@@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useState } from "preact/hooks";
import { render } from "preact";
import styles from "./mcp-app.module.css";
@@ -144,7 +144,7 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps)
- Server Time: {serverTime}
+ Server Time: {serverTime}
diff --git a/examples/basic-server-preact/tsconfig.json b/examples/basic-server-preact/tsconfig.json
index 9bf9389..32182d9 100644
--- a/examples/basic-server-preact/tsconfig.json
+++ b/examples/basic-server-preact/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-preact/tsconfig.server.json b/examples/basic-server-preact/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-preact/tsconfig.server.json
+++ b/examples/basic-server-preact/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-react/README.md b/examples/basic-server-react/README.md
index d2f2e0d..4b3f26c 100644
--- a/examples/basic-server-react/README.md
+++ b/examples/basic-server-react/README.md
@@ -28,29 +28,11 @@ Add to your MCP client configuration (stdio transport):
}
```
-### Local Development
-
-To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):
-
-```json
-{
- "mcpServers": {
- "basic-react": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-react && npm run build >&2 && node dist/index.js --stdio"
- ]
- }
- }
-}
-```
-
## Overview
- Tool registration with a linked UI resource
-- React UI using the [`useApp()`](https://apps.extensions.modelcontextprotocol.io/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook
-- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink)
+- React UI using the [`useApp()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook
+- 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)
## Key Files
@@ -72,4 +54,4 @@ npm run dev
## Build System
-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://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.
+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.
diff --git a/examples/basic-server-react/main.ts b/examples/basic-server-react/main.ts
index 21c5e18..ec187b6 100644
--- a/examples/basic-server-react/main.ts
+++ b/examples/basic-server-react/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json
index ea51a8a..df612e0 100644
--- a/examples/basic-server-react/package.json
+++ b/examples/basic-server-react/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-react",
- "version": "2.0.0",
+ "version": "0.4.2",
"type": "module",
"description": "Basic MCP App Server example using React",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-react"
},
"license": "MIT",
@@ -27,23 +27,18 @@
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun --watch main.ts",
- "serve:stdio": "bun main.ts --stdio",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
- "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio",
- "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"",
+ "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "../..",
+ "@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts
index d77c908..23f6dda 100644
--- a/examples/basic-server-react/server.ts
+++ b/examples/basic-server-react/server.ts
@@ -1,12 +1,8 @@
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
@@ -33,7 +29,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-react/src/global.css b/examples/basic-server-react/src/global.css
index 801291f..08d4da1 100644
--- a/examples/basic-server-react/src/global.css
+++ b/examples/basic-server-react/src/global.css
@@ -1,92 +1,18 @@
-:root {
- color-scheme: light dark;
-
- /*
- * Fallbacks for host style variables used by this app.
- * The host may provide these (and many more) via the host context.
- */
- --color-text-primary: light-dark(#1f2937, #f3f4f6);
- --color-text-inverse: light-dark(#f3f4f6, #1f2937);
- --color-text-info: light-dark(#1d4ed8, #60a5fa);
- --color-background-primary: light-dark(#ffffff, #1a1a1a);
- --color-background-inverse: light-dark(#1a1a1a, #ffffff);
- --color-background-info: light-dark(#eff6ff, #1e3a5f);
- --color-ring-primary: light-dark(#3b82f6, #60a5fa);
- --border-radius-md: 6px;
- --border-width-regular: 1px;
- --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
- --font-weight-normal: 400;
- --font-weight-bold: 700;
- --font-text-md-size: 1rem;
- --font-text-md-line-height: 1.5;
- --font-heading-3xl-size: 2.25rem;
- --font-heading-3xl-line-height: 1.1;
- --font-heading-2xl-size: 1.875rem;
- --font-heading-2xl-line-height: 1.2;
- --font-heading-xl-size: 1.5rem;
- --font-heading-xl-line-height: 1.25;
- --font-heading-lg-size: 1.25rem;
- --font-heading-lg-line-height: 1.3;
- --font-heading-md-size: 1rem;
- --font-heading-md-line-height: 1.4;
- --font-heading-sm-size: 0.875rem;
- --font-heading-sm-line-height: 1.4;
-
- /* Spacing derived from host typography */
- --spacing-unit: var(--font-text-md-size);
- --spacing-xs: calc(var(--spacing-unit) * 0.25);
- --spacing-sm: calc(var(--spacing-unit) * 0.5);
- --spacing-md: var(--spacing-unit);
- --spacing-lg: calc(var(--spacing-unit) * 1.5);
-
- /* App accent color (customize for your brand) */
- --color-accent: #2563eb;
- --color-text-on-accent: #ffffff;
-}
-
* {
box-sizing: border-box;
}
html, body {
- font-family: var(--font-sans);
- font-size: var(--font-text-md-size);
- font-weight: var(--font-weight-normal);
- line-height: var(--font-text-md-line-height);
- color: var(--color-text-primary);
+ font-family: system-ui, -apple-system, sans-serif;
+ font-size: 1rem;
}
-h1 {
- font-size: var(--font-heading-3xl-size);
- line-height: var(--font-heading-3xl-line-height);
-}
-h2 {
- font-size: var(--font-heading-2xl-size);
- line-height: var(--font-heading-2xl-line-height);
-}
-h3 {
- font-size: var(--font-heading-xl-size);
- line-height: var(--font-heading-xl-line-height);
-}
-h4 {
- font-size: var(--font-heading-lg-size);
- line-height: var(--font-heading-lg-line-height);
-}
-h5 {
- font-size: var(--font-heading-md-size);
- line-height: var(--font-heading-md-line-height);
-}
-h6 {
- font-size: var(--font-heading-sm-size);
- line-height: var(--font-heading-sm-line-height);
-}
-
-code, pre, kbd {
- font-family: var(--font-mono);
+code {
font-size: 1em;
}
-b, strong {
- font-weight: var(--font-weight-bold);
+/* Server time fills remaining width for consistent E2E screenshot masking */
+#server-time {
+ flex: 1;
+ min-width: 0;
}
diff --git a/examples/basic-server-react/src/mcp-app.module.css b/examples/basic-server-react/src/mcp-app.module.css
index ae08a8c..41a0456 100644
--- a/examples/basic-server-react/src/mcp-app.module.css
+++ b/examples/basic-server-react/src/mcp-app.module.css
@@ -1,4 +1,8 @@
.main {
+ --color-primary: #2563eb;
+ --color-primary-hover: #1d4ed8;
+ --color-notice-bg: #eff6ff;
+
width: 100%;
max-width: 425px;
box-sizing: border-box;
@@ -9,7 +13,7 @@
}
> * + * {
- margin-top: var(--spacing-lg);
+ margin-top: 1.5rem;
}
}
@@ -21,59 +25,48 @@
}
> * + * {
- margin-top: var(--spacing-sm);
+ margin-top: 0.5rem;
}
/* Server time row: flex layout for consistent mask width in E2E tests */
> p {
display: flex;
align-items: baseline;
- gap: var(--spacing-xs);
+ gap: 0.25em;
}
/* Consistent font for form inputs (inherits from global.css) */
textarea,
input {
- display: block;
font-family: inherit;
font-size: inherit;
}
button {
- padding: var(--spacing-sm) var(--spacing-md);
+ padding: 0.5rem 1rem;
border: none;
- border-radius: var(--border-radius-md);
- color: var(--color-text-on-accent);
- font-weight: var(--font-weight-bold);
- background-color: var(--color-accent);
+ border-radius: 6px;
+ color: white;
+ font-weight: bold;
+ background-color: var(--color-primary);
cursor: pointer;
- &:hover {
- background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse));
- }
-
+ &:hover,
&:focus-visible {
- outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary);
- outline-offset: var(--border-width-regular);
+ background-color: var(--color-primary-hover);
}
}
}
.notice {
- padding: var(--spacing-sm) var(--spacing-md);
- color: var(--color-text-info);
+ padding: 0.5rem 0.75rem;
+ color: var(--color-primary);
text-align: center;
font-style: italic;
- background-color: var(--color-background-info);
+ background-color: var(--color-notice-bg);
&::before {
content: "โน๏ธ ";
font-style: normal;
}
}
-
-/* Server time fills remaining width for consistent E2E screenshot masking */
-.serverTime {
- flex: 1;
- min-width: 0;
-}
diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx
index 2b5fbef..a42fce7 100644
--- a/examples/basic-server-react/src/mcp-app.tsx
+++ b/examples/basic-server-react/src/mcp-app.tsx
@@ -3,7 +3,7 @@
*/
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import styles from "./mcp-app.module.css";
@@ -131,7 +131,7 @@ function GetTimeAppInner({ app, toolResult, hostContext }: GetTimeAppInnerProps)
- Server Time: {serverTime}
+ Server Time: {serverTime}
diff --git a/examples/basic-server-react/tsconfig.json b/examples/basic-server-react/tsconfig.json
index 3ecbdad..fc3c210 100644
--- a/examples/basic-server-react/tsconfig.json
+++ b/examples/basic-server-react/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-react/tsconfig.server.json b/examples/basic-server-react/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-react/tsconfig.server.json
+++ b/examples/basic-server-react/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-solid/README.md b/examples/basic-server-solid/README.md
index 43811a2..693fec3 100644
--- a/examples/basic-server-solid/README.md
+++ b/examples/basic-server-solid/README.md
@@ -26,29 +26,11 @@ Add to your MCP client configuration (stdio transport):
}
```
-### Local Development
-
-To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):
-
-```json
-{
- "mcpServers": {
- "basic-solid": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-solid && npm run build >&2 && node dist/index.js --stdio"
- ]
- }
- }
-}
-```
-
## Overview
- Tool registration with a linked UI resource
-- Solid UI using the [`App`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) class
-- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink)
+- Solid UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class
+- 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)
## Key Files
@@ -70,4 +52,4 @@ npm run dev
## Build System
-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://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.
+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.
diff --git a/examples/basic-server-solid/main.ts b/examples/basic-server-solid/main.ts
index 095ef8d..c8d9de2 100644
--- a/examples/basic-server-solid/main.ts
+++ b/examples/basic-server-solid/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json
index b3ca909..313cc3f 100644
--- a/examples/basic-server-solid/package.json
+++ b/examples/basic-server-solid/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "2.0.0",
+ "version": "0.4.2",
"type": "module",
"description": "Basic MCP App Server example using Solid",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-solid"
},
"license": "MIT",
@@ -17,22 +17,17 @@
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun --watch main.ts",
- "serve:stdio": "bun main.ts --stdio",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
- "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio",
- "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"",
+ "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "../..",
+ "@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"solid-js": "1.9.10",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
@@ -43,7 +38,7 @@
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0",
- "vite-plugin-solid": "^2.11.12"
+ "vite-plugin-solid": "^2.0.0"
},
"types": "dist/server.d.ts",
"exports": {
diff --git a/examples/basic-server-solid/server.ts b/examples/basic-server-solid/server.ts
index addb522..2ed2d93 100644
--- a/examples/basic-server-solid/server.ts
+++ b/examples/basic-server-solid/server.ts
@@ -1,12 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -32,7 +28,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-solid/src/global.css b/examples/basic-server-solid/src/global.css
index 801291f..08d4da1 100644
--- a/examples/basic-server-solid/src/global.css
+++ b/examples/basic-server-solid/src/global.css
@@ -1,92 +1,18 @@
-:root {
- color-scheme: light dark;
-
- /*
- * Fallbacks for host style variables used by this app.
- * The host may provide these (and many more) via the host context.
- */
- --color-text-primary: light-dark(#1f2937, #f3f4f6);
- --color-text-inverse: light-dark(#f3f4f6, #1f2937);
- --color-text-info: light-dark(#1d4ed8, #60a5fa);
- --color-background-primary: light-dark(#ffffff, #1a1a1a);
- --color-background-inverse: light-dark(#1a1a1a, #ffffff);
- --color-background-info: light-dark(#eff6ff, #1e3a5f);
- --color-ring-primary: light-dark(#3b82f6, #60a5fa);
- --border-radius-md: 6px;
- --border-width-regular: 1px;
- --font-sans: ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
- --font-weight-normal: 400;
- --font-weight-bold: 700;
- --font-text-md-size: 1rem;
- --font-text-md-line-height: 1.5;
- --font-heading-3xl-size: 2.25rem;
- --font-heading-3xl-line-height: 1.1;
- --font-heading-2xl-size: 1.875rem;
- --font-heading-2xl-line-height: 1.2;
- --font-heading-xl-size: 1.5rem;
- --font-heading-xl-line-height: 1.25;
- --font-heading-lg-size: 1.25rem;
- --font-heading-lg-line-height: 1.3;
- --font-heading-md-size: 1rem;
- --font-heading-md-line-height: 1.4;
- --font-heading-sm-size: 0.875rem;
- --font-heading-sm-line-height: 1.4;
-
- /* Spacing derived from host typography */
- --spacing-unit: var(--font-text-md-size);
- --spacing-xs: calc(var(--spacing-unit) * 0.25);
- --spacing-sm: calc(var(--spacing-unit) * 0.5);
- --spacing-md: var(--spacing-unit);
- --spacing-lg: calc(var(--spacing-unit) * 1.5);
-
- /* App accent color (customize for your brand) */
- --color-accent: #2563eb;
- --color-text-on-accent: #ffffff;
-}
-
* {
box-sizing: border-box;
}
html, body {
- font-family: var(--font-sans);
- font-size: var(--font-text-md-size);
- font-weight: var(--font-weight-normal);
- line-height: var(--font-text-md-line-height);
- color: var(--color-text-primary);
+ font-family: system-ui, -apple-system, sans-serif;
+ font-size: 1rem;
}
-h1 {
- font-size: var(--font-heading-3xl-size);
- line-height: var(--font-heading-3xl-line-height);
-}
-h2 {
- font-size: var(--font-heading-2xl-size);
- line-height: var(--font-heading-2xl-line-height);
-}
-h3 {
- font-size: var(--font-heading-xl-size);
- line-height: var(--font-heading-xl-line-height);
-}
-h4 {
- font-size: var(--font-heading-lg-size);
- line-height: var(--font-heading-lg-line-height);
-}
-h5 {
- font-size: var(--font-heading-md-size);
- line-height: var(--font-heading-md-line-height);
-}
-h6 {
- font-size: var(--font-heading-sm-size);
- line-height: var(--font-heading-sm-line-height);
-}
-
-code, pre, kbd {
- font-family: var(--font-mono);
+code {
font-size: 1em;
}
-b, strong {
- font-weight: var(--font-weight-bold);
+/* Server time fills remaining width for consistent E2E screenshot masking */
+#server-time {
+ flex: 1;
+ min-width: 0;
}
diff --git a/examples/basic-server-solid/src/mcp-app.module.css b/examples/basic-server-solid/src/mcp-app.module.css
index ae08a8c..41a0456 100644
--- a/examples/basic-server-solid/src/mcp-app.module.css
+++ b/examples/basic-server-solid/src/mcp-app.module.css
@@ -1,4 +1,8 @@
.main {
+ --color-primary: #2563eb;
+ --color-primary-hover: #1d4ed8;
+ --color-notice-bg: #eff6ff;
+
width: 100%;
max-width: 425px;
box-sizing: border-box;
@@ -9,7 +13,7 @@
}
> * + * {
- margin-top: var(--spacing-lg);
+ margin-top: 1.5rem;
}
}
@@ -21,59 +25,48 @@
}
> * + * {
- margin-top: var(--spacing-sm);
+ margin-top: 0.5rem;
}
/* Server time row: flex layout for consistent mask width in E2E tests */
> p {
display: flex;
align-items: baseline;
- gap: var(--spacing-xs);
+ gap: 0.25em;
}
/* Consistent font for form inputs (inherits from global.css) */
textarea,
input {
- display: block;
font-family: inherit;
font-size: inherit;
}
button {
- padding: var(--spacing-sm) var(--spacing-md);
+ padding: 0.5rem 1rem;
border: none;
- border-radius: var(--border-radius-md);
- color: var(--color-text-on-accent);
- font-weight: var(--font-weight-bold);
- background-color: var(--color-accent);
+ border-radius: 6px;
+ color: white;
+ font-weight: bold;
+ background-color: var(--color-primary);
cursor: pointer;
- &:hover {
- background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse));
- }
-
+ &:hover,
&:focus-visible {
- outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary);
- outline-offset: var(--border-width-regular);
+ background-color: var(--color-primary-hover);
}
}
}
.notice {
- padding: var(--spacing-sm) var(--spacing-md);
- color: var(--color-text-info);
+ padding: 0.5rem 0.75rem;
+ color: var(--color-primary);
text-align: center;
font-style: italic;
- background-color: var(--color-background-info);
+ background-color: var(--color-notice-bg);
&::before {
content: "โน๏ธ ";
font-style: normal;
}
}
-
-/* Server time fills remaining width for consistent E2E screenshot masking */
-.serverTime {
- flex: 1;
- min-width: 0;
-}
diff --git a/examples/basic-server-solid/src/mcp-app.tsx b/examples/basic-server-solid/src/mcp-app.tsx
index 2d78b7b..fa054e6 100644
--- a/examples/basic-server-solid/src/mcp-app.tsx
+++ b/examples/basic-server-solid/src/mcp-app.tsx
@@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { createEffect, createSignal, onMount, Show } from "solid-js";
import { render } from "solid-js/web";
import styles from "./mcp-app.module.css";
@@ -149,7 +149,7 @@ function GetTimeAppInner(props: GetTimeAppInnerProps) {
- Server Time: {serverTime()}
+ Server Time: {serverTime()}
diff --git a/examples/basic-server-solid/tsconfig.json b/examples/basic-server-solid/tsconfig.json
index bb13a01..84f0efe 100644
--- a/examples/basic-server-solid/tsconfig.json
+++ b/examples/basic-server-solid/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-solid/tsconfig.server.json b/examples/basic-server-solid/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-solid/tsconfig.server.json
+++ b/examples/basic-server-solid/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-svelte/README.md b/examples/basic-server-svelte/README.md
index ec6a0d2..ff599af 100644
--- a/examples/basic-server-svelte/README.md
+++ b/examples/basic-server-svelte/README.md
@@ -26,29 +26,11 @@ Add to your MCP client configuration (stdio transport):
}
```
-### Local Development
-
-To test local modifications, use this configuration (replace `~/code/ext-apps` with your clone path):
-
-```json
-{
- "mcpServers": {
- "basic-svelte": {
- "command": "bash",
- "args": [
- "-c",
- "cd ~/code/ext-apps/examples/basic-server-svelte && npm run build >&2 && node dist/index.js --stdio"
- ]
- }
- }
-}
-```
-
## Overview
- Tool registration with a linked UI resource
-- Svelte 5 UI using the [`App`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) class
-- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink)
+- Svelte 5 UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class
+- 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)
## Key Files
@@ -70,4 +52,4 @@ npm run dev
## Build System
-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://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata.
+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.
diff --git a/examples/basic-server-svelte/main.ts b/examples/basic-server-svelte/main.ts
index 734ec0c..6c50a25 100644
--- a/examples/basic-server-svelte/main.ts
+++ b/examples/basic-server-svelte/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json
index 19fe4c9..608311e 100644
--- a/examples/basic-server-svelte/package.json
+++ b/examples/basic-server-svelte/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "2.0.0",
+ "version": "0.4.2",
"type": "module",
"description": "Basic MCP App Server example using Svelte",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-svelte"
},
"license": "MIT",
@@ -17,22 +17,17 @@
"build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"",
"watch": "cross-env INPUT=mcp-app.html vite build --watch",
"serve": "bun --watch main.ts",
- "serve:stdio": "bun main.ts --stdio",
"start": "cross-env NODE_ENV=development npm run build && npm run serve",
- "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio",
- "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"",
+ "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' 'npm run serve'",
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "../..",
+ "@modelcontextprotocol/sdk": "^1.24.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"svelte": "^5.0.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.0",
diff --git a/examples/basic-server-svelte/server.ts b/examples/basic-server-svelte/server.ts
index 9414285..da603ca 100644
--- a/examples/basic-server-svelte/server.ts
+++ b/examples/basic-server-svelte/server.ts
@@ -1,12 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -32,7 +28,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-svelte/src/App.svelte b/examples/basic-server-svelte/src/App.svelte
index cabcc31..48fbe66 100644
--- a/examples/basic-server-svelte/src/App.svelte
+++ b/examples/basic-server-svelte/src/App.svelte
@@ -7,7 +7,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
function extractTime(result: CallToolResult): string {
const { text } = result.content?.find((c) => c.type === "text")!;
@@ -111,7 +111,7 @@ async function handleOpenLink() {
Watch activity in the DevTools console!
- Server Time: {serverTime}
+ Server Time: {serverTime}
@@ -133,6 +133,10 @@ async function handleOpenLink() {
-
-
-