diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c444585..6848e4c 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,6 +10,36 @@ 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
@@ -30,7 +60,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Verify no private URLs in package-lock.json
shell: bash
@@ -40,14 +70,19 @@ jobs:
shell: bash
run: node scripts/check-versions.mjs
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
- node-version: "20"
+ node-version: "22"
+ cache: npm
- - run: npm install
+ - run: npm ci
- 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
@@ -66,31 +101,62 @@ 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@v4
+ - uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
- node-version: "20"
+ node-version: "22"
+ cache: npm
- - uses: astral-sh/setup-uv@v5
+ - uses: astral-sh/setup-uv@v7
- 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 chromium
+ run: npx playwright install --with-deps --only-shell chromium
- name: Run E2E tests
run: npx playwright test --reporter=list
- name: Upload test results
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v6
if: failure()
with:
name: test-results
@@ -103,9 +169,9 @@ jobs:
runs-on: windows-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- - uses: Vampire/setup-wsl@v5
+ - uses: Vampire/setup-wsl@v6
with:
distribution: Ubuntu-24.04
@@ -113,13 +179,18 @@ jobs:
shell: wsl-bash {0}
run: |
sudo apt-get update
- curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
+ curl -fsSL https://deb.nodesource.com/setup_22.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: |
- npm install
+ cp -r "$(pwd)" ~/ext-apps && cd ~/ext-apps
+ npm ci
npm run build
npm run examples:build
npm test
@@ -146,17 +217,20 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
- node-version: "20"
+ node-version: "22"
- 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/${{ github.event.pull_request.head.repo.full_name || github.repository }}#${{ github.head_ref || github.ref_name }}"
+ npm install "git+https://github.com/${HEAD_REPO}#${HEAD_REF}"
# 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
new file mode 100644
index 0000000..3a170cd
--- /dev/null
+++ b/.github/workflows/docs-preview.yml
@@ -0,0 +1,65 @@
+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 0fbb170..a046c29 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -12,12 +12,13 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
- node-version: "20"
- - run: npm install
+ node-version: "22"
+ cache: npm
+ - run: npm ci
- run: npm run build
- run: npm run docs
- uses: peaceiris/actions-gh-pages@v4
@@ -26,3 +27,4 @@ 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 3b237cb..ed36c15 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@v4
+ - uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
@@ -31,17 +31,21 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
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'
@@ -53,11 +57,11 @@ jobs:
id-token: write
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
@@ -88,7 +92,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment: Release
- needs: [publish]
+ needs: [build, test]
permissions:
contents: read
@@ -107,6 +111,8 @@ jobs:
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
+ - debug-server
+ - lazy-auth-server
- map-server
- pdf-server
- scenario-modeler-server
@@ -119,11 +125,11 @@ jobs:
- wiki-explorer-server
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
@@ -133,7 +139,45 @@ 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
+ run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public ${{ steps.npm-tag.outputs.tag }}
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 283577b..478c5b8 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@v4
- - uses: actions/setup-node@v4
+ - uses: actions/checkout@v6
+ - uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
@@ -24,11 +24,17 @@ 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 52795d6..182f01a 100644
--- a/.github/workflows/update-snapshots.yml
+++ b/.github/workflows/update-snapshots.yml
@@ -16,16 +16,20 @@ permissions:
jobs:
update-snapshots:
- # Run on workflow_dispatch OR when someone comments "/update-snapshots" on a PR
+ # 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.
if: >
github.event_name == 'workflow_dispatch' ||
- (github.event.issue.pull_request && contains(github.event.comment.body, '/update-snapshots'))
+ (github.event.issue.pull_request &&
+ contains(github.event.comment.body, '/update-snapshots') &&
+ contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association))
runs-on: ubuntu-latest
steps:
- name: Get PR branch
if: github.event_name == 'issue_comment'
id: pr
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
const pr = await github.rest.pulls.get({
@@ -38,7 +42,7 @@ jobs:
- name: Add reaction to comment
if: github.event_name == 'issue_comment'
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
await github.rest.reactions.createForIssueComment({
@@ -48,7 +52,7 @@ jobs:
content: 'rocket'
});
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.branch || steps.pr.outputs.ref || github.ref }}
token: ${{ secrets.GITHUB_TOKEN }}
@@ -57,11 +61,12 @@ jobs:
with:
bun-version: latest
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
- node-version: "20"
+ node-version: "22"
+ cache: npm
- - uses: astral-sh/setup-uv@v5
+ - uses: astral-sh/setup-uv@v7
- run: npm ci
@@ -88,7 +93,7 @@ jobs:
- name: Comment on PR
if: github.event_name == 'issue_comment'
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
const changed = '${{ steps.commit.outputs.changed }}' === 'true';
diff --git a/.gitignore b/.gitignore
index 9c6a137..5b5486b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,9 @@ intermediate-findings/
playwright-report/
test-results/
__pycache__/
-*.pyc
\ No newline at end of file
+*.pyc
+
+# MCPB bundles (built artifacts)
+*.mcpb
+.mcpb-stage/
+.mcpb-smoke/
\ No newline at end of file
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 6fd6718..ae5b3c7 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -14,8 +14,16 @@ 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
-# Stage any changes to generated files (they may have been reformatted by prettier)
+# 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)
git add src/generated/
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..214c29d
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+registry=https://registry.npmjs.org/
diff --git a/.prettierignore b/.prettierignore
index f5edcf9..c51df7b 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -4,4 +4,6 @@ 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 9bbd2f0..219b947 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 (draft): `specification/draft/apps.mdx`
+Specification (stable): `specification/2026-01-26/apps.mdx`
## Commands
@@ -39,7 +39,10 @@ npm test
# Check JSDoc comment syntax and `{@link}` references
npm exec typedoc -- --treatValidationWarningsAsErrors --emit none
-# Regenerate package-lock.json (especially on setups w/ custom npm registry)
+# 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.
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 && \
@@ -57,8 +60,8 @@ rm -fR package-lock.json node_modules && \
### Key Source Files
-- `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/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/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
@@ -67,12 +70,12 @@ rm -fR package-lock.json node_modules && \
### Protocol Flow
```
-View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
+View (App/Protocol) <--PostMessageTransport--> Host (AppBridge/Protocol) <--separate 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` request, receives host capabilities and context
+3. View sends `ui/initialize` (the only handshake on the iframe channel) and receives authoritative Apps 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 3793622..e28dcd1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -25,6 +25,15 @@ 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
@@ -40,6 +49,408 @@ 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
+ Styled with host CSS variables, fonts, and safe area insets` tag before executing it, partially rendering a table as data arrives, or incrementally populating a chart. ```ts source="../src/app.examples.ts#App_ontoolinputpartial_progressiveRendering" -let toolInputs: Record| null = null; -let toolInputsPartial: Record | null = null; +const codePreview = document.querySelector ("#code-preview")!; +const canvas = document.querySelector ("#canvas")!; app.ontoolinputpartial = (params) => { - toolInputsPartial = params.arguments as Record ; - render(); + codePreview.textContent = (params.arguments?.code as string) ?? ""; + codePreview.style.display = "block"; + canvas.style.display = "none"; }; app.ontoolinput = (params) => { - toolInputs = params.arguments as Record ; - toolInputsPartial = null; - render(); + codePreview.style.display = "none"; + canvas.style.display = "block"; + render(params.arguments?.code as string); }; - -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. -_See [`examples/threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server) for a full implementation of this pattern._ +> [!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). diff --git a/docs/patterns.tsx b/docs/patterns.tsx index f13268f..bb9eaa4 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -14,13 +14,83 @@ import { applyHostStyleVariables, } from "../src/styles.js"; import { randomUUID } from "node:crypto"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { + CallToolResult, + ReadResourceResult, +} from "@modelcontextprotocol/server"; import type { McpUiHostContext } from "../src/types.js"; -import { useApp, useHostStyles } from "../src/react/index.js"; +import { useEffect, useState } from "react"; +import { useApp } from "../src/react/index.js"; import { registerAppTool } from "../src/server/index.js"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server"; 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) */ @@ -43,14 +113,14 @@ function chunkedDataServer(server: McpServer) { { title: "Read Data Bytes", description: "Load binary data in chunks", - inputSchema: { + inputSchema: z.object({ 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"] } }, @@ -147,10 +217,54 @@ function chunkedDataClient(app: App, resourceId: string) { } /** - * Example: Unified host styling (theme, CSS variables, fonts) + * Example: Serving binary blobs via resources (server-side) */ -function hostStylingVanillaJs(app: App) { - //#region hostStylingVanillaJs +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 applyHostContext(ctx: McpUiHostContext) { if (ctx.theme) { applyDocumentTheme(ctx.theme); @@ -161,48 +275,81 @@ function hostStylingVanillaJs(app: App) { 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 styles after connecting + // Apply initial context after connecting app.connect().then(() => { const ctx = app.getHostContext(); if (ctx) { applyHostContext(ctx); } }); - //#endregion hostStylingVanillaJs + //#endregion hostContextVanillaJs } /** - * Example: Host styling with React (CSS variables, theme, fonts) + * Example: Adapting to host context with React (CSS variables, theme, fonts, safe areas) */ -function hostStylingReact() { - //#region hostStylingReact +function hostContextReact() { + //#region hostContextReact 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 })); + }; + }, }); - // Apply all host styles (variables, theme, fonts) - useHostStyles(app, app?.getHostContext()); + // 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]); return ( -); } - //#endregion hostStylingReact + //#endregion hostContextReact } /** @@ -284,9 +431,9 @@ function visibilityBasedPause( const observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { - animation.play(); + animation.play(); // or startPolling(), etc } else { - animation.pause(); + animation.pause(); // or stopPolling(), etc } }); }); @@ -302,10 +449,14 @@ function visibilityBasedPause( } // Suppress unused variable warnings +void pollingVanillaJs; +void pollingReact; void chunkedDataServer; void chunkedDataClient; -void hostStylingVanillaJs; -void hostStylingReact; +void binaryBlobResourceServer; +void binaryBlobResourceClient; +void hostContextVanillaJs; +void hostContextReact; void persistViewStateServer; void persistViewState; void visibilityBasedPause; diff --git a/docs/quickstart.md b/docs/quickstart.md index 8146824..a021912 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,5 +1,7 @@ --- 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 @@ -19,7 +21,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 18+. +You'll also need Node.js 20+. ## 1. Set up the project @@ -35,7 +37,7 @@ Install the dependencies you'll need: ```bash npm init -y -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors +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 -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env ``` @@ -44,9 +46,12 @@ 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 'cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch' 'tsx watch main.ts'" +npm pkg set scripts.start='concurrently --raw "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"""`. +Styled with host CSS variables and fonts
-Uses [data-theme] selectors
+ Styled with host CSS variables, fonts, and safe area insetsCreate
@@ -54,6 +59,7 @@ npm pkg set scripts.start="concurrently 'cross-env NODE_ENV=development INPUT=mc ```json source="../examples/quickstart/tsconfig.json" { "compilerOptions": { + "types": ["node", "vite/client"], "target": "ESNext", "lib": ["ESNext", "DOM", "DOM.Iterable"], "module": "ESNext", @@ -82,6 +88,7 @@ npm pkg set scripts.start="concurrently 'cross-env NODE_ENV=development INPUT=mc ```json source="../examples/quickstart/tsconfig.server.json" { "compilerOptions": { + "types": ["node"], "target": "ES2022", "lib": ["ES2022"], "module": "NodeNext", @@ -108,7 +115,7 @@ npm pkg set scripts.start="concurrently 'cross-env NODE_ENV=development INPUT=mc ```ts source="../examples/quickstart/vite.config.ts" -import { defineConfig } from "vite"; +import { createLogger, defineConfig } from "vite"; import { viteSingleFile } from "vite-plugin-singlefile"; const INPUT = process.env.INPUT; @@ -118,7 +125,14 @@ 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, @@ -166,9 +180,10 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -193,7 +208,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async () => { @@ -228,10 +243,10 @@ export function createServer(): McpServer { ```ts source="../examples/quickstart/main.ts" -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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -251,7 +266,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); @@ -447,8 +462,9 @@ MCP server listening on http://localhost:3001/mcp ```bash git clone https://github.com/modelcontextprotocol/ext-apps.git -cd ext-apps/examples/basic-host +cd ext-apps npm install +cd examples/basic-host npm start ``` @@ -468,4 +484,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://modelcontextprotocol.github.io/ext-apps/api/) +- **API reference**: See the full [API documentation](https://apps.extensions.modelcontextprotocol.io/api/) diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md index 06873da..a8eb333 100644 --- a/docs/testing-mcp-apps.md +++ b/docs/testing-mcp-apps.md @@ -1,5 +1,7 @@ --- 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 @@ -21,10 +23,15 @@ 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/examples/basic-host + cd ext-apps 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 @@ -59,8 +66,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.ai/docs/connectors/custom/remote-mcp) - - [Local MCP servers (over stdio)](https://claude.ai/docs/connectors/custom/desktop-extensions) + - [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) - [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 d3ce4f9..cefe9d5 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": "0.4.2", + "version": "2.0.0", "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/ext-apps": "../..", - "@modelcontextprotocol/sdk": "^1.24.0", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/express": "^5.0.0", @@ -29,7 +29,6 @@ "prettier": "^3.6.2", "typescript": "^5.9.3", "vite": "^6.0.0", - "vite-plugin-singlefile": "^2.3.0", - "vitest": "^3.2.4" + "vite-plugin-singlefile": "^2.3.0" } } diff --git a/examples/basic-host/serve.ts b/examples/basic-host/serve.ts index 03c3a0b..bc3fbff 100644 --- a/examples/basic-host/serve.ts +++ b/examples/basic-host/serve.ts @@ -82,6 +82,8 @@ 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 434169a..5f680ff 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -1,8 +1,12 @@ 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 } 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 { + Client, + SSEClientTransport, + StreamableHTTPClientTransport, + type CallToolResult, + type Resource, + type Tool, +} from "@modelcontextprotocol/client"; import { getTheme, onThemeChange } from "./theme"; import { HOST_STYLE_VARIABLES } from "./host-styles"; @@ -22,6 +26,7 @@ export interface ServerInfo { name: string; client: Client; tools: Maptsconfig.json:; + resources: Map ; appHtmlCache: Map ; } @@ -36,7 +41,12 @@ 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())); - return { name, client, tools, appHtmlCache: new Map() }; + // 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() }; } async function connectWithFallback(serverUrl: URL): Promise { @@ -128,14 +138,23 @@ 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 ec18e2b..0bd09dd 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; - top: 1rem; + bottom: 1rem; right: 1rem; width: 2.5rem; height: 2.5rem; @@ -165,7 +165,7 @@ margin: 0; padding: 1rem; max-width: none; - background: white; + background: var(--color-bg); border: none; border-radius: 0; display: flex; @@ -201,29 +201,30 @@ width: 32px; height: 32px; padding: 0; - border: 1px solid #ccc; + border: 1px solid var(--color-border); border-radius: 4px; - background: #f5f5f5; + background: var(--color-bg-secondary); + color: var(--color-text); font-size: 1.25rem; cursor: pointer; &:hover { - background: #e0e0e0; + background: var(--color-bg); } } .collapsiblePanel { margin: 0.5rem 0; padding: 0.5rem 0.75rem; - border: 1px solid #e0e0e0; + border: 1px solid var(--color-border); border-radius: 4px; - background-color: #fafafa; + background-color: var(--color-bg-secondary); font-size: 0.875rem; cursor: pointer; transition: background-color 0.15s; &:hover { - background-color: #f0f0f0; + background-color: var(--color-bg); } } @@ -235,23 +236,23 @@ .collapsibleLabel { font-weight: 600; - color: #555; + color: var(--color-text); } .collapsibleSize { - color: #888; + color: var(--color-text-secondary); font-size: 0.75rem; } .collapsibleToggle { margin-left: auto; - color: #888; + color: var(--color-text-secondary); font-size: 0.75rem; } .collapsiblePreview { margin-top: 0.25rem; - color: #666; + color: var(--color-text-secondary); font-family: monospace; font-size: 0.8rem; white-space: nowrap; @@ -263,7 +264,8 @@ margin: 0.5rem 0 0; padding: 0.5rem; border-radius: 4px; - background-color: #f5f5f5; + background-color: var(--color-bg); + color: var(--color-text); 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 3d488a7..00c0d52 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/sdk/types.js"; +import type { Tool } from "@modelcontextprotocol/client"; 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 1caf1c5..6f1dd93 100644 --- a/examples/basic-host/src/sandbox.ts +++ b/examples/basic-host/src/sandbox.ts @@ -1,5 +1,5 @@ -import type { McpUiSandboxProxyReadyNotification, McpUiSandboxResourceReadyNotification } from "../../../dist/src/types"; -import { buildAllowAttribute } from "../../../dist/src/app-bridge"; +import type { McpUiSandboxProxyReadyNotification, McpUiSandboxResourceReadyNotification } from "@modelcontextprotocol/ext-apps/app-bridge"; +import { buildAllowAttribute } from "@modelcontextprotocol/ext-apps/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 9d7c3bc..3c143ca 100644 --- a/examples/basic-host/tsconfig.json +++ b/examples/basic-host/tsconfig.json @@ -1,5 +1,6 @@ { "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 b85fb90..6a75ddb 100644 --- a/examples/basic-server-preact/README.md +++ b/examples/basic-server-preact/README.md @@ -26,11 +26,29 @@ 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://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) +- 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) ## Key Files @@ -52,4 +70,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://modelcontextprotocol.github.io/ext-apps/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://apps.extensions.modelcontextprotocol.io/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 7642632..ffdff52 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 { 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; 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 StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index d58b2f4..60aa40d 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": "0.4.2", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-preact" }, "license": "MIT", @@ -17,17 +17,22 @@ "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", - "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' '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\"", "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "../..", - "@modelcontextprotocol/sdk": "^1.24.0", + "@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", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@preact/preset-vite": "^2.0.0", diff --git a/examples/basic-server-preact/server.ts b/examples/basic-server-preact/server.ts index bf14ea5..dfec94a 100644 --- a/examples/basic-server-preact/server.ts +++ b/examples/basic-server-preact/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; 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") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _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 08d4da1..801291f 100644 --- a/examples/basic-server-preact/src/global.css +++ b/examples/basic-server-preact/src/global.css @@ -1,18 +1,92 @@ +: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: system-ui, -apple-system, sans-serif; - font-size: 1rem; + 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); } -code { +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); font-size: 1em; } -/* Server time fills remaining width for consistent E2E screenshot masking */ -#server-time { - flex: 1; - min-width: 0; +b, strong { + font-weight: var(--font-weight-bold); } diff --git a/examples/basic-server-preact/src/mcp-app.module.css b/examples/basic-server-preact/src/mcp-app.module.css index 41a0456..ae08a8c 100644 --- a/examples/basic-server-preact/src/mcp-app.module.css +++ b/examples/basic-server-preact/src/mcp-app.module.css @@ -1,8 +1,4 @@ .main { - --color-primary: #2563eb; - --color-primary-hover: #1d4ed8; - --color-notice-bg: #eff6ff; - width: 100%; max-width: 425px; box-sizing: border-box; @@ -13,7 +9,7 @@ } > * + * { - margin-top: 1.5rem; + margin-top: var(--spacing-lg); } } @@ -25,48 +21,59 @@ } > * + * { - margin-top: 0.5rem; + margin-top: var(--spacing-sm); } /* Server time row: flex layout for consistent mask width in E2E tests */ > p { display: flex; align-items: baseline; - gap: 0.25em; + gap: var(--spacing-xs); } /* Consistent font for form inputs (inherits from global.css) */ textarea, input { + display: block; font-family: inherit; font-size: inherit; } button { - padding: 0.5rem 1rem; + padding: var(--spacing-sm) var(--spacing-md); border: none; - border-radius: 6px; - color: white; - font-weight: bold; - background-color: var(--color-primary); + border-radius: var(--border-radius-md); + color: var(--color-text-on-accent); + font-weight: var(--font-weight-bold); + background-color: var(--color-accent); cursor: pointer; - &:hover, + &:hover { + background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse)); + } + &:focus-visible { - background-color: var(--color-primary-hover); + outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary); + outline-offset: var(--border-width-regular); } } } .notice { - padding: 0.5rem 0.75rem; - color: var(--color-primary); + padding: var(--spacing-sm) var(--spacing-md); + color: var(--color-text-info); text-align: center; font-style: italic; - background-color: var(--color-notice-bg); + background-color: var(--color-background-info); &::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 d5b6b60..351a8ec 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/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; 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) diff --git a/examples/basic-server-preact/tsconfig.json b/examples/basic-server-preact/tsconfig.json index 32182d9..9bf9389 100644 --- a/examples/basic-server-preact/tsconfig.json +++ b/examples/basic-server-preact/tsconfig.json @@ -1,5 +1,6 @@ { "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 05ddd8e..27eb28f 100644 --- a/examples/basic-server-preact/tsconfig.server.json +++ b/examples/basic-server-preact/tsconfig.server.json @@ -1,5 +1,6 @@ { "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 4b3f26c..d2f2e0d 100644 --- a/examples/basic-server-react/README.md +++ b/examples/basic-server-react/README.md @@ -28,11 +28,29 @@ 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://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) +- 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) ## Key Files @@ -54,4 +72,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://modelcontextprotocol.github.io/ext-apps/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://apps.extensions.modelcontextprotocol.io/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 ec187b6..21c5e18 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/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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; 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 StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index df612e0..ea51a8a 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": "0.4.2", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using React", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-react" }, "license": "MIT", @@ -27,18 +27,23 @@ "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", - "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' '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\"", "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "../..", - "@modelcontextprotocol/sdk": "^1.24.0", + "@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", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 23f6dda..d77c908 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -1,8 +1,12 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; 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") @@ -29,7 +33,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise- Server Time:
{serverTime}+ Server Time:{serverTime}=> { diff --git a/examples/basic-server-react/src/global.css b/examples/basic-server-react/src/global.css index 08d4da1..801291f 100644 --- a/examples/basic-server-react/src/global.css +++ b/examples/basic-server-react/src/global.css @@ -1,18 +1,92 @@ +: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: system-ui, -apple-system, sans-serif; - font-size: 1rem; + 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); } -code { +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); font-size: 1em; } -/* Server time fills remaining width for consistent E2E screenshot masking */ -#server-time { - flex: 1; - min-width: 0; +b, strong { + font-weight: var(--font-weight-bold); } diff --git a/examples/basic-server-react/src/mcp-app.module.css b/examples/basic-server-react/src/mcp-app.module.css index 41a0456..ae08a8c 100644 --- a/examples/basic-server-react/src/mcp-app.module.css +++ b/examples/basic-server-react/src/mcp-app.module.css @@ -1,8 +1,4 @@ .main { - --color-primary: #2563eb; - --color-primary-hover: #1d4ed8; - --color-notice-bg: #eff6ff; - width: 100%; max-width: 425px; box-sizing: border-box; @@ -13,7 +9,7 @@ } > * + * { - margin-top: 1.5rem; + margin-top: var(--spacing-lg); } } @@ -25,48 +21,59 @@ } > * + * { - margin-top: 0.5rem; + margin-top: var(--spacing-sm); } /* Server time row: flex layout for consistent mask width in E2E tests */ > p { display: flex; align-items: baseline; - gap: 0.25em; + gap: var(--spacing-xs); } /* Consistent font for form inputs (inherits from global.css) */ textarea, input { + display: block; font-family: inherit; font-size: inherit; } button { - padding: 0.5rem 1rem; + padding: var(--spacing-sm) var(--spacing-md); border: none; - border-radius: 6px; - color: white; - font-weight: bold; - background-color: var(--color-primary); + border-radius: var(--border-radius-md); + color: var(--color-text-on-accent); + font-weight: var(--font-weight-bold); + background-color: var(--color-accent); cursor: pointer; - &:hover, + &:hover { + background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse)); + } + &:focus-visible { - background-color: var(--color-primary-hover); + outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary); + outline-offset: var(--border-width-regular); } } } .notice { - padding: 0.5rem 0.75rem; - color: var(--color-primary); + padding: var(--spacing-sm) var(--spacing-md); + color: var(--color-text-info); text-align: center; font-style: italic; - background-color: var(--color-notice-bg); + background-color: var(--color-background-info); &::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 a42fce7..2b5fbef 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/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; 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) diff --git a/examples/basic-server-react/tsconfig.json b/examples/basic-server-react/tsconfig.json index fc3c210..3ecbdad 100644 --- a/examples/basic-server-react/tsconfig.json +++ b/examples/basic-server-react/tsconfig.json @@ -1,5 +1,6 @@ { "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 05ddd8e..27eb28f 100644 --- a/examples/basic-server-react/tsconfig.server.json +++ b/examples/basic-server-react/tsconfig.server.json @@ -1,5 +1,6 @@ { "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 693fec3..43811a2 100644 --- a/examples/basic-server-solid/README.md +++ b/examples/basic-server-solid/README.md @@ -26,11 +26,29 @@ 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://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) +- 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) ## Key Files @@ -52,4 +70,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://modelcontextprotocol.github.io/ext-apps/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://apps.extensions.modelcontextprotocol.io/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 c8d9de2..095ef8d 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/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 { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; 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 StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 313cc3f..b3ca909 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": "0.4.2", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-solid" }, "license": "MIT", @@ -17,17 +17,22 @@ "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", - "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' '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\"", "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "../..", - "@modelcontextprotocol/sdk": "^1.24.0", + "@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", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", @@ -38,7 +43,7 @@ "typescript": "^5.9.3", "vite": "^6.0.0", "vite-plugin-singlefile": "^2.3.0", - "vite-plugin-solid": "^2.0.0" + "vite-plugin-solid": "^2.11.12" }, "types": "dist/server.d.ts", "exports": { diff --git a/examples/basic-server-solid/server.ts b/examples/basic-server-solid/server.ts index 2ed2d93..addb522 100644 --- a/examples/basic-server-solid/server.ts +++ b/examples/basic-server-solid/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; 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") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise- Server Time:
{serverTime}+ Server Time:{serverTime}=> { diff --git a/examples/basic-server-solid/src/global.css b/examples/basic-server-solid/src/global.css index 08d4da1..801291f 100644 --- a/examples/basic-server-solid/src/global.css +++ b/examples/basic-server-solid/src/global.css @@ -1,18 +1,92 @@ +: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: system-ui, -apple-system, sans-serif; - font-size: 1rem; + 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); } -code { +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); font-size: 1em; } -/* Server time fills remaining width for consistent E2E screenshot masking */ -#server-time { - flex: 1; - min-width: 0; +b, strong { + font-weight: var(--font-weight-bold); } diff --git a/examples/basic-server-solid/src/mcp-app.module.css b/examples/basic-server-solid/src/mcp-app.module.css index 41a0456..ae08a8c 100644 --- a/examples/basic-server-solid/src/mcp-app.module.css +++ b/examples/basic-server-solid/src/mcp-app.module.css @@ -1,8 +1,4 @@ .main { - --color-primary: #2563eb; - --color-primary-hover: #1d4ed8; - --color-notice-bg: #eff6ff; - width: 100%; max-width: 425px; box-sizing: border-box; @@ -13,7 +9,7 @@ } > * + * { - margin-top: 1.5rem; + margin-top: var(--spacing-lg); } } @@ -25,48 +21,59 @@ } > * + * { - margin-top: 0.5rem; + margin-top: var(--spacing-sm); } /* Server time row: flex layout for consistent mask width in E2E tests */ > p { display: flex; align-items: baseline; - gap: 0.25em; + gap: var(--spacing-xs); } /* Consistent font for form inputs (inherits from global.css) */ textarea, input { + display: block; font-family: inherit; font-size: inherit; } button { - padding: 0.5rem 1rem; + padding: var(--spacing-sm) var(--spacing-md); border: none; - border-radius: 6px; - color: white; - font-weight: bold; - background-color: var(--color-primary); + border-radius: var(--border-radius-md); + color: var(--color-text-on-accent); + font-weight: var(--font-weight-bold); + background-color: var(--color-accent); cursor: pointer; - &:hover, + &:hover { + background-color: color-mix(in srgb, var(--color-accent) 85%, var(--color-background-inverse)); + } + &:focus-visible { - background-color: var(--color-primary-hover); + outline: calc(var(--border-width-regular) * 2) solid var(--color-ring-primary); + outline-offset: var(--border-width-regular); } } } .notice { - padding: 0.5rem 0.75rem; - color: var(--color-primary); + padding: var(--spacing-sm) var(--spacing-md); + color: var(--color-text-info); text-align: center; font-style: italic; - background-color: var(--color-notice-bg); + background-color: var(--color-background-info); &::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 fa054e6..2d78b7b 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/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; 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) { diff --git a/examples/basic-server-solid/tsconfig.json b/examples/basic-server-solid/tsconfig.json index 84f0efe..bb13a01 100644 --- a/examples/basic-server-solid/tsconfig.json +++ b/examples/basic-server-solid/tsconfig.json @@ -1,5 +1,6 @@ { "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 05ddd8e..27eb28f 100644 --- a/examples/basic-server-solid/tsconfig.server.json +++ b/examples/basic-server-solid/tsconfig.server.json @@ -1,5 +1,6 @@ { "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 ff599af..ec6a0d2 100644 --- a/examples/basic-server-svelte/README.md +++ b/examples/basic-server-svelte/README.md @@ -26,11 +26,29 @@ 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://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) +- 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) ## Key Files @@ -52,4 +70,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://modelcontextprotocol.github.io/ext-apps/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://apps.extensions.modelcontextprotocol.io/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 6c50a25..734ec0c 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 { 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; 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 StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 608311e..19fe4c9 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": "0.4.2", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { "type": "git", - "url": "https://github.com/modelcontextprotocol/ext-apps", + "url": "git+https://github.com/modelcontextprotocol/ext-apps.git", "directory": "examples/basic-server-svelte" }, "license": "MIT", @@ -17,17 +17,22 @@ "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", - "dev": "cross-env NODE_ENV=development concurrently 'npm run watch' '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\"", "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "../..", - "@modelcontextprotocol/sdk": "^1.24.0", + "@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", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "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 da603ca..9414285 100644 --- a/examples/basic-server-svelte/server.ts +++ b/examples/basic-server-svelte/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; 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") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise- Server Time:
{serverTime()}+ Server Time:{serverTime()}=> { diff --git a/examples/basic-server-svelte/src/App.svelte b/examples/basic-server-svelte/src/App.svelte index 48fbe66..cabcc31 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/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; 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!
-@@ -133,10 +133,6 @@ async function handleOpenLink() { + + +Server Time:
+{serverTime}Server Time:
{serverTime}