diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6848e4c..fb41938 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -114,21 +114,6 @@ jobs:
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:
@@ -147,10 +132,8 @@ jobs:
- run: npm ci
- # Headless runs without a channel use chromium-headless-shell, so the
- # full Chromium build (about 165 MB) would only be downloaded, never run.
- name: Install Playwright browsers
- run: npx playwright install --with-deps --only-shell chromium
+ run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npx playwright test --reporter=list
@@ -182,14 +165,9 @@ jobs:
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: |
- cp -r "$(pwd)" ~/ext-apps && cd ~/ext-apps
npm ci
npm run build
npm run examples:build
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index ed36c15..ece35d3 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -42,10 +42,6 @@ jobs:
- 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'
@@ -92,7 +88,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment: Release
- needs: [build, test]
+ needs: [publish]
permissions:
contents: read
@@ -112,7 +108,6 @@ jobs:
- cohort-heatmap-server
- customer-segmentation-server
- debug-server
- - lazy-auth-server
- map-server
- pdf-server
- scenario-modeler-server
@@ -139,18 +134,8 @@ jobs:
- name: Build example
run: npm run build --workspace examples/${{ matrix.example }}
- - name: Determine npm tag
- id: npm-tag
- run: |
- VERSION=$(node -p "require('./package.json').version")
- if [[ "$VERSION" == *"-"* ]]; then
- echo "tag=--tag beta" >> $GITHUB_OUTPUT
- else
- echo "tag=" >> $GITHUB_OUTPUT
- fi
-
- name: Publish example
- run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public ${{ steps.npm-tag.outputs.tag }}
+ run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }}
@@ -158,7 +143,7 @@ jobs:
runs-on: ubuntu-latest
if: github.event_name == 'release'
environment: Release
- needs: [build, test]
+ needs: [publish-examples]
permissions:
contents: write
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 478c5b8..5f1d3ac 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -34,7 +34,6 @@ jobs:
./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/.husky/pre-commit b/.husky/pre-commit
index ae5b3c7..b0d5ff5 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -15,9 +15,8 @@ if grep -E '"resolved": "https?://' package-lock.json | grep -v registry.npmjs.o
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)
+# (avoids sweeping unrelated WIP into the commit)
+STAGED=$(git diff --name-only --cached)
npm run build:all
npm run prettier:fix
diff --git a/.prettierignore b/.prettierignore
index c51df7b..6d5fc6f 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -5,5 +5,4 @@ examples/basic-server-*/**/*.tsx
examples/quickstart/**/*.ts
**/vendor/**
**/.build/**
-**/.claude/**
SKILL.md
diff --git a/AGENTS.md b/AGENTS.md
index 219b947..6bfc8cd 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -60,8 +60,8 @@ rm -fR package-lock.json node_modules && \
### Key Source Files
-- `src/app.ts` - `App` subclasses `Protocol` from `@modelcontextprotocol/client`, handles View initialization, tool calls, and messaging
-- `src/app-bridge.ts` - `AppBridge` subclasses `Protocol` from `@modelcontextprotocol/client` for the iframe channel and proxies through a separate outer `Client`
+- `src/app.ts` - `App` class extends MCP Protocol, handles guest initialization, tool calls, messaging
+- `src/app-bridge.ts` - `AppBridge` class for hosts, proxies MCP requests, sends tool input/results to guests
- `src/server/index.ts` - Helpers for MCP servers to register tools/resources with UI metadata
- `src/types.ts` - Protocol types re-exported from `spec.types.ts` and Zod schemas from `generated/schema.ts` (auto-generated during build)
- `src/message-transport.ts` - `PostMessageTransport` for iframe communication
@@ -70,12 +70,12 @@ rm -fR package-lock.json node_modules && \
### Protocol Flow
```
-View (App/Protocol) <--PostMessageTransport--> Host (AppBridge/Protocol) <--separate MCP Client--> MCP Server
+View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
```
1. Host creates iframe with view HTML
2. View creates `App` instance and calls `connect()` with `PostMessageTransport`
-3. View sends `ui/initialize` (the only handshake on the iframe channel) and receives authoritative Apps capabilities and context
+3. View sends `ui/initialize` request, receives host capabilities and context
4. Host sends `sendToolInput()` with tool arguments after initialization
5. View can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()`
6. Host sends `sendToolResult()` when tool execution completes
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e28dcd1..7ca6368 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -27,14 +27,6 @@ We welcome contributions to the MCP Apps SDK! This document outlines the process
- 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
Start the development environment with hot reloading:
@@ -502,7 +494,7 @@ Alternatively, use the [workflow dispatch](https://github.com/modelcontextprotoc
## Code of Conduct
-This project follows our [Code of Conduct](https://github.com/modelcontextprotocol/.github/blob/main/CODE_OF_CONDUCT.md). Please review it before contributing.
+This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please review it before contributing.
## Reporting Issues
@@ -539,17 +531,40 @@ Before publishing releases, ensure the following are configured:
### Publishing a Release
-1. **Bump the version** across the root and all workspace packages:
+Releases are published automatically via GitHub Actions when a GitHub Release is created.
+
+#### Steps to publish:
+
+1. **Update the version** in `package.json`:
```bash
- npm run bump -- minor # or: patch | major | prerelease --preid=beta | 1.7.0
+ # For a regular release
+ npm version patch # or minor, or major
+
+ # For a beta release
+ npm version prerelease --preid=beta
```
- Commit and open a PR with a grouped changelog in the body (see prior `chore: bump …` PRs for the format).
+2. **Commit the version bump** (if not done by `npm version`):
-2. **Merge the PR**, then [draft a GitHub Release](https://github.com/modelcontextprotocol/ext-apps/releases/new) — create a `vX.Y.Z` tag on `main`, paste the changelog, publish.
+ ```bash
+ git add package.json
+ git commit -m "Bump version to X.Y.Z"
+ git push origin main
+ ```
-3. **Approve the deployment** — publishing the Release triggers the [npm-publish workflow](https://github.com/modelcontextprotocol/ext-apps/actions/workflows/npm-publish.yml); all publish jobs wait together for a single "Review deployments" approval.
+3. **Create a GitHub Release**:
+ - Go to [Releases](https://github.com/modelcontextprotocol/ext-apps/releases)
+ - Click "Draft a new release"
+ - Create a new tag matching the version (e.g., `v0.1.0`)
+ - Set the target branch (usually `main`)
+ - Write release notes describing the changes
+ - Click "Publish release"
+
+4. **Monitor the workflow**:
+ - The [npm-publish workflow](https://github.com/modelcontextprotocol/ext-apps/actions/workflows/npm-publish.yml) will trigger automatically
+ - It runs build and test jobs before publishing
+ - On success, the package is published to npm with provenance
#### npm Tags
@@ -573,7 +588,7 @@ To release a patch for an older version:
### Testing Pre-releases
-Every commit and PR automatically publishes a preview package via [pkg-pr-new](https://github.com/stackblitz-labs/pkg.pr.new). Check the PR comments or workflow logs for the install command.
+Every commit and PR automatically publishes a preview package via [pkg-pr-new](https://github.com/pkg-pr-new/pkg-pr-new). Check the PR comments or workflow logs for the install command.
---
diff --git a/README.md b/README.md
index ab4004d..7348e15 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
Why
·
- Quickstart
+ Quickstart
·
API Docs
·
@@ -89,8 +89,6 @@ it will guide you through the rest.
-
-
> [!NOTE]
@@ -122,36 +120,12 @@ resources:
## Getting Started
-Requires Node.js 20+. The base MCP SDK packages are `^2.0.0` peers of
-`ext-apps` (`@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it for you).
-
-For a View or host:
-
```bash
-npm install -S @modelcontextprotocol/ext-apps \
- @modelcontextprotocol/client@^2.0.0 \
- zod@^4.2.0
+npm install -S @modelcontextprotocol/ext-apps
```
-For an MCP server, add the server package (and, for HTTP transports, the Node
-and Express adapters):
-
-```bash
-npm install -S @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
-```
-
-The wire protocol is unchanged between `ext-apps` 1.x and 2.x: a 2.x View works
-in a 1.x host and a 2.x host renders 1.x Views. See the
-[migration guide](https://apps.extensions.modelcontextprotocol.io/api/documents/migrate-to-v2.html)
-when upgrading from 1.x.
-
**New here?** Start with the
-[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)
+[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html)
to build your first MCP App.
## Using the SDK
@@ -165,7 +139,7 @@ UI metadata.
| `@modelcontextprotocol/ext-apps` | Build interactive Views (App class, PostMessageTransport) | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/app.html) |
| `@modelcontextprotocol/ext-apps/react` | React hooks for Views (useApp, useHostStyles, etc.) | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/_modelcontextprotocol_ext-apps_react.html) |
| `@modelcontextprotocol/ext-apps/app-bridge` | Embed and communicate with Views in your chat client | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/app-bridge.html) |
-| `@modelcontextprotocol/ext-apps/server` | Register tools and resources on your MCP server | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/server-helpers.html) |
+| `@modelcontextprotocol/ext-apps/server` | Register tools and resources on your MCP server | [API Docs →](https://apps.extensions.modelcontextprotocol.io/api/modules/server.html) |
There's no _supported_ host implementation in this repo (beyond the
[examples/basic-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host)
@@ -272,7 +246,7 @@ point your client at a local build:
## Resources
-- [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)
+- [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html)
- [API Documentation](https://apps.extensions.modelcontextprotocol.io/api/)
- [Specification (2026-01-26)](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx)
([Draft](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx))
diff --git a/build.bun.ts b/build.bun.ts
index 61361e6..a129d5d 100644
--- a/build.bun.ts
+++ b/build.bun.ts
@@ -1,9 +1,6 @@
#!/usr/bin/env bun
import { $ } from "bun";
-import { cpSync, mkdirSync, rmSync } from "node:fs";
-
-// Avoid publishing artifacts left behind by an earlier build or branch.
-rmSync("dist", { recursive: true, force: true });
+import { cpSync, mkdirSync } from "node:fs";
// Run TypeScript compiler for type declarations
await $`tsc`;
@@ -34,15 +31,9 @@ function buildJs(
});
}
-// Peer dependencies stay external in the standard entry points so consumers
-// share one base MCP SDK and Zod instance. The *-with-deps entry points keep
-// bundling these dependencies for standalone browser use.
-const PEER_EXTERNALS = [
- "@modelcontextprotocol/client",
- "@modelcontextprotocol/core",
- "@modelcontextprotocol/server",
- "zod",
-];
+// zod is a peerDependency — keep it external so consumers share a single
+// zod instance (instanceof ZodError / schema.extend() break with duplicate copies).
+const PEER_EXTERNALS = ["@modelcontextprotocol/sdk", "zod"];
await Promise.all([
buildJs("src/app.ts", {
diff --git a/docs/authorization.md b/docs/authorization.md
index df7adc6..59fc26c 100644
--- a/docs/authorization.md
+++ b/docs/authorization.md
@@ -150,7 +150,7 @@ registerAppTool(
"get_account_balance",
{
description: "Get account balance",
- inputSchema: z.object({ accountId: z.string() }),
+ inputSchema: { accountId: z.string() },
},
async ({ accountId }) => {
if (!authInfo) {
diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md
deleted file mode 100644
index f9341d0..0000000
--- a/docs/migrate-to-2.md
+++ /dev/null
@@ -1,130 +0,0 @@
----
-title: Migrate to v2
-group: Getting Started
-description: Upgrade from ext-apps 1.x to 2.x — new base MCP SDK peer packages, API changes for Views and hosts, and what stays wire-compatible.
----
-
-# Migrating from ext-apps 1.x to 2.x
-
-ext-apps 2.x is built on the base MCP TypeScript SDK 2.x, which replaced the
-single `@modelcontextprotocol/sdk` package with `@modelcontextprotocol/client`,
-`server`, `core`, `node` and `express`. The MCP Apps wire protocol did not
-change; the breaking changes are in dependencies and in the TypeScript API.
-
-## Host compatibility
-
-The `ui/*` messages exchanged over the iframe channel are byte-identical to
-1.x. A 2.x View runs in a 1.x host and a 2.x host renders 1.x Views. The only
-host-side deltas are in error responses (see below).
-
-## What stays the same
-
-`App` / `AppBridge` constructors, the `on*` setters, `addEventListener`,
-`callServerTool`, `readServerResource`, `registerAppResource`, the React hooks
-and the `_meta.ui.resourceUri` handling are unchanged. Published `.d.ts` files
-now resolve under `moduleResolution: NodeNext` / `Node16` without workarounds
-(#705).
-
-## Peer dependencies by role
-
-| Role | Install |
-| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| View author | `@modelcontextprotocol/ext-apps`, `@modelcontextprotocol/client@^2.0.0`, `zod@^4.2.0` (+ `react`/`react-dom` for `./react`) |
-| Host author | same as View author |
-| MCP server author | View author packages + `@modelcontextprotocol/server@^2.0.0`; for HTTP transports also `@modelcontextprotocol/node@^2.0.0` (`NodeStreamableHTTPServerTransport`) and `@modelcontextprotocol/express@^2.0.0` (`createMcpExpressApp`, which peers on `express`) |
-| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) |
-
-`@modelcontextprotocol/client` is a required peer (`App` and `AppBridge` extend
-its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it without you listing it;
-`@modelcontextprotocol/server` stays optional and is only needed for the
-`./server` helpers. Node.js 20+ is required.
-
-## Breaking changes
-
-- **Peer packages.** `@modelcontextprotocol/sdk@^1` is replaced by the split
- 2.x packages above, all at `^2.0.0`. Remove the 1.x package from your
- project: the two SDKs share no classes or types (a 1.x `McpError` or
- `Client` cannot be passed to 2.x code), even though they speak the same
- wire protocol.
-- **zod 3 dropped.** The peer range is `zod@^4.2.0`. Tool schemas must
- implement Standard JSON Schema (`~standard.jsonSchema`): zod 4.2+, ArkType,
- Valibot. zod 4.0 and 4.1 do not expose `~standard.jsonSchema` and are
- rejected too. Raw zod shapes (`{ q: z.string() }`) still work with
- `registerAppTool` but are deprecated; wrap them with `z.object({...})`.
-- **`App` / `AppBridge` extend `Protocol` from `@modelcontextprotocol/client`.**
- `ProtocolWithEvents` is gone; use the SDK's `Protocol`. The `AppRequest`,
- `AppNotification` and `AppResult` unions are kept as deprecated type
- aliases (nothing in 2.x consumes them) and will be removed in 3.0.
-- **Handler context.** Custom request handlers, `registerAppTool` callbacks
- (server) and `app.registerTool` callbacks (View) receive the SDK 2.x
- context: `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` →
- `extra.mcpReq.id`; `extra.sessionId` is unchanged and `extra.authInfo`
- moved to `extra.http?.authInfo`.
-- **`setRequestHandler` / `setNotificationHandler` take method names.**
- `app.setRequestHandler(SomeRequestSchema, handler)` becomes
- `app.setRequestHandler("some/method", { params: SomeParamsSchema }, (params, ctx) => …)`
- for custom methods (the handler receives the parsed params); the two-argument
- `setRequestHandler("tools/call", handler)` form exists only for spec-defined
- method names. The 1.x `(Schema, handler)` form still works on `App` and
- `AppBridge` as a deprecated overload: it logs a one-time warning, hands the
- handler the whole `{ method, params }` message as before, and gives request
- handlers a 1.x-shaped `extra` (`signal`, `requestId`, `sessionId`, `_meta`,
- `sendRequest`, `sendNotification`, `authInfo`; exported as
- `LegacyRequestHandlerExtra`). It will be removed in 3.0.
-- **Double-registration guard covers the SDK's own handlers.** Registering a
- handler for `notifications/progress` or `notifications/cancelled` (which
- the base `Protocol` already owns) now throws `already registered`, as
- `ping` and the `on*`-owned methods did in 1.x. Use `addEventListener` or
- the SDK's progress callbacks instead of replacing these.
-- **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`);
- local conditions are `SdkError` with a string `code`: request timeout →
- `"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a
- request with an `AbortSignal` also rejects with `"REQUEST_TIMEOUT"` (the
- message is the abort reason). Messages no longer carry the `MCP error N:`
- prefix.
- A resource-not-found reply that carries `data.uri` arrives as
- `ResourceNotFoundError` (a `ProtocolError` subclass, `code` `-32602`, with a
- `.uri` getter).
-
-## Host-side wire deltas
-
-Observed when a 2.x `AppBridge` answers a View; a 1.x View sees these too.
-
-| Situation | 1.x host | 2.x host |
-| --------------------------------------------------------- | ------------------------------ | --------------------------------------------------- |
-| A handler throws `-32002` (resource not found) | `error.code: -32002` | `error.code: -32602` (the SDK never emits `-32002`) |
-| Invalid params on a `ui/*` request | `-32603` with a zod issue dump | `-32602 Invalid params for : …` |
-| Error message text | `MCP error -32602: …` prefix | plain message |
-| `tools/call` to an unknown tool through a 2.x `McpServer` | `result.isError: true` | JSON-RPC error `-32602` (`callServerTool` rejects) |
-
-## `schema.json`
-
-The published `./schema.json` export is regenerated from the 2.x core schemas:
-
-- `McpUiToolResultNotification.params.structuredContent` is any JSON value (was
- `type: "object"`).
-- `McpUiToolResultNotification.params._meta` documents
- `io.modelcontextprotocol/serverInfo` and no longer lists `progressToken` /
- `related-task` (both still pass through).
-- `McpUiHostContext.toolInfo.tool.outputSchema` is a loose object (only
- `$schema` is documented); `inputSchema.properties` values are now typed as
- JSON values.
-- A recursive JSON-value definition (`__schema0`) is added under the `$defs` of
- `McpUiHostContext`, `McpUiHostContextChangedNotification` and
- `McpUiInitializeResult`.
-
-## Checklist
-
-1. `npm uninstall @modelcontextprotocol/sdk` and install the packages for your
- role from the table above.
-2. Replace `sdk/...` imports with the split packages (`sdk/server/mcp.js` →
- `@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` →
- `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`,
- `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, hand-written
- Express routing → `createMcpExpressApp` from `@modelcontextprotocol/express`,
- `sdk/types.js` → `@modelcontextprotocol/client` or `server` for the types,
- `@modelcontextprotocol/core` for the zod schemas).
-3. Wrap raw zod shapes with `z.object({...})`.
-4. Update custom handlers to the `extra.mcpReq.*` context and method-keyed
- `setRequestHandler` calls.
-5. Replace `McpError` / numeric-code checks with `ProtocolError` / `SdkError`.
diff --git a/docs/migrate_from_openai_apps.md b/docs/migrate_from_openai_apps.md
index 22d5d9f..d91281b 100644
--- a/docs/migrate_from_openai_apps.md
+++ b/docs/migrate_from_openai_apps.md
@@ -66,7 +66,7 @@ The server-side changes involve updating metadata structure and using helper fun
#### Before (OpenAI)
```typescript
-import { McpServer } from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
function createServer() {
@@ -78,7 +78,7 @@ function createServer() {
{
title: "Shopping Cart",
description: "Display the user's shopping cart",
- inputSchema: z.object({ userId: z.string() }),
+ inputSchema: { userId: z.string() },
annotations: { readOnlyHint: true },
_meta: {
"openai/outputTemplate": "ui://view/cart.html",
@@ -126,7 +126,7 @@ function createServer() {
#### After (MCP Apps)
```typescript
-import { McpServer } from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import {
registerAppTool,
registerAppResource,
@@ -144,7 +144,7 @@ function createServer() {
{
title: "Shopping Cart",
description: "Display the user's shopping cart",
- inputSchema: z.object({ userId: z.string() }),
+ inputSchema: { userId: z.string() },
annotations: { readOnlyHint: true },
_meta: { ui: { resourceUri: "ui://view/cart.html" } },
},
diff --git a/docs/overview.md b/docs/overview.md
index 8eda95d..28f5b23 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -56,11 +56,7 @@ flowchart LR
- **Host** — The chat client (e.g., Claude Desktop) that connects to servers, embeds Views in iframes, and proxies communication between them.
- **View** — The UI running inside a sandboxed iframe. It receives tool data from the Host and can call server tools or send messages back to the chat.
-The View's `App` and the Host's `AppBridge` both subclass the `Protocol` class
-exported by `@modelcontextprotocol/client` for the iframe channel. A separate outer `Client` connects
-the Host to the actual MCP Server. Keeping those two connections separate
-preserves the Apps-only iframe handshake and prevents iframe capabilities from
-leaking into the server connection.
+The View acts as an MCP client, the Host acts as a proxy, and the Server is a standard MCP server.
## Lifecycle
@@ -97,7 +93,7 @@ sequenceDiagram
```
1. **Discovery** — The Host learns about tools and their UI resources when connecting to the server.
-2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` (the only handshake on the iframe channel) and receives host context (theme, capabilities, container dimensions). `ui/initialize` is authoritative for Apps capabilities and identity, and `ui/notifications/initialized` is the public View-ready signal.
+2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). This handshake ensures the View is ready before receiving data.
3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering). This separation lets servers provide rich data to the UI without bloating the model's context.
4. **Interactive phase** — The user interacts with the View. The View can call tools, send messages, or update context.
5. **Teardown** — Before unmounting, the Host notifies the View so it can save state or release resources.
diff --git a/docs/patterns.md b/docs/patterns.md
index 884de26..87a6d82 100644
--- a/docs/patterns.md
+++ b/docs/patterns.md
@@ -19,7 +19,7 @@ registerAppTool(
"update-quantity",
{
description: "Update item quantity in cart",
- inputSchema: z.object({ itemId: z.string(), quantity: z.number() }),
+ inputSchema: { itemId: z.string(), quantity: z.number() },
_meta: {
ui: {
resourceUri: "ui://shop/cart.html",
@@ -127,14 +127,14 @@ registerAppTool(
{
title: "Read Data Bytes",
description: "Load binary data in chunks",
- inputSchema: z.object({
+ inputSchema: {
id: z.string().describe("Resource identifier"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
.number()
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
- }),
+ },
outputSchema: DataChunkSchema,
// Hidden from model - only callable by the App
_meta: { ui: { visibility: ["app"] } },
@@ -281,7 +281,10 @@ server.registerResource(
```ts source="./patterns.tsx#binaryBlobResourceClient"
-const result = await app.readServerResource({ uri: `video://${videoId}` });
+const result = await app.request(
+ { method: "resources/read", params: { uri: `video://${videoId}` } },
+ ReadResourceResultSchema,
+);
const content = result.contents[0];
if (!content || !("blob" in content)) {
diff --git a/docs/patterns.tsx b/docs/patterns.tsx
index bb9eaa4..6b87692 100644
--- a/docs/patterns.tsx
+++ b/docs/patterns.tsx
@@ -17,12 +17,16 @@ import { randomUUID } from "node:crypto";
import type {
CallToolResult,
ReadResourceResult,
-} from "@modelcontextprotocol/server";
+} from "@modelcontextprotocol/sdk/types.js";
+import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js";
import type { McpUiHostContext } from "../src/types.js";
import { useEffect, useState } from "react";
import { useApp } from "../src/react/index.js";
import { registerAppTool } from "../src/server/index.js";
-import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
+import {
+ McpServer,
+ ResourceTemplate,
+} from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
/**
@@ -113,14 +117,14 @@ function chunkedDataServer(server: McpServer) {
{
title: "Read Data Bytes",
description: "Load binary data in chunks",
- inputSchema: z.object({
+ inputSchema: {
id: z.string().describe("Resource identifier"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
.number()
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
- }),
+ },
outputSchema: DataChunkSchema,
// Hidden from model - only callable by the App
_meta: { ui: { visibility: ["app"] } },
@@ -248,7 +252,10 @@ function binaryBlobResourceServer(
*/
async function binaryBlobResourceClient(app: App, videoId: string) {
//#region binaryBlobResourceClient
- const result = await app.readServerResource({ uri: `video://${videoId}` });
+ const result = await app.request(
+ { method: "resources/read", params: { uri: `video://${videoId}` } },
+ ReadResourceResultSchema,
+ );
const content = result.contents[0];
if (!content || !("blob" in content)) {
diff --git a/docs/quickstart.md b/docs/quickstart.md
index a021912..3e00796 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -21,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 20+.
+You'll also need Node.js 18+.
## 1. Set up the project
@@ -37,7 +37,7 @@ Install the dependencies you'll need:
```bash
npm init -y
-npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 @modelcontextprotocol/node@^2.0.0 @modelcontextprotocol/express@^2.0.0 zod@^4.2.0 express cors
+npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors
npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env
```
@@ -59,7 +59,6 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN
```json source="../examples/quickstart/tsconfig.json"
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
@@ -88,7 +87,6 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN
```json source="../examples/quickstart/tsconfig.server.json"
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
@@ -180,10 +178,9 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import fs from "node:fs/promises";
import path from "node:path";
-import { z } from "zod";
const DIST_DIR = path.join(import.meta.dirname, "dist");
@@ -208,7 +205,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async () => {
@@ -243,10 +240,10 @@ export function createServer(): McpServer {
```ts source="../examples/quickstart/main.ts"
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -266,7 +263,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md
index a8eb333..df1396c 100644
--- a/docs/testing-mcp-apps.md
+++ b/docs/testing-mcp-apps.md
@@ -28,10 +28,6 @@ The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/ex
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
diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json
index cefe9d5..b29f04a 100644
--- a/examples/basic-host/package.json
+++ b/examples/basic-host/package.json
@@ -1,7 +1,7 @@
{
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host",
"name": "@modelcontextprotocol/ext-apps-basic-host",
- "version": "2.0.0",
+ "version": "1.4.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/client": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/express": "^5.0.0",
@@ -29,6 +29,7 @@
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"vite": "^6.0.0",
- "vite-plugin-singlefile": "^2.3.0"
+ "vite-plugin-singlefile": "^2.3.0",
+ "vitest": "^3.2.4"
}
}
diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts
index 5f680ff..31e3698 100644
--- a/examples/basic-host/src/implementation.ts
+++ b/examples/basic-host/src/implementation.ts
@@ -1,12 +1,8 @@
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
-import {
- Client,
- SSEClientTransport,
- StreamableHTTPClientTransport,
- type CallToolResult,
- type Resource,
- type Tool,
-} from "@modelcontextprotocol/client";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
+import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
+import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
import { getTheme, onThemeChange } from "./theme";
import { HOST_STYLE_VARIABLES } from "./host-styles";
diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx
index 00c0d52..3d488a7 100644
--- a/examples/basic-host/src/index.tsx
+++ b/examples/basic-host/src/index.tsx
@@ -1,5 +1,5 @@
import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge";
-import type { Tool } from "@modelcontextprotocol/client";
+import type { Tool } from "@modelcontextprotocol/sdk/types.js";
import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation";
diff --git a/examples/basic-host/tsconfig.json b/examples/basic-host/tsconfig.json
index 3c143ca..9d7c3bc 100644
--- a/examples/basic-host/tsconfig.json
+++ b/examples/basic-host/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-preact/main.ts b/examples/basic-server-preact/main.ts
index ffdff52..7642632 100644
--- a/examples/basic-server-preact/main.ts
+++ b/examples/basic-server-preact/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json
index 60aa40d..a6c0343 100644
--- a/examples/basic-server-preact/package.json
+++ b/examples/basic-server-preact/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Basic MCP App Server example using Preact",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-preact"
},
"license": "MIT",
@@ -24,15 +24,12 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"preact": "^10.0.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@preact/preset-vite": "^2.0.0",
diff --git a/examples/basic-server-preact/server.ts b/examples/basic-server-preact/server.ts
index dfec94a..bf14ea5 100644
--- a/examples/basic-server-preact/server.ts
+++ b/examples/basic-server-preact/server.ts
@@ -1,12 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -32,7 +28,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-preact/src/mcp-app.tsx b/examples/basic-server-preact/src/mcp-app.tsx
index 351a8ec..a8bdc68 100644
--- a/examples/basic-server-preact/src/mcp-app.tsx
+++ b/examples/basic-server-preact/src/mcp-app.tsx
@@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useState } from "preact/hooks";
import { render } from "preact";
import styles from "./mcp-app.module.css";
diff --git a/examples/basic-server-preact/tsconfig.json b/examples/basic-server-preact/tsconfig.json
index 9bf9389..32182d9 100644
--- a/examples/basic-server-preact/tsconfig.json
+++ b/examples/basic-server-preact/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-preact/tsconfig.server.json b/examples/basic-server-preact/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-preact/tsconfig.server.json
+++ b/examples/basic-server-preact/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-react/main.ts b/examples/basic-server-react/main.ts
index 21c5e18..ec187b6 100644
--- a/examples/basic-server-react/main.ts
+++ b/examples/basic-server-react/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json
index ea51a8a..f75d662 100644
--- a/examples/basic-server-react/package.json
+++ b/examples/basic-server-react/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-react",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Basic MCP App Server example using React",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-react"
},
"license": "MIT",
@@ -34,16 +34,13 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts
index d77c908..23f6dda 100644
--- a/examples/basic-server-react/server.ts
+++ b/examples/basic-server-react/server.ts
@@ -1,12 +1,8 @@
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
@@ -33,7 +29,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx
index 2b5fbef..6bb5540 100644
--- a/examples/basic-server-react/src/mcp-app.tsx
+++ b/examples/basic-server-react/src/mcp-app.tsx
@@ -3,7 +3,7 @@
*/
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import styles from "./mcp-app.module.css";
diff --git a/examples/basic-server-react/tsconfig.json b/examples/basic-server-react/tsconfig.json
index 3ecbdad..fc3c210 100644
--- a/examples/basic-server-react/tsconfig.json
+++ b/examples/basic-server-react/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-react/tsconfig.server.json b/examples/basic-server-react/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-react/tsconfig.server.json
+++ b/examples/basic-server-react/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-solid/main.ts b/examples/basic-server-solid/main.ts
index 095ef8d..c8d9de2 100644
--- a/examples/basic-server-solid/main.ts
+++ b/examples/basic-server-solid/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json
index b3ca909..f193fa1 100644
--- a/examples/basic-server-solid/package.json
+++ b/examples/basic-server-solid/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Basic MCP App Server example using Solid",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-solid"
},
"license": "MIT",
@@ -24,15 +24,12 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"solid-js": "1.9.10",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
@@ -43,7 +40,7 @@
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0",
- "vite-plugin-solid": "^2.11.12"
+ "vite-plugin-solid": "^2.0.0"
},
"types": "dist/server.d.ts",
"exports": {
diff --git a/examples/basic-server-solid/server.ts b/examples/basic-server-solid/server.ts
index addb522..2ed2d93 100644
--- a/examples/basic-server-solid/server.ts
+++ b/examples/basic-server-solid/server.ts
@@ -1,12 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -32,7 +28,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-solid/src/mcp-app.tsx b/examples/basic-server-solid/src/mcp-app.tsx
index 2d78b7b..66d47f3 100644
--- a/examples/basic-server-solid/src/mcp-app.tsx
+++ b/examples/basic-server-solid/src/mcp-app.tsx
@@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { createEffect, createSignal, onMount, Show } from "solid-js";
import { render } from "solid-js/web";
import styles from "./mcp-app.module.css";
diff --git a/examples/basic-server-solid/tsconfig.json b/examples/basic-server-solid/tsconfig.json
index bb13a01..84f0efe 100644
--- a/examples/basic-server-solid/tsconfig.json
+++ b/examples/basic-server-solid/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-solid/tsconfig.server.json b/examples/basic-server-solid/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-solid/tsconfig.server.json
+++ b/examples/basic-server-solid/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-svelte/main.ts b/examples/basic-server-svelte/main.ts
index 734ec0c..6c50a25 100644
--- a/examples/basic-server-svelte/main.ts
+++ b/examples/basic-server-svelte/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json
index 19fe4c9..90dd6c6 100644
--- a/examples/basic-server-svelte/package.json
+++ b/examples/basic-server-svelte/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Basic MCP App Server example using Svelte",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-svelte"
},
"license": "MIT",
@@ -24,15 +24,12 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"svelte": "^5.0.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^5.0.0",
diff --git a/examples/basic-server-svelte/server.ts b/examples/basic-server-svelte/server.ts
index 9414285..da603ca 100644
--- a/examples/basic-server-svelte/server.ts
+++ b/examples/basic-server-svelte/server.ts
@@ -1,12 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -32,7 +28,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-svelte/src/App.svelte b/examples/basic-server-svelte/src/App.svelte
index cabcc31..fde128d 100644
--- a/examples/basic-server-svelte/src/App.svelte
+++ b/examples/basic-server-svelte/src/App.svelte
@@ -7,7 +7,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
function extractTime(result: CallToolResult): string {
const { text } = result.content?.find((c) => c.type === "text")!;
diff --git a/examples/basic-server-svelte/tsconfig.json b/examples/basic-server-svelte/tsconfig.json
index 4fd3cd7..5edb66a 100644
--- a/examples/basic-server-svelte/tsconfig.json
+++ b/examples/basic-server-svelte/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-svelte/tsconfig.server.json b/examples/basic-server-svelte/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-svelte/tsconfig.server.json
+++ b/examples/basic-server-svelte/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-vanillajs/main.ts b/examples/basic-server-vanillajs/main.ts
index 4de7331..286fa34 100644
--- a/examples/basic-server-vanillajs/main.ts
+++ b/examples/basic-server-vanillajs/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json
index ebfebbb..e97eb68 100644
--- a/examples/basic-server-vanillajs/package.json
+++ b/examples/basic-server-vanillajs/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-vanillajs",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Basic MCP App Server example using vanilla JavaScript",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-vanillajs"
},
"license": "MIT",
@@ -24,14 +24,11 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts
index 693d861..5b2daf7 100644
--- a/examples/basic-server-vanillajs/server.ts
+++ b/examples/basic-server-vanillajs/server.ts
@@ -1,9 +1,6 @@
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -32,7 +29,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
outputSchema: z.object({
time: z.string(),
}),
diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts
index 6b5084a..c3168c2 100644
--- a/examples/basic-server-vanillajs/src/mcp-app.ts
+++ b/examples/basic-server-vanillajs/src/mcp-app.ts
@@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import "./global.css";
import "./mcp-app.css";
diff --git a/examples/basic-server-vanillajs/tsconfig.json b/examples/basic-server-vanillajs/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/basic-server-vanillajs/tsconfig.json
+++ b/examples/basic-server-vanillajs/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-vanillajs/tsconfig.server.json b/examples/basic-server-vanillajs/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-vanillajs/tsconfig.server.json
+++ b/examples/basic-server-vanillajs/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-vue/main.ts b/examples/basic-server-vue/main.ts
index d774269..669a071 100644
--- a/examples/basic-server-vue/main.ts
+++ b/examples/basic-server-vue/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json
index 858dd89..ad8c119 100644
--- a/examples/basic-server-vue/package.json
+++ b/examples/basic-server-vue/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-vue",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Basic MCP App Server example using Vue",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/basic-server-vue"
},
"license": "MIT",
@@ -24,15 +24,12 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"vue": "^3.5.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/basic-server-vue/server.ts b/examples/basic-server-vue/server.ts
index 9adb911..5aa72de 100644
--- a/examples/basic-server-vue/server.ts
+++ b/examples/basic-server-vue/server.ts
@@ -1,12 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -32,7 +28,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-vue/src/App.vue b/examples/basic-server-vue/src/App.vue
index a45c262..196ad64 100644
--- a/examples/basic-server-vue/src/App.vue
+++ b/examples/basic-server-vue/src/App.vue
@@ -7,7 +7,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
function extractTime(result: CallToolResult): string {
const { text } = result.content?.find((c) => c.type === "text")!;
diff --git a/examples/basic-server-vue/tsconfig.json b/examples/basic-server-vue/tsconfig.json
index 4fd3cd7..5edb66a 100644
--- a/examples/basic-server-vue/tsconfig.json
+++ b/examples/basic-server-vue/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-vue/tsconfig.server.json b/examples/basic-server-vue/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/basic-server-vue/tsconfig.server.json
+++ b/examples/basic-server-vue/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/budget-allocator-server/main.ts b/examples/budget-allocator-server/main.ts
index 02c03cb..cc488f0 100644
--- a/examples/budget-allocator-server/main.ts
+++ b/examples/budget-allocator-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json
index 6e41622..71b7ace 100644
--- a/examples/budget-allocator-server/package.json
+++ b/examples/budget-allocator-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-budget-allocator",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Budget allocator MCP App Server with interactive visualization",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/budget-allocator-server"
},
"license": "MIT",
@@ -26,14 +26,12 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts
index 01f5dbf..e522918 100755
--- a/examples/budget-allocator-server/server.ts
+++ b/examples/budget-allocator-server/server.ts
@@ -4,11 +4,11 @@
* Provides budget configuration, 24 months of historical allocation data,
* and industry benchmarks by company stage.
*/
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -269,8 +269,8 @@ export function createServer(): McpServer {
{
title: "Get Budget Data",
description:
- "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage. The widget is interactive and exposes tools for reading/modifying allocations, adjusting budgets, and comparing against industry benchmarks.",
- inputSchema: z.object({}),
+ "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage",
+ inputSchema: {},
outputSchema: BudgetDataResponseSchema,
_meta: { ui: { resourceUri } },
},
diff --git a/examples/budget-allocator-server/src/mcp-app.ts b/examples/budget-allocator-server/src/mcp-app.ts
index a9bae12..4e8c62d 100644
--- a/examples/budget-allocator-server/src/mcp-app.ts
+++ b/examples/budget-allocator-server/src/mcp-app.ts
@@ -3,7 +3,6 @@
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { Chart, registerables } from "chart.js";
-import { z } from "zod";
import "./global.css";
import "./mcp-app.css";
@@ -627,295 +626,6 @@ function handleHostContextChanged(ctx: McpUiHostContext) {
app.onhostcontextchanged = handleHostContextChanged;
-// Register tools for model interaction
-app.registerTool(
- "get-allocations",
- {
- title: "Get Budget Allocations",
- description:
- "Get the current budget allocations including total budget, percentages, and amounts per category",
- },
- async () => {
- if (!state.config) {
- return {
- content: [
- { type: "text" as const, text: "Error: Configuration not loaded" },
- ],
- isError: true,
- };
- }
-
- const allocations: Record = {};
- for (const category of state.config.categories) {
- const percent = state.allocations.get(category.id) ?? 0;
- allocations[category.id] = {
- percent,
- amount: (percent / 100) * state.totalBudget,
- };
- }
-
- const result = {
- totalBudget: state.totalBudget,
- currency: state.config.currency,
- currencySymbol: state.config.currencySymbol,
- selectedStage: state.selectedStage,
- allocations,
- categories: state.config.categories.map((c) => ({
- id: c.id,
- name: c.name,
- color: c.color,
- })),
- };
-
- return {
- content: [
- { type: "text" as const, text: JSON.stringify(result, null, 2) },
- ],
- structuredContent: result,
- };
- },
-);
-
-app.registerTool(
- "set-allocation",
- {
- title: "Set Category Allocation",
- description: "Set the allocation percentage for a specific budget category",
- inputSchema: z.object({
- categoryId: z
- .string()
- .describe(
- "Category ID (e.g., 'marketing', 'engineering', 'operations', 'sales', 'rd')",
- ),
- percent: z
- .number()
- .min(0)
- .max(100)
- .describe("Allocation percentage (0-100)"),
- }),
- },
- async (args) => {
- if (!state.config) {
- return {
- content: [
- { type: "text" as const, text: "Error: Configuration not loaded" },
- ],
- isError: true,
- };
- }
-
- const category = state.config.categories.find(
- (c) => c.id === args.categoryId,
- );
- if (!category) {
- return {
- content: [
- {
- type: "text" as const,
- text: `Error: Category "${args.categoryId}" not found. Available: ${state.config.categories.map((c) => c.id).join(", ")}`,
- },
- ],
- isError: true,
- };
- }
-
- handleSliderChange(args.categoryId, args.percent);
-
- // Also update the slider UI
- const slider = document.querySelector(
- `.slider-row[data-category-id="${args.categoryId}"] .slider`,
- ) as HTMLInputElement | null;
- if (slider) {
- slider.value = String(args.percent);
- }
-
- const amount = (args.percent / 100) * state.totalBudget;
- return {
- content: [
- {
- type: "text" as const,
- text: `Set ${category.name} allocation to ${args.percent.toFixed(1)}% (${state.config.currencySymbol}${amount.toLocaleString()})`,
- },
- ],
- };
- },
-);
-
-app.registerTool(
- "set-total-budget",
- {
- title: "Set Total Budget",
- description: "Set the total budget amount",
- inputSchema: z.object({
- amount: z.number().positive().describe("Total budget amount"),
- }),
- },
- async (args) => {
- if (!state.config) {
- return {
- content: [
- { type: "text" as const, text: "Error: Configuration not loaded" },
- ],
- isError: true,
- };
- }
-
- state.totalBudget = args.amount;
-
- // Update the budget selector if this amount is a preset
- const budgetSelector = document.getElementById(
- "budget-selector",
- ) as HTMLSelectElement | null;
- if (budgetSelector) {
- const option = Array.from(budgetSelector.options).find(
- (opt) => parseInt(opt.value) === args.amount,
- );
- if (option) {
- budgetSelector.value = String(args.amount);
- }
- }
-
- updateAllSliderAmounts();
- updateStatusBar();
- updateComparisonSummary();
-
- return {
- content: [
- {
- type: "text" as const,
- text: `Total budget set to ${state.config.currencySymbol}${args.amount.toLocaleString()}`,
- },
- ],
- };
- },
-);
-
-app.registerTool(
- "set-company-stage",
- {
- title: "Set Company Stage",
- description:
- "Set the company stage for benchmark comparison (e.g., 'Seed', 'Series A', 'Series B', 'Growth')",
- inputSchema: z.object({
- stage: z.string().describe("Company stage ID"),
- }),
- },
- async (args) => {
- if (!state.analytics) {
- return {
- content: [
- { type: "text" as const, text: "Error: Analytics not loaded" },
- ],
- isError: true,
- };
- }
-
- if (!state.analytics.stages.includes(args.stage)) {
- return {
- content: [
- {
- type: "text" as const,
- text: `Error: Stage "${args.stage}" not found. Available: ${state.analytics.stages.join(", ")}`,
- },
- ],
- isError: true,
- };
- }
-
- state.selectedStage = args.stage;
-
- // Update the stage selector UI
- const stageSelector = document.getElementById(
- "stage-selector",
- ) as HTMLSelectElement | null;
- if (stageSelector) {
- stageSelector.value = args.stage;
- }
-
- // Update all badges and summary
- if (state.config) {
- for (const category of state.config.categories) {
- updatePercentileBadge(category.id);
- }
- updateComparisonSummary();
- }
-
- return {
- content: [
- {
- type: "text" as const,
- text: `Company stage set to "${args.stage}"`,
- },
- ],
- };
- },
-);
-
-app.registerTool(
- "get-benchmark-comparison",
- {
- title: "Get Benchmark Comparison",
- description:
- "Compare current allocations against industry benchmarks for the selected stage",
- },
- async () => {
- if (!state.config || !state.analytics) {
- return {
- content: [{ type: "text" as const, text: "Error: Data not loaded" }],
- isError: true,
- };
- }
-
- const benchmark = state.analytics.benchmarks.find(
- (b) => b.stage === state.selectedStage,
- );
- if (!benchmark) {
- return {
- content: [
- {
- type: "text" as const,
- text: `Error: No benchmark data for stage "${state.selectedStage}"`,
- },
- ],
- isError: true,
- };
- }
-
- const comparison: Record<
- string,
- { current: number; p25: number; p50: number; p75: number; status: string }
- > = {};
-
- for (const category of state.config.categories) {
- const current = state.allocations.get(category.id) ?? 0;
- const benchmarkData = benchmark.categoryBenchmarks[category.id];
- let status = "within range";
- if (current < benchmarkData.p25) status = "below p25";
- else if (current > benchmarkData.p75) status = "above p75";
-
- comparison[category.id] = {
- current,
- p25: benchmarkData.p25,
- p50: benchmarkData.p50,
- p75: benchmarkData.p75,
- status,
- };
- }
-
- const result = {
- stage: state.selectedStage,
- comparison,
- };
-
- return {
- content: [
- { type: "text" as const, text: JSON.stringify(result, null, 2) },
- ],
- structuredContent: result,
- };
- },
-);
-
// Handle theme changes
window
.matchMedia("(prefers-color-scheme: dark)")
diff --git a/examples/budget-allocator-server/tsconfig.json b/examples/budget-allocator-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/budget-allocator-server/tsconfig.json
+++ b/examples/budget-allocator-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/budget-allocator-server/tsconfig.server.json b/examples/budget-allocator-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/budget-allocator-server/tsconfig.server.json
+++ b/examples/budget-allocator-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/cohort-heatmap-server/main.ts b/examples/cohort-heatmap-server/main.ts
index 8b33603..d59903b 100644
--- a/examples/cohort-heatmap-server/main.ts
+++ b/examples/cohort-heatmap-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json
index 7e2d2e2..d753706 100644
--- a/examples/cohort-heatmap-server/package.json
+++ b/examples/cohort-heatmap-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-cohort-heatmap",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Cohort heatmap MCP App Server for retention analysis",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/cohort-heatmap-server"
},
"license": "MIT",
@@ -26,15 +26,13 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts
index 129e2dc..6d07144 100644
--- a/examples/cohort-heatmap-server/server.ts
+++ b/examples/cohort-heatmap-server/server.ts
@@ -1,7 +1,5 @@
-import {
- McpServer,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -180,8 +178,8 @@ export function createServer(): McpServer {
title: "Get Cohort Retention Data",
description:
"Returns cohort retention heatmap data showing customer retention over time by signup month",
- inputSchema: GetCohortDataInputSchema,
- outputSchema: CohortDataSchema,
+ inputSchema: GetCohortDataInputSchema.shape,
+ outputSchema: CohortDataSchema.shape,
_meta: { ui: { resourceUri } },
},
async ({ metric, periodType, cohortCount, maxPeriods }) => {
diff --git a/examples/cohort-heatmap-server/tsconfig.json b/examples/cohort-heatmap-server/tsconfig.json
index 3ecbdad..fc3c210 100644
--- a/examples/cohort-heatmap-server/tsconfig.json
+++ b/examples/cohort-heatmap-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/cohort-heatmap-server/tsconfig.server.json b/examples/cohort-heatmap-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/cohort-heatmap-server/tsconfig.server.json
+++ b/examples/cohort-heatmap-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/customer-segmentation-server/main.ts b/examples/customer-segmentation-server/main.ts
index c788757..130076c 100644
--- a/examples/customer-segmentation-server/main.ts
+++ b/examples/customer-segmentation-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json
index bac3b89..ade9dae 100644
--- a/examples/customer-segmentation-server/package.json
+++ b/examples/customer-segmentation-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-customer-segmentation",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Customer segmentation MCP App Server with filtering",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/customer-segmentation-server"
},
"license": "MIT",
@@ -26,14 +26,12 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts
index 7cc1c1f..244415d 100644
--- a/examples/customer-segmentation-server/server.ts
+++ b/examples/customer-segmentation-server/server.ts
@@ -1,8 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -100,8 +100,8 @@ export function createServer(): McpServer {
title: "Get Customer Data",
description:
"Returns customer data with segment information for visualization. Optionally filter by segment.",
- inputSchema: GetCustomerDataInputSchema,
- outputSchema: GetCustomerDataOutputSchema,
+ inputSchema: GetCustomerDataInputSchema.shape,
+ outputSchema: GetCustomerDataOutputSchema.shape,
_meta: { ui: { resourceUri } },
},
async ({ segment }): Promise => {
diff --git a/examples/customer-segmentation-server/tsconfig.json b/examples/customer-segmentation-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/customer-segmentation-server/tsconfig.json
+++ b/examples/customer-segmentation-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/customer-segmentation-server/tsconfig.server.json b/examples/customer-segmentation-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/customer-segmentation-server/tsconfig.server.json
+++ b/examples/customer-segmentation-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/debug-server/main.ts b/examples/debug-server/main.ts
index 9fb0825..3c89a97 100644
--- a/examples/debug-server/main.ts
+++ b/examples/debug-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json
index e28f4d3..15dc903 100644
--- a/examples/debug-server/package.json
+++ b/examples/debug-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-debug",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Debug MCP App Server for testing all SDK capabilities",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/debug-server"
},
"license": "MIT",
@@ -34,11 +34,9 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
- "zod": "^4.2.0"
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/debug-server/server.ts b/examples/debug-server/server.ts
index 068f94d..fd3452a 100644
--- a/examples/debug-server/server.ts
+++ b/examples/debug-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import { appendFileSync } from "node:fs";
import path from "node:path";
diff --git a/examples/debug-server/tsconfig.json b/examples/debug-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/debug-server/tsconfig.json
+++ b/examples/debug-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/debug-server/tsconfig.server.json b/examples/debug-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/debug-server/tsconfig.server.json
+++ b/examples/debug-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/integration-server/main.ts b/examples/integration-server/main.ts
index 9a42842..c45787e 100644
--- a/examples/integration-server/main.ts
+++ b/examples/integration-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json
index 7e04f4a..5686a02 100644
--- a/examples/integration-server/package.json
+++ b/examples/integration-server/package.json
@@ -1,6 +1,6 @@
{
"name": "integration-server",
- "version": "2.0.0",
+ "version": "1.4.0",
"private": true,
"type": "module",
"scripts": {
@@ -15,16 +15,13 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts
index 346f4f7..e44689d 100644
--- a/examples/integration-server/server.ts
+++ b/examples/integration-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -33,7 +33,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time.",
- inputSchema: z.object({}),
+ inputSchema: {},
outputSchema: z.object({
time: z.string(),
}),
@@ -72,7 +72,7 @@ export function createServer(): McpServer {
);
// Sample downloadable resource — used to demo ResourceLink in ui/download-file
- server.registerResource(
+ server.resource(
SAMPLE_DOWNLOAD_URI,
SAMPLE_DOWNLOAD_URI,
{
diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx
index 281a356..7f0b18c 100644
--- a/examples/integration-server/src/mcp-app.tsx
+++ b/examples/integration-server/src/mcp-app.tsx
@@ -3,7 +3,7 @@
*/
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import styles from "./mcp-app.module.css";
diff --git a/examples/integration-server/tsconfig.json b/examples/integration-server/tsconfig.json
index 3ecbdad..fc3c210 100644
--- a/examples/integration-server/tsconfig.json
+++ b/examples/integration-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/integration-server/tsconfig.server.json b/examples/integration-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/integration-server/tsconfig.server.json
+++ b/examples/integration-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/lazy-auth-server/README.md b/examples/lazy-auth-server/README.md
deleted file mode 100644
index f2e7545..0000000
--- a/examples/lazy-auth-server/README.md
+++ /dev/null
@@ -1,105 +0,0 @@
-# Example: Lazy Auth Server
-
-An MCP App example demonstrating **lazy (on-demand) auth**: the server connects and lists tools without any authentication, and only asks for OAuth when a _protected_ tool is actually called — by answering `401` with a `WWW-Authenticate` header. A public MCP App renders an "Auth me" button; clicking it calls a protected tool via [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool). The host sees the 401, runs the OAuth flow, retries, and the result renders inline.
-
-The embedded OAuth authorization server is a deliberately minimal mock (HS256 JWTs, stateless auth codes, auto-approve consent page) so the whole flow runs from a single process with no external dependencies. It is **not** a production authorization server.
-
-## Tools
-
-| Tool | Auth | Description |
-| ------------------- | ------------- | ----------------------------------------------------------------------------------------------------- |
-| `show_auth_button` | public | Renders buttons: "Auth me" (calls `get_secret`), "Revoke token" (calls `revoke_auth_token`) |
-| `get_secret` | **protected** | Returns secret data (requires Bearer token) |
-| `revoke_auth_token` | **protected** | Revokes the caller's **entire auth session** (access + refresh token) → forces full re-auth |
-| `elicit_url` | public | URL elicitation via `elicitInput` (blocks until the elicitation completes) |
-| `elicit_by_error` | public | URL elicitation via the `-32042` (`UrlElicitationRequired`) error; succeeds on retry after completion |
-
-## Getting Started
-
-```bash
-npm install
-npm start
-# → MCP endpoint at http://localhost:3097/mcp
-```
-
-To test with a remote MCP host, expose the server through a public tunnel (see [Testing MCP Apps](../../docs/testing-mcp-apps.md)) and set `PUBLIC_URL` to the tunnel URL so OAuth metadata and callback URLs use it:
-
-```bash
-PUBLIC_URL=https:// npm start
-```
-
-This example is HTTP-only (no stdio mode): the lazy-auth flow relies on HTTP status codes and OAuth endpoints.
-
-## Environment Variables
-
-| Var | Required | Description |
-| --------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `JWT_SECRET` | recommended | 32+ byte secret for HS256 signing (`openssl rand -hex 32`). A dev-only default is used if unset. |
-| `PORT` | no | Local port (default 3097) |
-| `PUBLIC_URL` | non-local | Public base URL of the server (e.g. your tunnel URL). Required for non-localhost deployments; client-supplied `Host` headers are only trusted for loopback hosts |
-| `ACCESS_TOKEN_TTL_SECONDS` | no | Default access-token lifetime (default **30**, short on purpose so you can watch the host's refresh flow kick in) |
-| `REFRESH_TOKEN_TTL_SECONDS` | no | Default refresh-token lifetime (default **300**). Between the access and refresh TTLs, calls succeed via silent refresh; past it, a full re-auth is required |
-| `REACTIVE_AUTH_ONLY` | no | Set `1` to remove auth metadata from the root `/.well-known/oauth-*` paths so hosts can't discover auth preemptively — discovery then only happens via the 401 |
-
-### Per-connection token lifetimes
-
-The short defaults are great for watching the refresh flow, but slow or automated clients may want tokens that survive a whole session. Any client can request a different access-token lifetime by connecting to a TTL-scoped MCP endpoint path (capped at 24 hours):
-
-```
-https:///ttl/3600/mcp ← tokens for this connection live 1 hour
-```
-
-This works through [RFC 8707 resource indicators](https://www.rfc-editor.org/rfc/rfc8707): MCP hosts send the MCP server URL as the `resource` parameter in OAuth authorization and token requests, and this server issues tokens for that grant with the lifetime encoded in the path (refresh tokens are extended to at least match). The TTL is a _path_ segment rather than a query param because hosts canonicalize resource indicators and strip query strings. Each TTL endpoint also enforces its value as a maximum token age, so connecting to a path with a _lower_ TTL than a token's issued lifetime forces the refresh flow. To exercise the full **re-auth** flow, call the `revoke_auth_token` tool.
-
-## Mounting under a base path
-
-`createApp()` can also be mounted inside another Express app, so an existing server can host this example at a sub-path of its own origin:
-
-```ts
-import { createApp } from "@modelcontextprotocol/server-lazy-auth";
-
-hostApp.use("/lazy-auth", createApp());
-// → MCP endpoint at https:///lazy-auth/mcp
-```
-
-All advertised URLs (OAuth metadata, `WWW-Authenticate` `resource_metadata`, PRM `resource`, elicitation callbacks) include the mount path automatically, derived from Express's `req.baseUrl`. When `PUBLIC_URL` is set, it must include the mount path (e.g. `https://example.com/lazy-auth`).
-
-One thing the mounted app cannot do for itself: [RFC 8414](https://www.rfc-editor.org/rfc/rfc8414#section-3) / [RFC 9728](https://www.rfc-editor.org/rfc/rfc9728) put well-known discovery documents at the _root_ of the origin with the path inserted after the well-known prefix (`/.well-known/oauth-authorization-server/lazy-auth`), and MCP SDK clients only try that insertion form. The host app must rewrite those root paths into the mount before its other routes:
-
-```ts
-hostApp.use((req, _res, next) => {
- const m = req.url.match(
- /^\/\.well-known\/(oauth-authorization-server|oauth-protected-resource)\/lazy-auth(\/.*)?$/,
- );
- if (m) req.url = `/lazy-auth/.well-known/${m[1]}${m[2] ?? ""}`;
- next();
-});
-```
-
-Rewriting into the mount (rather than calling the sub-app directly) keeps `req.baseUrl` — and therefore every advertised URL — consistent.
-
-## How It Works
-
-1. **Connect without auth** — `initialize`, `tools/list`, and public tool calls succeed with no `Authorization` header.
-2. **Protected tool → 401** — when `get_secret` or `revoke_auth_token` is called without a (valid) Bearer token, the server responds `401` with `WWW-Authenticate: Bearer resource_metadata="…/auth/prm"`.
-3. **Discovery** — the host follows `resource_metadata` to the protected-resource metadata (RFC 9728), which points at the authorization server metadata (RFC 8414).
-4. **OAuth flow** — the host runs the authorization-code + PKCE flow against the mock `/authorize` and `/token` endpoints (a small consent page keeps the popup visible).
-5. **Retry** — the host retries the tool call with the Bearer token and the secret renders inline in the app.
-6. **Refresh + revocation** — access tokens expire after 30 seconds and refresh tokens after 5 minutes by default, so all three states are easy to observe: direct success (<30s), silent refresh (30s–5min), and full re-auth (>5min). Connections can request different lifetimes via the `/ttl//mcp` endpoint path (see [Per-connection token lifetimes](#per-connection-token-lifetimes)), and `revoke_auth_token` invalidates the whole session immediately.
-
-The two `elicit_*` tools demonstrate the complementary pattern of [URL elicitation](https://modelcontextprotocol.io/specification/draft/client/elicitation), where the server asks the user to open a URL (e.g. to complete sign-in) either by blocking inside the tool call (`elicit_url`) or by failing with the `-32042` error and succeeding on retry (`elicit_by_error`).
-
-## Architecture
-
-- **Stateless auth codes** — grant details are encoded _inside_ the authorization code as a 5-minute JWT, so nothing needs to be stored between requests.
-- **Short-lived tokens** — access tokens default to a **30 second** TTL and refresh tokens to **5 minutes**: first `get_secret` succeeds → wait >30s → next call 401s → host refreshes → retry succeeds → wait >5min → full re-auth. Per-connection overrides via the `/ttl//mcp` endpoint path.
-- **HS256** — a single shared secret; no key-pair persistence.
-- **Per-request MCP server** — each `/mcp` request gets a fresh `McpServer` + `StreamableHTTPServerTransport` (stateless, no session IDs).
-- **Session revocation** — all tokens from one OAuth session share a `sid` claim; `revoke_auth_token` adds the sid to an in-memory revocation list checked by both token verification and the refresh grant.
-
-## Key Files
-
-- [`server.ts`](server.ts) - OAuth endpoints, discovery metadata, and the MCP server with public + protected tools
-- [`main.ts`](main.ts) - HTTP entry point
-- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.ts`](src/mcp-app.ts) - Public app with the "Auth me" button
-- [`secret-app.html`](secret-app.html) / [`src/secret-app.ts`](src/secret-app.ts) - Protected app rendered for `get_secret`
diff --git a/examples/lazy-auth-server/main.ts b/examples/lazy-auth-server/main.ts
deleted file mode 100644
index a96f34c..0000000
--- a/examples/lazy-auth-server/main.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/**
- * Entry point for running the Lazy Auth demo MCP server.
- * Run with: npx mcp-server-lazy-auth
- * Or: node dist/index.js
- *
- * This example is HTTP-only (no stdio mode): the lazy-auth flow it demonstrates
- * relies on HTTP status codes (401 + WWW-Authenticate) and OAuth endpoints.
- *
- * To test with a remote MCP host, expose the server through a public tunnel
- * and set PUBLIC_URL to the tunnel URL so OAuth metadata and callback URLs
- * use it (see docs/testing-mcp-apps.md in the repository root).
- */
-import { createApp, PORT } from "./server.js";
-
-async function main() {
- const app = createApp();
-
- const httpServer = app.listen(PORT, (err) => {
- if (err) {
- console.error("Failed to start server:", err);
- process.exit(1);
- }
- console.log(
- `Lazy Auth demo MCP server listening on http://localhost:${PORT}/mcp`,
- );
- console.log(
- ` Tools: show_auth_button, get_secret [PROTECTED], revoke_auth_token [PROTECTED], elicit_url, elicit_by_error`,
- );
- });
-
- const shutdown = () => {
- console.log("\nShutting down...");
- httpServer.close(() => process.exit(0));
- };
- process.on("SIGINT", shutdown);
- process.on("SIGTERM", shutdown);
-}
-
-main().catch((e) => {
- console.error(e);
- process.exit(1);
-});
diff --git a/examples/lazy-auth-server/mcp-app.html b/examples/lazy-auth-server/mcp-app.html
deleted file mode 100644
index 7040f15..0000000
--- a/examples/lazy-auth-server/mcp-app.html
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
- Lazy Auth Demo
-
-
-
- Public App
-
- No auth was needed to render this view. Click below to invoke the
- protected
- get_secret tool — the host will run the OAuth flow on 401
- and retry.
-
-
- Auth me
- Revoke token
- ⤢
-
-
-
-
-
-
diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json
deleted file mode 100644
index dbd4601..0000000
--- a/examples/lazy-auth-server/package.json
+++ /dev/null
@@ -1,53 +0,0 @@
-{
- "name": "@modelcontextprotocol/server-lazy-auth",
- "version": "2.0.0",
- "type": "module",
- "description": "MCP App example demonstrating lazy (on-demand) OAuth: public tools work unauthenticated, protected tools return 401 + WWW-Authenticate so the host runs the OAuth flow only when needed",
- "repository": {
- "type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
- "directory": "examples/lazy-auth-server"
- },
- "license": "MIT",
- "main": "dist/server.js",
- "files": [
- "dist"
- ],
- "scripts": {
- "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && cross-env INPUT=secret-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": "concurrently \"cross-env INPUT=mcp-app.html vite build --watch\" \"cross-env INPUT=secret-app.html vite build --watch\"",
- "serve": "bun --watch main.ts",
- "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\"",
- "prepublishOnly": "npm run build"
- },
- "dependencies": {
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
- "cors": "^2.8.5",
- "express": "^5.1.0",
- "jose": "^6.0.0",
- "zod": "^4.2.0"
- },
- "devDependencies": {
- "@types/cors": "^2.8.19",
- "@types/express": "^5.0.0",
- "@types/node": "22.10.0",
- "concurrently": "^9.2.1",
- "cross-env": "^10.1.0",
- "typescript": "^5.9.3",
- "vite": "^6.0.0",
- "vite-plugin-singlefile": "^2.3.0"
- },
- "types": "dist/server.d.ts",
- "exports": {
- ".": {
- "types": "./dist/server.d.ts",
- "default": "./dist/server.js"
- }
- },
- "bin": {
- "mcp-server-lazy-auth": "dist/index.js"
- }
-}
diff --git a/examples/lazy-auth-server/secret-app.html b/examples/lazy-auth-server/secret-app.html
deleted file mode 100644
index b2ab00b..0000000
--- a/examples/lazy-auth-server/secret-app.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
- Lazy Auth Demo — Secret
-
-
-
- Protected App
- Waiting for secret data…
-
-
-
-
diff --git a/examples/lazy-auth-server/server.ts b/examples/lazy-auth-server/server.ts
deleted file mode 100644
index 5240008..0000000
--- a/examples/lazy-auth-server/server.ts
+++ /dev/null
@@ -1,1134 +0,0 @@
-/**
- * Lazy Auth demo MCP server.
- *
- * Demonstrates "lazy" (on-demand) OAuth for MCP servers and Apps:
- *
- * - The server connects without authentication: `initialize`, `tools/list`,
- * and public tool calls all succeed with no Bearer token.
- * - Protected tools (`get_secret`, `revoke_auth_token`) return 401 with a
- * `WWW-Authenticate` header pointing at protected-resource metadata, so the
- * host only runs the OAuth flow when a protected tool is actually called.
- * - The `show_auth_button` MCP App is public; clicking its button calls the
- * protected `get_secret` tool via `callServerTool`. The host sees the 401,
- * runs OAuth, retries, and the result renders inline.
- *
- * The embedded OAuth authorization server is intentionally minimal and
- * stateless (HS256 JWTs, auth codes encoded as short-lived JWTs) so the demo
- * can run anywhere with a single env var. It is NOT a production AS.
- */
-import {
- registerAppResource,
- registerAppTool,
- RESOURCE_MIME_TYPE,
-} from "@modelcontextprotocol/ext-apps/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import {
- McpServer,
- UrlElicitationRequiredError,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
-import cors from "cors";
-import express, { type Express, type Request, type Response } from "express";
-import { SignJWT, jwtVerify } from "jose";
-import crypto from "node:crypto";
-import fs from "node:fs/promises";
-import path from "node:path";
-import { fileURLToPath } from "node:url";
-import { z } from "zod";
-
-// Works both from source (server.ts) and compiled (dist/server.js). Derived
-// from import.meta.url rather than import.meta.filename/dirname, which are
-// undefined in some module-VM contexts (e.g. importing this package from
-// jest).
-const SERVER_FILE = fileURLToPath(import.meta.url);
-const DIST_DIR = SERVER_FILE.endsWith(".ts")
- ? path.join(path.dirname(SERVER_FILE), "dist")
- : path.dirname(SERVER_FILE);
-
-// ─── Config ──────────────────────────────────────────────────────────────────
-
-export const PORT = parseInt(process.env.PORT ?? "3097", 10);
-const JWT_SECRET = new TextEncoder().encode(
- process.env.JWT_SECRET ??
- "dev-insecure-secret-do-not-use-in-production-" + "x".repeat(20),
-);
-const PROTECTED_TOOLS = new Set(["get_secret", "revoke_auth_token"]);
-// Token lifetimes. Defaults are deliberately short so the host's refresh flow
-// (30s–5min after auth) and full re-auth (>5min) are easy to observe in a demo.
-// Slow or automated clients can request longer-lived tokens per connection by
-// connecting to a TTL-scoped endpoint path (/ttl//mcp) — see
-// ttlFromResource() and the README.
-const ACCESS_TOKEN_TTL_SECONDS = parseInt(
- process.env.ACCESS_TOKEN_TTL_SECONDS ?? "30",
- 10,
-);
-const REFRESH_TOKEN_TTL_SECONDS = parseInt(
- process.env.REFRESH_TOKEN_TTL_SECONDS ?? "300",
- 10,
-);
-// Cap on per-connection `?token_ttl_s=` requests.
-const MAX_TOKEN_TTL_SECONDS = 86400;
-// When true: root well-known PRM returns *stub* metadata WITHOUT authorization_servers,
-// and root well-known AS is 404. Hosts probing well-known on connect see "resource has
-// PRM but no auth configured" → connect without OAuth. Real PRM (with authorization_servers)
-// is only at /auth/prm, reachable via WWW-Authenticate on 401. Default false = standard.
-const REACTIVE_AUTH_ONLY = process.env.REACTIVE_AUTH_ONLY === "1";
-
-/** Hostname is localhost / a loopback address (URL-style, so IPv6 keeps its brackets). */
-function isLoopbackHostname(hostname: string): boolean {
- return (
- ["localhost", "127.0.0.1", "[::1]"].includes(hostname) ||
- hostname.endsWith(".localhost")
- );
-}
-
-/**
- * Resolve the public base URL for this deployment.
- *
- * Precedence: the PUBLIC_URL env var, then the request's Host header (loopback
- * hosts only), then http://localhost:PORT. Host / X-Forwarded-* headers naming
- * non-loopback hosts are deliberately NOT trusted — a client could spoof them
- * to make OAuth metadata and callback URLs point at a host it controls.
- * Non-localhost deployments (e.g. behind a public tunnel) must set PUBLIC_URL
- * explicitly.
- */
-function resolvePublicUrl(req?: Request): URL {
- // Mount path when this app is mounted inside another Express app
- // (e.g. app.use("/lazy-auth", createApp())). Empty when standalone.
- // PUBLIC_URL, when set, must already include any mount path.
- const basePath = req?.baseUrl ?? "";
- const envUrl = process.env.PUBLIC_URL;
- if (envUrl) return new URL(envUrl.endsWith("/") ? envUrl : envUrl + "/");
- const host = req?.headers.host;
- if (host) {
- try {
- const url = new URL(`http://${host}${basePath}/`);
- if (isLoopbackHostname(url.hostname)) return url;
- } catch {
- // Malformed Host header → fall through to the localhost default.
- }
- }
- return new URL(`http://localhost:${PORT}${basePath}/`);
-}
-
-/** Public base URL as a string with no trailing slash (may include a base path). */
-function publicBaseHref(req?: Request): string {
- const href = resolvePublicUrl(req).href;
- return href.endsWith("/") ? href.slice(0, -1) : href;
-}
-
-/** OAuth issuer. In REACTIVE_AUTH_ONLY mode, uses a /auth subpath so root well-known 404s.
- * Otherwise uses the public base URL (origin + any mount path). */
-const ISSUER_SUFFIX = REACTIVE_AUTH_ONLY ? "/auth" : "";
-function resolveIssuer(req?: Request): string {
- return publicBaseHref(req) + ISSUER_SUFFIX;
-}
-
-// ─── Mock OAuth (HS256, stateless codes) ─────────────────────────────────────
-
-interface CodePayload {
- client_id: string;
- redirect_uri: string;
- code_challenge?: string;
- code_challenge_method?: string;
- scope?: string;
- /** Requested access-token TTL, from `?token_ttl_s=` on the resource indicator. */
- token_ttl_s?: number;
-}
-
-/** Validate + cap a raw TTL value (from a path segment or query param). */
-function parseTtlSeconds(raw: string | undefined): number | undefined {
- const ttl = parseInt(raw ?? "", 10);
- return Number.isFinite(ttl) && ttl > 0
- ? Math.min(ttl, MAX_TOKEN_TTL_SECONDS)
- : undefined;
-}
-
-/**
- * Extract the requested access-token TTL from a resource indicator (RFC 8707).
- *
- * MCP hosts send `resource=` — the URL the client connected
- * to — in authorization and token requests. Clients opt into a non-default
- * token lifetime by connecting to a TTL-scoped MCP endpoint path:
- *
- * /ttl//mcp (e.g. /ttl/3600/mcp for 1-hour tokens)
- *
- * The TTL lives in the PATH because hosts canonicalize resource indicators and
- * strip query strings (verified against real hosts), so `?token_ttl_s=` query
- * params would never reach this server's OAuth endpoints. The query param is
- * still honored as a fallback for clients that preserve it.
- */
-function ttlFromResource(resource: string | undefined): number | undefined {
- if (!resource) return undefined;
- try {
- const url = new URL(resource);
- const pathMatch = url.pathname.match(/\/ttl\/(\d+)\/mcp$/);
- return (
- parseTtlSeconds(pathMatch?.[1]) ??
- parseTtlSeconds(url.searchParams.get("token_ttl_s") ?? undefined)
- );
- } catch {
- return undefined;
- }
-}
-
-interface AuthInfo {
- token: string;
- sub: string;
- sid: string;
-}
-
-// ─── Session revocation ───────────────────────────────────────────────────────
-//
-// Each OAuth session (authorize → code → tokens) gets a single `sid`; all access &
-// refresh tokens in that session carry it. Revoking the sid invalidates both → host
-// must do a full re-OAuth (refresh grant also fails).
-//
-// Storage is an in-memory Map, which is fine for a single-process demo server. If
-// you deploy this to a multi-instance/serverless environment, back it with a
-// shared store instead.
-
-const revokedSids = new Map(); // sid → unix GC time
-// Revoked-sid entries need only outlive the longest token TTL; add small clock-skew grace.
-const REVOCATION_GC_TTL_SECONDS =
- Math.max(
- ACCESS_TOKEN_TTL_SECONDS,
- REFRESH_TOKEN_TTL_SECONDS,
- MAX_TOKEN_TTL_SECONDS,
- ) + 10;
-
-function revokeSession(sid: string): void {
- revokedSids.set(
- sid,
- Math.floor(Date.now() / 1000) + REVOCATION_GC_TTL_SECONDS,
- );
- const now = Math.floor(Date.now() / 1000);
- for (const [k, gc] of revokedSids) if (gc < now) revokedSids.delete(k);
-}
-
-function isSessionRevoked(sid: string | undefined): boolean {
- if (!sid) return false;
- return revokedSids.has(sid);
-}
-
-async function signAccessToken(
- sub: string,
- scope: string,
- sid: string,
- issuer: string,
- audience: string,
- ttlSeconds: number = ACCESS_TOKEN_TTL_SECONDS,
-): Promise {
- return new SignJWT({ sub, scope, sid })
- .setProtectedHeader({ alg: "HS256" })
- .setIssuedAt()
- .setIssuer(issuer)
- .setAudience(audience)
- .setExpirationTime(`${ttlSeconds}s`)
- .sign(JWT_SECRET);
-}
-
-async function signRefreshToken(
- sub: string,
- scope: string,
- sid: string,
- issuer: string,
- ttlSeconds: number = REFRESH_TOKEN_TTL_SECONDS,
- accessTtlSeconds?: number,
-): Promise {
- // access_ttl_s carries the grant's requested access-token TTL forward so
- // refreshed access tokens keep the same lifetime as the original.
- return new SignJWT({
- sub,
- scope,
- sid,
- typ: "refresh",
- ...(accessTtlSeconds !== undefined
- ? { access_ttl_s: accessTtlSeconds }
- : {}),
- })
- .setProtectedHeader({ alg: "HS256" })
- .setIssuedAt()
- .setIssuer(issuer)
- .setExpirationTime(`${ttlSeconds}s`)
- .sign(JWT_SECRET);
-}
-
-async function verifyRefreshToken(
- token: string,
- issuer: string,
-): Promise<
- | { sub: string; scope: string; sid: string; accessTtlSeconds?: number }
- | undefined
-> {
- try {
- const { payload } = await jwtVerify(token, JWT_SECRET, { issuer });
- if (payload.typ !== "refresh") return undefined;
- const sid = payload.sid as string | undefined;
- if (isSessionRevoked(sid)) return undefined; // session revoked → refresh fails
- return {
- sub: payload.sub ?? "",
- scope: (payload.scope as string) ?? "",
- sid: sid ?? "",
- accessTtlSeconds:
- typeof payload.access_ttl_s === "number"
- ? payload.access_ttl_s
- : undefined,
- };
- } catch {
- return undefined;
- }
-}
-
-/** Encode grant details inside the code itself (5min expiry) — no server storage. */
-async function signAuthCode(
- payload: CodePayload,
- issuer: string,
-): Promise {
- return new SignJWT({ ...payload, typ: "code" })
- .setProtectedHeader({ alg: "HS256" })
- .setIssuedAt()
- .setJti(crypto.randomUUID())
- .setIssuer(issuer)
- .setExpirationTime("5m")
- .sign(JWT_SECRET);
-}
-
-// Authorization codes are single-use (RFC 6749 §4.1.2): remember redeemed code
-// IDs until the code's own 5-minute expiry makes replay impossible anyway.
-const redeemedCodeJtis = new Map(); // jti → unix GC time
-
-async function verifyAuthCode(
- code: string,
- issuer: string,
-): Promise {
- try {
- const { payload } = await jwtVerify(code, JWT_SECRET, { issuer });
- if (payload.typ !== "code" || !payload.jti) return undefined;
- const now = Math.floor(Date.now() / 1000);
- for (const [k, gc] of redeemedCodeJtis)
- if (gc < now) redeemedCodeJtis.delete(k);
- if (redeemedCodeJtis.has(payload.jti)) return undefined; // already redeemed
- redeemedCodeJtis.set(payload.jti, now + 5 * 60 + 10);
- return payload as unknown as CodePayload;
- } catch {
- return undefined;
- }
-}
-
-async function verifyAccessToken(
- token: string,
- issuer: string,
- maxAgeSeconds?: number,
-): Promise {
- try {
- const { payload } = await jwtVerify(token, JWT_SECRET, { issuer });
- if (payload.typ === "code" || payload.typ === "refresh") return undefined; // only plain access tokens
- const sid = payload.sid as string | undefined;
- if (isSessionRevoked(sid)) return undefined; // session revoked
- // Per-connection TTL override (?token_ttl_s= on the MCP URL): treat tokens
- // older than maxAgeSeconds as expired even though their exp claim is still
- // in the future. Lets one deployment serve both long-TTL (default) and
- // short-TTL (refresh-flow testing) connections.
- if (maxAgeSeconds !== undefined && typeof payload.iat === "number") {
- const ageSeconds = Math.floor(Date.now() / 1000) - payload.iat;
- if (ageSeconds > maxAgeSeconds) return undefined;
- }
- return { token, sub: payload.sub ?? "", sid: sid ?? "" };
- } catch {
- return undefined;
- }
-}
-
-function escapeHtml(s: string) {
- return s.replace(
- /[&<>"']/g,
- (c) =>
- ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[
- c
- ]!,
- );
-}
-
-/**
- * This demo AS has no client registry, so it can't check redirect URIs against
- * pre-registered values. At minimum, only allow https targets (or http on
- * loopback for local development hosts) so the consent page never links to,
- * or redirects the authorization code to, an unexpected scheme.
- */
-function isAllowedRedirectUri(redirectUri: string): boolean {
- try {
- const url = new URL(redirectUri);
- if (url.protocol === "https:") return true;
- return url.protocol === "http:" && isLoopbackHostname(url.hostname);
- } catch {
- return false;
- }
-}
-
-async function handleAuthorize(req: Request, res: Response) {
- const {
- client_id,
- redirect_uri,
- state,
- code_challenge,
- code_challenge_method,
- scope,
- approved,
- resource,
- } = req.query as Record;
- if (!redirect_uri) {
- res.status(400).json({
- error: "invalid_request",
- error_description: "Missing redirect_uri",
- });
- return;
- }
- if (!isAllowedRedirectUri(redirect_uri)) {
- res.status(400).json({
- error: "invalid_request",
- error_description: "redirect_uri must use https (or http on localhost)",
- });
- return;
- }
- // PKCE is mandatory (the MCP auth spec requires it of clients, and this AS
- // only advertises S256). Rejecting up front keeps stolen-code attacks out of
- // the demo even though it has no real data to protect.
- if (!code_challenge || code_challenge_method !== "S256") {
- res.status(400).json({
- error: "invalid_request",
- error_description: "PKCE with S256 code_challenge is required",
- });
- return;
- }
- const issuer = resolveIssuer(req);
-
- if (approved !== "1") {
- // Show consent page. Keeps the OAuth popup visible so users can see the flow.
- const approveUrl = new URL(publicBaseHref(req) + "/authorize");
- for (const [k, v] of Object.entries(req.query))
- if (v) approveUrl.searchParams.set(k, String(v));
- approveUrl.searchParams.set("approved", "1");
- const denyUrl = new URL(redirect_uri);
- denyUrl.searchParams.set("error", "access_denied");
- if (state) denyUrl.searchParams.set("state", state);
- res.type("text/html").send(/*html*/ `
-Authorize
-
-
-
-
🔑 Mock Authorization
-
An application is requesting access:
-
Client ${escapeHtml(client_id ?? "(none)")}
-Scope ${escapeHtml(scope ?? "(default)")}
-Redirect ${escapeHtml(redirect_uri)}
-
-
-
-`);
- return;
- }
-
- // Per-grant token TTL, requested via `?token_ttl_s=` on the resource
- // indicator (the MCP URL the host is authorizing for). Encoded into the
- // auth code so /token can issue tokens with the requested lifetime.
- const tokenTtl = ttlFromResource(resource);
- const code = await signAuthCode(
- {
- client_id,
- redirect_uri,
- code_challenge,
- code_challenge_method,
- scope,
- ...(tokenTtl !== undefined ? { token_ttl_s: tokenTtl } : {}),
- },
- issuer,
- );
- const redirectUrl = new URL(redirect_uri);
- redirectUrl.searchParams.set("code", code);
- if (state) redirectUrl.searchParams.set("state", state);
- console.log(
- `[auth] issued code for client_id=${client_id}` +
- (tokenTtl !== undefined ? ` (token_ttl_s=${tokenTtl})` : ""),
- );
- res.redirect(redirectUrl.href);
-}
-
-async function handleToken(req: Request, res: Response) {
- const { grant_type, code, code_verifier, refresh_token, resource } = req.body;
- const issuer = resolveIssuer(req);
- const audience = resolvePublicUrl(req).href;
-
- if (grant_type === "refresh_token") {
- const refreshClaims = await verifyRefreshToken(refresh_token, issuer);
- if (!refreshClaims) {
- console.log(`[auth] refresh rejected (invalid/revoked)`);
- res.status(400).json({
- error: "invalid_grant",
- error_description: "Refresh token invalid or revoked",
- });
- return;
- }
- // Refreshed access tokens keep the grant's requested TTL: prefer the
- // resource indicator on this request, else the TTL carried in the refresh
- // token, else the default.
- const accessTtl =
- ttlFromResource(resource) ??
- refreshClaims.accessTtlSeconds ??
- ACCESS_TOKEN_TTL_SECONDS;
- const access_token = await signAccessToken(
- refreshClaims.sub,
- refreshClaims.scope,
- refreshClaims.sid,
- issuer,
- audience,
- accessTtl,
- );
- console.log(
- `[auth] refreshed access token (sid=${refreshClaims.sid}, ttl=${accessTtl}s)`,
- );
- res.json({
- access_token,
- token_type: "Bearer",
- expires_in: accessTtl,
- scope: refreshClaims.scope,
- refresh_token,
- });
- return;
- }
- if (grant_type !== "authorization_code") {
- res.status(400).json({ error: "unsupported_grant_type" });
- return;
- }
- const stored = await verifyAuthCode(code, issuer);
- if (!stored) {
- res.status(400).json({
- error: "invalid_grant",
- error_description: "Invalid or expired code",
- });
- return;
- }
- // PKCE verification (challenges are always present — /authorize requires them).
- if (!code_verifier) {
- res.status(400).json({
- error: "invalid_grant",
- error_description: "Missing code_verifier",
- });
- return;
- }
- const hash = crypto
- .createHash("sha256")
- .update(code_verifier)
- .digest("base64url");
- if (hash !== stored.code_challenge) {
- res.status(400).json({
- error: "invalid_grant",
- error_description: "PKCE verification failed",
- });
- return;
- }
- // RFC 6749 §4.1.3 redirect_uri binding: if the client includes redirect_uri
- // in the token request it must match the one from the authorization request.
- // (OAuth 2.1 clients may omit it and rely on PKCE, which is enforced above.)
- if (req.body.redirect_uri && req.body.redirect_uri !== stored.redirect_uri) {
- res.status(400).json({
- error: "invalid_grant",
- error_description: "redirect_uri does not match authorization request",
- });
- return;
- }
- const scope = stored.scope ?? "read:secret";
- const sid = crypto.randomBytes(16).toString("hex"); // new session per authorization_code grant
- // Per-grant TTL: prefer the resource indicator on this request (RFC 8707),
- // else the TTL encoded in the auth code at /authorize, else the default.
- // Refresh tokens must outlive the access tokens they can mint.
- const accessTtl =
- ttlFromResource(resource) ?? stored.token_ttl_s ?? ACCESS_TOKEN_TTL_SECONDS;
- const refreshTtl = Math.max(REFRESH_TOKEN_TTL_SECONDS, accessTtl);
- const access_token = await signAccessToken(
- "mock-user-123",
- scope,
- sid,
- issuer,
- audience,
- accessTtl,
- );
- const refresh = await signRefreshToken(
- "mock-user-123",
- scope,
- sid,
- issuer,
- refreshTtl,
- accessTtl,
- );
- console.log(
- `[auth] exchanged code → token (client_id=${stored.client_id}, scope=${scope}, sid=${sid}, access_ttl=${accessTtl}s, refresh_ttl=${refreshTtl}s)`,
- );
- res.json({
- access_token,
- token_type: "Bearer",
- expires_in: accessTtl,
- scope,
- refresh_token: refresh,
- });
-}
-
-// ─── Elicitation state (module-level — survives across SHTTP requests) ──────
-//
-// Streamable HTTP creates a fresh McpServer per request, so state must live at
-// module scope. This is fine for a single-process demo server; back it with a
-// shared store if you deploy multiple instances.
-
-/** elicitationIds the user has completed via /elicitation-callback */
-const completedElicitations = new Set();
-/** elicit_by_error: remember the last-issued eid so retries can check it */
-let lastIssuedElicitationId: string | undefined;
-
-// ─── MCP Server ──────────────────────────────────────────────────────────────
-
-/**
- * Creates a new MCP server instance with tools and resources registered.
- *
- * @param authInfo - Verified Bearer token info, if the request carried one.
- * @param req - The inbound HTTP request (used to resolve the public URL).
- */
-export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
- const server = new McpServer({ name: "Lazy Auth Demo", version: "1.0.0" });
-
- const buttonUri = "ui://lazy-auth/mcp-app.html";
- registerAppTool(
- server,
- "show_auth_button",
- {
- title: "Show Auth Button",
- description:
- "Public tool: shows a button that triggers the protected get_secret tool.",
- inputSchema: z.object({}),
- _meta: { ui: { resourceUri: buttonUri } },
- },
- async (): Promise => ({
- content: [{ type: "text", text: "ok" }],
- }),
- );
- registerAppResource(
- server,
- buttonUri,
- buttonUri,
- { mimeType: RESOURCE_MIME_TYPE },
- async (): Promise => {
- const html = await fs.readFile(
- path.join(DIST_DIR, "mcp-app.html"),
- "utf-8",
- );
- return {
- contents: [
- { uri: buttonUri, mimeType: RESOURCE_MIME_TYPE, text: html },
- ],
- };
- },
- );
-
- const secretUri = "ui://lazy-auth/secret-app.html";
- registerAppTool(
- server,
- "get_secret",
- {
- title: "Get Secret",
- description:
- "Protected tool: returns secret data. Requires authentication.",
- inputSchema: z.object({}),
- _meta: { ui: { resourceUri: secretUri } },
- },
- async (): Promise => {
- if (!authInfo) {
- return {
- isError: true,
- content: [{ type: "text", text: "Authentication required." }],
- };
- }
- const secret = {
- subject: authInfo.sub,
- secret: "the-answer-is-42",
- issuedAt: new Date().toISOString(),
- };
- return {
- content: [{ type: "text", text: JSON.stringify(secret, null, 2) }],
- };
- },
- );
- registerAppResource(
- server,
- secretUri,
- secretUri,
- { mimeType: RESOURCE_MIME_TYPE },
- async (): Promise => {
- const html = await fs.readFile(
- path.join(DIST_DIR, "secret-app.html"),
- "utf-8",
- );
- return {
- contents: [
- { uri: secretUri, mimeType: RESOURCE_MIME_TYPE, text: html },
- ],
- };
- },
- );
-
- server.registerTool(
- "revoke_auth_token",
- {
- title: "Revoke Auth Token",
- description:
- "Protected tool: revokes the caller's entire auth session (access + refresh token).",
- inputSchema: z.object({}),
- },
- async (): Promise => {
- if (!authInfo) {
- return {
- isError: true,
- content: [{ type: "text", text: "Authentication required." }],
- };
- }
- revokeSession(authInfo.sid);
- console.log(
- `[auth] revoked session sid=${authInfo.sid} (total revoked sessions: ${revokedSids.size})`,
- );
- return {
- content: [
- {
- type: "text",
- text: `Session revoked (sid: ${authInfo.sid}). Access + refresh token invalid → full re-auth required.`,
- },
- ],
- };
- },
- );
-
- // ── Elicitation test tools ────────────────────────────────────────────────
- // Public tools — no auth. Used to exercise a host's URL-elicitation flow
- // end-to-end over Streamable HTTP.
-
- const base = publicBaseHref(req);
- const callbackUrl = (eid: string) =>
- `${base}/elicitation-callback?id=${encodeURIComponent(eid)}`;
-
- server.registerTool(
- "elicit_url",
- {
- title: "Elicit URL (session-callback)",
- description:
- "URL elicitation via elicitInput. Blocks until the client accepts; sends ElicitCompleteNotification before returning.",
- inputSchema: z.object({}),
- },
- async (_args, extra): Promise => {
- const eid = crypto.randomUUID();
- // Defer notifier creation — checks client capabilities which throws if
- // client doesn't advertise elicitation. Create lazily after elicitInput
- // succeeds (implies client supports it).
- console.log(`[elicit_url] eid=${eid} — blocking on elicitInput`);
- const result = await server.server.elicitInput(
- {
- mode: "url",
- url: callbackUrl(eid),
- message: "Please open this URL to continue.",
- elicitationId: eid,
- },
- { relatedRequestId: extra.mcpReq.id, timeout: 300_000 },
- );
- // Send completion notification before returning — lands on the same SSE
- // stream as this tool response. For Streamable HTTP this is the ONLY
- // window where the server can push a notification (stream closes right
- // after). The notification is optional per spec — skip if the client
- // doesn't support it.
- try {
- const notifier = server.server.createElicitationCompletionNotifier(eid);
- await notifier();
- console.log(
- `[elicit_url] eid=${eid} — notified, result=${result.action}`,
- );
- } catch (e) {
- console.log(
- `[elicit_url] eid=${eid} — notify skipped: ${e instanceof Error ? e.message : e}, result=${result.action}`,
- );
- }
- return {
- content: [
- { type: "text", text: `elicit_url: ${JSON.stringify(result)}` },
- ],
- };
- },
- );
-
- server.registerTool(
- "elicit_by_error",
- {
- title: "Elicit by -32042 Error",
- description:
- "Throws UrlElicitationRequiredError (-32042). Client must open the URL, then retry. On retry, if the last-issued elicitationId was completed, sends ElicitCompleteNotification and succeeds.",
- inputSchema: z.object({}),
- },
- async (): Promise => {
- // Retry path: check if the user completed the elicitation we last issued.
- // Module-level state survives across Streamable HTTP requests in a single
- // process. Checking ANY completed eid (not just last-issued) would be more
- // forgiving, but last-issued is the stricter real-world pattern hosts must
- // handle.
- if (
- lastIssuedElicitationId &&
- completedElicitations.has(lastIssuedElicitationId)
- ) {
- const eid = lastIssuedElicitationId;
- completedElicitations.delete(eid);
- lastIssuedElicitationId = undefined;
- // Send notification DURING this (retry) tool call — only window where
- // the Streamable HTTP stream is open. The notification is optional per
- // spec — createElicitationCompletionNotifier throws if the client
- // doesn't advertise the capability, but the tool still succeeds.
- try {
- const notifier =
- server.server.createElicitationCompletionNotifier(eid);
- await notifier();
- console.log(
- `[elicit_by_error] eid=${eid} — completed on retry, notified`,
- );
- } catch (e) {
- console.log(
- `[elicit_by_error] eid=${eid} — completed on retry, notify skipped: ${e instanceof Error ? e.message : e}`,
- );
- }
- return {
- content: [
- {
- type: "text",
- text: `Auth completed (${eid}). Tool succeeded on retry.`,
- },
- ],
- };
- }
- const eid = crypto.randomUUID();
- lastIssuedElicitationId = eid;
- console.log(`[elicit_by_error] eid=${eid} — throwing -32042`);
- throw new UrlElicitationRequiredError([
- {
- mode: "url",
- url: callbackUrl(eid),
- message:
- "This tool requires authentication. Please open the link to continue.",
- elicitationId: eid,
- },
- ]);
- },
- );
-
- return server;
-}
-
-// ─── Express app ─────────────────────────────────────────────────────────────
-
-/**
- * Creates the Express app: OAuth endpoints, discovery metadata, and the /mcp
- * endpoint with lazy (401-triggered) auth on protected tools.
- */
-export function createApp(): Express {
- const app = express();
- // Wildcard CORS is deliberate: browser-based MCP hosts connect to this demo
- // from arbitrary origins and must read WWW-Authenticate to drive the lazy
- // auth flow. There are no cookies or ambient credentials to protect — all
- // auth is an explicit Bearer header, and /token is a credential-less PKCE
- // exchange — so cross-origin reads expose nothing a direct request wouldn't.
- app.use(cors({ exposedHeaders: ["WWW-Authenticate"] }));
- app.use(express.json());
- app.use(express.urlencoded({ extended: true }));
-
- // Request tracing: log every inbound request (incl. query params) so we can
- // see what hosts probe during connect and what they send through OAuth.
- app.use((req, _res, next) => {
- const ua = (req.headers["user-agent"] ?? "").slice(0, 80);
- const auth = req.headers.authorization ? "[Bearer]" : "[no-auth]";
- const query = Object.keys(req.query).length
- ? ` query=${JSON.stringify(req.query)}`
- : "";
- console.log(`[req] ${req.method} ${req.path} ${auth}${query} ua=${ua}`);
- next();
- });
-
- // OAuth endpoints
- app.get("/authorize", handleAuthorize);
- app.post("/token", handleToken);
-
- // OAuth discovery metadata. In REACTIVE_AUTH_ONLY mode, PRM + AS are kept off the
- // ROOT /.well-known paths so MCP hosts can't proactively probe them and trigger
- // preemptive auth during connection — discovery is purely reactive:
- // 401 → WWW-Authenticate → PRM → AS → OAuth flow.
- const PRM_PATH = "/auth/prm";
-
- function buildAsMetadata(req: Request) {
- const base = publicBaseHref(req);
- return {
- issuer: resolveIssuer(req), // subpath issuer → well-known at /.well-known/.../auth
- authorization_endpoint: `${base}/authorize`,
- token_endpoint: `${base}/token`,
- response_types_supported: ["code"],
- grant_types_supported: ["authorization_code", "refresh_token"],
- code_challenge_methods_supported: ["S256"],
- token_endpoint_auth_methods_supported: ["none"],
- scopes_supported: ["read:secret"],
- client_id_metadata_document_supported: true,
- };
- }
-
- // AS metadata: serve at the issuer's well-known location.
- // - REACTIVE_AUTH_ONLY: only at /.well-known/.../auth (subpath); root 404s.
- // - Default: also at root /.well-known/... (standard; some hosts probe this on connect).
- if (ISSUER_SUFFIX) {
- // Per RFC 8414 §3: issuer /auth → /.well-known/oauth-authorization-server/auth
- app.get(
- `/.well-known/oauth-authorization-server${ISSUER_SUFFIX}`,
- (req, res) => res.json(buildAsMetadata(req)),
- );
- // RFC 8615 style (well-known between host and path):
- app.get(
- `${ISSUER_SUFFIX}/.well-known/oauth-authorization-server`,
- (req, res) => res.json(buildAsMetadata(req)),
- );
- }
- if (!REACTIVE_AUTH_ONLY) {
- // Default mode: full AS metadata at root well-known (standard).
- app.get("/.well-known/oauth-authorization-server", (req, res) =>
- res.json(buildAsMetadata(req)),
- );
- }
-
- // PRM: full version at custom path (referenced via WWW-Authenticate on 401).
- function buildPrm(req: Request, includeAuth: boolean, resourcePath = "/mcp") {
- const base = publicBaseHref(req);
- return {
- resource: `${base}${resourcePath}`,
- ...(includeAuth
- ? {
- authorization_servers: [resolveIssuer(req)],
- scopes_supported: ["read:secret"],
- bearer_methods_supported: ["header"],
- }
- : {}),
- };
- }
- /** Resource path for a TTL-scoped MCP endpoint, validating the TTL segment. */
- function ttlResourcePath(rawTtl: string): string | undefined {
- const ttl = parseTtlSeconds(rawTtl);
- return ttl !== undefined ? `/ttl/${ttl}/mcp` : undefined;
- }
- app.get(PRM_PATH, (req, res) => res.json(buildPrm(req, true)));
- app.get(`${PRM_PATH}/ttl/:ttl`, (req, res) => {
- const resourcePath = ttlResourcePath(String(req.params.ttl));
- if (!resourcePath) {
- res.status(404).json({ error: "invalid_ttl" });
- return;
- }
- res.json(buildPrm(req, true, resourcePath));
- });
- // Root + path-dependent well-known PRM: in REACTIVE mode these 404 so hosts don't
- // interpret their presence as "server has OAuth". In default mode, full PRM.
- if (!REACTIVE_AUTH_ONLY) {
- app.get("/.well-known/oauth-protected-resource", (req, res) =>
- res.json(buildPrm(req, true)),
- );
- app.get("/.well-known/oauth-protected-resource/mcp", (req, res) =>
- res.json(buildPrm(req, true)),
- );
- // Well-known PRM for TTL-scoped MCP endpoints (RFC 9728 derives this URL
- // from the resource path, so each TTL path has its own).
- app.get(
- "/.well-known/oauth-protected-resource/ttl/:ttl/mcp",
- (req, res) => {
- const resourcePath = ttlResourcePath(String(req.params.ttl));
- if (!resourcePath) {
- res.status(404).json({ error: "invalid_ttl" });
- return;
- }
- res.json(buildPrm(req, true, resourcePath));
- },
- );
- }
-
- // /register: some MCP hosts fall through to OAuth dynamic client registration (RFC 7591)
- // when all well-known probes 404. We don't support it — return a proper OAuth error so
- // the host concludes "auth not available here" and connects without OAuth, instead of
- // treating a generic 404 as a connection failure.
- app.post("/register", (_req, res) => {
- res.status(400).json({
- error: "invalid_request",
- error_description:
- "Dynamic client registration is not supported. This server uses per-tool auth triggered via WWW-Authenticate on 401.",
- });
- });
-
- // MCP endpoint handler, shared by the default and TTL-scoped paths.
- // pathTtl (from /ttl//mcp) doubles as the acceptance window for
- // Bearer tokens: tokens older than it are rejected even if their issued
- // lifetime was longer.
- async function handleMcp(
- req: Request,
- res: Response,
- pathTtl: number | undefined,
- ) {
- const base = publicBaseHref(req);
- const resourceMetadataUrl =
- pathTtl !== undefined
- ? `${base}${PRM_PATH}/ttl/${pathTtl}`
- : `${base}${PRM_PATH}`;
-
- const body = req.body;
- const messages = Array.isArray(body) ? body : body ? [body] : [];
- const needsAuth = messages.some(
- (msg: { method?: string; params?: { name?: string } }) =>
- msg?.method === "tools/call" &&
- PROTECTED_TOOLS.has(msg.params?.name ?? ""),
- );
-
- // Acceptance window: the path TTL, else the `?token_ttl_s=` query param
- // (fallback for clients that preserve query strings).
- const tokenMaxAgeSeconds =
- pathTtl ?? parseTtlSeconds(String(req.query.token_ttl_s ?? ""));
-
- let authInfo: AuthInfo | undefined;
- const authHeader = req.headers.authorization;
- if (authHeader?.startsWith("Bearer ")) {
- authInfo = await verifyAccessToken(
- authHeader.slice(7),
- resolveIssuer(req),
- tokenMaxAgeSeconds,
- );
- if (!authInfo && needsAuth) {
- console.log(`[mcp] 401: invalid token for protected tool`);
- res
- .status(401)
- .set(
- "WWW-Authenticate",
- `Bearer error="invalid_token", error_description="The access token is invalid", resource_metadata="${resourceMetadataUrl}"`,
- )
- .json({
- error: "invalid_token",
- error_description: "The access token is invalid",
- });
- return;
- }
- } else if (needsAuth) {
- console.log(`[mcp] 401: no token for protected tool`);
- res
- .status(401)
- .set(
- "WWW-Authenticate",
- `Bearer resource_metadata="${resourceMetadataUrl}"`,
- )
- .json({
- error: "invalid_token",
- error_description: "Authorization required",
- });
- return;
- }
-
- const server = createServer(authInfo, req);
- const transport = new NodeStreamableHTTPServerTransport({
- sessionIdGenerator: undefined,
- });
- res.on("close", () => {
- transport.close().catch(() => {});
- server.close().catch(() => {});
- });
-
- try {
- await server.connect(transport);
- await transport.handleRequest(req, res, req.body);
- } catch (error) {
- console.error("[mcp] Error:", error);
- if (!res.headersSent) {
- res.status(500).json({
- jsonrpc: "2.0",
- error: { code: -32603, message: "Internal server error" },
- id: null,
- });
- }
- }
- }
-
- // Default MCP endpoint (default token TTLs).
- app.all("/mcp", (req: Request, res: Response) =>
- handleMcp(req, res, undefined),
- );
- // TTL-scoped MCP endpoint: tokens for connections made through this path are
- // issued with (and limited to) the requested lifetime, e.g. /ttl/3600/mcp.
- app.all("/ttl/:ttl/mcp", (req: Request, res: Response) => {
- const pathTtl = parseTtlSeconds(String(req.params.ttl));
- if (pathTtl === undefined) {
- res.status(404).json({ error: "invalid_ttl" });
- return;
- }
- return handleMcp(req, res, pathTtl);
- });
-
- // Elicitation callback — marks an eid as completed. Module-level state;
- // the next elicit_by_error retry reads it. Minimal confirmation page so
- // the popup shows something.
- app.get("/elicitation-callback", (req, res) => {
- const id = String(req.query.id ?? "");
- if (!id) {
- res.status(400).type("text/plain").send("missing ?id=");
- return;
- }
- completedElicitations.add(id);
- console.log(
- `[callback] eid=${id} completed (total: ${completedElicitations.size})`,
- );
- res
- .type("text/html")
- .send(
- `Done ` +
- `` +
- `✓ Elicitation completed ` +
- `${escapeHtml(id)}
` +
- `You can close this tab. The tool will succeed on the next retry.
`,
- );
- });
-
- // Simple landing page
- app.get("/", (req, res) => {
- const base = publicBaseHref(req);
- res
- .type("text/plain")
- .send(
- `Lazy Auth Demo — MCP server\n\n` +
- ` MCP endpoint: ${base}/mcp\n` +
- ` ${base}/ttl//mcp (custom token TTL, e.g. /ttl/3600/mcp)\n` +
- ` AS metadata: ${base}/.well-known/oauth-authorization-server${ISSUER_SUFFIX}\n` +
- ` PRM metadata: ${base}${PRM_PATH}\n\n` +
- `Tools:\n` +
- ` - show_auth_button (public)\n` +
- ` - get_secret (protected, requires Bearer token)\n` +
- ` - revoke_auth_token (protected, revokes caller's auth session)\n` +
- ` - elicit_url (public, URL elicitation via elicitInput)\n` +
- ` - elicit_by_error (public, URL elicitation via -32042 error)\n`,
- );
- });
-
- return app;
-}
diff --git a/examples/lazy-auth-server/src/global.css b/examples/lazy-auth-server/src/global.css
deleted file mode 100644
index 54a9860..0000000
--- a/examples/lazy-auth-server/src/global.css
+++ /dev/null
@@ -1,100 +0,0 @@
-:root {
- color-scheme: light dark;
-
- /*
- * Fallbacks for host style variables used by this app.
- * The host may provide these (and many more) via the host context.
- */
- --color-text-primary: light-dark(#1f2937, #f3f4f6);
- --color-text-inverse: light-dark(#f3f4f6, #1f2937);
- --color-text-info: light-dark(#1d4ed8, #60a5fa);
- --color-background-primary: light-dark(#ffffff, #1a1a1a);
- --color-background-inverse: light-dark(#1a1a1a, #ffffff);
- --color-background-info: light-dark(#eff6ff, #1e3a5f);
- --color-ring-primary: light-dark(#3b82f6, #60a5fa);
- --border-radius-md: 6px;
- --border-width-regular: 1px;
- --font-sans:
- ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji",
- "Segoe UI Symbol", "Noto Color Emoji";
- --font-mono:
- ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
- "Courier New", monospace;
- --font-weight-normal: 400;
- --font-weight-bold: 700;
- --font-text-md-size: 1rem;
- --font-text-md-line-height: 1.5;
- --font-heading-3xl-size: 2.25rem;
- --font-heading-3xl-line-height: 1.1;
- --font-heading-2xl-size: 1.875rem;
- --font-heading-2xl-line-height: 1.2;
- --font-heading-xl-size: 1.5rem;
- --font-heading-xl-line-height: 1.25;
- --font-heading-lg-size: 1.25rem;
- --font-heading-lg-line-height: 1.3;
- --font-heading-md-size: 1rem;
- --font-heading-md-line-height: 1.4;
- --font-heading-sm-size: 0.875rem;
- --font-heading-sm-line-height: 1.4;
-
- /* Spacing derived from host typography */
- --spacing-unit: var(--font-text-md-size);
- --spacing-xs: calc(var(--spacing-unit) * 0.25);
- --spacing-sm: calc(var(--spacing-unit) * 0.5);
- --spacing-md: var(--spacing-unit);
- --spacing-lg: calc(var(--spacing-unit) * 1.5);
-
- /* App accent color (customize for your brand) */
- --color-accent: #2563eb;
- --color-text-on-accent: #ffffff;
-}
-
-* {
- box-sizing: border-box;
-}
-
-html,
-body {
- font-family: var(--font-sans);
- font-size: var(--font-text-md-size);
- font-weight: var(--font-weight-normal);
- line-height: var(--font-text-md-line-height);
- color: var(--color-text-primary);
-}
-
-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;
-}
-
-b,
-strong {
- font-weight: var(--font-weight-bold);
-}
diff --git a/examples/lazy-auth-server/src/mcp-app.css b/examples/lazy-auth-server/src/mcp-app.css
deleted file mode 100644
index 800b224..0000000
--- a/examples/lazy-auth-server/src/mcp-app.css
+++ /dev/null
@@ -1,82 +0,0 @@
-.main {
- width: 100%;
- max-width: 480px;
- box-sizing: border-box;
-
- > * {
- margin-top: 0;
- margin-bottom: 0;
- }
-
- > * + * {
- margin-top: var(--spacing-md);
- }
-}
-
-.actions {
- display: flex;
- flex-wrap: wrap;
- gap: var(--spacing-sm);
- /* Keep a long press on or near the buttons from starting text selection
- or raising the iOS callout menu instead of a click. */
- -webkit-user-select: none;
- user-select: none;
- -webkit-touch-callout: none;
-}
-
-button {
- /* Match the consent page's 56px tap targets (server.ts .btn). */
- min-height: 56px;
- min-width: 56px;
- padding: var(--spacing-sm) var(--spacing-md);
- border: none;
- border-radius: var(--border-radius-md);
- color: var(--color-text-on-accent);
- font-weight: var(--font-weight-bold);
- background-color: var(--color-accent);
- cursor: pointer;
- /* Suppress the double-tap-to-zoom gesture so a tap resolves to a click
- immediately instead of waiting to see if a second tap follows. */
- touch-action: manipulation;
- -webkit-tap-highlight-color: transparent;
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
- }
-
- &:active:not(:disabled) {
- filter: brightness(0.85);
- }
-
- &:hover:not(:disabled) {
- background-color: color-mix(
- in srgb,
- var(--color-accent) 85%,
- var(--color-background-inverse)
- );
- }
-
- &:focus-visible {
- outline: calc(var(--border-width-regular) * 2) solid
- var(--color-ring-primary);
- outline-offset: var(--border-width-regular);
- }
-}
-
-.result {
- padding: var(--spacing-sm) var(--spacing-md);
- border: var(--border-width-regular) solid
- color-mix(in srgb, var(--color-text-primary) 25%, transparent);
- border-radius: var(--border-radius-md);
- font-family: var(--font-mono);
- font-size: 0.875em;
- white-space: pre-wrap;
- word-break: break-word;
-}
-
-.error {
- color: var(--color-text-danger, light-dark(#b91c1c, #f87171));
- white-space: pre-wrap;
- word-break: break-word;
-}
diff --git a/examples/lazy-auth-server/src/mcp-app.ts b/examples/lazy-auth-server/src/mcp-app.ts
deleted file mode 100644
index f959d71..0000000
--- a/examples/lazy-auth-server/src/mcp-app.ts
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * @file Public app view: buttons that call the protected tools.
- *
- * The view itself renders without auth. Clicking "Auth me" calls the protected
- * `get_secret` tool via `callServerTool`; the host receives a 401 from the
- * server, runs the OAuth flow, retries the call, and the result renders here.
- */
-import {
- App,
- applyDocumentTheme,
- applyHostFonts,
- applyHostStyleVariables,
- type McpUiHostContext,
-} from "@modelcontextprotocol/ext-apps";
-import "./global.css";
-import "./mcp-app.css";
-
-const mainEl = document.querySelector(".main") as HTMLElement;
-const authBtn = document.getElementById("auth-btn") as HTMLButtonElement;
-const revokeBtn = document.getElementById("revoke-btn") as HTMLButtonElement;
-const fullscreenBtn = document.getElementById(
- "fullscreen-btn",
-) as HTMLButtonElement;
-const outputEl = document.getElementById("output")!;
-
-function handleHostContextChanged(ctx: McpUiHostContext) {
- if (ctx.theme) {
- applyDocumentTheme(ctx.theme);
- }
- if (ctx.styles?.variables) {
- applyHostStyleVariables(ctx.styles.variables);
- }
- 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`;
- }
-}
-
-const app = new App(
- { name: "Lazy Auth Demo", version: "1.0.0" },
- { availableDisplayModes: ["inline", "fullscreen"] },
-);
-
-// The host echoes the result of the originating `show_auth_button` call; nothing to render.
-app.ontoolresult = () => {};
-app.onerror = console.error;
-app.onhostcontextchanged = handleHostContextChanged;
-
-function renderOutput(text: string, isError: boolean) {
- const div = document.createElement("div");
- div.className = isError ? "error" : "result";
- div.textContent = text;
- outputEl.replaceChildren(div);
-}
-
-/**
- * Calls a (protected) server tool. The interesting part happens on the host:
- * the server answers 401 + WWW-Authenticate, the host runs the OAuth flow,
- * then retries the call and returns the result here.
- *
- * The default MCP request timeout is 60s; the OAuth flow involves a human
- * approving a consent page, which can easily take longer. Use a generous
- * timeout so the pending call survives the sign-in round trip.
- */
-const AUTH_FLOW_TIMEOUT_MS = 5 * 60_000;
-
-async function callTool(name: string) {
- outputEl.replaceChildren();
- try {
- console.info(`Calling ${name} tool...`);
- const result = await app.callServerTool(
- { name, arguments: {} },
- { timeout: AUTH_FLOW_TIMEOUT_MS },
- );
- const text =
- result.content?.find((c) => c.type === "text")?.text ?? "(no content)";
- renderOutput(text, result.isError === true);
- } catch (e) {
- renderOutput(String(e instanceof Error ? e.message : e), true);
- }
-}
-
-authBtn.addEventListener("click", async () => {
- authBtn.disabled = true;
- try {
- await callTool("get_secret");
- } finally {
- authBtn.disabled = false;
- }
-});
-
-revokeBtn.addEventListener("click", async () => {
- revokeBtn.disabled = true;
- try {
- await callTool("revoke_auth_token");
- } finally {
- revokeBtn.disabled = false;
- }
-});
-
-let displayMode = "inline";
-fullscreenBtn.addEventListener("click", async () => {
- fullscreenBtn.disabled = true;
- try {
- const next = displayMode === "fullscreen" ? "inline" : "fullscreen";
- const { mode } = await app.requestDisplayMode({ mode: next });
- displayMode = mode;
- } catch (e) {
- console.warn("Display mode not supported:", e);
- } finally {
- fullscreenBtn.disabled = false;
- }
-});
-
-app.connect().then(() => {
- const ctx = app.getHostContext();
- if (ctx) {
- handleHostContextChanged(ctx);
- displayMode = ctx.displayMode ?? "inline";
- }
-});
diff --git a/examples/lazy-auth-server/src/secret-app.ts b/examples/lazy-auth-server/src/secret-app.ts
deleted file mode 100644
index aeb6857..0000000
--- a/examples/lazy-auth-server/src/secret-app.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-/**
- * @file Protected app view: renders the result of the `get_secret` tool.
- *
- * This view is only reached after the host has completed the OAuth flow
- * (the tool call that produces it requires a Bearer token).
- */
-import {
- App,
- applyDocumentTheme,
- applyHostFonts,
- applyHostStyleVariables,
- type McpUiHostContext,
-} from "@modelcontextprotocol/ext-apps";
-import "./global.css";
-import "./mcp-app.css";
-
-const mainEl = document.querySelector(".main") as HTMLElement;
-const outputEl = document.getElementById("output")!;
-
-function handleHostContextChanged(ctx: McpUiHostContext) {
- if (ctx.theme) {
- applyDocumentTheme(ctx.theme);
- }
- if (ctx.styles?.variables) {
- applyHostStyleVariables(ctx.styles.variables);
- }
- 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`;
- }
-}
-
-const app = new App({ name: "Lazy Auth Demo (Secret)", version: "1.0.0" });
-
-app.onerror = console.error;
-app.onhostcontextchanged = handleHostContextChanged;
-
-app.ontoolresult = (result) => {
- const text =
- result.content?.find((c) => c.type === "text")?.text ?? "(no content)";
- const div = document.createElement("div");
- div.className = result.isError ? "error" : "result";
- div.textContent = text;
- outputEl.replaceChildren(div);
-};
-
-app.connect().then(() => {
- const ctx = app.getHostContext();
- if (ctx) {
- handleHostContextChanged(ctx);
- }
-});
diff --git a/examples/lazy-auth-server/tsconfig.json b/examples/lazy-auth-server/tsconfig.json
deleted file mode 100644
index d6a42ce..0000000
--- a/examples/lazy-auth-server/tsconfig.json
+++ /dev/null
@@ -1,20 +0,0 @@
-{
- "compilerOptions": {
- "types": ["node", "vite/client"],
- "target": "ESNext",
- "lib": ["ESNext", "DOM", "DOM.Iterable"],
- "module": "ESNext",
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "resolveJsonModule": true,
- "isolatedModules": true,
- "verbatimModuleSyntax": true,
- "noEmit": true,
- "strict": true,
- "skipLibCheck": true,
- "noUnusedLocals": true,
- "noUnusedParameters": true,
- "noFallthroughCasesInSwitch": true
- },
- "include": ["src", "server.ts", "main.ts"]
-}
diff --git a/examples/lazy-auth-server/tsconfig.server.json b/examples/lazy-auth-server/tsconfig.server.json
deleted file mode 100644
index 27eb28f..0000000
--- a/examples/lazy-auth-server/tsconfig.server.json
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "compilerOptions": {
- "types": ["node"],
- "target": "ES2022",
- "lib": ["ES2022"],
- "module": "NodeNext",
- "moduleResolution": "NodeNext",
- "declaration": true,
- "emitDeclarationOnly": true,
- "outDir": "./dist",
- "rootDir": ".",
- "strict": true,
- "skipLibCheck": true,
- "esModuleInterop": true,
- "resolveJsonModule": true
- },
- "include": ["server.ts"]
-}
diff --git a/examples/lazy-auth-server/vite.config.ts b/examples/lazy-auth-server/vite.config.ts
deleted file mode 100644
index 6ff6d99..0000000
--- a/examples/lazy-auth-server/vite.config.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { defineConfig } from "vite";
-import { viteSingleFile } from "vite-plugin-singlefile";
-
-const INPUT = process.env.INPUT;
-if (!INPUT) {
- throw new Error("INPUT environment variable is not set");
-}
-
-const isDevelopment = process.env.NODE_ENV === "development";
-
-export default defineConfig({
- plugins: [viteSingleFile()],
- build: {
- sourcemap: isDevelopment ? "inline" : undefined,
- cssMinify: !isDevelopment,
- minify: !isDevelopment,
-
- rollupOptions: {
- input: INPUT,
- },
- outDir: "dist",
- emptyOutDir: false,
- },
-});
diff --git a/examples/map-server/README.md b/examples/map-server/README.md
index e1e044a..4c4de5f 100644
--- a/examples/map-server/README.md
+++ b/examples/map-server/README.md
@@ -122,7 +122,7 @@ Vanilla TypeScript app that:
- [`server.ts`](server.ts) - MCP server with geocode and show-map tools
- [`mcp-app.html`](mcp-app.html) / [`src/mcp-app.ts`](src/mcp-app.ts) - CesiumJS globe UI
-- [`main.ts`](main.ts) - HTTP and stdio server utilities
+- [`server-utils.ts`](server-utils.ts) - HTTP server utilities
## Notes
diff --git a/examples/map-server/main.ts b/examples/map-server/main.ts
index 8038394..f91f57b 100644
--- a/examples/map-server/main.ts
+++ b/examples/map-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/map-server/package.json b/examples/map-server/package.json
index cc4585f..a8d727b 100644
--- a/examples/map-server/package.json
+++ b/examples/map-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-map",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "MCP App Server example with CesiumJS 3D globe and geocoding",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/map-server"
},
"license": "MIT",
@@ -26,14 +26,11 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/map-server/server.ts b/examples/map-server/server.ts
index 5767d46..540d35c 100644
--- a/examples/map-server/server.ts
+++ b/examples/map-server/server.ts
@@ -5,11 +5,11 @@
* - geocode: Search for places using OpenStreetMap Nominatim
* - show-map: Display an interactive 3D globe at a given location
*/
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -147,8 +147,8 @@ export function createServer(): McpServer {
{
title: "Show Map",
description:
- "Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location. The widget is interactive and exposes tools for navigation (fly to locations) and querying the current view.",
- inputSchema: z.object({
+ "Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location.",
+ inputSchema: {
west: z
.number()
.optional()
@@ -173,7 +173,7 @@ export function createServer(): McpServer {
.string()
.optional()
.describe("Optional label to display on the map"),
- }),
+ },
_meta: { ui: { resourceUri } },
},
async ({ west, south, east, north, label }): Promise => ({
@@ -196,13 +196,13 @@ export function createServer(): McpServer {
title: "Geocode",
description:
"Search for places using OpenStreetMap. Returns coordinates and bounding boxes for up to 5 matches.",
- inputSchema: z.object({
+ inputSchema: {
query: z
.string()
.describe(
"Place name or address to search for (e.g., 'Paris', 'Golden Gate Bridge', '1600 Pennsylvania Ave')",
),
- }),
+ },
},
async ({ query }): Promise => {
try {
diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts
index d3090c7..4fda132 100644
--- a/examples/map-server/src/mcp-app.ts
+++ b/examples/map-server/src/mcp-app.ts
@@ -6,8 +6,7 @@
* a navigate-to tool for the host to control navigation.
*/
import { App } from "@modelcontextprotocol/ext-apps";
-import type { ContentBlock } from "@modelcontextprotocol/client";
-import { z } from "zod";
+import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js";
// TypeScript declaration for Cesium loaded from CDN
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -560,76 +559,6 @@ function setViewToBoundingBox(cesiumViewer: any, bbox: BoundingBox): void {
);
}
-/**
- * Fly camera to view a bounding box with animation
- */
-function flyToBoundingBox(
- cesiumViewer: any,
- bbox: BoundingBox,
- duration = 2,
-): Promise {
- return new Promise((resolve) => {
- const { destination, centerLon, centerLat, height } =
- calculateDestination(bbox);
-
- log.info("flyTo destination:", centerLon, centerLat, "height:", height);
-
- cesiumViewer.camera.flyTo({
- destination,
- orientation: {
- heading: 0,
- pitch: Cesium.Math.toRadians(-90), // Look straight down
- roll: 0,
- },
- duration,
- complete: () => {
- log.info(
- "flyTo complete, camera height:",
- cesiumViewer.camera.positionCartographic.height,
- );
- resolve();
- },
- cancel: () => {
- log.warn("flyTo cancelled");
- resolve();
- },
- });
- });
-}
-
-// Label element for displaying location info
-let labelElement: HTMLDivElement | null = null;
-
-/**
- * Set or clear the label displayed on the map
- */
-function setLabel(text?: string): void {
- if (!labelElement) {
- labelElement = document.createElement("div");
- labelElement.style.cssText = `
- position: absolute;
- top: 10px;
- left: 10px;
- background: rgba(0, 0, 0, 0.7);
- color: white;
- padding: 8px 12px;
- border-radius: 4px;
- font-family: sans-serif;
- font-size: 14px;
- z-index: 100;
- pointer-events: none;
- `;
- document.body.appendChild(labelElement);
- }
-
- if (text) {
- labelElement.textContent = text;
- labelElement.style.display = "block";
- } else {
- labelElement.style.display = "none";
- }
-}
-
/**
* Wait for globe tiles to finish loading
*/
@@ -872,110 +801,57 @@ app.ontoolinput = async (params) => {
}
};
-// Register tools for the model to interact with this component
-app.registerTool(
- "navigate-to",
- {
- title: "Navigate To",
- description: "Navigate the globe to a new bounding box location",
- inputSchema: z.object({
- west: z.number().describe("Western longitude (-180 to 180)"),
- south: z.number().describe("Southern latitude (-90 to 90)"),
- east: z.number().describe("Eastern longitude (-180 to 180)"),
- north: z.number().describe("Northern latitude (-90 to 90)"),
- duration: z
- .number()
- .optional()
- .describe("Animation duration in seconds (default: 2)"),
- label: z.string().optional().describe("Optional label to display"),
- }),
- },
- async (args) => {
- if (!viewer) {
- return {
- content: [
- { type: "text" as const, text: "Error: Viewer not initialized" },
- ],
- isError: true,
- };
- }
+/*
+ Register tools for the model to interact w/ this component
+ Needs https://github.com/modelcontextprotocol/ext-apps/pull/72
+*/
+// app.registerTool(
+// "navigate-to",
+// {
+// title: "Navigate To",
+// description: "Navigate the globe to a new bounding box location",
+// inputSchema: z.object({
+// west: z.number().describe("Western longitude (-180 to 180)"),
+// south: z.number().describe("Southern latitude (-90 to 90)"),
+// east: z.number().describe("Eastern longitude (-180 to 180)"),
+// north: z.number().describe("Northern latitude (-90 to 90)"),
+// duration: z
+// .number()
+// .optional()
+// .describe("Animation duration in seconds (default: 2)"),
+// label: z.string().optional().describe("Optional label to display"),
+// }),
+// },
+// async (args) => {
+// if (!viewer) {
+// return {
+// content: [
+// { type: "text" as const, text: "Error: Viewer not initialized" },
+// ],
+// isError: true,
+// };
+// }
- const bbox: BoundingBox = {
- west: args.west,
- south: args.south,
- east: args.east,
- north: args.north,
- };
+// const bbox: BoundingBox = {
+// west: args.west,
+// south: args.south,
+// east: args.east,
+// north: args.north,
+// };
- await flyToBoundingBox(viewer, bbox, args.duration ?? 2);
- setLabel(args.label);
+// await flyToBoundingBox(viewer, bbox, args.duration ?? 2);
+// setLabel(args.label);
- return {
- content: [
- {
- type: "text" as const,
- text: `Navigated to: W:${bbox.west.toFixed(4)}, S:${bbox.south.toFixed(4)}, E:${bbox.east.toFixed(4)}, N:${bbox.north.toFixed(4)}${args.label ? ` (${args.label})` : ""}`,
- },
- ],
- };
- },
-);
-
-app.registerTool(
- "get-current-view",
- {
- title: "Get Current View",
- description:
- "Get the current camera position and bounding box visible on the globe",
- },
- async () => {
- if (!viewer) {
- return {
- content: [
- { type: "text" as const, text: "Error: Viewer not initialized" },
- ],
- isError: true,
- };
- }
-
- const camera = viewer.camera;
- const positionCartographic = camera.positionCartographic;
- const latitude = Cesium.Math.toDegrees(positionCartographic.latitude);
- const longitude = Cesium.Math.toDegrees(positionCartographic.longitude);
- const height = positionCartographic.height;
-
- // Get the visible bounding box
- const rectangle = viewer.camera.computeViewRectangle();
- let bbox = null;
- if (rectangle) {
- bbox = {
- west: Cesium.Math.toDegrees(rectangle.west),
- south: Cesium.Math.toDegrees(rectangle.south),
- east: Cesium.Math.toDegrees(rectangle.east),
- north: Cesium.Math.toDegrees(rectangle.north),
- };
- }
-
- const viewData = {
- camera: {
- latitude,
- longitude,
- height,
- },
- bbox,
- };
-
- return {
- content: [
- {
- type: "text" as const,
- text: JSON.stringify(viewData, null, 2),
- },
- ],
- structuredContent: viewData,
- };
- },
-);
+// return {
+// content: [
+// {
+// type: "text" as const,
+// text: `Navigated to: W:${bbox.west.toFixed(4)}, S:${bbox.south.toFixed(4)}, E:${bbox.east.toFixed(4)}, N:${bbox.north.toFixed(4)}${args.label ? ` (${args.label})` : ""}`,
+// },
+// ],
+// };
+// },
+// );
// Handle tool result - extract viewUUID and restore persisted view if available
app.ontoolresult = async (result) => {
diff --git a/examples/map-server/tsconfig.json b/examples/map-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/map-server/tsconfig.json
+++ b/examples/map-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/map-server/tsconfig.server.json b/examples/map-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/map-server/tsconfig.server.json
+++ b/examples/map-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/pdf-server/build-mcpb.mjs b/examples/pdf-server/build-mcpb.mjs
index 2917334..c1c3469 100644
--- a/examples/pdf-server/build-mcpb.mjs
+++ b/examples/pdf-server/build-mcpb.mjs
@@ -43,21 +43,6 @@ writeFileSync(
path.join(stage, "manifest.json"),
JSON.stringify(manifest, null, 2),
);
-// The bundle must ship the ext-apps build from this checkout, not whatever
-// the registry has: pack the repo root and point the staged manifest at it.
-const repoRoot = path.resolve(here, "..", "..");
-const packed = JSON.parse(
- execSync(
- "npm pack --json --ignore-scripts --pack-destination " +
- JSON.stringify(stage),
- {
- cwd: repoRoot,
- encoding: "utf8",
- },
- ).replace(/^[^[]*/, ""),
-);
-pkg.dependencies["@modelcontextprotocol/ext-apps"] =
- "file:./" + packed[0].filename;
writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2));
const run = (cmd) => execSync(cmd, { cwd: stage, stdio: "inherit" });
diff --git a/examples/pdf-server/main.ts b/examples/pdf-server/main.ts
index bb7b57f..1ff848a 100644
--- a/examples/pdf-server/main.ts
+++ b/examples/pdf-server/main.ts
@@ -6,10 +6,10 @@
import fs from "node:fs";
import path from "node:path";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import {
@@ -39,7 +39,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json
index c8d9cf8..cdcda33 100644
--- a/examples/pdf-server/package.json
+++ b/examples/pdf-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-pdf",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/pdf-server"
},
"license": "MIT",
@@ -25,16 +25,13 @@
"build:mcpb": "npm run build && node build-mcpb.mjs"
},
"dependencies": {
- "@cantoo/pdf-lib": "^2.6.5",
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
+ "pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.0.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts
index 0b2a5eb..a2f64ee 100644
--- a/examples/pdf-server/server.test.ts
+++ b/examples/pdf-server/server.test.ts
@@ -2,16 +2,11 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
-import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
-import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
-import { PDFDocument } from "@cantoo/pdf-lib";
-import { makeRandomJpeg } from "../../tests/helpers/range-counting-server";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import {
createPdfCache,
createServer,
- extractFormSchema,
- PdfCacheRangeTransport,
- MAX_CHUNK_BYTES,
validateUrl,
isAncestorDir,
allowedLocalFiles,
@@ -76,72 +71,6 @@ describe("PDF Cache with Timeouts", () => {
});
});
- describe("byte-cap LRU eviction", () => {
- const tenBytes = new Uint8Array(10);
-
- async function fill(cache: PdfCache, url: string): Promise {
- const m = spyOn(globalThis, "fetch").mockResolvedValueOnce(
- new Response(tenBytes, { status: 200 }),
- );
- try {
- await cache.readPdfRange(url, 0, 1024);
- } finally {
- m.mockRestore();
- }
- }
-
- it("evicts least-recently-used entry when total exceeds cap", async () => {
- const cache = createPdfCache(25);
- try {
- await fill(cache, "https://arxiv.org/pdf/a");
- await fill(cache, "https://arxiv.org/pdf/b");
- expect(cache.getCacheSize()).toBe(2);
-
- // Touch A so B becomes least-recently-used
- await cache.readPdfRange("https://arxiv.org/pdf/a", 0, 1);
-
- // Inserting C (10B) pushes total to 30 > 25 → evict LRU (B)
- await fill(cache, "https://arxiv.org/pdf/c");
- expect(cache.getCacheSize()).toBe(2);
-
- // A and C still served from cache; B re-fetches
- const m = spyOn(globalThis, "fetch").mockResolvedValue(
- new Response(tenBytes, { status: 200 }),
- );
- try {
- await cache.readPdfRange("https://arxiv.org/pdf/a", 0, 1);
- await cache.readPdfRange("https://arxiv.org/pdf/c", 0, 1);
- expect(m).toHaveBeenCalledTimes(0);
- await cache.readPdfRange("https://arxiv.org/pdf/b", 0, 1);
- expect(m).toHaveBeenCalledTimes(1);
- } finally {
- m.mockRestore();
- }
- } finally {
- cache.clearCache();
- }
- });
-
- it("evicts multiple entries if a single insert exceeds the cap", async () => {
- const cache = createPdfCache(25);
- try {
- await fill(cache, "https://arxiv.org/pdf/a");
- await fill(cache, "https://arxiv.org/pdf/b");
- const big = spyOn(globalThis, "fetch").mockResolvedValueOnce(
- new Response(new Uint8Array(20), { status: 200 }),
- );
- try {
- await cache.readPdfRange("https://arxiv.org/pdf/big", 0, 1024);
- } finally {
- big.mockRestore();
- }
- expect(cache.getCacheSize()).toBe(1);
- } finally {
- cache.clearCache();
- }
- });
- });
-
describe("readPdfRange caching behavior", () => {
const testUrl = "https://arxiv.org/pdf/test-pdf";
const testData = new Uint8Array([0x25, 0x50, 0x44, 0x46]); // %PDF header
@@ -294,266 +223,6 @@ describe("PDF Cache with Timeouts", () => {
// through manual testing or E2E tests.
});
-describe("PdfCacheRangeTransport", () => {
- it("accumulates ranges larger than MAX_CHUNK_BYTES into one onDataRange call", async () => {
- const big = MAX_CHUNK_BYTES * 2 + 100;
- const reads: Array<[number, number]> = [];
- const t = new PdfCacheRangeTransport("u", big, async (_u, off, n) => {
- reads.push([off, n]);
- return {
- data: new Uint8Array(Math.min(n, MAX_CHUNK_BYTES)),
- totalBytes: big,
- };
- });
- const delivered: Array<[number, number]> = [];
- t.addRangeListener((begin: number, chunk: Uint8Array) =>
- delivered.push([begin, chunk.length]),
- );
- t.requestDataRange(0, big);
- await new Promise((r) => setTimeout(r, 10));
- // pdf.js's reader is keyed by the original begin and removed after one
- // delivery, so deliver() must call onDataRange exactly once with the
- // accumulated buffer — multiple calls would throw inside pdfjs.
- expect(delivered).toEqual([[0, big]]);
- expect(reads).toEqual([
- [0, MAX_CHUNK_BYTES],
- [MAX_CHUNK_BYTES, MAX_CHUNK_BYTES],
- [MAX_CHUNK_BYTES * 2, 100],
- ]);
- });
-
- it("rejects .failed when a range fetch errors instead of hanging", async () => {
- const t = new PdfCacheRangeTransport("u", 1000, async () => {
- throw new Error("network down");
- });
- t.requestDataRange(0, 100);
- await expect(
- Promise.race([
- t.failed,
- new Promise((r) => setTimeout(() => r("timeout"), 200)),
- ]),
- ).rejects.toThrow("network down");
- });
-
- it("rejects .failed on zero-length response (would otherwise spin)", async () => {
- const t = new PdfCacheRangeTransport("u", 1000, async () => ({
- data: new Uint8Array(0),
- totalBytes: 1000,
- }));
- t.requestDataRange(0, 100);
- await expect(t.failed).rejects.toThrow(/empty range/);
- });
-
- it("getDocument resolves on a >1MB PDF when readPdfRange clamps to MAX_CHUNK_BYTES", async () => {
- // pdfjs coalesces adjacent missing chunks into one requestDataRange that
- // can exceed MAX_CHUNK_BYTES. deliver() must accumulate clamped reads and
- // hand pdfjs a single onDataRange(begin, fullBuffer). This test fails if
- // deliver() either truncates or calls onDataRange more than once per
- // requestDataRange (pdf.mjs _onReceiveData matches by exact begin).
- const d = await PDFDocument.create();
- const img = await d.embedJpg(makeRandomJpeg(1_100_000));
- const page = d.addPage([612, 792]);
- page.drawImage(img, { x: 36, y: 36, width: 540, height: 720 });
- const bytes = await d.save();
- expect(bytes.length).toBeGreaterThan(2 * MAX_CHUNK_BYTES);
-
- const readClamped: PdfCache["readPdfRange"] = async (_u, off, n) => {
- const len = Math.min(n, MAX_CHUNK_BYTES, bytes.length - off);
- return { data: bytes.slice(off, off + len), totalBytes: bytes.length };
- };
- // Record the spans pdfjs actually requests so the test fails fast if it
- // never asks for >MAX_CHUNK_BYTES (i.e. can't go vacuously green).
- const spans: number[] = [];
- class RecordingTransport extends PdfCacheRangeTransport {
- override requestDataRange(begin: number, end: number): void {
- spans.push(end - begin);
- super.requestDataRange(begin, end);
- }
- }
- const transport = new RecordingTransport(
- "mem://big",
- bytes.length,
- readClamped,
- );
-
- const orHang = (p: Promise, what: string): Promise =>
- Promise.race([
- p,
- transport.failed,
- new Promise((_, rej) =>
- setTimeout(() => rej(new Error(`${what} hung`)), 5000),
- ),
- ]);
-
- const doc = await orHang(
- getDocument({
- range: transport,
- length: bytes.length,
- disableAutoFetch: true,
- disableStream: true,
- rangeChunkSize: 64 * 1024,
- }).promise,
- "getDocument",
- );
- const p1 = await orHang(doc.getPage(1), "getPage");
- // getPage() alone doesn't decode the image XObject; getOperatorList() does,
- // which is what triggers the >512KB coalesced range request.
- await orHang(p1.getOperatorList(), "getOperatorList");
- expect(Math.max(...spans)).toBeGreaterThan(MAX_CHUNK_BYTES);
- doc.destroy();
- });
-});
-
-describe("display_pdf transport-error handling", () => {
- it("returns (does not hang) when range fetches fail mid-load", async () => {
- // First fetch = the 1-byte size probe → 206 with Content-Range so
- // display_pdf gets totalBytes. Every subsequent fetch (made by
- // PdfCacheRangeTransport via readPdfRange) rejects, which must surface
- // through transport.failed → orFail() → outer catch, not hang.
- let calls = 0;
- const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
- async () => {
- if (calls++ === 0) {
- return new Response(new Uint8Array(1), {
- status: 206,
- headers: { "Content-Range": "bytes 0-0/50000" },
- });
- }
- throw new Error("network down");
- },
- );
-
- const server = createServer();
- const client = new Client({ name: "t", version: "1" });
- const [ct, st] = InMemoryTransport.createLinkedPair();
- await Promise.all([server.connect(st), client.connect(ct)]);
-
- try {
- const result = await Promise.race([
- client.callTool({
- name: "display_pdf",
- arguments: { url: "https://arxiv.org/pdf/err-test" },
- }),
- new Promise((_, rej) =>
- setTimeout(
- () => rej(new Error("display_pdf hung on transport error")),
- 3000,
- ),
- ),
- ]);
- expect(result.isError).toBeFalsy();
- const sc = result.structuredContent as { formFields?: unknown };
- expect(sc.formFields).toBeUndefined();
- expect(calls).toBeGreaterThan(1);
- } finally {
- mockFetch.mockRestore();
- await client.close();
- await server.close();
- }
- });
-});
-
-describe("extractFormSchema field-tree handling", () => {
- async function schemaFor(bytes: Uint8Array) {
- const doc = await getDocument({ data: bytes }).promise;
- try {
- const fo = (await doc.getFieldObjects()) as Parameters<
- typeof extractFormSchema
- >[1];
- return await extractFormSchema(doc, fo);
- } finally {
- doc.destroy();
- }
- }
-
- it("handles pdf-lib separated field/widget structure", async () => {
- const d = await PDFDocument.create();
- const form = d.getForm();
- d.addPage([612, 792]);
- form
- .createTextField("alpha")
- .addToPage(d.getPage(0), { x: 50, y: 700, width: 200, height: 20 });
- form
- .createCheckBox("agree")
- .addToPage(d.getPage(0), { x: 50, y: 660, width: 20, height: 20 });
- form
- .createDropdown("choice")
- .addToPage(d.getPage(0), { x: 50, y: 620, width: 100, height: 20 });
-
- const schema = await schemaFor(await d.save());
- expect(schema).not.toBeNull();
- expect(schema!.properties.alpha).toEqual({
- type: "string",
- title: "alpha",
- });
- expect(schema!.properties.agree).toEqual({
- type: "boolean",
- title: "agree",
- });
- expect(schema!.properties.choice.type).toBe("string");
- });
-
- it("handles fields with multiple widgets across pages", async () => {
- const d = await PDFDocument.create();
- const form = d.getForm();
- d.addPage([612, 792]);
- d.addPage([612, 792]);
- const tf = form.createTextField("shared");
- tf.addToPage(d.getPage(0), { x: 50, y: 700, width: 200, height: 20 });
- tf.addToPage(d.getPage(1), { x: 50, y: 700, width: 200, height: 20 });
-
- const schema = await schemaFor(await d.save());
- expect(schema?.properties.shared).toEqual({
- type: "string",
- title: "shared",
- });
- });
-
- it("skips container nodes and finds leaf fields (W-9 style)", async () => {
- const bytes = fs.readFileSync(
- path.join(__dirname, "../../tests/helpers/assets/fw9.pdf"),
- );
- const doc = await getDocument({ data: new Uint8Array(bytes) }).promise;
- try {
- const fo = (await doc.getFieldObjects()) as Parameters<
- typeof extractFormSchema
- >[1];
- // Container nodes (no leaf type) should not crash extraction
- expect(fo!["topmostSubform[0]"]).toBeDefined();
- // Schema is null for W-9 (mechanical names), but extraction must not throw
- const schema = await extractFormSchema(doc, fo);
- expect(schema).toBeNull();
- } finally {
- doc.destroy();
- }
- });
-
- it("returns null when no AcroForm present", async () => {
- const d = await PDFDocument.create();
- d.addPage([612, 792]);
- const schema = await schemaFor(await d.save());
- expect(schema).toBeNull();
- });
-});
-
-describe("validateUrl loopback HTTP allow (PDF_SERVER_ALLOW_LOOPBACK_HTTP)", () => {
- it("rejects http://127.0.0.1 by default", () => {
- expect(validateUrl("http://127.0.0.1:9999/x.pdf").valid).toBe(false);
- });
-
- it("accepts http://127.0.0.1 only when the env gate is set, and never non-loopback http", () => {
- const prev = process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP;
- process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP = "1";
- try {
- expect(validateUrl("http://127.0.0.1:9999/x.pdf").valid).toBe(true);
- expect(validateUrl("http://169.254.169.254/").valid).toBe(false);
- } finally {
- if (prev === undefined) delete process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP;
- else process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP = prev;
- }
- });
-});
-
describe("validateUrl with MCP roots (allowedLocalDirs)", () => {
const savedFiles = new Set(allowedLocalFiles);
const savedDirs = new Set(allowedLocalDirs);
diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts
index e67cd47..ba16431 100644
--- a/examples/pdf-server/server.ts
+++ b/examples/pdf-server/server.ts
@@ -13,30 +13,27 @@
import { randomUUID } from "crypto";
import fs from "node:fs";
import path from "node:path";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
registerAppResource,
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import {
- McpServer,
+ RootsListChangedNotificationSchema,
type CallToolResult,
- type ElicitResult,
- type PrimitiveSchemaDefinition,
type ReadResourceResult,
- type Server,
-} from "@modelcontextprotocol/server";
+} from "@modelcontextprotocol/sdk/types.js";
// Stub DOMMatrix/ImageData/Path2D before pdfjs-dist loads — its legacy
// build instantiates DOMMatrix at module scope and the @napi-rs/canvas
// polyfill is unreliable under npx. See ./pdfjs-polyfill.ts for details.
import "./pdfjs-polyfill.js";
import {
getDocument,
- PDFDataRangeTransport,
VerbosityLevel,
version as PDFJS_VERSION,
} from "pdfjs-dist/legacy/build/pdf.mjs";
-import type { PDFDocumentProxy } from "pdfjs-dist/types/src/display/api.js";
/**
* PDF Standard-14 fonts from CDN. Used by both server and viewer so we
@@ -63,6 +60,10 @@ class FetchStandardFontDataFactory {
return new Uint8Array(await res.arrayBuffer());
}
}
+import type {
+ PrimitiveSchemaDefinition,
+ ElicitResult,
+} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// =============================================================================
@@ -82,9 +83,6 @@ export const CACHE_MAX_LIFETIME_MS = 60_000; // 60 seconds
/** Max size for cached PDFs (defensive limit to prevent memory exhaustion) */
export const CACHE_MAX_PDF_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
-/** Max total bytes across all cache entries; oldest evicted first when exceeded. */
-export const CACHE_MAX_TOTAL_BYTES = 256 * 1024 * 1024; // 256MB
-
/** Allowed local file paths (CLI args + file roots — read access). */
export const allowedLocalFiles = new Set();
@@ -307,33 +305,6 @@ function waitForSaveData(
});
}
-const pendingStateRequests = new Map void>();
-
-/**
- * Wait for the viewer to report its current state (page, zoom, selection, …)
- * as a JSON string. Same timeout/abort semantics as waitForSaveData.
- */
-function waitForViewerState(
- requestId: string,
- signal?: AbortSignal,
-): Promise {
- return new Promise((resolve, reject) => {
- const settle = (v: string | Error) => {
- clearTimeout(timer);
- signal?.removeEventListener("abort", onAbort);
- pendingStateRequests.delete(requestId);
- v instanceof Error ? reject(v) : resolve(v);
- };
- const onAbort = () => settle(new Error("interact request cancelled"));
- const timer = setTimeout(
- () => settle(new Error("Timeout waiting for viewer state")),
- GET_PAGES_TIMEOUT_MS,
- );
- signal?.addEventListener("abort", onAbort);
- pendingStateRequests.set(requestId, settle);
- });
-}
-
interface QueueEntry {
commands: PdfCommand[];
/** Timestamp of the most recent enqueue or dequeue */
@@ -366,6 +337,9 @@ export const viewSourcePaths = new Map();
/** Valid form field names per viewer UUID (populated during display_pdf) */
const viewFieldNames = new Map>();
+/** Detailed form field info per viewer UUID (populated during display_pdf) */
+const viewFieldInfo = new Map();
+
/**
* Active fs.watch per view. Only created for local files when interact is
* enabled (stdio). Watcher is re-established on `rename` events to survive
@@ -382,7 +356,7 @@ const viewFileWatches = new Map();
/**
* Per-view heartbeat. THIS is what the sweep iterates — not commandQueues.
*
- * Why not commandQueues: display_pdf populates viewFieldNames/
+ * Why not commandQueues: display_pdf populates viewFieldNames/viewFieldInfo/
* viewFileWatches but never touches commandQueues (only enqueueCommand does,
* and it's triply gated). And dequeueCommands deletes the entry on every poll,
* so even when it exists the sweep's TTL window is ~200ms wide. Net effect:
@@ -404,6 +378,7 @@ function pruneStaleQueues(): void {
viewLastActivity.delete(uuid);
commandQueues.delete(uuid);
viewFieldNames.delete(uuid);
+ viewFieldInfo.delete(uuid);
viewsPolled.delete(uuid);
viewSourcePaths.delete(uuid);
stopFileWatch(uuid);
@@ -638,19 +613,10 @@ export function validateUrl(url: string): {
// Remote URL - require HTTPS
try {
const parsed = new URL(url);
- if (parsed.protocol === "https:") return { valid: true };
- // Loopback HTTP is opt-in (test fixtures, local dev). Off by default so a
- // remotely-deployed server can't be made to probe its own internal ports.
- if (
- process.env.PDF_SERVER_ALLOW_LOOPBACK_HTTP &&
- parsed.protocol === "http:" &&
- (parsed.hostname === "127.0.0.1" ||
- parsed.hostname === "localhost" ||
- parsed.hostname === "[::1]")
- ) {
- return { valid: true };
+ if (parsed.protocol !== "https:") {
+ return { valid: false, error: `Only HTTPS URLs are allowed: ${url}` };
}
- return { valid: false, error: `Only HTTPS URLs are allowed: ${url}` };
+ return { valid: true };
} catch {
return { valid: false, error: `Invalid URL: ${url}` };
}
@@ -702,11 +668,8 @@ export interface PdfCache {
* - CACHE_INACTIVITY_TIMEOUT_MS of no access (resets on each access)
* - CACHE_MAX_LIFETIME_MS from creation (absolute timeout)
*/
-export function createPdfCache(
- maxTotalBytes: number = CACHE_MAX_TOTAL_BYTES,
-): PdfCache {
+export function createPdfCache(): PdfCache {
const cache = new Map();
- let totalBytes = 0;
/** Delete a cache entry and clear its timers */
function deleteCacheEntry(url: string): void {
@@ -714,7 +677,6 @@ export function createPdfCache(
if (entry) {
clearTimeout(entry.inactivityTimer);
clearTimeout(entry.maxLifetimeTimer);
- totalBytes -= entry.data.length;
cache.delete(url);
}
}
@@ -730,10 +692,6 @@ export function createPdfCache(
deleteCacheEntry(url);
}, CACHE_INACTIVITY_TIMEOUT_MS);
- // Move to end of insertion order so size-cap eviction is LRU.
- cache.delete(url);
- cache.set(url, entry);
-
return entry.data;
}
@@ -742,12 +700,6 @@ export function createPdfCache(
// Clear any existing entry first
deleteCacheEntry(url);
- // Evict least-recently-used entries until under the byte cap.
- for (const oldest of cache.keys()) {
- if (totalBytes + data.length <= maxTotalBytes) break;
- deleteCacheEntry(oldest);
- }
-
const entry: CacheEntry = {
data,
createdAt: Date.now(),
@@ -760,7 +712,6 @@ export function createPdfCache(
};
cache.set(url, entry);
- totalBytes += data.length;
}
/** Slice a cached or freshly-fetched full body to the requested range. */
@@ -885,61 +836,6 @@ export function createPdfCache(
};
}
-/**
- * pdf.js range transport backed by {@link PdfCache.readPdfRange}. Lets
- * getDocument() fetch only the byte ranges it needs (xref, /AcroForm dict)
- * instead of the whole file. With disableAutoFetch, a PDF without form
- * fields is opened with ~5% of bytes fetched.
- *
- * pdf.js has no upstream error channel on PDFDataRangeTransport (its
- * `abort()` is a no-op stub it calls *on* us, not the other way). Callers
- * must `Promise.race` their pdf.js awaits against {@link failed}, which
- * rejects on the first fetch error.
- */
-export class PdfCacheRangeTransport extends PDFDataRangeTransport {
- /** Rejects on the first range-fetch error; never resolves. */
- readonly failed: Promise;
- private fail!: (e: unknown) => void;
-
- constructor(
- private url: string,
- length: number,
- private readPdfRange: PdfCache["readPdfRange"],
- ) {
- super(length, null);
- this.failed = new Promise((_, reject) => {
- this.fail = reject;
- });
- // Don't crash the process if no one is racing yet.
- this.failed.catch(() => {});
- }
-
- override requestDataRange(begin: number, end: number): void {
- void this.deliver(begin, end).catch((e) => this.fail(e));
- }
-
- /**
- * pdf.js coalesces adjacent missing chunks into one unbounded request, but
- * readPdfRange clamps each call to MAX_CHUNK_BYTES. Its reader is keyed by
- * the original `begin` and removed after one delivery, so we must accumulate
- * slices and call onDataRange exactly once with the full buffer.
- */
- private async deliver(begin: number, end: number): Promise {
- const buf = new Uint8Array(end - begin);
- let off = 0;
- while (off < buf.length) {
- const want = Math.min(buf.length - off, MAX_CHUNK_BYTES);
- const { data } = await this.readPdfRange(this.url, begin + off, want);
- if (data.length === 0) {
- throw new Error(`empty range at ${begin + off} for ${this.url}`);
- }
- buf.set(data.subarray(0, Math.min(data.length, buf.length - off)), off);
- off += data.length;
- }
- this.onDataRange(begin, buf);
- }
-}
-
// =============================================================================
// MCP Roots
// =============================================================================
@@ -1015,142 +911,135 @@ interface FormFieldInfo {
options?: string[];
}
-/**
- * Open `url` via {@link PdfCacheRangeTransport} and return form metadata.
- * Uses `disableAutoFetch` so PDFs without an AcroForm are probed with only
- * the trailer/xref/catalog (~5-25% of bytes); PDFs with forms still walk
- * every page via {@link extractFormFieldInfo} but those are typically small.
- * All errors (including range-fetch failures surfaced via
- * {@link PdfCacheRangeTransport.failed}) resolve to empty results.
- */
-async function probeFormFields(
- url: string,
- totalBytes: number,
- readPdfRange: PdfCache["readPdfRange"],
-): Promise<{
- formSchema: Awaited>;
- fieldInfo: FormFieldInfo[];
-}> {
- // Assigned sequentially below so a throw in extractFormFieldInfo (no per-page
- // guard, unlike extractFormSchema) doesn't discard an already-computed schema.
- let formSchema: Awaited> = null;
- let fieldInfo: FormFieldInfo[] = [];
- try {
- const transport = new PdfCacheRangeTransport(url, totalBytes, readPdfRange);
- const orFail = (p: Promise): Promise =>
- Promise.race([p, transport.failed]);
- const pdfDoc = await orFail(
- getDocument({
- range: transport,
- length: totalBytes,
- disableAutoFetch: true,
- disableStream: true,
- rangeChunkSize: 64 * 1024,
- standardFontDataUrl: STANDARD_FONT_DATA_URL,
- StandardFontDataFactory: FetchStandardFontDataFactory,
- verbosity: VerbosityLevel.ERRORS,
- }).promise,
- );
- try {
- const fieldObjects = (await orFail(pdfDoc.getFieldObjects())) as Record<
- string,
- PdfJsFieldObject[]
- > | null;
- if (fieldObjects && Object.keys(fieldObjects).length > 0) {
- formSchema = await orFail(extractFormSchema(pdfDoc, fieldObjects));
- fieldInfo = await orFail(extractFormFieldInfo(pdfDoc));
- }
- } finally {
- pdfDoc.destroy();
- }
- } catch {
- // Non-fatal — return whatever was assigned before the throw.
- }
- return { formSchema, fieldInfo };
-}
-
/**
* Extract detailed form field info (name, type, page, bounding box, label)
* from a PDF. Bounding boxes are converted to model coordinates (top-left origin).
*/
async function extractFormFieldInfo(
- pdfDoc: PDFDocumentProxy,
+ url: string,
+ readRange: (
+ url: string,
+ offset: number,
+ byteCount: number,
+ ) => Promise<{ data: Uint8Array; totalBytes: number }>,
): Promise {
+ const { totalBytes } = await readRange(url, 0, 1);
+ const { data } = await readRange(url, 0, totalBytes);
+
+ const loadingTask = getDocument({
+ data,
+ standardFontDataUrl: STANDARD_FONT_DATA_URL,
+ StandardFontDataFactory: FetchStandardFontDataFactory,
+ // We only introspect form fields (never render) — silence residual
+ // warnings like "Unimplemented border style: inset".
+ verbosity: VerbosityLevel.ERRORS,
+ });
+ const pdfDoc = await loadingTask.promise;
+
const fields: FormFieldInfo[] = [];
- for (let i = 1; i <= pdfDoc.numPages; i++) {
- const page = await pdfDoc.getPage(i);
- const pageHeight = page.getViewport({ scale: 1.0 }).height;
- const annotations = await page.getAnnotations();
- for (const ann of annotations) {
- // Only include form widgets (annotationType 20)
- if (ann.annotationType !== 20) continue;
- if (!ann.rect) continue;
+ try {
+ for (let i = 1; i <= pdfDoc.numPages; i++) {
+ const page = await pdfDoc.getPage(i);
+ const pageHeight = page.getViewport({ scale: 1.0 }).height;
+ const annotations = await page.getAnnotations();
+ for (const ann of annotations) {
+ // Only include form widgets (annotationType 20)
+ if (ann.annotationType !== 20) continue;
+ if (!ann.rect) continue;
- const fieldName = ann.fieldName || "";
- const fieldType = ann.fieldType || "unknown";
+ const fieldName = ann.fieldName || "";
+ const fieldType = ann.fieldType || "unknown";
- // PDF rect is [x1, y1, x2, y2] in bottom-left origin
- const x1 = Math.min(ann.rect[0], ann.rect[2]);
- const y1 = Math.min(ann.rect[1], ann.rect[3]);
- const x2 = Math.max(ann.rect[0], ann.rect[2]);
- const y2 = Math.max(ann.rect[1], ann.rect[3]);
- const width = x2 - x1;
- const height = y2 - y1;
+ // PDF rect is [x1, y1, x2, y2] in bottom-left origin
+ const x1 = Math.min(ann.rect[0], ann.rect[2]);
+ const y1 = Math.min(ann.rect[1], ann.rect[3]);
+ const x2 = Math.max(ann.rect[0], ann.rect[2]);
+ const y2 = Math.max(ann.rect[1], ann.rect[3]);
+ const width = x2 - x1;
+ const height = y2 - y1;
- // Convert to model coords (top-left origin): modelY = pageHeight - pdfY - height
- const modelY = pageHeight - y2;
+ // Convert to model coords (top-left origin): modelY = pageHeight - pdfY - height
+ const modelY = pageHeight - y2;
- // Choice widgets (combo/listbox) carry `options` as
- // [{exportValue, displayValue}]. Expose export values — that's
- // what fill_form needs.
- let options: string[] | undefined;
- if (Array.isArray(ann.options) && ann.options.length > 0) {
- options = ann.options
- .map((o: { exportValue?: string }) => o?.exportValue)
- .filter((v: unknown): v is string => typeof v === "string");
+ // Choice widgets (combo/listbox) carry `options` as
+ // [{exportValue, displayValue}]. Expose export values — that's
+ // what fill_form needs.
+ let options: string[] | undefined;
+ if (Array.isArray(ann.options) && ann.options.length > 0) {
+ options = ann.options
+ .map((o: { exportValue?: string }) => o?.exportValue)
+ .filter((v: unknown): v is string => typeof v === "string");
+ }
+
+ fields.push({
+ name: fieldName,
+ type: fieldType,
+ page: i,
+ x: Math.round(x1),
+ y: Math.round(modelY),
+ width: Math.round(width),
+ height: Math.round(height),
+ ...(ann.alternativeText ? { label: ann.alternativeText } : undefined),
+ // Radio: buttonValue is the per-widget export value — the only
+ // thing distinguishing three `size [Btn]` lines from each other.
+ ...(ann.radioButton && ann.buttonValue != null
+ ? { exportValue: String(ann.buttonValue) }
+ : undefined),
+ ...(options?.length ? { options } : undefined),
+ });
}
-
- fields.push({
- name: fieldName,
- type: fieldType,
- page: i,
- x: Math.round(x1),
- y: Math.round(modelY),
- width: Math.round(width),
- height: Math.round(height),
- ...(ann.alternativeText ? { label: ann.alternativeText } : undefined),
- // Radio: buttonValue is the per-widget export value — the only
- // thing distinguishing three `size [Btn]` lines from each other.
- ...(ann.radioButton && ann.buttonValue != null
- ? { exportValue: String(ann.buttonValue) }
- : undefined),
- ...(options?.length ? { options } : undefined),
- });
}
+ } finally {
+ pdfDoc.destroy();
}
return fields;
}
-export async function extractFormSchema(
- pdfDoc: PDFDocumentProxy,
- fieldObjects: Record | null,
+async function extractFormSchema(
+ url: string,
+ readRange: (
+ url: string,
+ offset: number,
+ byteCount: number,
+ ) => Promise<{ data: Uint8Array; totalBytes: number }>,
): Promise<{
type: "object";
properties: Record;
required?: string[];
} | null> {
+ // Read full PDF bytes
+ const { totalBytes } = await readRange(url, 0, 1);
+ const { data } = await readRange(url, 0, totalBytes);
+
+ const loadingTask = getDocument({
+ data,
+ standardFontDataUrl: STANDARD_FONT_DATA_URL,
+ StandardFontDataFactory: FetchStandardFontDataFactory,
+ // We only introspect form fields (never render) — silence residual
+ // warnings like "Unimplemented border style: inset".
+ verbosity: VerbosityLevel.ERRORS,
+ });
+ const pdfDoc = await loadingTask.promise;
+
+ let fieldObjects: Record | null;
+ try {
+ fieldObjects = (await pdfDoc.getFieldObjects()) as Record<
+ string,
+ PdfJsFieldObject[]
+ > | null;
+ } catch {
+ pdfDoc.destroy();
+ return null;
+ }
if (!fieldObjects || Object.keys(fieldObjects).length === 0) {
+ pdfDoc.destroy();
return null;
}
const properties: Record = {};
for (const [name, fields] of Object.entries(fieldObjects)) {
- // pdfjs returns the full field-tree array: for separated structures
- // (pdf-lib) the typed widget is at [1+] behind a container at [0]; for
- // merged/leaf entries (W-9, most authoring tools) it's at [0]. Pick the
- // first entry that actually has a field type.
- const field = fields.find((f) => f.type) ?? fields[0];
+ const field = fields[0]; // first widget determines the type
if (!field.editable) continue;
switch (field.type) {
@@ -1215,6 +1104,7 @@ export async function extractFormSchema(
return /[[\]().]/.test(name) || /^[A-Z0-9_]+$/.test(name);
});
+ pdfDoc.destroy();
if (Object.keys(properties).length === 0) return null;
if (hasMechanicalNames) return null;
@@ -1261,12 +1151,6 @@ export interface CreateServerOptions {
debug?: boolean;
}
-// Module-level singletons so they survive across createServer() calls — in
-// stateless HTTP deployments a fresh server is created per request, and
-// per-instance caches are discarded immediately.
-const sharedPdfCache = createPdfCache();
-let cachedAppHtml: string | undefined;
-
export function createServer(options: CreateServerOptions = {}): McpServer {
const { enableInteract = false, useClientRoots = false } = options;
const debug = options.debug ?? false;
@@ -1279,22 +1163,21 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
refreshRoots(server.server);
};
server.server.setNotificationHandler(
- "notifications/roots/list_changed",
+ RootsListChangedNotificationSchema,
async () => {
await refreshRoots(server.server);
},
);
}
- const { readPdfRange } = sharedPdfCache;
+ // Create session-local cache (isolated per server instance)
+ const { readPdfRange } = createPdfCache();
// Tool: list_pdfs - List available PDFs
- server.registerTool(
+ server.tool(
"list_pdfs",
- {
- description: "List available PDFs that can be displayed",
- inputSchema: z.object({}),
- },
+ "List available PDFs that can be displayed",
+ {},
async (): Promise => {
const seen = new Set();
const localFiles: string[] = [];
@@ -1376,7 +1259,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
title: "Read PDF Bytes",
description:
"Read a range of bytes from a PDF (max 512KB per request). The model should NOT call this tool directly.",
- inputSchema: z.object({
+ inputSchema: {
url: z.string().describe("PDF URL or local file path"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
@@ -1385,7 +1268,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
.max(MAX_CHUNK_BYTES)
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
- }),
+ },
outputSchema: z.object({
url: z.string(),
bytes: z.string().describe("Base64 encoded bytes"),
@@ -1456,7 +1339,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
description: disableInteract
? `Show and render a PDF in a read-only viewer.
-Use this tool when the user wants to view or read a PDF. The renderer displays the document for viewing. The widget exposes app-registered tools for page navigation, text extraction, searching, and zoom control.
+Use this tool when the user wants to view or read a PDF. The renderer displays the document for viewing.
Accepts local files (use list_pdfs), client MCP root directories, or any HTTPS URL.`
: `Open a PDF in an interactive viewer. Call this ONCE per PDF.
@@ -1467,12 +1350,11 @@ Returns a viewUUID in structuredContent. Pass it to \`interact\`:
- add_annotations, update_annotations, remove_annotations, highlight_text
- fill_form (fill PDF form fields)
- navigate, search, find, search_navigate, zoom
-- get_text, get_screenshot, get_viewer_state (extract content / read selection & current page)
-- save_as (write annotated PDF to disk)
+- get_text, get_screenshot (extract content)
Accepts local files (use list_pdfs), client MCP root directories, or any HTTPS URL.
Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before display.`,
- inputSchema: z.object({
+ inputSchema: {
url: z
.string()
.default(DEFAULT_PDF)
@@ -1488,7 +1370,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before
"If true and the PDF has form fields, prompt the user to fill them before displaying",
),
}),
- }),
+ },
outputSchema: z.object({
viewUUID: z
.string()
@@ -1546,7 +1428,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before
const { totalBytes } = await readPdfRange(normalized, 0, 1);
const uuid = randomUUID();
// Start the heartbeat now so the sweep can clean up viewFieldNames/
- // viewFileWatches even if no interact calls ever happen.
+ // viewFieldInfo/viewFileWatches even if no interact calls ever happen.
if (!disableInteract) touchView(uuid);
// Check writability (governs save button; see isWritablePath doc).
@@ -1574,19 +1456,33 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before
}
}
- const { formSchema, fieldInfo } = await probeFormFields(
- normalized,
- totalBytes,
- readPdfRange,
- );
+ // Extract form field schema (used for elicitation and field name validation)
+ let formSchema: Awaited> = null;
+ try {
+ formSchema = await extractFormSchema(normalized, readPdfRange);
+ } catch {
+ // Non-fatal — PDF may not have form fields
+ }
if (formSchema) {
viewFieldNames.set(uuid, new Set(Object.keys(formSchema.properties)));
}
- if (fieldInfo.length > 0 && !viewFieldNames.has(uuid)) {
- viewFieldNames.set(
- uuid,
- new Set(fieldInfo.map((f) => f.name).filter(Boolean)),
- );
+
+ // Extract detailed form field info (page, bounding box, label)
+ let fieldInfo: FormFieldInfo[] = [];
+ try {
+ fieldInfo = await extractFormFieldInfo(normalized, readPdfRange);
+ if (fieldInfo.length > 0) {
+ viewFieldInfo.set(uuid, fieldInfo);
+ // Also populate viewFieldNames from field info if not already set
+ if (!viewFieldNames.has(uuid)) {
+ viewFieldNames.set(
+ uuid,
+ new Set(fieldInfo.map((f) => f.name).filter(Boolean)),
+ );
+ }
+ }
+ } catch {
+ // Non-fatal
}
// Elicit form field values if requested and client supports it
@@ -1754,7 +1650,6 @@ URL: ${normalized}`,
"fill_form",
"get_text",
"get_screenshot",
- "get_viewer_state",
"save_as",
])
.describe("Action to perform"),
@@ -2343,26 +2238,6 @@ URL: ${normalized}`,
);
}
}
- case "get_viewer_state": {
- const requestId = randomUUID();
- enqueueCommand(uuid, { type: "get_viewer_state", requestId });
- let state: string;
- try {
- await ensureViewerIsPolling(uuid);
- state = await waitForViewerState(requestId, signal);
- } catch (err) {
- return {
- content: [
- {
- type: "text",
- text: `Error: ${err instanceof Error ? err.message : String(err)}`,
- },
- ],
- isError: true,
- };
- }
- return { content: [{ type: "text", text: state }] };
- }
default:
return {
content: [{ type: "text", text: `Unknown action: ${action}` }],
@@ -2420,12 +2295,11 @@ Example — add a signature image and a stamp, then screenshot to verify:
**TEXT/SCREENSHOTS**:
• get_text: extract text from pages. Optional \`page\` for single page, or \`intervals\` for ranges [{start?,end?}]. Max 20 pages.
• get_screenshot: capture a single page as PNG image. Requires \`page\`.
-• get_viewer_state: snapshot of the live viewer — JSON {currentPage, pageCount, zoom, displayMode, selectedAnnotationIds, selection:{text,contextBefore,contextAfter,boundingRect}|null}. Use this to read what the user has selected or which page they're on.
**FORMS** — fill_form: fill fields with \`fields\` array of {name, value}.
**SAVE** — save_as: write the annotated PDF (annotations + form values) to a file. Pass \`path\` (absolute path or file://) for a new location, or omit \`path\` to overwrite the original. Set \`overwrite: true\` to replace an existing file (always required when omitting \`path\`).`,
- inputSchema: z.object({
+ inputSchema: {
viewUUID: z
.string()
.describe(
@@ -2446,7 +2320,6 @@ Example — add a signature image and a stamp, then screenshot to verify:
"fill_form",
"get_text",
"get_screenshot",
- "get_viewer_state",
"save_as",
])
.optional()
@@ -2522,7 +2395,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
.describe(
"Array of commands to execute sequentially. More efficient than separate calls. Tip: end with get_pages+getScreenshots to verify changes.",
),
- }),
+ },
},
async (
{
@@ -2599,7 +2472,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
const result = await processInteractCommand(
uuid,
commandList[i],
- extra.mcpReq.signal,
+ extra.signal,
);
if (result.isError) {
const errText = result.content
@@ -2656,7 +2529,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Submit Page Data",
description:
"Submit rendered page data for a get_pages request (used by viewer). The model should NOT call this tool directly.",
- inputSchema: z.object({
+ inputSchema: {
requestId: z
.string()
.describe("The request ID from the get_pages command"),
@@ -2669,7 +2542,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
}),
)
.describe("Page data entries"),
- }),
+ },
_meta: { ui: { visibility: ["app"] } },
},
async ({ requestId, pages }): Promise => {
@@ -2699,7 +2572,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Submit Save Data",
description:
"Submit annotated PDF bytes for a save_as request (used by viewer). The model should NOT call this tool directly.",
- inputSchema: z.object({
+ inputSchema: {
requestId: z
.string()
.describe("The request ID from the save_as command"),
@@ -2708,7 +2581,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
.string()
.optional()
.describe("Error message if the viewer failed to build bytes"),
- }),
+ },
_meta: { ui: { visibility: ["app"] } },
},
async ({ requestId, data, error }): Promise => {
@@ -2730,48 +2603,6 @@ Example — add a signature image and a stamp, then screenshot to verify:
},
);
- // Tool: submit_viewer_state (app-only) - Viewer reports its live state
- registerAppTool(
- server,
- "submit_viewer_state",
- {
- title: "Submit Viewer State",
- description:
- "Submit a viewer-state snapshot for a get_viewer_state request (used by viewer). The model should NOT call this tool directly.",
- inputSchema: z.object({
- requestId: z
- .string()
- .describe("The request ID from the get_viewer_state command"),
- state: z
- .string()
- .optional()
- .describe("JSON-encoded viewer state snapshot"),
- error: z
- .string()
- .optional()
- .describe("Error message if the viewer failed to read state"),
- }),
- _meta: { ui: { visibility: ["app"] } },
- },
- async ({ requestId, state, error }): Promise => {
- const settle = pendingStateRequests.get(requestId);
- if (!settle) {
- return {
- content: [
- { type: "text", text: `No pending request for ${requestId}` },
- ],
- isError: true,
- };
- }
- if (error || !state) {
- settle(new Error(error || "Viewer returned no state"));
- } else {
- settle(state);
- }
- return { content: [{ type: "text", text: "Submitted" }] };
- },
- );
-
// Tool: poll_pdf_commands (app-only) - Poll for pending commands
registerAppTool(
server,
@@ -2780,9 +2611,9 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Poll PDF Commands",
description:
"Poll for pending commands for a PDF viewer. The model should NOT call this tool directly.",
- inputSchema: z.object({
+ inputSchema: {
viewUUID: z.string().describe("The viewUUID of the PDF viewer"),
- }),
+ },
_meta: { ui: { visibility: ["app"] } },
},
async ({ viewUUID: uuid }): Promise => {
@@ -2827,10 +2658,10 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Save PDF",
description:
"Save annotated PDF bytes back to a local file. The model should NOT call this tool directly — use interact with action: save_as instead.",
- inputSchema: z.object({
+ inputSchema: {
url: z.string().describe("Original PDF URL or local file path"),
data: z.string().describe("Base64-encoded PDF bytes"),
- }),
+ },
outputSchema: z.object({
filePath: z.string(),
mtimeMs: z.number(),
@@ -2909,10 +2740,10 @@ Example — add a signature image and a stamp, then screenshot to verify:
RESOURCE_URI,
{ mimeType: RESOURCE_MIME_TYPE },
async (): Promise => {
- const html = (cachedAppHtml ??= await fs.promises.readFile(
+ const html = await fs.promises.readFile(
path.join(DIST_DIR, "mcp-app.html"),
"utf-8",
- ));
+ );
return {
contents: [
{
diff --git a/examples/pdf-server/src/annotation-panel.ts b/examples/pdf-server/src/annotation-panel.ts
index 9fd4457..6b41095 100644
--- a/examples/pdf-server/src/annotation-panel.ts
+++ b/examples/pdf-server/src/annotation-panel.ts
@@ -289,23 +289,10 @@ function panelFieldNames(): Set {
return new Set([...formFieldValues.keys(), ...pdfBaselineFormValues.keys()]);
}
-/** Baseline annotations the user has deleted. Shown crossed-out in the panel
- * (mirroring cleared form fields) so they can be reverted, and so save knows
- * to strip their refs from /Annots. */
-function removedBaselineAnnotations(): PdfAnnotationDef[] {
- return deps
- .state()
- .pdfBaselineAnnotations.filter((a) => !annotationMap.has(a.id));
-}
-
/** Total count of annotations + form fields for the sidebar badge.
* Uses the union so cleared baseline items still contribute. */
function sidebarItemCount(): number {
- return (
- annotationMap.size +
- removedBaselineAnnotations().length +
- panelFieldNames().size
- );
+ return annotationMap.size + panelFieldNames().size;
}
export function updateAnnotationsBadge(): void {
@@ -351,8 +338,6 @@ export function getAnnotationLabel(def: PdfAnnotationDef): string {
return "Line";
case "image":
return "Image";
- case "imported":
- return `${def.subtype} (from PDF)`;
}
}
@@ -396,8 +381,6 @@ export function getAnnotationColor(def: PdfAnnotationDef): string {
return "#333";
case "image":
return "#999";
- case "imported":
- return "#666";
}
}
@@ -442,14 +425,6 @@ export function renderAnnotationPanel(): void {
byPage.get(page)!.push(tracked);
}
- // Removed baseline annotations: still listed (crossed-out) so they can be
- // reverted before save strips them from the file.
- const removedByPage = new Map();
- for (const def of removedBaselineAnnotations()) {
- if (!removedByPage.has(def.page)) removedByPage.set(def.page, []);
- removedByPage.get(def.page)!.push(def);
- }
-
// Group form fields by page — iterate the UNION so cleared baseline
// fields remain visible (crossed out) with a per-item revert button.
const fieldsByPage = new Map();
@@ -466,11 +441,7 @@ export function renderAnnotationPanel(): void {
}
// Collect all pages that have annotations or form fields
- const allPages = new Set([
- ...byPage.keys(),
- ...removedByPage.keys(),
- ...fieldsByPage.keys(),
- ]);
+ const allPages = new Set([...byPage.keys(), ...fieldsByPage.keys()]);
const sortedPages = [...allPages].sort((a, b) => a - b);
// Sort annotations within each page by Y position (descending = top-first in PDF coords)
@@ -495,9 +466,8 @@ export function renderAnnotationPanel(): void {
const sectionKey = `page-${pageNum}`;
const isOpen = panelState.openAccordionSection === sectionKey;
const annotations = byPage.get(pageNum) ?? [];
- const removed = removedByPage.get(pageNum) ?? [];
const fields = fieldsByPage.get(pageNum) ?? [];
- const itemCount = annotations.length + removed.length + fields.length;
+ const itemCount = annotations.length + fields.length;
appendAccordionSection(
`Page ${pageNum} (${itemCount})`,
@@ -513,10 +483,6 @@ export function renderAnnotationPanel(): void {
for (const tracked of annotations) {
body.appendChild(createAnnotationCard(tracked));
}
- // Then removed baseline annotations (crossed-out, revertable)
- for (const def of removed) {
- body.appendChild(createRemovedAnnotationCard(def));
- }
},
);
}
@@ -682,55 +648,6 @@ function createAnnotationCard(tracked: TrackedAnnotation): HTMLElement {
return card;
}
-/**
- * Card for a baseline annotation the user deleted: crossed-out, no select/
- * navigate (it has no DOM on the page anymore), revert button puts it back
- * into `annotationMap` so it renders again and save leaves it in the file.
- */
-function createRemovedAnnotationCard(def: PdfAnnotationDef): HTMLElement {
- const card = document.createElement("div");
- card.className = "annotation-card annotation-card-cleared";
- card.dataset.annotationId = def.id;
-
- const row = document.createElement("div");
- row.className = "annotation-card-row";
-
- const swatch = document.createElement("div");
- swatch.className = "annotation-card-swatch annotation-card-swatch-cleared";
- swatch.innerHTML = ` `;
- row.appendChild(swatch);
-
- const typeLabel = document.createElement("span");
- typeLabel.className = "annotation-card-type";
- typeLabel.textContent = getAnnotationLabel(def);
- row.appendChild(typeLabel);
-
- const preview = getAnnotationPreview(def);
- if (preview) {
- const previewEl = document.createElement("span");
- previewEl.className = "annotation-card-preview";
- previewEl.textContent = preview;
- row.appendChild(previewEl);
- }
-
- const revertBtn = document.createElement("button");
- revertBtn.className = "annotation-card-delete";
- revertBtn.title = "Restore annotation from file";
- revertBtn.innerHTML = REVERT_SVG;
- revertBtn.addEventListener("click", (e) => {
- e.stopPropagation();
- annotationMap.set(def.id, { def: { ...def }, elements: [] });
- updateAnnotationsBadge();
- renderAnnotationPanel();
- deps.renderPage();
- deps.persistAnnotations();
- });
- row.appendChild(revertBtn);
-
- card.appendChild(row);
- return card;
-}
-
/** Revert one field to its PDF-stored baseline value. */
function revertFieldToBaseline(name: string): void {
const base = pdfBaselineFormValues.get(name);
@@ -914,15 +831,8 @@ function clearFieldInStorage(name: string): void {
meta?.[0]?.defaultValue ??
"";
const type = meta?.find((f) => f.type)?.type;
- // Radio: per-widget BOOLEANS, never a string. pdf.js's
- // RadioButtonWidgetAnnotation render() has inverted string coercion (see
- // setFieldInStorage), so writing the same string to every widget checks
- // the wrong one. {value:false} on all = nothing selected.
- if (type === "radiobutton") {
- for (const id of ids) storage.setValue(id, { value: false });
- return;
- }
- const clearValue = type === "checkbox" ? (dv ?? "Off") : (dv ?? "");
+ const clearValue =
+ type === "checkbox" || type === "radiobutton" ? (dv ?? "Off") : (dv ?? "");
for (const id of ids) storage.setValue(id, { value: clearValue });
}
diff --git a/examples/pdf-server/src/commands.ts b/examples/pdf-server/src/commands.ts
index 6d27edf..c469ebd 100644
--- a/examples/pdf-server/src/commands.ts
+++ b/examples/pdf-server/src/commands.ts
@@ -66,5 +66,4 @@ export type PdfCommand =
getScreenshots: boolean;
}
| { type: "save_as"; requestId: string }
- | { type: "get_viewer_state"; requestId: string }
| { type: "file_changed"; mtimeMs: number };
diff --git a/examples/pdf-server/src/mcp-app.css b/examples/pdf-server/src/mcp-app.css
index a87dbcd..b6bbb43 100644
--- a/examples/pdf-server/src/mcp-app.css
+++ b/examples/pdf-server/src/mcp-app.css
@@ -227,10 +227,6 @@ body {
align-items: flex-start;
padding: 1rem;
background: var(--bg200);
- /* JS owns pinch (inline pinch-in → fullscreen). pan-x/pan-y keeps native
- * scrolling; the fullscreen rule below repeats this with a comment on the
- * iOS-Safari preventDefault backstop. */
- touch-action: pan-x pan-y;
}
.page-wrapper {
@@ -314,20 +310,9 @@ body {
}
.main.fullscreen .toolbar {
- /* Hosts wrap fullscreen in their own header (title + close), which already
- * clears the top safe-area — so no --safe-top here, only sides. */
- padding: 0.25rem calc(0.5rem + var(--safe-right, 0px)) 0.25rem
- calc(0.5rem + var(--safe-left, 0px));
- min-height: 40px;
- /* Inline can wrap (narrow chat bubble); fullscreen has the width, and
- * wrapping would double the bar height + desync the search-bar offset. */
- flex-wrap: nowrap;
-}
-
-.main.fullscreen .search-bar {
- /* -1px overlaps the toolbar border so the dropdown looks attached. */
- top: 39px;
- right: calc(var(--safe-right, 0px) - 1px);
+ padding-top: calc(0.5rem + var(--safe-top, 0px));
+ padding-left: calc(0.5rem + var(--safe-left, 0px));
+ padding-right: calc(0.5rem + var(--safe-right, 0px));
}
.main.fullscreen .viewer {
@@ -590,21 +575,6 @@ body {
user-select: none;
}
-/* Annotation imported verbatim from the PDF (Stamp/Ink/etc. via
- * annotationCanvasMap). The body is the rasterized appearance canvas;
- * if pdf.js didn't divert it (no hasOwnCanvas), the box stays transparent
- * over the main-canvas pixel and just captures clicks. */
-.annotation-imported {
- position: absolute;
- pointer-events: auto;
- cursor: grab;
- user-select: none;
-}
-.annotation-imported:hover {
- outline: 1px dashed var(--accent, #2563eb);
- outline-offset: 1px;
-}
-
/* Selection visuals */
.annotation-selected {
outline: 2px solid var(--accent, #2563eb);
diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts
index 45e0c74..7e53959 100644
--- a/examples/pdf-server/src/mcp-app.ts
+++ b/examples/pdf-server/src/mcp-app.ts
@@ -12,11 +12,8 @@ import {
applyDocumentTheme,
applyHostStyleVariables,
} from "@modelcontextprotocol/ext-apps";
-import type {
- CallToolResult,
- ContentBlock,
-} from "@modelcontextprotocol/client";
-import { z } from "zod";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js";
import * as pdfjsLib from "pdfjs-dist";
import { AnnotationLayer, AnnotationMode, TextLayer } from "pdfjs-dist";
import "pdfjs-dist/web/pdf_viewer.css";
@@ -28,17 +25,14 @@ import {
type LineAnnotation,
type StampAnnotation,
type ImageAnnotation,
- type ImportedAnnotation,
type NoteAnnotation,
type FreetextAnnotation,
- cssColorToRgb,
serializeDiff,
deserializeDiff,
mergeAnnotations,
computeDiff,
isDiffEmpty,
buildAnnotatedPdfBytes,
- parseAnnotationRef,
importPdfjsAnnotation,
uint8ArrayToBase64,
convertFromModelCoords,
@@ -152,11 +146,6 @@ const imageCache = new Map();
/** Annotations imported from the PDF file (baseline for diff computation). */
let pdfBaselineAnnotations: PdfAnnotationDef[] = [];
-/** Pages whose native annotations have already been imported into the baseline. */
-const baselineScannedPages = new Set();
-/** Native-annotation ids the user deleted (from restored localStorage diff) —
- * the lazy per-page scan must NOT re-add these to annotationMap. */
-const restoredRemovedIds = new Set();
// Dirty flag — tracks unsaved local changes
let isDirty = false;
@@ -352,11 +341,6 @@ async function refitScale(): Promise {
// needs height changes too (rotation, browser chrome on mobile).
let lastContainerW = 0;
let lastContainerH = 0;
-/** One-shot: refit on the next resize even if it's a shrink in inline mode.
- * Set on fullscreen→inline so the page snaps to the new (smaller) width
- * once the host has actually resized the iframe — the inline `grewW` gate
- * would otherwise swallow that shrink. */
-let forceNextResizeRefit = false;
const containerResizeObserver = new ResizeObserver(([entry]) => {
const { width: w, height: h } = entry.contentRect;
const grewW = w > lastContainerW + 1;
@@ -364,12 +348,7 @@ const containerResizeObserver = new ResizeObserver(([entry]) => {
Math.abs(w - lastContainerW) > 1 || Math.abs(h - lastContainerH) > 1;
lastContainerW = w;
lastContainerH = h;
- if (forceNextResizeRefit && changed) {
- forceNextResizeRefit = false;
- refitScale();
- } else if (currentDisplayMode === "fullscreen" ? changed : grewW) {
- refitScale();
- }
+ if (currentDisplayMode === "fullscreen" ? changed : grewW) refitScale();
});
containerResizeObserver.observe(canvasContainerEl as HTMLElement);
@@ -1393,10 +1372,6 @@ const DRAGGABLE_TYPES = new Set([
"stamp",
"note",
"image",
- // "imported" is draggable in the UI but the move does NOT persist to the
- // PDF on save (addAnnotationDicts skips it). Resize/rotate stay disabled
- // — the appearance bitmap would just stretch.
- "imported",
]);
function setupAnnotationInteraction(
@@ -1904,25 +1879,6 @@ function paintAnnotationsOnCanvas(
}
break;
}
-
- case "imported": {
- const s = pdfRectToScreen(
- { x: def.x, y: def.y, width: def.width, height: def.height },
- viewport,
- );
- const bmp = annotationCanvasMap.get(def.pdfjsId);
- ctx.save();
- if (bmp) {
- ctx.drawImage(bmp, s.left, s.top, s.width, s.height);
- } else {
- ctx.strokeStyle = "#666";
- ctx.lineWidth = 1;
- ctx.setLineDash([3, 3]);
- ctx.strokeRect(s.left, s.top, s.width, s.height);
- }
- ctx.restore();
- break;
- }
}
}
}
@@ -1977,20 +1933,13 @@ function renderAnnotation(
viewport: { width: number; height: number; scale: number },
): HTMLElement[] {
switch (def.type) {
- case "highlight": {
- // Force translucency: def.color is an opaque hex (e.g. "#ffff00"), which
- // would override the rgba()/mix-blend-mode in CSS and hide the text.
- const rgb = def.color ? cssColorToRgb(def.color) : null;
- const bg = rgb
- ? `rgba(${Math.round(rgb.r * 255)}, ${Math.round(rgb.g * 255)}, ${Math.round(rgb.b * 255)}, 0.35)`
- : undefined;
+ case "highlight":
return renderRectsAnnotation(
def.rects,
"annotation-highlight",
viewport,
- bg ? { background: bg } : {},
+ def.color ? { background: def.color } : {},
);
- }
case "underline":
return renderRectsAnnotation(
def.rects,
@@ -2020,8 +1969,6 @@ function renderAnnotation(
return [renderLineAnnotation(def, viewport)];
case "image":
return [renderImageAnnotation(def, viewport)];
- case "imported":
- return [renderImportedAnnotation(def, viewport)];
}
}
@@ -2210,45 +2157,6 @@ function renderImageAnnotation(
return el;
}
-/**
- * Per-annotation appearance bitmaps from page.render(). Keyed by pdf.js
- * annotation id (e.g. "118R"). Populated for the current page only —
- * cleared at the start of each renderPage().
- */
-const annotationCanvasMap = new Map();
-
-function renderImportedAnnotation(
- def: ImportedAnnotation,
- viewport: { width: number; height: number; scale: number },
-): HTMLElement {
- const screen = pdfRectToScreen(
- { x: def.x, y: def.y, width: def.width, height: def.height },
- viewport,
- );
- const el = document.createElement("div");
- el.className = "annotation-imported";
- el.style.left = `${screen.left}px`;
- el.style.top = `${screen.top}px`;
- el.style.width = `${screen.width}px`;
- el.style.height = `${screen.height}px`;
- el.title = `${def.subtype} (from PDF)`;
-
- // page.render() may or may not have produced a separate canvas for this
- // annotation (hasOwnCanvas depends on the PDF's flags). When it did, use
- // it as a pixel-faithful body; when it didn't, the appearance is on the
- // main canvas already, so leave the box transparent — it still captures
- // clicks for select/delete.
- const canvas = annotationCanvasMap.get(def.pdfjsId);
- if (canvas) {
- canvas.style.width = "100%";
- canvas.style.height = "100%";
- canvas.style.display = "block";
- canvas.style.pointerEvents = "none";
- el.appendChild(canvas);
- }
- return el;
-}
-
// =============================================================================
// Annotation CRUD
// =============================================================================
@@ -2502,99 +2410,17 @@ async function renderPageOffscreen(pageNum: number): Promise {
return dataUrl.split(",")[1];
}
-/**
- * Snapshot the live viewer for `interact({action:"get_viewer_state"})`.
- *
- * Selection is read from `window.getSelection()` at call time — no caching;
- * if the user navigated away or nothing is selected, `selection` is `null`.
- * `boundingRect` is in model coords (PDF points, origin top-left, y-down) so
- * it can be fed straight back into `add_annotations`.
- */
-async function handleGetViewerState(requestId: string): Promise {
- const CONTEXT_CHARS = 200;
-
- let selection: {
- text: string;
- contextBefore: string;
- contextAfter: string;
- boundingRect: { x: number; y: number; width: number; height: number };
- } | null = null;
-
- const sel = window.getSelection();
- const selectedText = sel?.toString().replace(/\s+/g, " ").trim();
- if (sel && selectedText && sel.rangeCount > 0) {
- // Only treat it as a PDF selection if it lives inside the text layer of
- // the rendered page (not the toolbar, search box, etc.).
- const range = sel.getRangeAt(0);
- const anchor =
- range.commonAncestorContainer.nodeType === Node.ELEMENT_NODE
- ? (range.commonAncestorContainer as Element)
- : range.commonAncestorContainer.parentElement;
- if (anchor && textLayerEl.contains(anchor)) {
- // Context: locate selection in the page's extracted text and slice
- // ±CONTEXT_CHARS around it. Falls back to empty strings if fuzzy
- // match fails (still return text + rect — they're the load-bearing
- // bits).
- const pageText = pageTextCache.get(currentPage) ?? "";
- const loc = findSelectionInText(pageText, selectedText);
- const contextBefore = loc
- ? pageText.slice(Math.max(0, loc.start - CONTEXT_CHARS), loc.start)
- : "";
- const contextAfter = loc
- ? pageText.slice(loc.end, loc.end + CONTEXT_CHARS)
- : "";
-
- // Single bounding box, page-relative model coords. getBoundingClientRect
- // is viewport-relative; subtract the page-wrapper origin then divide by
- // scale → PDF points (top-left origin, y-down — matches the coord
- // system documented in the interact tool description).
- const r = range.getBoundingClientRect();
- const origin = pageWrapperEl.getBoundingClientRect();
- const round = (n: number) => Math.round(n * 100) / 100;
- selection = {
- text: selectedText,
- contextBefore,
- contextAfter,
- boundingRect: {
- x: round((r.left - origin.left) / scale),
- y: round((r.top - origin.top) / scale),
- width: round(r.width / scale),
- height: round(r.height / scale),
- },
- };
- }
- }
-
- const state = {
- currentPage,
- pageCount: totalPages,
- zoom: Math.round(scale * 100),
- displayMode: currentDisplayMode,
- selectedAnnotationIds: [...selectedAnnotationIds],
- selection,
- };
-
- await app.callServerTool({
- name: "submit_viewer_state",
- arguments: { requestId, state: JSON.stringify(state, null, 2) },
- });
-}
-
-/**
- * Collect text and/or screenshots for a set of page intervals.
- * Shared by the server-driven `get_pages` command (via handleGetPages)
- * and the app-registered `get_text` / `get_screenshot` tools.
- */
-async function collectPageData(
- intervals: Array<{ start?: number; end?: number }>,
- getText: boolean,
- getScreenshots: boolean,
-): Promise> {
- const allPages = expandIntervals(intervals);
+async function handleGetPages(cmd: {
+ requestId: string;
+ intervals: Array<{ start?: number; end?: number }>;
+ getText: boolean;
+ getScreenshots: boolean;
+}): Promise {
+ const allPages = expandIntervals(cmd.intervals);
const pages = allPages.slice(0, MAX_GET_PAGES);
log.info(
- `collectPageData: ${pages.length} pages (${pages[0]}..${pages[pages.length - 1]}), text=${getText}, screenshots=${getScreenshots}`,
+ `get_pages: ${pages.length} pages (${pages[0]}..${pages[pages.length - 1]}), text=${cmd.getText}, screenshots=${cmd.getScreenshots}`,
);
const results: Array<{
@@ -2608,7 +2434,7 @@ async function collectPageData(
page: pageNum,
};
- if (getText) {
+ if (cmd.getText) {
// Use cached text if available, otherwise extract on the fly
let text = pageTextCache.get(pageNum);
if (text == null && pdfDocument) {
@@ -2621,7 +2447,7 @@ async function collectPageData(
pageTextCache.set(pageNum, text);
} catch (err) {
log.error(
- `collectPageData: text extraction failed for page ${pageNum}:`,
+ `get_pages: text extraction failed for page ${pageNum}:`,
err,
);
text = "";
@@ -2630,35 +2456,17 @@ async function collectPageData(
entry.text = text ?? "";
}
- if (getScreenshots) {
+ if (cmd.getScreenshots) {
try {
entry.image = await renderPageOffscreen(pageNum);
} catch (err) {
- log.error(
- `collectPageData: screenshot failed for page ${pageNum}:`,
- err,
- );
+ log.error(`get_pages: screenshot failed for page ${pageNum}:`, err);
}
}
results.push(entry);
}
- return results;
-}
-
-async function handleGetPages(cmd: {
- requestId: string;
- intervals: Array<{ start?: number; end?: number }>;
- getText: boolean;
- getScreenshots: boolean;
-}): Promise {
- const results = await collectPageData(
- cmd.intervals,
- cmd.getText,
- cmd.getScreenshots,
- );
-
// Submit results back to server
try {
await app.callServerTool({
@@ -2686,60 +2494,52 @@ function annotationStorageKey(): string | null {
}
/**
- * Import one page's native annotations into the baseline. Called lazily from
- * renderPage() so we don't walk every page (and pull most of the file via
- * range requests) before the user sees anything. Idempotent per page.
+ * Import annotations from the loaded PDF to establish the baseline.
+ * These are the annotations that exist in the PDF file itself.
*/
-function scanPageBaselineAnnotations(
- pageNum: number,
- annotations: unknown[],
-): void {
- if (baselineScannedPages.has(pageNum)) return;
- baselineScannedPages.add(pageNum);
- let imported = 0;
- for (let i = 0; i < annotations.length; i++) {
- // Isolate each annotation: a malformed one must not bubble up to the
- // caller's form-layer try in renderPage() (which would skip
- // AnnotationLayer.render and hide form widgets for the whole page).
+async function loadBaselineAnnotations(
+ doc: pdfjsLib.PDFDocumentProxy,
+): Promise {
+ pdfBaselineAnnotations = [];
+ for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
try {
- const ann = annotations[i] as {
- annotationType?: number;
- subtype?: string;
- name?: string;
- rect?: number[];
- };
- const def = importPdfjsAnnotation(ann, pageNum, i);
- if (def) {
- pdfBaselineAnnotations.push(def);
- imported++;
- if (!annotationMap.has(def.id) && !restoredRemovedIds.has(def.id)) {
- annotationMap.set(def.id, { def, elements: [] });
+ const page = await doc.getPage(pageNum);
+ const annotations = await page.getAnnotations();
+ for (let i = 0; i < annotations.length; i++) {
+ const ann = annotations[i];
+ const def = importPdfjsAnnotation(ann, pageNum, i);
+ if (def) {
+ pdfBaselineAnnotations.push(def);
+ // Add to annotationMap if not already present (from localStorage restore)
+ if (!annotationMap.has(def.id)) {
+ annotationMap.set(def.id, { def, elements: [] });
+ }
+ } else if (ann.annotationType !== 20) {
+ // Widget (type 20) is expected to be skipped; anything else we
+ // don't import will still be painted by page.render() onto the
+ // canvas as unselectable pixels. Log so we can diagnose
+ // "ghost annotations" (visible but not in panel, not clickable).
+ log.info(
+ `[WARN] Baseline: skipped PDF annotation on page ${pageNum}`,
+ `type=${ann.annotationType}`,
+ `subtype=${ann.subtype ?? "?"}`,
+ `name=${ann.name ?? "?"}`,
+ `rect=${ann.rect ? JSON.stringify(ann.rect) : "none"}`,
+ );
}
- } else if (ann.annotationType !== 20) {
- // Widget (type 20) is expected to be skipped; anything else we
- // don't import will still be painted by page.render() onto the
- // canvas as unselectable pixels. Log so we can diagnose
- // "ghost annotations" (visible but not in panel, not clickable).
- log.info(
- `[WARN] Baseline: skipped PDF annotation on page ${pageNum}`,
- `type=${ann.annotationType}`,
- `subtype=${ann.subtype ?? "?"}`,
- `name=${ann.name ?? "?"}`,
- `rect=${ann.rect ? JSON.stringify(ann.rect) : "none"}`,
- );
}
} catch (err) {
- log.info(`Baseline: page ${pageNum} annotation import failed`, err);
- }
- }
- if (imported > 0) {
- try {
- updateAnnotationsBadge();
- renderAnnotationPanel();
- } catch (err) {
- log.info(`Baseline: page ${pageNum} panel update failed`, err);
+ // Log the error — a thrown import for one annotation silently
+ // drops the REST of that page's annotations too.
+ log.info(
+ `[WARN] Baseline: page ${pageNum} annotation import failed:`,
+ err,
+ );
}
}
+ log.info(
+ `Loaded ${pdfBaselineAnnotations.length} baseline annotations from PDF`,
+ );
}
function persistAnnotations(): void {
@@ -2755,20 +2555,6 @@ function persistAnnotations(): void {
pdfBaselineFormValues,
);
- // computeDiff only sees baseline ids from pages we've already scanned.
- // Carry forward restored tombstones for unvisited pages so the first
- // persist after restore doesn't drop them. Once every page is scanned the
- // baseline is complete and computeDiff is authoritative on its own —
- // dropping the carry-forward then also stops a stale id (no longer in the
- // file) from pinning dirty=true forever.
- if (baselineScannedPages.size < totalPages) {
- for (const id of restoredRemovedIds) {
- if (!annotationMap.has(id) && !diff.removed.includes(id)) {
- diff.removed.push(id);
- }
- }
- }
-
// Dirty tracks whether there are unsaved changes. Undoing back to baseline
// yields an empty diff → clean again → save button disables.
if (!isRestoring) setDirty(!isDiffEmpty(diff));
@@ -2794,11 +2580,11 @@ function restoreAnnotations(): void {
const diff = deserializeDiff(raw);
// Merge baseline + diff. The loop below is add-only, so we MUST also
- // delete: the per-page baseline scan re-seeds annotationMap with every
- // native id it encounters — including ones in diff.removed. Without the
- // deletes here AND the restoredRemovedIds tombstones below, the zombie
- // survives, and the next persistAnnotations() sees it in currentIds →
- // computeDiff produces removed=[] → the deletion is permanently lost.
+ // delete: loadBaselineAnnotations() runs between the two restore calls
+ // and re-seeds annotationMap with every baseline id — including the
+ // ones in diff.removed. Without this, the zombie survives the restore,
+ // and the next persistAnnotations() sees it in currentIds → computeDiff
+ // produces removed=[] → the deletion is permanently lost from storage.
const merged = mergeAnnotations(pdfBaselineAnnotations, diff);
for (const def of merged) {
if (!annotationMap.has(def.id)) {
@@ -2807,9 +2593,6 @@ function restoreAnnotations(): void {
}
for (const id of diff.removed) {
annotationMap.delete(id);
- // Tombstone so the lazy per-page baseline scan (which runs AFTER this
- // restore) doesn't resurrect it.
- restoredRemovedIds.add(id);
}
// Restore form fields
@@ -2882,8 +2665,7 @@ function normaliseFieldValue(
*/
async function buildFieldNameMap(
doc: pdfjsLib.PDFDocumentProxy,
-): Promise {
- let pushedToStorage = false;
+): Promise {
fieldNameToIds.clear();
radioButtonValues.clear();
fieldNameToPage.clear();
@@ -2901,14 +2683,6 @@ async function buildFieldNameMap(
// getFieldObjects may fail on some PDFs
}
- // No AcroForm → nothing to map. Skip the per-page widget walk so form-free
- // PDFs (the common large case) don't pull every page after first paint.
- // getFieldObjects() itself only reads the catalog/AcroForm dict via range
- // transport, so this gate is cheap.
- if (!cachedFieldObjects || Object.keys(cachedFieldObjects).length === 0) {
- return false;
- }
-
// Scan every page's widget annotations to collect the CORRECT storage keys,
// plus labels, pages, positions, AND fieldValue (what the widget renders
// — which can differ from getFieldObjects().value if the PDF is internally
@@ -2987,16 +2761,12 @@ async function buildFieldNameMap(
if (!widgetIds) continue; // no widget → not rendered anyway
// Type comes from getFieldObjects (widget annot data doesn't have it).
- // Value: prefer the AcroForm field-tree value over the widget's
- // fieldValue. pdf-lib's save() can leave a page widget pointing at a
- // stale /V while the field tree has the new one (seen with comb text
- // fields), and getAnnotations() reads the widget. If the two disagree
- // we push the field-tree value into annotationStorage so the rendered
- // input matches what's actually in /AcroForm.
+ // Value comes from the widget annotation (fall back to field-dict if
+ // the widget didn't expose one).
const type = fieldArr.find((f) => f.type)?.type;
- const fieldTreeRaw = fieldArr.find((f) => f.value != null)?.value;
- const widgetRaw = widgetFieldValues.get(name);
- const raw = fieldTreeRaw ?? widgetRaw;
+ const raw = widgetFieldValues.has(name)
+ ? widgetFieldValues.get(name)
+ : fieldArr.find((f) => f.value != null)?.value;
const v = normaliseFieldValue(type, raw);
if (v !== null) {
pdfBaselineFormValues.set(name, v);
@@ -3004,13 +2774,6 @@ async function buildFieldNameMap(
// restored localStorage diff (applied in restoreAnnotations) will
// overwrite specific fields the user changed.
if (!formFieldValues.has(name)) formFieldValues.set(name, v);
- // Widget out of sync with field tree → force storage so
- // AnnotationLayer renders the field-tree value, not the stale
- // widget. (syncFormValuesToStorage skips baseline==current.)
- if (fieldTreeRaw != null && fieldTreeRaw !== widgetRaw) {
- setFieldInStorage(name, v);
- pushedToStorage = true;
- }
}
// Skip parent entries with no concrete id (radio groups: the /T tree
@@ -3025,7 +2788,6 @@ async function buildFieldNameMap(
}
log.info(`Built field name map: ${fieldNameToIds.size} fields`);
- return pushedToStorage;
}
/**
@@ -3113,39 +2875,15 @@ async function getAnnotatedPdfBytes(): Promise {
}
}
- // Baseline annotations the user deleted: strip their refs from /Annots so
- // they don't reappear on reload. Include restored tombstones for pages we
- // haven't scanned yet — those ids aren't in pdfBaselineAnnotations but the
- // ref is still parseable from the id string. Ids without a recoverable ref
- // (page-index fallback) can't be removed by-ref and are skipped.
- const removedIds = new Set();
- for (const a of pdfBaselineAnnotations) {
- if (!annotationMap.has(a.id)) removedIds.add(a.id);
- }
- for (const id of restoredRemovedIds) {
- if (!annotationMap.has(id)) removedIds.add(id);
- }
- const removedRefs = [...removedIds]
- .map(parseAnnotationRef)
- .filter((r): r is NonNullable => r !== null);
-
- // Only write fields that actually changed vs. what's already in the PDF.
- // Unchanged fields are no-ops at best, and at worst trip pdf-lib edge
- // cases (max-length text, missing /Yes appearance, …) on fields the user
- // never touched — which, before the per-field catch in
- // buildAnnotatedPdfBytes, aborted every subsequent field.
- //
- // Fields the user cleared (present in baseline, absent from formFieldValues
- // after clearAllItems()) still need an explicit "" / false so pdf-lib
- // overwrites the original /V instead of leaving it intact.
- const formFieldsOut = new Map();
- for (const [name, value] of formFieldValues) {
- if (pdfBaselineFormValues.get(name) !== value) {
- formFieldsOut.set(name, value);
- }
- }
+ // buildAnnotatedPdfBytes gates on formFields.size > 0 and only writes
+ // entries present in the map. After clearAllItems() the map is empty →
+ // zero setText/uncheck calls → pdf-lib leaves original /V intact →
+ // the "stripped PDF" we promised keeps all its form data. To actually
+ // clear, send an explicit sentinel for every baseline field the user
+ // dropped: "" for text, false for checkbox (matching baseline type).
+ const formFieldsOut = new Map(formFieldValues);
for (const [name, baselineValue] of pdfBaselineFormValues) {
- if (!formFieldValues.has(name)) {
+ if (!formFieldsOut.has(name)) {
formFieldsOut.set(name, typeof baselineValue === "boolean" ? false : "");
}
}
@@ -3153,7 +2891,6 @@ async function getAnnotatedPdfBytes(): Promise {
fullBytes as Uint8Array,
annotations,
formFieldsOut,
- removedRefs,
);
}
@@ -3195,6 +2932,15 @@ async function savePdf(): Promise {
const sc = result.structuredContent as { mtimeMs?: number } | undefined;
lastSavedMtime = sc?.mtimeMs ?? null;
+ // Rebase: the file on disk now contains our annotations + form values.
+ // Update the baseline so future diffs are relative to what was saved.
+ pdfBaselineAnnotations = [...annotationMap.values()].map((t) => ({
+ ...t.def,
+ }));
+ pdfBaselineFormValues.clear();
+ for (const [k, v] of formFieldValues) pdfBaselineFormValues.set(k, v);
+
+ setDirty(false); // → updateSaveBtn() disables button
const key = annotationStorageKey();
if (key) {
try {
@@ -3203,13 +2949,6 @@ async function savePdf(): Promise {
/* ignore */
}
}
- // Reload from the bytes we just wrote. The previous approach (rebase
- // baselines but keep the old pdfDocument) drifts: subsequent renders
- // still rasterize stripped annotations from the old bytes, and the
- // field/widget split that pdf-lib's save can create isn't reflected
- // until reload anyway. Reload makes "what you see = what's on disk"
- // an invariant. (file_changed echo is suppressed by lastSavedMtime.)
- await reloadPdf();
}
} catch (err) {
log.error("Save failed:", err);
@@ -3313,15 +3052,8 @@ async function renderPage() {
// resize so the size handoff is atomic.
pageWrapperEl.style.transform = "";
- // Retina: pass dpr via page.render's `transform` (NOT ctx.scale).
- // pdf.js sizes per-annotation canvases as
- // width = rectW * outputScaleX * viewport.scale
- // and outputScaleX is read from transform[0] (defaults to 1). A bare
- // ctx.scale(dpr,dpr) leaves outputScaleX at 1, so the
- // annotationCanvasMap canvases get a half-sized backing store on
- // retina while their internal setTransform IS dpr-aware → the
- // appearance renders 2× too big into a 1× buffer → cropped/shifted.
- const dprTransform = dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined;
+ // Scale context for retina
+ ctx.scale(dpr, dpr);
// Clear and setup text layer
textLayerEl.innerHTML = "";
@@ -3330,29 +3062,11 @@ async function renderPage() {
// Set --scale-factor so CSS font-size/transform rules work correctly.
textLayerEl.style.setProperty("--scale-factor", `${scale}`);
- // Render canvas - track the task so we can cancel it.
- //
- // annotationCanvasMap: pdf.js diverts annotations whose appearance needs
- // its own bitmap (Stamp/Ink/FreeText/etc. with hasOwnCanvas) into
- // per-id canvases instead of compositing onto the main canvas.
- // renderImportedAnnotation() pulls from this map so those annotations
- // become movable DOM elements with pixel-faithful visuals — instead of
- // unselectable canvas pixels (the old "ghost annotation" problem) or
- // our lossy text-label re-render.
- annotationCanvasMap.clear();
+ // Render canvas - track the task so we can cancel it
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const renderTask = (page.render as any)({
canvasContext: ctx,
viewport,
- transform: dprTransform,
- annotationCanvasMap,
- // isEditing forces hasOwnCanvas=true for stamps regardless of /F
- // NoRotate (StampAnnotation.mustBeViewedWhenEditing in pdf.worker).
- // Without this, stamps without NoRotate composite onto the main canvas
- // and deleting the "imported" overlay leaves an unclickable pixel
- // behind. Other markup types still gate on noRotate; for those the
- // overlay stays a transparent click-box (delete is UI-only until save).
- isEditing: true,
});
currentRenderTask = renderTask;
@@ -3410,9 +3124,6 @@ async function renderPage() {
formLayerEl.style.setProperty("--total-scale-factor", `${scale}`);
try {
const annotations = await page.getAnnotations();
- // Lazy baseline import — piggyback on the annotations we just fetched
- // for this page instead of walking all pages upfront.
- scanPageBaselineAnnotations(pageToRender, annotations);
if (annotations.length > 0) {
const linkService = {
getDestinationHash: () => "#",
@@ -3436,15 +3147,8 @@ async function renderPage() {
commentManager: null,
} as any);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
- // Only feed Widgets (form fields) here. Markup annotations are
- // owned by #annotation-layer; letting AnnotationLayer create
- // elements for them in #form-layer adds invisible
- // pointer-events:auto boxes that steal clicks from our overlays.
- const widgetAnns = annotations.filter(
- (a: { subtype?: string }) => a.subtype === "Widget",
- );
await annotationLayer.render({
- annotations: widgetAnns,
+ annotations,
div: formLayerEl,
page,
viewport,
@@ -3620,10 +3324,6 @@ function zoomIn() {
function zoomOut() {
userHasZoomed = true;
- // Intentionally NOT floored at fit-to-page (unlike pinch). Hosts may
- // overlay UI on the iframe without reporting it in safeAreaInsets, so
- // "fit" can leave the page bottom hidden; the button is the escape hatch
- // to shrink past it. Pinch still rubber-bands at fit (see commitPinch).
scale = Math.max(scale - 0.25, ZOOM_MIN);
renderPage().then(scrollSelectionIntoView);
}
@@ -3699,10 +3399,6 @@ formLayerEl.addEventListener("input", (e) => {
if (!target.checked) return; // unchecking siblings — ignore
const wid = target.getAttribute("data-element-id");
value = (wid && radioButtonValues.get(wid)) ?? target.value;
- } else if (target instanceof HTMLSelectElement && target.multiple) {
- // .value on a is only the first option; join them all
- // so save can select() the full set on a PDFOptionList.
- value = Array.from(target.selectedOptions, (o) => o.value).join(",");
} else {
value = target.value;
}
@@ -4065,39 +3761,18 @@ let pinchStartScale = 1.0;
let previewScale = 1.0;
/** Debounce timer — wheel events have no end event, so we wait for quiet. */
let pinchSettleTimer: ReturnType | null = null;
-/** computeFitScale() snapshot at gesture start (async — may be null briefly). */
-let fitScaleAtPinchStart: number | null = null;
-/** Guards against firing toggleFullscreen() once per wheel event during a
- * single inline pinch-in gesture. */
-let modeTransitionInFlight = false;
function beginPinch() {
pinchStartScale = scale;
previewScale = scale;
- // Seed synchronously when we can (at fit ⇔ !userHasZoomed) so the very
- // first updatePinch already has the right floor — avoids a one-frame
- // jitter when the async computeFitScale resolves mid-gesture.
- fitScaleAtPinchStart = userHasZoomed ? null : scale;
- void computeFitScale().then((s) => (fitScaleAtPinchStart = s));
// transform-origin matches the flex layout's anchor (justify-content:
// center, align-items: flex-start) so the preview and the committed
// canvas grow from the same point — otherwise the page jumps on release.
pageWrapperEl.style.transformOrigin = "50% 0";
}
-/** Fit-to-page floor for fullscreen (committed scale never goes below this).
- * The preview is allowed to overshoot down to 0.75×fit for rubber-band
- * feedback; release below 0.9×fit exits to inline, otherwise snaps to fit. */
-function pinchFitFloor(): number | null {
- return currentDisplayMode === "fullscreen" ? fitScaleAtPinchStart : null;
-}
-
function updatePinch(nextScale: number) {
- const fit = pinchFitFloor();
- // Rubber-band: preview may dip to 0.75×fit so the user sees the page pull
- // away as they pinch out. Committed scale is clamped to fit in commitPinch.
- const previewFloor = fit !== null ? fit * 0.75 : ZOOM_MIN;
- previewScale = Math.min(ZOOM_MAX, Math.max(previewFloor, nextScale));
+ previewScale = Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, nextScale));
// Transform is RELATIVE to the rendered canvas (which sits at
// pinchStartScale), so a previewScale equal to pinchStartScale → ratio 1.
pageWrapperEl.style.transform = `scale(${previewScale / pinchStartScale})`;
@@ -4105,37 +3780,13 @@ function updatePinch(nextScale: number) {
}
function commitPinch() {
- const fit = pinchFitFloor();
- // Pinched out past fit (page visibly pulled away) → exit fullscreen.
- // Only when the gesture *started* near fit, so a single big pinch-out
- // from deep zoom lands at fit instead of ejecting unexpectedly.
- if (
- fit !== null &&
- pinchStartScale <= fit * 1.05 &&
- previewScale < fit * 0.9
- ) {
+ if (Math.abs(previewScale - scale) < 0.01) {
+ // Dead-zone — no re-render. Clear here since renderPage won't run.
pageWrapperEl.style.transform = "";
- userHasZoomed = false; // let refitScale() size the inline view
- forceNextResizeRefit = true; // ResizeObserver inline path ignores shrinks
- modeTransitionInFlight = true;
- void toggleFullscreen().finally(() => {
- setTimeout(() => (modeTransitionInFlight = false), 250);
- });
- return;
- }
- // Committed scale never below fit in fullscreen — overshoot snaps back.
- const target =
- fit !== null
- ? Math.max(fit, previewScale)
- : Math.max(ZOOM_MIN, previewScale);
- if (Math.abs(target - scale) < 0.01) {
- // Snap-back / dead-zone — no re-render needed.
- pageWrapperEl.style.transform = "";
- zoomLevelEl.textContent = `${Math.round(scale * 100)}%`;
return;
}
userHasZoomed = true;
- scale = target;
+ scale = previewScale;
// renderPage clears the transform in the same frame as the canvas
// resize (after its first await) so there's no snap-back.
renderPage().then(scrollSelectionIntoView);
@@ -4153,23 +3804,8 @@ canvasContainerEl.addEventListener(
// Trackpad pinch arrives as wheel with ctrlKey set (Chrome/FF/Edge on
// macOS+Windows, Safari on macOS). MUST check before the deltaX/deltaY
// comparison below — pinch deltas come through on deltaY.
- if (e.ctrlKey) {
+ if (e.ctrlKey && currentDisplayMode === "fullscreen") {
e.preventDefault();
- if (currentDisplayMode !== "fullscreen") {
- // Inline: pinch-in (deltaY<0) is a request to go fullscreen.
- // Pinch-out is ignored — nothing smaller than inline.
- if (e.deltaY < 0 && !modeTransitionInFlight) {
- modeTransitionInFlight = true;
- void toggleFullscreen().finally(() => {
- // Hold the latch through the settle window so the tail of the
- // gesture doesn't immediately start zooming the new fullscreen
- // view (or, worse, re-toggle).
- setTimeout(() => (modeTransitionInFlight = false), 250);
- });
- }
- return;
- }
- if (modeTransitionInFlight) return; // swallow gesture tail post-toggle
if (pinchSettleTimer === null) beginPinch();
// exp(-deltaY * k) makes equal-magnitude in/out deltas inverse —
// pinch out then back lands where you started. Clamp per event so a
@@ -4190,15 +3826,10 @@ canvasContainerEl.addEventListener(
// Only intercept horizontal scroll, let vertical scroll through
if (Math.abs(e.deltaX) <= Math.abs(e.deltaY)) return;
- // If the page overflows horizontally, let native panning handle it
- // (no page changes). Checking actual overflow rather than `scale > 1.0`
- // because fullscreen fit-scale is often >100% with the page still fully
- // visible — we want swipe-to-page there. +1 absorbs sub-pixel rounding.
- if (canvasContainerEl.scrollWidth > canvasContainerEl.clientWidth + 1) {
- return;
- }
+ // When zoomed, let natural panning happen (no page changes)
+ if (scale > 1.0) return;
- // No horizontal overflow → swipe changes pages.
+ // At 100% zoom, handle page navigation
e.preventDefault();
horizontalScrollAccumulator += e.deltaX;
if (horizontalScrollAccumulator > SCROLL_THRESHOLD) {
@@ -4227,7 +3858,7 @@ canvasContainerEl.addEventListener(
"touchstart",
(event) => {
const e = event as TouchEvent;
- if (e.touches.length !== 2) return;
+ if (e.touches.length !== 2 || currentDisplayMode !== "fullscreen") return;
// No preventDefault here — keep iOS Safari happy. We block native
// pinch-zoom via touch-action CSS + preventDefault on touchmove.
touchStartDist = touchDist(e.touches);
@@ -4242,21 +3873,7 @@ canvasContainerEl.addEventListener(
const e = event as TouchEvent;
if (e.touches.length !== 2 || touchStartDist === 0) return;
e.preventDefault(); // stop the browser zooming the whole viewport
- const ratio = touchDist(e.touches) / touchStartDist;
- if (currentDisplayMode !== "fullscreen") {
- // Inline: a clear pinch-in means "go fullscreen". 1.15× threshold
- // avoids triggering on jittery two-finger taps/scrolls.
- if (ratio > 1.15 && !modeTransitionInFlight) {
- modeTransitionInFlight = true;
- touchStartDist = 0; // end this gesture; fullscreen will refit
- pageWrapperEl.style.transform = "";
- void toggleFullscreen().finally(() => {
- setTimeout(() => (modeTransitionInFlight = false), 250);
- });
- }
- return;
- }
- updatePinch(pinchStartScale * ratio);
+ updatePinch(pinchStartScale * (touchDist(e.touches) / touchStartDist));
},
{ passive: false },
);
@@ -4267,11 +3884,6 @@ canvasContainerEl.addEventListener("touchend", (event) => {
// REMAINING set — lifting one of two leaves length 1.
if (touchStartDist === 0 || e.touches.length >= 2) return;
touchStartDist = 0;
- if (currentDisplayMode !== "fullscreen") {
- // Inline pinch that didn't cross the threshold — discard preview.
- pageWrapperEl.style.transform = "";
- return;
- }
commitPinch();
});
@@ -4457,8 +4069,6 @@ async function reloadPdf(): Promise {
undoStack.length = 0;
redoStack.length = 0;
pdfBaselineAnnotations = [];
- baselineScannedPages.clear();
- restoredRemovedIds.clear();
pdfBaselineFormValues.clear();
pageTextCache.clear();
pageTextItemsCache.clear();
@@ -4502,14 +4112,13 @@ async function reloadPdf(): Promise {
log.info("PDF reloaded:", totalPages, "pages,", totalBytes, "bytes");
showViewer();
- // Render immediately — baseline-annotation scan now happens per-page
- // inside renderPage(); buildFieldNameMap below early-returns when no
- // AcroForm is present. See same pattern in the initial-load path.
+ // Render immediately — annotation/form scans below are O(numPages) and
+ // do NOT block the canvas. See same pattern in the initial-load path.
await renderPage();
- const seeded = await buildFieldNameMap(document);
+ await loadBaselineAnnotations(document);
+ await buildFieldNameMap(document);
syncFormValuesToStorage();
- if (seeded) await renderPage();
updateAnnotationsBadge();
renderAnnotationPanel();
@@ -4562,11 +4171,6 @@ async function loadPdfProgressively(urlToLoad: string): Promise<{
const loadingTask = pdfjsLib.getDocument({
range: transport,
standardFontDataUrl: STANDARD_FONT_DATA_URL,
- // Only fetch ranges renderPage()/getFieldObjects() actually ask for.
- // Without these pdfjs background-prefetches the whole file regardless of
- // the per-page lazy scans below.
- disableAutoFetch: true,
- disableStream: true,
});
try {
@@ -4731,25 +4335,17 @@ app.ontoolresult = async (result: CallToolResult) => {
scale = fitScale;
log.info("Initial fit scale:", scale);
}
- // Restore any persisted user diff BEFORE first render so the per-page
- // baseline scan inside renderPage() can honour the removed-id tombstones
- // and not resurrect annotations the user deleted last session.
- // restoreAnnotations is sync (localStorage read) so first paint is not
- // delayed.
- restoreAnnotations();
await renderPage();
+ // Import annotations from the PDF to establish baseline
+ await loadBaselineAnnotations(document);
+ // Restore any persisted user diff
+ restoreAnnotations();
+
// Build field name → annotation ID mapping for form filling
- const seeded = await buildFieldNameMap(document);
+ await buildFieldNameMap(document);
// Pre-populate annotationStorage from restored formFieldValues
syncFormValuesToStorage();
- // buildFieldNameMap may have pushed AcroForm-tree values into storage
- // (when the page widget's /V is stale vs the field dict — pdf-lib's save
- // can leave them split). The first renderPage above ran BEFORE that, so
- // the form layer shows the stale widget value. Re-render so it picks up
- // storage. Only when something was actually seeded — most PDFs don't hit
- // this and the extra render would be pure waste.
- if (seeded) await renderPage();
updateAnnotationsBadge();
// Save button visibility driven by setDirty()/updateSaveBtn();
@@ -4976,23 +4572,6 @@ async function processCommands(commands: PdfCommand[]): Promise {
.catch(() => {});
}
break;
- case "get_viewer_state":
- // Same await-before-next-poll discipline as get_pages/save_as.
- try {
- await handleGetViewerState(cmd.requestId);
- } catch (err) {
- log.error("get_viewer_state failed — submitting error:", err);
- await app
- .callServerTool({
- name: "submit_viewer_state",
- arguments: {
- requestId: cmd.requestId,
- error: err instanceof Error ? err.message : String(err),
- },
- })
- .catch(() => {});
- }
- break;
case "file_changed": {
// Skip our own save_pdf echo: either save is still in flight, or the
// event's mtime matches what save_pdf just returned.
@@ -5128,20 +4707,16 @@ function handleHostContextChanged(ctx: McpUiHostContext) {
if (panelState.open) {
setAnnotationPanelOpen(true);
}
- if (!isFullscreen) {
- // Fullscreen zoom level is meaningless inline — always refit on exit,
- // however it was triggered (pinch, button, host Escape/×).
- userHasZoomed = false;
- // The iframe shrink lands after this handler; let the ResizeObserver
- // do one refit on that shrink (its inline branch normally ignores
- // shrinks to avoid a requestFitToContent feedback loop).
- forceNextResizeRefit = true;
- }
if (wasFullscreen !== isFullscreen) {
- // Fast-path refit (computeFitScale reads displayMode). The iframe may
- // not have its final size yet — the ResizeObserver one-shot above
- // covers the inline-shrink case once it does.
- void refitScale();
+ // Mode changed → refit. computeFitScale reads displayMode, so
+ // this scales UP to fill on enter and back DOWN to ≤1.0 on exit.
+ // refitScale → renderPage → requestFitToContent handles the
+ // host-resize on exit. If userHasZoomed, refit no-ops; on exit fall
+ // back to requestFitToContent so the iframe still shrinks to whatever
+ // scale the user left it at.
+ void refitScale().then(() => {
+ if (!isFullscreen && userHasZoomed) requestFitToContent();
+ });
}
updateFullscreenButton();
}
@@ -5173,398 +4748,6 @@ app.onteardown = async () => {
app.onhostcontextchanged = handleHostContextChanged;
-// =============================================================================
-// App-registered tools — 1:1 with the server's `interact` commands.
-//
-// Each tool constructs the matching PdfCommand and dispatches through
-// processCommands(), so the command-handling logic lives in exactly one
-// place. The server-side `interact` tool remains for hosts that don't
-// support app-registered tools.
-// =============================================================================
-
-/** Shared zod shapes mirroring server.ts interact schema. */
-const FormFieldSchema = z.object({
- name: z.string(),
- value: z.union([z.string(), z.boolean()]),
-});
-const PageIntervalSchema = z.object({
- start: z.number().min(1).optional(),
- end: z.number().min(1).optional(),
-});
-
-/** Dispatch a command via processCommands and return a text result. */
-async function runCommand(
- cmd: PdfCommand,
- okText: string | (() => string),
-): Promise {
- if (!pdfDocument) {
- return {
- content: [{ type: "text" as const, text: "Error: No document loaded" }],
- isError: true,
- };
- }
- await processCommands([cmd]);
- const text = typeof okText === "function" ? okText() : okText;
- return { content: [{ type: "text" as const, text }] };
-}
-
-app.registerTool(
- "get-document-info",
- {
- title: "Get Document Info",
- description:
- "Get information about the current PDF document including title, current page, total pages, and zoom level",
- },
- async () => {
- if (!pdfDocument) {
- return {
- content: [{ type: "text" as const, text: "Error: No document loaded" }],
- isError: true,
- };
- }
- const info = {
- title: pdfTitle || "Untitled",
- url: pdfUrl,
- currentPage,
- totalPages,
- scale,
- displayMode: currentDisplayMode,
- };
- return {
- content: [{ type: "text" as const, text: JSON.stringify(info, null, 2) }],
- structuredContent: info,
- };
- },
-);
-
-app.registerTool(
- "navigate",
- {
- title: "Navigate",
- description: "Jump to a specific page in the document",
- inputSchema: z.object({
- page: z.number().int().min(1).describe("Page number (1-indexed)"),
- }),
- },
- async ({ page }) => {
- if (pdfDocument && (page < 1 || page > totalPages)) {
- return {
- content: [
- {
- type: "text" as const,
- text: `Error: Page ${page} out of range (1-${totalPages})`,
- },
- ],
- isError: true,
- };
- }
- return runCommand(
- { type: "navigate", page },
- `Navigated to page ${page}/${totalPages}`,
- );
- },
-);
-
-app.registerTool(
- "search",
- {
- title: "Search",
- description:
- "Search for text and highlight matches in the viewer UI. Opens the search bar and jumps to the first match.",
- inputSchema: z.object({
- query: z.string().describe("Text to search for"),
- }),
- },
- async ({ query }) =>
- runCommand(
- { type: "search", query },
- () => `Searched for "${query}": ${allMatches.length} match(es)`,
- ),
-);
-
-app.registerTool(
- "find",
- {
- title: "Find",
- description:
- "Silent search — locate matches without opening the search UI. Use before search_navigate.",
- inputSchema: z.object({
- query: z.string().describe("Text to search for"),
- }),
- },
- async ({ query }) =>
- runCommand(
- { type: "find", query },
- () => `Found ${allMatches.length} match(es) for "${query}"`,
- ),
-);
-
-app.registerTool(
- "search_navigate",
- {
- title: "Search Navigate",
- description:
- "Jump to the Nth search match (0-indexed). Call search or find first.",
- inputSchema: z.object({
- matchIndex: z.number().int().min(0).describe("Match index (0-indexed)"),
- }),
- },
- async ({ matchIndex }) => {
- if (allMatches.length === 0) {
- return {
- content: [
- {
- type: "text" as const,
- text: "Error: No search results. Call search or find first.",
- },
- ],
- isError: true,
- };
- }
- if (matchIndex >= allMatches.length) {
- return {
- content: [
- {
- type: "text" as const,
- text: `Error: matchIndex ${matchIndex} out of range (0-${allMatches.length - 1})`,
- },
- ],
- isError: true,
- };
- }
- return runCommand(
- { type: "search_navigate", matchIndex },
- `Jumped to match ${matchIndex + 1}/${allMatches.length} on page ${allMatches[matchIndex].pageNum}`,
- );
- },
-);
-
-app.registerTool(
- "zoom",
- {
- title: "Zoom",
- description: "Set the zoom scale for the document",
- inputSchema: z.object({
- scale: z
- .number()
- .min(0.5)
- .max(3.0)
- .describe("Zoom scale, 1.0 = 100% (range: 0.5-3.0)"),
- }),
- },
- async ({ scale }) =>
- runCommand(
- { type: "zoom", scale },
- `Zoom set to ${Math.round(scale * 100)}%`,
- ),
-);
-
-app.registerTool(
- "add_annotations",
- {
- title: "Add Annotations",
- description:
- "Add one or more annotations (highlight, note, rectangle, circle, line, stamp, image, freetext). Each needs id, type, page, and type-specific geometry.",
- inputSchema: z.object({
- annotations: z
- .array(z.record(z.string(), z.any()))
- .min(1)
- .describe(
- "Annotation objects. Each needs: id, type, page, plus type-specific fields (x, y, width, height, rects, color, content, etc.)",
- ),
- }),
- },
- async ({ annotations }) =>
- runCommand(
- {
- type: "add_annotations",
- annotations: annotations as PdfAnnotationDef[],
- },
- `Added ${annotations.length} annotation(s)`,
- ),
-);
-
-app.registerTool(
- "update_annotations",
- {
- title: "Update Annotations",
- description:
- "Patch existing annotations by id. Only id and type are required; other fields are merged.",
- inputSchema: z.object({
- annotations: z
- .array(z.record(z.string(), z.any()))
- .min(1)
- .describe("Partial annotation objects. Each needs: id, type."),
- }),
- },
- async ({ annotations }) =>
- runCommand(
- {
- type: "update_annotations",
- annotations: annotations as Extract<
- PdfCommand,
- { type: "update_annotations" }
- >["annotations"],
- },
- `Updated ${annotations.length} annotation(s)`,
- ),
-);
-
-app.registerTool(
- "remove_annotations",
- {
- title: "Remove Annotations",
- description: "Delete annotations by id",
- inputSchema: z.object({
- ids: z.array(z.string()).min(1).describe("Annotation IDs to remove"),
- }),
- },
- async ({ ids }) =>
- runCommand(
- { type: "remove_annotations", ids },
- `Removed ${ids.length} annotation(s)`,
- ),
-);
-
-app.registerTool(
- "highlight_text",
- {
- title: "Highlight Text",
- description:
- "Auto-locate text and add a highlight annotation. Searches the document (or a specific page) and highlights the first match.",
- inputSchema: z.object({
- query: z.string().describe("Text to locate and highlight"),
- page: z
- .number()
- .int()
- .min(1)
- .optional()
- .describe("Restrict search to this page"),
- color: z.string().optional().describe("Highlight color (CSS color)"),
- content: z.string().optional().describe("Tooltip/note content"),
- }),
- },
- async ({ query, page, color, content }) => {
- const id = `ht_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
- return runCommand(
- { type: "highlight_text", id, query, page, color, content },
- `Highlighted "${query}"${page ? ` on page ${page}` : ""} (id: ${id})`,
- );
- },
-);
-
-app.registerTool(
- "fill_form",
- {
- title: "Fill Form",
- description: "Fill PDF form fields by name",
- inputSchema: z.object({
- fields: z
- .array(FormFieldSchema)
- .min(1)
- .describe(
- "Form fields: { name, value } where value is string or boolean",
- ),
- }),
- },
- async ({ fields }) =>
- runCommand(
- { type: "fill_form", fields },
- `Filled ${fields.length} field(s): ${fields.map((f) => f.name).join(", ")}`,
- ),
-);
-
-app.registerTool(
- "get_text",
- {
- title: "Get Text",
- description:
- "Extract text from one or more pages. Returns one text block per page.",
- inputSchema: z.object({
- page: z
- .number()
- .int()
- .min(1)
- .optional()
- .describe("Single page (shorthand for intervals: [{start:N, end:N}])"),
- intervals: z
- .array(PageIntervalSchema)
- .optional()
- .describe(
- "Page ranges. Each has optional start/end. [{start:1,end:5}], [{}] = all pages. Max 20 pages.",
- ),
- }),
- },
- async ({ page, intervals }) => {
- if (!pdfDocument) {
- return {
- content: [{ type: "text" as const, text: "Error: No document loaded" }],
- isError: true,
- };
- }
- const resolved = intervals ?? (page ? [{ start: page, end: page }] : [{}]);
- const data = await collectPageData(resolved, true, false);
- const parts = data
- .filter((e) => e.text != null)
- .map((e) => ({
- type: "text" as const,
- text: `--- Page ${e.page} ---\n${e.text}`,
- }));
- return {
- content:
- parts.length > 0
- ? parts
- : [{ type: "text" as const, text: "No text content returned" }],
- structuredContent: { pages: data },
- };
- },
-);
-
-app.registerTool(
- "get_screenshot",
- {
- title: "Get Screenshot",
- description: "Render a page to a JPEG image for visual analysis",
- inputSchema: z.object({
- page: z.number().int().min(1).describe("Page number to render"),
- }),
- },
- async ({ page }) => {
- if (!pdfDocument) {
- return {
- content: [{ type: "text" as const, text: "Error: No document loaded" }],
- isError: true,
- };
- }
- const data = await collectPageData(
- [{ start: page, end: page }],
- false,
- true,
- );
- const entry = data[0];
- if (entry?.image) {
- return {
- content: [
- {
- type: "image" as const,
- data: entry.image,
- mimeType: "image/jpeg",
- },
- ],
- };
- }
- return {
- content: [
- {
- type: "text" as const,
- text: `Error: screenshot failed for page ${page}`,
- },
- ],
- isError: true,
- };
- },
-);
-
// Connect to host
app
.connect()
diff --git a/examples/pdf-server/src/pdf-annotations.test.ts b/examples/pdf-server/src/pdf-annotations.test.ts
index 40e63ff..fe34d83 100644
--- a/examples/pdf-server/src/pdf-annotations.test.ts
+++ b/examples/pdf-server/src/pdf-annotations.test.ts
@@ -10,7 +10,6 @@ import {
defaultColor,
importPdfjsAnnotation,
buildAnnotatedPdfBytes,
- parseAnnotationRef,
base64ToUint8Array,
uint8ArrayToBase64,
convertFromModelCoords,
@@ -18,13 +17,7 @@ import {
type PdfAnnotationDef,
type AnnotationDiff,
} from "./pdf-annotations";
-import {
- PDFDocument,
- PDFDict,
- PDFName,
- PDFArray,
- PDFNumber,
-} from "@cantoo/pdf-lib";
+import { PDFDocument, PDFDict, PDFName, PDFArray, PDFNumber } from "pdf-lib";
// =============================================================================
// Diff Model
@@ -373,92 +366,6 @@ describe("computeDiff", () => {
current.map((a) => a.id).sort(),
);
});
-
- // Backs the post-computeDiff union step in mcp-app.ts persistAnnotations /
- // getAnnotatedPdfBytes: with the lazy per-page baseline scan, computeDiff
- // alone cannot produce `removed` for natives on pages not yet visited, so
- // callers must union restoredRemovedIds. These tests pin the contract those
- // call sites depend on.
- describe("partial baseline (lazy-scan tombstone preservation)", () => {
- const tombstoned = "pdf-5-0";
- const userNote: PdfAnnotationDef = {
- type: "note",
- id: "u1",
- page: 1,
- x: 10,
- y: 10,
- content: "unrelated edit",
- };
-
- /** Mirrors the union loop in mcp-app.ts persistAnnotations. */
- function unionRestored(
- diff: AnnotationDiff,
- restored: Iterable,
- currentIds: Set,
- ): void {
- for (const id of restored) {
- if (!currentIds.has(id) && !diff.removed.includes(id)) {
- diff.removed.push(id);
- }
- }
- }
-
- it("computeDiff alone drops tombstones for unscanned pages; the union step preserves them", () => {
- const baseline: PdfAnnotationDef[] = []; // page with the native not yet scanned
- const current = [userNote];
- const diff = computeDiff(baseline, current, new Map());
- expect(diff.removed).toEqual([]); // proves the union step is load-bearing
-
- unionRestored(diff, [tombstoned], new Set(current.map((a) => a.id)));
-
- expect(diff.removed).toEqual([tombstoned]);
- expect(isDiffEmpty(diff)).toBe(false);
- expect(deserializeDiff(serializeDiff(diff)).removed).toEqual([
- tombstoned,
- ]);
- });
-
- it("a tombstone the user re-added is excluded from the union", () => {
- const reAdded: PdfAnnotationDef = {
- type: "note",
- id: tombstoned,
- page: 3,
- x: 0,
- y: 0,
- content: "back",
- };
- const current = [userNote, reAdded];
- const diff = computeDiff([], current, new Map());
- unionRestored(diff, [tombstoned], new Set(current.map((a) => a.id)));
- expect(diff.removed).toEqual([]);
- });
-
- it("union does not duplicate ids once the page is scanned and computeDiff produces them", () => {
- const native: PdfAnnotationDef = {
- type: "note",
- id: tombstoned,
- page: 3,
- x: 0,
- y: 0,
- content: "native",
- };
- const diff = computeDiff([native], [userNote], new Map());
- expect(diff.removed).toEqual([tombstoned]);
- unionRestored(diff, [tombstoned], new Set([userNote.id]));
- expect(diff.removed).toEqual([tombstoned]);
- });
-
- it("removedRefs from restored tombstones parse for buildAnnotatedPdfBytes; non-ref ids are skipped", () => {
- const restored = new Set(["pdf-5-0", "pdf-12R", "pdf-2-idx-7"]);
- const removedRefs = [...restored]
- .map(parseAnnotationRef)
- .filter((r): r is NonNullable => r !== null);
- expect(removedRefs).toEqual([
- { objectNumber: 5, generationNumber: 0 },
- { objectNumber: 12, generationNumber: 0 },
- ]);
- });
- });
});
// =============================================================================
@@ -553,8 +460,7 @@ describe("importPdfjsAnnotation", () => {
annotationType: 9,
ref: { num: 5, gen: 0 },
rect: [72, 700, 272, 712],
- // pdf.js emits a FLAT Float32Array, not nested arrays.
- quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]),
+ quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]],
color: new Uint8ClampedArray([255, 255, 0]),
contentsObj: { str: "Important" },
};
@@ -568,57 +474,12 @@ describe("importPdfjsAnnotation", () => {
expect((result as any).color).toBe("#ffff00");
});
- it("imports a multi-line highlight (multiple quads → multiple rects)", () => {
- // Regression: the parser iterated quadPoints as if nested; pdf.js's
- // flat array yielded numbers, so rects stayed empty and import bailed.
- const ann = {
- annotationType: 9,
- ref: { num: 8, gen: 0 },
- quadPoints: new Float32Array([
- // line 1
- 72, 712, 272, 712, 72, 700, 272, 700,
- // line 2
- 72, 698, 200, 698, 72, 686, 200, 686,
- ]),
- color: new Uint8ClampedArray([255, 255, 0]),
- };
- const result = importPdfjsAnnotation(ann, 1, 0);
- expect(result).not.toBeNull();
- expect(result!.type).toBe("highlight");
- expect((result as any).rects).toHaveLength(2);
- expect((result as any).rects[0]).toEqual({
- x: 72,
- y: 700,
- width: 200,
- height: 12,
- });
- expect((result as any).rects[1]).toEqual({
- x: 72,
- y: 686,
- width: 128,
- height: 12,
- });
- });
-
- it("falls back to ann.rect when quadPoints is absent", () => {
- const ann = {
- annotationType: 9,
- ref: { num: 9, gen: 0 },
- rect: [72, 700, 272, 712],
- color: new Uint8ClampedArray([255, 255, 0]),
- };
- const result = importPdfjsAnnotation(ann, 1, 0);
- expect(result).not.toBeNull();
- expect((result as any).rects).toHaveLength(1);
- });
-
it("imports an underline annotation", () => {
const ann = {
annotationType: 10,
ref: { num: 6, gen: 0 },
rect: [72, 700, 272, 712],
- // pdf.js emits a FLAT Float32Array, not nested arrays.
- quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]),
+ quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]],
color: new Uint8ClampedArray([255, 0, 0]),
};
const result = importPdfjsAnnotation(ann, 1, 0);
@@ -632,8 +493,7 @@ describe("importPdfjsAnnotation", () => {
annotationType: 12,
ref: { num: 7, gen: 0 },
rect: [72, 700, 272, 712],
- // pdf.js emits a FLAT Float32Array, not nested arrays.
- quadPoints: new Float32Array([72, 712, 272, 712, 72, 700, 272, 700]),
+ quadPoints: [[72, 712, 272, 712, 72, 700, 272, 700]],
color: new Uint8ClampedArray([255, 0, 0]),
};
const result = importPdfjsAnnotation(ann, 2, 0);
@@ -642,76 +502,6 @@ describe("importPdfjsAnnotation", () => {
expect(result!.page).toBe(2);
});
- it("imports an unsupported subtype as 'imported' (placement only)", () => {
- // annotationType 15 = Ink, not in PDFJS_TYPE_MAP. We keep it as a
- // placement-only "imported" record so it's listed in the panel and
- // rendered from annotationCanvasMap instead of being dropped.
- const ann = {
- annotationType: 15,
- subtype: "Ink",
- id: "200R",
- rect: [100, 200, 180, 260],
- };
- const result = importPdfjsAnnotation(ann, 3, 0);
- expect(result).not.toBeNull();
- expect(result!.type).toBe("imported");
- expect(result!.page).toBe(3);
- expect((result as any).pdfjsId).toBe("200R");
- expect((result as any).subtype).toBe("Ink");
- expect((result as any).width).toBeCloseTo(80);
- expect((result as any).height).toBeCloseTo(60);
- });
-
- it("imports an appearance-stream stamp as 'imported' (not text-label)", () => {
- // A Stamp with hasAppearance carries a custom visual (e.g. an image
- // signature) that our text-label StampAnnotation can't reproduce.
- const ann = {
- annotationType: 13,
- subtype: "Stamp",
- id: "118R",
- rect: [420, 760, 514, 792],
- hasAppearance: true,
- contentsObj: { str: "DRAFT" },
- };
- const result = importPdfjsAnnotation(ann, 1, 0);
- expect(result!.type).toBe("imported");
- expect((result as any).pdfjsId).toBe("118R");
- expect((result as any).subtype).toBe("Stamp");
- });
-
- it("computeDiff: 'imported' present in both baseline and current → no diff", () => {
- const imp: PdfAnnotationDef = {
- type: "imported",
- id: "pdf-118R",
- page: 1,
- x: 420,
- y: 760,
- width: 94,
- height: 32,
- pdfjsId: "118R",
- subtype: "Stamp",
- };
- const diff = computeDiff([imp], [imp], new Map());
- expect(diff.added).toHaveLength(0);
- expect(diff.removed).toHaveLength(0);
- });
-
- it("computeDiff: deleting an 'imported' annotation lists it in removed", () => {
- const imp: PdfAnnotationDef = {
- type: "imported",
- id: "pdf-118R",
- page: 1,
- x: 420,
- y: 760,
- width: 94,
- height: 32,
- pdfjsId: "118R",
- subtype: "Stamp",
- };
- const diff = computeDiff([imp], [], new Map());
- expect(diff.removed).toEqual(["pdf-118R"]);
- });
-
it("imports a note (Text) annotation", () => {
const ann = {
annotationType: 1,
@@ -881,29 +671,6 @@ describe("base64 helpers", () => {
// PDF Annotation Dict Creation (integration test with pdf-lib)
// =============================================================================
-describe("parseAnnotationRef", () => {
- it("parses pdf-- ids", () => {
- expect(parseAnnotationRef("pdf-118-0")).toEqual({
- objectNumber: 118,
- generationNumber: 0,
- });
- expect(parseAnnotationRef("pdf-5-2")).toEqual({
- objectNumber: 5,
- generationNumber: 2,
- });
- });
- it("parses pdf-R ids (pdf.js string id, gen=0)", () => {
- expect(parseAnnotationRef("pdf-118R")).toEqual({
- objectNumber: 118,
- generationNumber: 0,
- });
- });
- it("returns null for page-index fallback ids", () => {
- expect(parseAnnotationRef("pdf-1-idx-3")).toBeNull();
- expect(parseAnnotationRef("user-abc")).toBeNull();
- });
-});
-
describe("buildAnnotatedPdfBytes", () => {
let blankPdfBytes: Uint8Array;
@@ -924,56 +691,6 @@ describe("buildAnnotatedPdfBytes", () => {
expect(header).toBe("%PDF-");
});
- it("strips removedRefs entries from each page's /Annots array", async () => {
- // Seed: add two highlights, save, capture their object refs.
- const seeded = await buildAnnotatedPdfBytes(
- blankPdfBytes,
- [
- {
- type: "highlight",
- id: "h1",
- page: 1,
- rects: [{ x: 72, y: 700, width: 100, height: 12 }],
- color: "#ffff00",
- },
- {
- type: "highlight",
- id: "h2",
- page: 1,
- rects: [{ x: 72, y: 680, width: 100, height: 12 }],
- color: "#ffff00",
- },
- ],
- new Map(),
- );
- const seededDoc = await PDFDocument.load(seeded);
- const annots = seededDoc.getPage(0).node.Annots()!;
- expect(annots.size()).toBe(2);
- const ref0 = annots.get(0) as unknown as {
- objectNumber: number;
- generationNumber: number;
- };
-
- // Now remove the first one by ref.
- const stripped = await buildAnnotatedPdfBytes(seeded, [], new Map(), [
- {
- objectNumber: ref0.objectNumber,
- generationNumber: ref0.generationNumber,
- },
- ]);
- const strippedDoc = await PDFDocument.load(stripped);
- const remaining = strippedDoc.getPage(0).node.Annots();
- expect(remaining?.size() ?? 0).toBe(1);
- });
-
- it("removedRefs ignores refs not present in /Annots", async () => {
- const out = await buildAnnotatedPdfBytes(blankPdfBytes, [], new Map(), [
- { objectNumber: 9999, generationNumber: 0 },
- ]);
- const doc = await PDFDocument.load(out);
- expect(doc.getPage(0).node.Annots()?.size() ?? 0).toBe(0);
- });
-
it("adds highlight annotation to PDF", async () => {
const annotations: PdfAnnotationDef[] = [
{
@@ -1080,16 +797,16 @@ describe("buildAnnotatedPdfBytes", () => {
// Check /C (stroke color) = [1, 0, 0] for #ff0000
const cArr = annotDict.get(PDFName.of("C")) as PDFArray;
expect(cArr).toBeDefined();
- expect((cArr.get(0) as PDFNumber).asNumber()).toBe(1); // r
- expect((cArr.get(1) as PDFNumber).asNumber()).toBe(0); // g
- expect((cArr.get(2) as PDFNumber).asNumber()).toBe(0); // b
+ expect((cArr.get(0) as PDFNumber).value()).toBe(1); // r
+ expect((cArr.get(1) as PDFNumber).value()).toBe(0); // g
+ expect((cArr.get(2) as PDFNumber).value()).toBe(0); // b
// Check /IC (fill color) = [0, 1, 0] for #00ff00
const icArr = annotDict.get(PDFName.of("IC")) as PDFArray;
expect(icArr).toBeDefined();
- expect((icArr.get(0) as PDFNumber).asNumber()).toBe(0); // r
- expect((icArr.get(1) as PDFNumber).asNumber()).toBe(1); // g
- expect((icArr.get(2) as PDFNumber).asNumber()).toBe(0); // b
+ expect((icArr.get(0) as PDFNumber).value()).toBe(0); // r
+ expect((icArr.get(1) as PDFNumber).value()).toBe(1); // g
+ expect((icArr.get(2) as PDFNumber).value()).toBe(0); // b
});
it("adds freetext annotation to PDF", async () => {
@@ -1165,9 +882,9 @@ describe("buildAnnotatedPdfBytes", () => {
// Check /C color
const cArr = annotDict.get(PDFName.of("C")) as PDFArray;
expect(cArr).toBeDefined();
- expect((cArr.get(0) as PDFNumber).asNumber()).toBe(0);
- expect((cArr.get(1) as PDFNumber).asNumber()).toBe(1);
- expect((cArr.get(2) as PDFNumber).asNumber()).toBe(0);
+ expect((cArr.get(0) as PDFNumber).value()).toBe(0);
+ expect((cArr.get(1) as PDFNumber).value()).toBe(1);
+ expect((cArr.get(2) as PDFNumber).value()).toBe(0);
// Check appearance stream exists and contains the color
const ap = annotDict.get(PDFName.of("AP")) as PDFDict;
@@ -1388,76 +1105,6 @@ describe("buildAnnotatedPdfBytes", () => {
const form = (await PDFDocument.load(out)).getForm();
expect(form.getTextField("name").getText()).toBe("kept");
});
-
- it("a field that throws on write does not abort subsequent fields", async () => {
- // Regression for #577: the per-field try/catch was dropped, so the
- // first throwing field bubbled to the outer catch and silently dropped
- // every field after it. setText() throws when value exceeds maxLength.
- const doc = await PDFDocument.create();
- const page = doc.addPage([612, 792]);
- const form = doc.getForm();
- const limited = form.createTextField("limited");
- limited.setMaxLength(2);
- limited.addToPage(page, { x: 10, y: 700 });
- form.createTextField("after").addToPage(page, { x: 10, y: 660 });
- const fixture = await doc.save();
-
- const out = await buildAnnotatedPdfBytes(
- fixture,
- [],
- new Map([
- ["limited", "way too long"], // throws
- ["after", "kept"],
- ]),
- );
-
- const saved = (await PDFDocument.load(out)).getForm();
- expect(saved.getTextField("after").getText()).toBe("kept");
- // The throwing field is left at whatever pdf-lib could do with it —
- // we only assert it didn't poison "after".
- });
-
- it("radio misclassified as PDFCheckBox: string value selects the matching widget", async () => {
- // Some PDFs (e.g. IRS/third-party forms) omit the /Ff Radio bit, so
- // pdf-lib hands us a PDFCheckBox. The viewer stored pdf.js's
- // buttonValue ("0"/"1"), not a boolean — check() would always pick
- // the first widget. setButtonGroupValue writes /V + per-widget /AS
- // directly so the chosen widget sticks.
- const doc = await PDFDocument.create();
- const page = doc.addPage([612, 792]);
- const form = doc.getForm();
- const rg = form.createRadioGroup("Gender");
- rg.addOptionToPage("Male", page, { x: 10, y: 700 });
- rg.addOptionToPage("Female", page, { x: 60, y: 700 });
- // pdf-lib's addOptionToPage writes widget on-values "0","1". Clear the
- // Radio flag (bit 16) so the reloaded form classifies it as checkbox.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (rg.acroField as any).clearFlag(1 << 15);
- const fixture = await doc.save();
-
- // Sanity: reload sees it as PDFCheckBox now.
- const reForm = (await PDFDocument.load(fixture)).getForm();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const reField = reForm.getFieldMaybe("Gender") as any;
- expect(reField?.constructor?.name).toBe("PDFCheckBox");
-
- const out = await buildAnnotatedPdfBytes(
- fixture,
- [],
- new Map([["Gender", "1"]]),
- );
-
- const saved = await PDFDocument.load(out);
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- const acro = (saved.getForm().getFieldMaybe("Gender") as any).acroField;
- const v = acro.dict.get(PDFName.of("V"));
- expect(v).toBeInstanceOf(PDFName);
- expect((v as PDFName).decodeText()).toBe("1");
- // Second widget /AS is the on-state, first is /Off.
- const widgets = acro.getWidgets();
- expect(widgets[0].getAppearanceState()?.decodeText()).toBe("Off");
- expect(widgets[1].getAppearanceState()?.decodeText()).toBe("1");
- });
});
});
diff --git a/examples/pdf-server/src/pdf-annotations.ts b/examples/pdf-server/src/pdf-annotations.ts
index 201fe8d..25d9ebb 100644
--- a/examples/pdf-server/src/pdf-annotations.ts
+++ b/examples/pdf-server/src/pdf-annotations.ts
@@ -21,10 +21,8 @@ import {
PDFTextField,
PDFCheckBox,
PDFDropdown,
- PDFOptionList,
PDFRadioGroup,
- type PDFForm,
-} from "@cantoo/pdf-lib";
+} from "pdf-lib";
// =============================================================================
// Types
@@ -130,32 +128,6 @@ export interface ImageAnnotation extends AnnotationBase {
aspect?: "preserve" | "ignore";
}
-/**
- * An annotation that already exists in the loaded PDF and that we render
- * verbatim from its appearance stream (via pdf.js's annotationCanvasMap)
- * rather than re-modeling it with one of our shape types.
- *
- * Covers two cases:
- * - subtypes we don't model (Ink, Polygon, Caret, FileAttachment, …)
- * - subtypes we *could* model but whose appearance carries information
- * our model would drop (e.g. Stamp with an image signature)
- *
- * The rasterized canvas is supplied at render time by mcp-app.ts; this
- * struct only carries placement and identity. Coords are PDF user-space
- * (origin bottom-left), matching the other rect-shaped types.
- */
-export interface ImportedAnnotation extends AnnotationBase {
- type: "imported";
- x: number;
- y: number;
- width: number;
- height: number;
- /** pdf.js getAnnotations() id (e.g. "118R") — key into annotationCanvasMap. */
- pdfjsId: string;
- /** Original PDF /Subtype (e.g. "Stamp", "Ink") for the panel label. */
- subtype: string;
-}
-
export type PdfAnnotationDef =
| HighlightAnnotation
| UnderlineAnnotation
@@ -166,8 +138,7 @@ export type PdfAnnotationDef =
| LineAnnotation
| FreetextAnnotation
| StampAnnotation
- | ImageAnnotation
- | ImportedAnnotation;
+ | ImageAnnotation;
// =============================================================================
// Coordinate Conversion (model ↔ internal PDF coords)
@@ -201,7 +172,6 @@ export function convertFromModelCoords(
case "rectangle":
case "circle":
case "image":
- case "imported":
return { ...def, y: pageHeight - def.y - def.height };
case "line":
return {
@@ -402,7 +372,6 @@ export function defaultColor(type: PdfAnnotationDef["type"]): string {
case "stamp":
return "#cc0000";
case "image":
- case "imported":
return "#00000000";
}
}
@@ -524,11 +493,6 @@ export async function addAnnotationDicts(
const pages = pdfDoc.getPages();
for (const def of annotations) {
- // "imported" annotations are already in the source PDF — never
- // re-serialize them. Deletion is handled by buildAnnotatedPdfBytes'
- // removedRefs strip; moving an imported annotation is still UI-only.
- if (def.type === "imported") continue;
-
const pageIdx = def.page - 1;
if (pageIdx < 0 || pageIdx >= pages.length) continue;
const page = pages[pageIdx];
@@ -836,190 +800,59 @@ export async function addAnnotationDicts(
}
}
-/**
- * Select a radio-style button group by widget on-value, bypassing pdf-lib's
- * type-level guards. Used when pdf-lib classifies a radio as `PDFCheckBox`
- * (PDF lacks the /Ff Radio bit) — `check()` would always pick the first
- * widget. Mirrors `PDFAcroRadioButton.setValue` minus its `onValues` throw.
- */
-function setButtonGroupValue(
- field: PDFCheckBox | PDFRadioGroup,
- onValue: string,
-): void {
- const acro = field.acroField;
- const off = PDFName.of("Off");
- const widgets = acro.getWidgets();
- // Match by PDFName identity (pdf-lib interns names) — the viewer stored
- // pdf.js's buttonValue, which IS the widget's /AP /N on-state name.
- let target = onValue && onValue !== "Off" ? PDFName.of(onValue) : off;
- if (
- target !== off &&
- !widgets.some((w: { getOnValue(): PDFName | undefined }) => {
- return w.getOnValue() === target;
- })
- ) {
- // No widget has this on-state — leave as-is rather than corrupt /V.
- return;
- }
- acro.dict.set(PDFName.of("V"), target);
- for (const w of widgets) {
- const on = (w as { getOnValue(): PDFName | undefined }).getOnValue();
- (w as { setAppearanceState(s: PDFName): void }).setAppearanceState(
- on === target ? target : off,
- );
- }
-}
-
-/**
- * Recover the PDF object reference from an annotation id assigned by
- * `makeAnnotationId`. Handles both `pdf--` (from `ann.ref`) and
- * `pdf-R` (from pdf.js's string `ann.id`, gen always 0). Returns null for
- * page-index fallback ids — those have no stable ref to remove by.
- */
-export function parseAnnotationRef(
- id: string,
-): { objectNumber: number; generationNumber: number } | null {
- let m = /^pdf-(\d+)-(\d+)$/.exec(id);
- if (m) return { objectNumber: +m[1], generationNumber: +m[2] };
- m = /^pdf-(\d+)R$/.exec(id);
- if (m) return { objectNumber: +m[1], generationNumber: 0 };
- return null;
-}
-
/**
* Build annotated PDF bytes from the original document.
- * Applies user annotations and form fills, removes baseline annotations the
- * user deleted, returns Uint8Array of the new PDF.
+ * Applies user annotations and form fills, returns Uint8Array of the new PDF.
*/
export async function buildAnnotatedPdfBytes(
pdfBytes: Uint8Array,
annotations: PdfAnnotationDef[],
formFields: Map,
- removedRefs: { objectNumber: number; generationNumber: number }[] = [],
): Promise {
const pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true });
- // Strip baseline annotations the user deleted. We match on object number +
- // generation against each page's /Annots array entries (PDFRef). We don't
- // know which page they were on, so scan every page — /Annots arrays are
- // small and this only runs at save time.
- if (removedRefs.length > 0) {
- const wanted = new Set(
- removedRefs.map((r) => `${r.objectNumber} ${r.generationNumber}`),
- );
- for (const page of pdfDoc.getPages()) {
- const annots = page.node.Annots();
- if (!annots) continue;
- // Walk backwards so .remove(idx) doesn't shift unprocessed entries.
- for (let i = annots.size() - 1; i >= 0; i--) {
- const ref = annots.get(i) as {
- objectNumber?: number;
- generationNumber?: number;
- };
- if (
- ref?.objectNumber !== undefined &&
- wanted.has(`${ref.objectNumber} ${ref.generationNumber ?? 0}`)
- ) {
- annots.remove(i);
- }
- }
- }
- }
-
// Add proper PDF annotation objects
await addAnnotationDicts(pdfDoc, annotations);
// Apply form fills. Dispatch on actual field type — getTextField(name) throws
- // for dropdowns/radios, so we look up the generic field and instanceof it.
- // Each field is wrapped in its own try/catch: pdf-lib can throw on
- // length-constrained text, radios whose buttonValue maps to neither label
- // nor index, checkboxes missing a /Yes appearance, etc. One bad field must
- // not abort the rest of the loop (regressed in #577 when the inner catch
- // was dropped along with the type-specific getters).
+ // for dropdowns/radios, so the old try/catch silently dropped those on save.
if (formFields.size > 0) {
- let form: PDFForm | undefined;
try {
- form = pdfDoc.getForm();
- } catch {
- // No AcroForm in this PDF
- }
- if (form) {
+ const form = pdfDoc.getForm();
for (const [name, value] of formFields) {
- try {
- const field = form.getFieldMaybe(name);
- if (!field) continue;
+ const field = form.getFieldMaybe(name);
+ if (!field) continue;
- if (field instanceof PDFCheckBox) {
- const widgets = field.acroField.getWidgets();
- if (typeof value === "string" && widgets.length > 1) {
- // Multi-widget "checkbox" with a string value = pdf-lib
- // misclassified a radio group (PDF lacks the /Ff Radio flag).
- // The viewer stored pdf.js's buttonValue (the widget's on-state
- // name, e.g. "0"/"1"); check()/uncheck() would hit the FIRST
- // widget regardless. Write /V and per-widget /AS directly.
- setButtonGroupValue(field, value);
- } else {
- // Single-widget (real) checkbox. Viewer normally stores boolean,
- // but be liberal: any truthy non-"Off" string counts as checked.
- const on =
- value === true ||
- (typeof value === "string" && value !== "" && value !== "Off");
- if (on) field.check();
- else field.uncheck();
+ if (field instanceof PDFCheckBox) {
+ if (value) field.check();
+ else field.uncheck();
+ } else if (field instanceof PDFRadioGroup) {
+ // The viewer stores pdf.js's buttonValue, which for PDFs with an
+ // /Opt array is a numeric index ("0","1","2") rather than the
+ // option label pdf-lib's select() expects. Try the label first,
+ // then fall back to indexing into getOptions().
+ const opts = field.getOptions();
+ const s = String(value);
+ if (opts.includes(s)) {
+ field.select(s);
+ } else {
+ const idx = Number(s);
+ if (Number.isInteger(idx) && idx >= 0 && idx < opts.length) {
+ field.select(opts[idx]);
}
- } else if (field instanceof PDFRadioGroup) {
- // The viewer stores pdf.js's buttonValue, which for PDFs with an
- // /Opt array is a numeric index ("0","1","2") rather than the
- // option label pdf-lib's select() expects. Try the label first,
- // then fall back to indexing into getOptions().
- const opts = field.getOptions();
- const s = String(value);
- if (opts.includes(s)) {
- field.select(s);
- } else {
- const idx = Number(s);
- if (Number.isInteger(idx) && idx >= 0 && idx < opts.length) {
- field.select(opts[idx]);
- }
- // else: value is neither label nor index — leave unset
- }
- } else if (field instanceof PDFDropdown) {
- // select() auto-enables edit mode for values outside getOptions(),
- // so this works for both enumerated and free-text combos.
- field.select(String(value));
- } else if (field instanceof PDFOptionList) {
- // Viewer stores multiselect listboxes as a comma-joined string
- // (pdf.js's annotationStorage value for /Ch is string|string[];
- // our Map flattens arrays). Only select
- // values that are actually in the option list — pdf-lib throws
- // on unknowns and there's no edit-mode fallback like Dropdown.
- const opts = field.getOptions();
- const wanted = String(value)
- .split(",")
- .map((s) => s.trim())
- .filter((s) => opts.includes(s));
- if (wanted.length > 0) field.select(wanted);
- else field.clear();
- } else if (field instanceof PDFTextField) {
- field.setText(String(value));
+ // else: value is neither label nor index — leave unset
}
- // PDFButton, PDFSignature: no fill_form support yet
- } catch (err) {
- // Skip this field; carry on with the rest. Surfacing per-field
- // failures is the caller's job (see fill_form result), not save's.
- console.warn(`buildAnnotatedPdfBytes: skipped field "${name}":`, err);
+ } else if (field instanceof PDFDropdown) {
+ // select() auto-enables edit mode for values outside getOptions(),
+ // so this works for both enumerated and free-text combos.
+ field.select(String(value));
+ } else if (field instanceof PDFTextField) {
+ field.setText(String(value));
}
+ // PDFButton, PDFOptionList, PDFSignature: no fill_form support yet
}
- // Some PDFs ship widgets with no on-state appearance stream (no
- // /AP/N/). pdf-lib's check()/select()/setText() set /V and /AS
- // but don't synthesize the missing appearance, so other readers
- // (Preview, Acrobat) show the field as if unchanged. This generates a
- // default appearance for any field marked dirty above.
- try {
- form.updateFieldAppearances();
- } catch (err) {
- console.warn("buildAnnotatedPdfBytes: updateFieldAppearances:", err);
- }
+ } catch {
+ // pdfDoc.getForm() throws if the PDF has no AcroForm
}
}
@@ -1102,70 +935,46 @@ export function importPdfjsAnnotation(
pageNum: number,
index: number,
): PdfAnnotationDef | null {
- // Skip form widgets (they're handled separately by AnnotationLayer) and
- // auxiliary types that aren't user-visible markup:
- // 2 = Link (navigational, AnnotationLayer handles the click target)
- // 16 = Popup (the speech-bubble UI for a parent annotation, not content)
- if (
- ann.annotationType === 20 ||
- ann.annotationType === 2 ||
- ann.annotationType === 16
- ) {
- return null;
- }
+ const ourType = PDFJS_TYPE_MAP[ann.annotationType];
+ if (!ourType) return null;
+
+ // Skip form widgets (they're handled separately by AnnotationLayer)
+ if (ann.annotationType === 20) return null;
const id = makeAnnotationId(ann, pageNum, index);
const color = pdfjsColorToHex(ann.color);
- const ourType = PDFJS_TYPE_MAP[ann.annotationType];
-
- // Anything we don't model — and stamps whose visual is an appearance
- // stream we can't reproduce as a text label — are kept as "imported":
- // a placement-only record that the renderer fills with the rasterized
- // appearance from pdf.js's annotationCanvasMap. This keeps Ink, Polygon,
- // image-signature stamps, etc. visible AND selectable in our layer.
- const importAsBitmap = !ourType || (ourType === "stamp" && ann.hasAppearance);
- if (importAsBitmap) {
- if (!ann.rect) return null;
- const r = pdfjsRectToRect(ann.rect);
- return {
- type: "imported",
- id,
- page: pageNum,
- x: r.x,
- y: r.y,
- width: r.width,
- height: r.height,
- pdfjsId: String(ann.id ?? ""),
- subtype: String(ann.subtype ?? `type${ann.annotationType}`),
- };
- }
switch (ourType) {
case "highlight":
case "underline":
case "strikethrough": {
- // pdf.js emits quadPoints as a FLAT Float32Array [x1,y1,...,x4,y4, …]
- // (8 numbers per quad), NOT as nested arrays. Iterating it yields
- // numbers, so the old `for (const qp of …) if (qp.length>=8)` never
- // matched and every quad-based annotation was dropped (#506).
- const rects: Rect[] = [];
- const qp = ann.quadPoints as ArrayLike | undefined;
- if (qp && qp.length >= 8) {
- for (let i = 0; i + 8 <= qp.length; i += 8) {
- const xs = [qp[i], qp[i + 2], qp[i + 4], qp[i + 6]];
- const ys = [qp[i + 1], qp[i + 3], qp[i + 5], qp[i + 7]];
- const minX = Math.min(...xs);
- const minY = Math.min(...ys);
- rects.push({
- x: minX,
- y: minY,
- width: Math.max(...xs) - minX,
- height: Math.max(...ys) - minY,
- });
+ // PDF.js provides quadPoints as array of arrays [[x1,y1,x2,y2,...], ...]
+ // or rect as [x1,y1,x2,y2]
+ let rects: Rect[];
+ if (ann.quadPoints && ann.quadPoints.length > 0) {
+ rects = [];
+ for (const qp of ann.quadPoints) {
+ // Each quadPoint is [x1,y1,x2,y2,x3,y3,x4,y4]
+ // We need the bounding box
+ if (qp.length >= 8) {
+ const xs = [qp[0], qp[2], qp[4], qp[6]];
+ const ys = [qp[1], qp[3], qp[5], qp[7]];
+ const minX = Math.min(...xs);
+ const minY = Math.min(...ys);
+ const maxX = Math.max(...xs);
+ const maxY = Math.max(...ys);
+ rects.push({
+ x: minX,
+ y: minY,
+ width: maxX - minX,
+ height: maxY - minY,
+ });
+ }
}
- }
- if (rects.length === 0 && ann.rect) {
- rects.push(pdfjsRectToRect(ann.rect));
+ } else if (ann.rect) {
+ rects = [pdfjsRectToRect(ann.rect)];
+ } else {
+ return null;
}
if (rects.length === 0) return null;
diff --git a/examples/pdf-server/tsconfig.json b/examples/pdf-server/tsconfig.json
index 2fe7e62..0501827 100644
--- a/examples/pdf-server/tsconfig.json
+++ b/examples/pdf-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client", "bun"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/pdf-server/tsconfig.server.json b/examples/pdf-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/pdf-server/tsconfig.server.json
+++ b/examples/pdf-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json
index 425728e..3ecd3d5 100644
--- a/examples/qr-server/package.json
+++ b/examples/qr-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-qr",
- "version": "2.0.0",
+ "version": "1.4.0",
"private": true,
"scripts": {
"start": "uv run server.py",
@@ -8,6 +8,7 @@
"build": "echo 'No build step needed for Python server'"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^2.0.0"
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0"
}
}
diff --git a/examples/qr-server/server.py b/examples/qr-server/server.py
index c1c302e..d282d1a 100755
--- a/examples/qr-server/server.py
+++ b/examples/qr-server/server.py
@@ -26,7 +26,7 @@ VIEW_URI = "ui://qr-server/view.html"
HOST = os.environ.get("HOST", "0.0.0.0") # 0.0.0.0 for Docker compatibility
PORT = int(os.environ.get("PORT", "3001"))
-mcp = FastMCP("QR Code Server", stateless_http=True, host=HOST, port=PORT)
+mcp = FastMCP("QR Code Server", stateless_http=True)
# Embedded View HTML for self-contained usage (uv run or unbundled)
EMBEDDED_VIEW_HTML = """
diff --git a/examples/quickstart/main.ts b/examples/quickstart/main.ts
index 7f370a8..06895a4 100644
--- a/examples/quickstart/main.ts
+++ b/examples/quickstart/main.ts
@@ -1,7 +1,7 @@
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -21,7 +21,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json
index 57f8444..e94baa2 100644
--- a/examples/quickstart/package.json
+++ b/examples/quickstart/package.json
@@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/quickstart",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"private": true,
"description": "Quickstart MCP App Server example",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/quickstart"
},
"license": "MIT",
@@ -15,13 +15,10 @@
"start": "concurrently --raw \"cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch\" \"tsx watch main.ts\""
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
- "express": "^5.1.0",
- "zod": "^4.2.0"
+ "express": "^5.1.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/quickstart/server.ts b/examples/quickstart/server.ts
index 611d0f0..eb3c3b1 100644
--- a/examples/quickstart/server.ts
+++ b/examples/quickstart/server.ts
@@ -3,10 +3,9 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import fs from "node:fs/promises";
import path from "node:path";
-import { z } from "zod";
const DIST_DIR = path.join(import.meta.dirname, "dist");
@@ -31,7 +30,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async () => {
diff --git a/examples/quickstart/tsconfig.json b/examples/quickstart/tsconfig.json
index d6a42ce..6c553b5 100644
--- a/examples/quickstart/tsconfig.json
+++ b/examples/quickstart/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/quickstart/tsconfig.server.json b/examples/quickstart/tsconfig.server.json
index 72552e2..7e65f5f 100644
--- a/examples/quickstart/tsconfig.server.json
+++ b/examples/quickstart/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/say-server/package.json b/examples/say-server/package.json
index cc12249..bcb5a95 100644
--- a/examples/say-server/package.json
+++ b/examples/say-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-say",
- "version": "2.0.0",
+ "version": "1.4.0",
"private": true,
"description": "Streaming TTS MCP App Server with karaoke-style text highlighting",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/say-server"
},
"license": "MIT",
@@ -15,6 +15,7 @@
"build": "echo 'No build step needed for Python server'"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^2.0.0"
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0"
}
}
diff --git a/examples/scenario-modeler-server/main.ts b/examples/scenario-modeler-server/main.ts
index cc20e63..dbd3ab6 100644
--- a/examples/scenario-modeler-server/main.ts
+++ b/examples/scenario-modeler-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json
index e6f37c0..1e0a31a 100644
--- a/examples/scenario-modeler-server/package.json
+++ b/examples/scenario-modeler-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-scenario-modeler",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Financial scenario modeling MCP App Server",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/scenario-modeler-server"
},
"license": "MIT",
@@ -26,16 +26,14 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts
index c5fa05d..b59851c 100644
--- a/examples/scenario-modeler-server/server.ts
+++ b/examples/scenario-modeler-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -305,8 +305,8 @@ export function createServer(): McpServer {
title: "Get Scenario Data",
description:
"Returns SaaS scenario templates and optionally computes custom projections for given inputs",
- inputSchema: GetScenarioDataInputSchema,
- outputSchema: GetScenarioDataOutputSchema,
+ inputSchema: GetScenarioDataInputSchema.shape,
+ outputSchema: GetScenarioDataOutputSchema.shape,
_meta: { ui: { resourceUri } },
},
async (args: {
diff --git a/examples/scenario-modeler-server/tsconfig.json b/examples/scenario-modeler-server/tsconfig.json
index 3ecbdad..fc3c210 100644
--- a/examples/scenario-modeler-server/tsconfig.json
+++ b/examples/scenario-modeler-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/scenario-modeler-server/tsconfig.server.json b/examples/scenario-modeler-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/scenario-modeler-server/tsconfig.server.json
+++ b/examples/scenario-modeler-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/shadertoy-server/main.ts b/examples/shadertoy-server/main.ts
index 7e388f0..ac8f6ad 100644
--- a/examples/shadertoy-server/main.ts
+++ b/examples/shadertoy-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json
index aec56c6..6af2dd7 100644
--- a/examples/shadertoy-server/package.json
+++ b/examples/shadertoy-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-shadertoy",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/shadertoy-server"
},
"license": "MIT",
@@ -24,13 +24,11 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/shadertoy-server/server.ts b/examples/shadertoy-server/server.ts
index 50c27bc..a4bfe9e 100644
--- a/examples/shadertoy-server/server.ts
+++ b/examples/shadertoy-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -63,9 +63,7 @@ LIMITATIONS - Do NOT use:
- VR features (mainVR not available)
For procedural noise:
- float hash(vec2 p) { return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }
-
-The widget is interactive and exposes tools for updating shader source code and querying compilation status. Compilation errors are sent to model context automatically.`;
+ float hash(vec2 p) { return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453); }`;
const DEFAULT_FRAGMENT_SHADER = `void mainImage(out vec4 fragColor, in vec2 fragCoord) {
vec2 uv = fragCoord / iResolution.xy;
diff --git a/examples/shadertoy-server/src/mcp-app.ts b/examples/shadertoy-server/src/mcp-app.ts
index 7cbb849..546de5a 100644
--- a/examples/shadertoy-server/src/mcp-app.ts
+++ b/examples/shadertoy-server/src/mcp-app.ts
@@ -7,7 +7,6 @@ import {
applyHostStyleVariables,
applyDocumentTheme,
} from "@modelcontextprotocol/ext-apps";
-import { z } from "zod";
import "./global.css";
import "./mcp-app.css";
import ShaderToyLite, {
@@ -40,8 +39,7 @@ const log = {
// Get element references
const mainEl = document.querySelector(".main") as HTMLElement;
const canvas = document.getElementById("canvas") as HTMLCanvasElement;
-// TODO: code-preview streaming toggle was removed when ontoolinputpartial was
-// dropped during app-tool refactor — element still exists in HTML, restore if needed
+const codePreview = document.getElementById("code-preview") as HTMLPreElement;
const fullscreenBtn = document.getElementById(
"fullscreen-btn",
) as HTMLButtonElement;
@@ -102,39 +100,6 @@ fullscreenBtn.addEventListener("click", toggleFullscreen);
// ShaderToyLite instance
let shaderToy: ShaderToyLiteInstance | null = null;
-// Track current shader sources
-let currentShaderSources: ShaderInput = {
- fragmentShader: "",
-};
-
-// Track compilation status
-interface CompilationStatus {
- success: boolean;
- errors: string[];
- timestamp: number;
-}
-let lastCompilationStatus: CompilationStatus = {
- success: true,
- errors: [],
- timestamp: Date.now(),
-};
-
-// Intercept console.error to capture shader compilation errors
-const originalConsoleError = console.error.bind(console);
-const capturedErrors: string[] = [];
-console.error = (...args: unknown[]) => {
- originalConsoleError(...args);
- // Capture shader compilation errors
- const message = args.map((arg) => String(arg)).join(" ");
- if (
- message.includes("Shader compilation failed") ||
- message.includes("Program initialization failed") ||
- message.includes("Failed to compile")
- ) {
- capturedErrors.push(message);
- }
-};
-
// Create app instance
const app = new App({ name: "ShaderToy Renderer", version: "1.0.0" });
@@ -146,18 +111,35 @@ app.onteardown = async () => {
return {};
};
-// Helper function to compile shader and update status
-function compileAndUpdateStatus(input: ShaderInput): void {
- // Clear captured errors before compilation
- capturedErrors.length = 0;
+app.ontoolinputpartial = (params) => {
+ // Show code preview, hide canvas
+ codePreview.classList.add("visible");
+ canvas.classList.add("hidden");
+ const code = params.arguments?.fragmentShader;
+ codePreview.textContent = typeof code === "string" ? code : "";
+ codePreview.scrollTop = codePreview.scrollHeight;
+};
+
+app.ontoolinput = (params) => {
+ log.info("Received shader input");
+
+ // Hide code preview, show canvas
+ codePreview.classList.remove("visible");
+ canvas.classList.remove("hidden");
+
+ if (!isShaderInput(params.arguments)) {
+ log.error("Invalid tool input");
+ return;
+ }
+
+ const { fragmentShader, common, bufferA, bufferB, bufferC, bufferD } =
+ params.arguments;
// Initialize ShaderToyLite if needed
if (!shaderToy) {
shaderToy = new ShaderToyLite("canvas");
}
- const { fragmentShader, common, bufferA, bufferB, bufferC, bufferD } = input;
-
// Set common code (shared across all shaders)
shaderToy.setCommon(common || "");
@@ -185,45 +167,6 @@ function compileAndUpdateStatus(input: ShaderInput): void {
});
shaderToy.play();
-
- // Update compilation status
- const hasErrors = capturedErrors.length > 0;
- lastCompilationStatus = {
- success: !hasErrors,
- errors: [...capturedErrors],
- timestamp: Date.now(),
- };
-
- // Store current sources
- currentShaderSources = { ...input };
-
- // Send compilation status to model context if there are errors
- if (hasErrors) {
- app
- .updateModelContext({
- content: [
- {
- type: "text",
- text: `Shader compilation failed:\n${capturedErrors.join("\n")}`,
- },
- ],
- structuredContent: {
- compilationStatus: lastCompilationStatus,
- },
- })
- .catch((err) => log.error("Failed to update model context:", err));
- }
-}
-
-app.ontoolinput = (params) => {
- log.info("Received shader input");
-
- if (!isShaderInput(params.arguments)) {
- log.error("Invalid tool input");
- return;
- }
-
- compileAndUpdateStatus(params.arguments);
log.info("Setup complete");
};
@@ -231,97 +174,6 @@ app.onerror = log.error;
app.onhostcontextchanged = handleHostContextChanged;
-// Register tool: set-shader-source
-app.registerTool(
- "set-shader-source",
- {
- title: "Set Shader Source",
- description:
- "Update the shader source code. Compiles and runs the new shader immediately.",
- inputSchema: z.object({
- fragmentShader: z
- .string()
- .describe("The main fragment shader source code (mainImage function)"),
- common: z
- .string()
- .optional()
- .describe("Common code shared across all shaders"),
- bufferA: z
- .string()
- .optional()
- .describe("Buffer A shader source (for multi-pass rendering)"),
- bufferB: z
- .string()
- .optional()
- .describe("Buffer B shader source (for multi-pass rendering)"),
- bufferC: z
- .string()
- .optional()
- .describe("Buffer C shader source (for multi-pass rendering)"),
- bufferD: z
- .string()
- .optional()
- .describe("Buffer D shader source (for multi-pass rendering)"),
- }),
- },
- async (args) => {
- log.info("set-shader-source tool called");
-
- compileAndUpdateStatus(args);
-
- const result = lastCompilationStatus.success
- ? "Shader compiled and running successfully."
- : `Shader compilation failed:\n${lastCompilationStatus.errors.join("\n")}`;
-
- return {
- content: [{ type: "text" as const, text: result }],
- structuredContent: {
- success: lastCompilationStatus.success,
- errors: lastCompilationStatus.errors,
- timestamp: lastCompilationStatus.timestamp,
- },
- };
- },
-);
-
-// Register tool: get-shader-info
-app.registerTool(
- "get-shader-info",
- {
- title: "Get Shader Info",
- description: "Get the current shader source code and compilation status.",
- },
- async () => {
- log.info("get-shader-info tool called");
-
- const hasShader = currentShaderSources.fragmentShader.length > 0;
- const isPlaying = shaderToy?.isPlaying() ?? false;
-
- let statusText = "";
- if (!hasShader) {
- statusText = "No shader loaded.";
- } else if (lastCompilationStatus.success) {
- statusText = `Shader is ${isPlaying ? "running" : "paused"}.`;
- } else {
- statusText = `Shader has compilation errors:\n${lastCompilationStatus.errors.join("\n")}`;
- }
-
- return {
- content: [
- {
- type: "text" as const,
- text: `${statusText}\n\nCurrent fragment shader:\n${currentShaderSources.fragmentShader || "(none)"}`,
- },
- ],
- structuredContent: {
- sources: currentShaderSources,
- compilationStatus: lastCompilationStatus,
- isPlaying,
- },
- };
- },
-);
-
// Pause/resume shader based on visibility
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
diff --git a/examples/shadertoy-server/tsconfig.json b/examples/shadertoy-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/shadertoy-server/tsconfig.json
+++ b/examples/shadertoy-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/shadertoy-server/tsconfig.server.json b/examples/shadertoy-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/shadertoy-server/tsconfig.server.json
+++ b/examples/shadertoy-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/sheet-music-server/main.ts b/examples/sheet-music-server/main.ts
index d7bf3a0..8b143a7 100644
--- a/examples/sheet-music-server/main.ts
+++ b/examples/sheet-music-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json
index 85ca6bb..7e59e5b 100644
--- a/examples/sheet-music-server/package.json
+++ b/examples/sheet-music-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-sheet-music",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "MCP App Server for rendering and playing sheet music from ABC notation",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/sheet-music-server"
},
"license": "MIT",
@@ -24,14 +24,12 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"abcjs": "^6.4.4",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/sheet-music-server/server.ts b/examples/sheet-music-server/server.ts
index be7214a..cfd319b 100644
--- a/examples/sheet-music-server/server.ts
+++ b/examples/sheet-music-server/server.ts
@@ -1,8 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
diff --git a/examples/sheet-music-server/tsconfig.json b/examples/sheet-music-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/sheet-music-server/tsconfig.json
+++ b/examples/sheet-music-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/sheet-music-server/tsconfig.server.json b/examples/sheet-music-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/sheet-music-server/tsconfig.server.json
+++ b/examples/sheet-music-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/system-monitor-server/main.ts b/examples/system-monitor-server/main.ts
index c55c799..365a560 100644
--- a/examples/system-monitor-server/main.ts
+++ b/examples/system-monitor-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json
index 2dc3ef1..c0d1dcf 100644
--- a/examples/system-monitor-server/package.json
+++ b/examples/system-monitor-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-system-monitor",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "System monitor MCP App Server with real-time stats",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/system-monitor-server"
},
"license": "MIT",
@@ -26,15 +26,13 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "systeminformation": "^5.31.6",
- "zod": "^4.2.0"
+ "systeminformation": "^5.31.5",
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts
index a17bcf6..8fac5f2 100644
--- a/examples/system-monitor-server/server.ts
+++ b/examples/system-monitor-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -135,8 +135,8 @@ export function createServer(): McpServer {
title: "Get System Info",
description:
"Returns system information, including hostname, platform, CPU info, and memory.",
- inputSchema: z.object({}),
- outputSchema: SystemInfoSchema,
+ inputSchema: {},
+ outputSchema: SystemInfoSchema.shape,
_meta: { ui: { resourceUri } },
},
(): CallToolResult => {
@@ -156,8 +156,8 @@ export function createServer(): McpServer {
title: "Poll System Stats",
description:
"Returns dynamic system metrics for polling: per-core CPU timing, memory usage, and uptime. App-only.",
- inputSchema: z.object({}),
- outputSchema: PollStatsSchema,
+ inputSchema: {},
+ outputSchema: PollStatsSchema.shape,
_meta: { ui: { visibility: ["app"] } },
},
async (): Promise => {
diff --git a/examples/system-monitor-server/tsconfig.json b/examples/system-monitor-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/system-monitor-server/tsconfig.json
+++ b/examples/system-monitor-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/system-monitor-server/tsconfig.server.json b/examples/system-monitor-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/system-monitor-server/tsconfig.server.json
+++ b/examples/system-monitor-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/threejs-server/main.ts b/examples/threejs-server/main.ts
index ef16e26..13a2084 100644
--- a/examples/threejs-server/main.ts
+++ b/examples/threejs-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json
index 68864d9..d148669 100644
--- a/examples/threejs-server/package.json
+++ b/examples/threejs-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-threejs",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Three.js 3D visualization MCP App Server",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/threejs-server"
},
"license": "MIT",
@@ -26,17 +26,14 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"three": "^0.181.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts
index 0a13c19..ae320df 100644
--- a/examples/threejs-server/server.ts
+++ b/examples/threejs-server/server.ts
@@ -8,10 +8,8 @@ import {
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -163,7 +161,7 @@ export function createServer(): McpServer {
title: "Show Three.js Scene",
description:
"Render an interactive 3D scene with custom Three.js code. Supports transparent backgrounds (alpha: true) for seamless host UI integration. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.",
- inputSchema: z.object({
+ inputSchema: {
code: z
.string()
.default(DEFAULT_THREEJS_CODE)
@@ -174,7 +172,7 @@ export function createServer(): McpServer {
.positive()
.default(400)
.describe("Height in pixels"),
- }),
+ },
outputSchema: z.object({
success: z.boolean(),
}),
@@ -194,7 +192,7 @@ export function createServer(): McpServer {
{
title: "Learn Three.js",
description: "Get documentation and examples for using the Three.js View",
- inputSchema: z.object({}),
+ inputSchema: {},
},
async () => {
return {
diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx
index 44fee0c..9ef24d2 100644
--- a/examples/threejs-server/src/mcp-app-wrapper.tsx
+++ b/examples/threejs-server/src/mcp-app-wrapper.tsx
@@ -6,10 +6,9 @@
*/
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
-import type { CallToolResult } from "@modelcontextprotocol/client";
-import { StrictMode, useState, useCallback, useEffect, useRef } from "react";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import { StrictMode, useState, useEffect } from "react";
import { createRoot } from "react-dom/client";
-import { z } from "zod";
import ThreeJSApp from "./threejs-app.tsx";
import "./global.css";
@@ -17,20 +16,6 @@ import "./global.css";
// Types
// =============================================================================
-/**
- * Scene state tracked for view interaction tools.
- */
-export interface SceneState {
- /** Current Three.js code */
- code: string | null;
- /** Canvas height */
- height: number;
- /** Last error message if any */
- error: string | null;
- /** Whether the scene is currently rendering */
- isRendering: boolean;
-}
-
/**
* Props passed to the view component.
* This interface can be reused for other views.
@@ -46,105 +31,6 @@ export interface ViewProps> {
toolResult: CallToolResult | null;
/** Host context (theme, dimensions, locale, etc.) */
hostContext: McpUiHostContext | null;
- /** Call a tool on the MCP server */
- callServerTool: App["callServerTool"];
- /** Send a message to the host's chat */
- sendMessage: App["sendMessage"];
- /** Request the host to open a URL */
- openLink: App["openLink"];
- /** Send log messages to the host */
- sendLog: App["sendLog"];
- /** Callback to report scene errors */
- onSceneError: (error: string | null) => void;
- /** Callback to report scene is rendering */
- onSceneRendering: (isRendering: boolean) => void;
-}
-
-// =============================================================================
-// Widget Interaction Tools
-// =============================================================================
-
-/**
- * Registers widget interaction tools on the App instance.
- * These tools allow the model to interact with the Three.js scene.
- */
-function registerWidgetTools(
- app: App,
- sceneStateRef: React.RefObject,
- setToolInputs: (args: Record) => void,
-): void {
- // Tool: set-scene-source - Update the scene source/configuration
- app.registerTool(
- "set-scene-source",
- {
- title: "Set Scene Source",
- description:
- "Update the Three.js scene source code. The code will be executed in a sandboxed environment with access to THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, and height.",
- inputSchema: z.object({
- code: z.string().describe("JavaScript code to render the 3D scene"),
- height: z
- .number()
- .int()
- .positive()
- .optional()
- .describe("Height in pixels (optional, defaults to current)"),
- }),
- outputSchema: z.object({
- success: z.boolean(),
- code: z.string(),
- height: z.number(),
- }),
- },
- async (args) => {
- sceneStateRef.current.code = args.code;
- if (args.height !== undefined) {
- sceneStateRef.current.height = args.height;
- }
- sceneStateRef.current.error = null;
- setToolInputs({ code: args.code, height: sceneStateRef.current.height });
-
- const result = {
- success: true,
- code: args.code,
- height: sceneStateRef.current.height,
- };
-
- return {
- content: [{ type: "text" as const, text: JSON.stringify(result) }],
- structuredContent: result,
- };
- },
- );
-
- // Tool: get-scene-info - Get current scene state and any errors
- app.registerTool(
- "get-scene-info",
- {
- title: "Get Scene Info",
- description:
- "Get the current Three.js scene state including source code, dimensions, rendering status, and any errors.",
- outputSchema: z.object({
- code: z.string().nullable(),
- height: z.number(),
- error: z.string().nullable(),
- isRendering: z.boolean(),
- }),
- },
- async () => {
- const state = sceneStateRef.current;
- const result = {
- code: state.code,
- height: state.height,
- error: state.error,
- isRendering: state.isRendering,
- };
-
- return {
- content: [{ type: "text" as const, text: JSON.stringify(result) }],
- structuredContent: result,
- };
- },
- );
}
// =============================================================================
@@ -162,38 +48,14 @@ function McpAppWrapper() {
const [toolResult, setToolResult] = useState(null);
const [hostContext, setHostContext] = useState(null);
- // Scene state for widget interaction tools
- const sceneStateRef = useRef({
- code: null,
- height: 400,
- error: null,
- isRendering: false,
- });
-
- // Reference to app for tools to access updateModelContext
- const appRef = useRef(null);
-
const { app, error } = useApp({
appInfo: { name: "Three.js View", version: "1.0.0" },
- capabilities: { tools: {} },
+ capabilities: {},
onAppCreated: (app) => {
- appRef.current = app;
-
- // Register widget interaction tools before connect()
- registerWidgetTools(app, sceneStateRef, setToolInputs);
-
// Complete tool input (streaming finished)
app.ontoolinput = (params) => {
- const args = params.arguments as Record;
- setToolInputs(args);
+ setToolInputs(params.arguments as Record);
setToolInputsPartial(null);
- // Update scene state from tool input
- if (typeof args.code === "string") {
- sceneStateRef.current.code = args.code;
- }
- if (typeof args.height === "number") {
- sceneStateRef.current.height = args.height;
- }
};
// Partial tool input (streaming in progress)
app.ontoolinputpartial = (params) => {
@@ -248,53 +110,6 @@ function McpAppWrapper() {
}
}, [app]);
- // Memoized callbacks that forward to app methods
- const callServerTool = useCallback(
- (params, options) => app!.callServerTool(params, options),
- [app],
- );
- const sendMessage = useCallback(
- (params, options) => app!.sendMessage(params, options),
- [app],
- );
- const openLink = useCallback(
- (params, options) => app!.openLink(params, options),
- [app],
- );
- const sendLog = useCallback(
- (params) => app!.sendLog(params),
- [app],
- );
-
- // Callback for scene to report errors
- const onSceneError = useCallback((sceneError: string | null) => {
- sceneStateRef.current.error = sceneError;
-
- // Send errors to model context for awareness
- if (sceneError && appRef.current) {
- void appRef.current
- .updateModelContext({
- content: [
- {
- type: "text" as const,
- text: `Three.js Scene Error: ${sceneError}`,
- },
- ],
- structuredContent: {
- type: "scene_error",
- error: sceneError,
- timestamp: new Date().toISOString(),
- },
- })
- .catch(() => {});
- }
- }, []);
-
- // Callback for scene to report rendering state
- const onSceneRendering = useCallback((isRendering: boolean) => {
- sceneStateRef.current.isRendering = isRendering;
- }, []);
-
if (error) {
return Error: {error.message}
;
}
@@ -310,12 +125,6 @@ function McpAppWrapper() {
toolInputsPartial={toolInputsPartial}
toolResult={toolResult}
hostContext={hostContext}
- callServerTool={callServerTool}
- sendMessage={sendMessage}
- openLink={openLink}
- sendLog={sendLog}
- onSceneError={onSceneError}
- onSceneRendering={onSceneRendering}
/>
);
}
diff --git a/examples/threejs-server/src/threejs-app.tsx b/examples/threejs-server/src/threejs-app.tsx
index 966bd64..1de585f 100644
--- a/examples/threejs-server/src/threejs-app.tsx
+++ b/examples/threejs-server/src/threejs-app.tsx
@@ -200,12 +200,6 @@ export default function ThreeJSApp({
toolInputs,
toolInputsPartial,
hostContext,
- callServerTool: _callServerTool,
- sendMessage: _sendMessage,
- openLink: _openLink,
- sendLog: _sendLog,
- onSceneError,
- onSceneRendering,
}: ThreeJSAppProps) {
const [error, setError] = useState(null);
const [currentDisplayMode, setCurrentDisplayMode] = useState<
@@ -310,9 +304,6 @@ export default function ThreeJSApp({
animControllerRef.current = createAnimationController();
setError(null);
- onSceneError(null);
- onSceneRendering(true);
-
const w = containerWidth || containerRef.current.offsetWidth || 800;
const h = isFullscreen && hostHeight > 0 ? hostHeight : height;
executeThreeCode(
@@ -321,18 +312,12 @@ export default function ThreeJSApp({
w,
h,
animControllerRef.current.visibilityAwareRAF,
- ).catch((e) => {
- const msg = e instanceof Error ? e.message : "Unknown error";
- setError(msg);
- onSceneError(msg);
- onSceneRendering(false);
- });
+ ).catch((e) => setError(e instanceof Error ? e.message : "Unknown error"));
return () => {
canvas.removeEventListener("touchstart", preventDefault);
canvas.removeEventListener("touchmove", preventDefault);
animControllerRef.current?.cleanup();
- onSceneRendering(false);
};
}, [code, height, containerWidth, isFullscreen, hostHeight]);
diff --git a/examples/threejs-server/tsconfig.json b/examples/threejs-server/tsconfig.json
index 3ecbdad..fc3c210 100644
--- a/examples/threejs-server/tsconfig.json
+++ b/examples/threejs-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/threejs-server/tsconfig.server.json b/examples/threejs-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/threejs-server/tsconfig.server.json
+++ b/examples/threejs-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/transcript-server/README.md b/examples/transcript-server/README.md
index 559ffad..3b3fa42 100644
--- a/examples/transcript-server/README.md
+++ b/examples/transcript-server/README.md
@@ -56,7 +56,7 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w
### Prerequisites
-- Node.js 20+
+- Node.js 18+
- Chrome, Edge, or Safari (Web Speech API support)
### Installation
diff --git a/examples/transcript-server/main.ts b/examples/transcript-server/main.ts
index e86ad7c..68db8dc 100644
--- a/examples/transcript-server/main.ts
+++ b/examples/transcript-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -25,7 +25,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json
index 46a8f98..462c4a1 100644
--- a/examples/transcript-server/package.json
+++ b/examples/transcript-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-transcript",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "MCP App Server for live speech transcription",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/transcript-server"
},
"license": "MIT",
@@ -24,13 +24,11 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/transcript-server/server.ts b/examples/transcript-server/server.ts
index 863eb29..56bf88d 100644
--- a/examples/transcript-server/server.ts
+++ b/examples/transcript-server/server.ts
@@ -1,8 +1,8 @@
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import {
@@ -10,7 +10,6 @@ import {
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")
@@ -34,7 +33,7 @@ export function createServer(): McpServer {
title: "Transcribe Speech",
description:
"Opens a live speech transcription interface using the Web Speech API.",
- inputSchema: z.object({}),
+ inputSchema: {},
_meta: { ui: { resourceUri } },
},
async (): Promise => {
diff --git a/examples/transcript-server/tsconfig.json b/examples/transcript-server/tsconfig.json
index 00e4801..535267b 100644
--- a/examples/transcript-server/tsconfig.json
+++ b/examples/transcript-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client", "dom-speech-recognition"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/transcript-server/tsconfig.server.json b/examples/transcript-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/transcript-server/tsconfig.server.json
+++ b/examples/transcript-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/video-resource-server/main.ts b/examples/video-resource-server/main.ts
index 1983373..082fa5a 100644
--- a/examples/video-resource-server/main.ts
+++ b/examples/video-resource-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json
index 4a7a61f..16566ac 100644
--- a/examples/video-resource-server/package.json
+++ b/examples/video-resource-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-video-resource",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "MCP App Server demonstrating video resources served as base64 blobs",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/video-resource-server"
},
"license": "MIT",
@@ -24,14 +24,11 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts
index 8ae6240..2cb317e 100644
--- a/examples/video-resource-server/server.ts
+++ b/examples/video-resource-server/server.ts
@@ -12,9 +12,11 @@ import {
import {
McpServer,
ResourceTemplate,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+} from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -133,14 +135,14 @@ Available videos:
${Object.entries(VIDEO_LIBRARY)
.map(([id, v]) => `- ${id}: ${v.description}`)
.join("\n")}`,
- inputSchema: z.object({
+ inputSchema: {
videoId: z
.enum(Object.keys(VIDEO_LIBRARY) as [string, ...string[]])
.default("bunny-1mb")
.describe(
`Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`,
),
- }),
+ },
outputSchema: z.object({
videoUri: z.string(),
description: z.string(),
diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts
index 2a3f8f9..88ed913 100644
--- a/examples/video-resource-server/src/mcp-app.ts
+++ b/examples/video-resource-server/src/mcp-app.ts
@@ -5,7 +5,7 @@
* The video is served as a base64 blob and converted to a data URI for playback.
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import "./global.css";
import "./mcp-app.css";
diff --git a/examples/video-resource-server/tsconfig.json b/examples/video-resource-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/video-resource-server/tsconfig.json
+++ b/examples/video-resource-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/video-resource-server/tsconfig.server.json b/examples/video-resource-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/video-resource-server/tsconfig.server.json
+++ b/examples/video-resource-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/wiki-explorer-server/main.ts b/examples/wiki-explorer-server/main.ts
index bfeeffb..cc7b3a2 100644
--- a/examples/wiki-explorer-server/main.ts
+++ b/examples/wiki-explorer-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
-import { createMcpExpressApp } from "@modelcontextprotocol/express";
-import type { McpServer } from "@modelcontextprotocol/server";
-import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
+import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new NodeStreamableHTTPServerTransport({
+ const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json
index 52304cf..1bfc981 100644
--- a/examples/wiki-explorer-server/package.json
+++ b/examples/wiki-explorer-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-wiki-explorer",
- "version": "2.0.0",
+ "version": "1.4.0",
"type": "module",
"description": "Wikipedia link explorer MCP App Server with graph visualization",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
+ "url": "https://github.com/modelcontextprotocol/ext-apps",
"directory": "examples/wiki-explorer-server"
},
"license": "MIT",
@@ -26,15 +26,12 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cheerio": "^1.0.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts
index 8bc4eb7..fe2e89b 100644
--- a/examples/wiki-explorer-server/server.ts
+++ b/examples/wiki-explorer-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
-import {
- McpServer,
- type CallToolResult,
- type ReadResourceResult,
-} from "@modelcontextprotocol/server";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import type {
+ CallToolResult,
+ ReadResourceResult,
+} from "@modelcontextprotocol/sdk/types.js";
import * as cheerio from "cheerio";
import fs from "node:fs/promises";
import path from "node:path";
@@ -86,7 +86,7 @@ export function createServer(): McpServer {
{
title: "Get First-Degree Links",
description:
- "Returns all Wikipedia pages that the given page links to directly. The widget is interactive and exposes tools for exploring the graph (expanding nodes to see their links), searching for articles, and querying visible nodes.",
+ "Returns all Wikipedia pages that the given page links to directly.",
inputSchema: z.object({
url: z
.string()
diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts
index 9833d9c..69bec05 100644
--- a/examples/wiki-explorer-server/src/mcp-app.ts
+++ b/examples/wiki-explorer-server/src/mcp-app.ts
@@ -2,8 +2,7 @@
* Wiki Explorer - Force-directed graph visualization of Wikipedia link networks
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/client";
-import { z } from "zod";
+import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
import {
forceCenter,
forceCollide,
@@ -398,346 +397,6 @@ function handleHostContextChanged(ctx: McpUiHostContext) {
app.onhostcontextchanged = handleHostContextChanged;
-// =============================================================================
-// Widget Interaction Tools
-// =============================================================================
-
-// Tool: Search for a Wikipedia article and navigate to it
-app.registerTool(
- "search-article",
- {
- title: "Search Article",
- description:
- "Search for a Wikipedia article and add it to the graph as the new starting point",
- inputSchema: z.object({
- query: z.string().describe("Search query for Wikipedia article"),
- }),
- },
- async (args) => {
- const { query } = args as { query: string };
-
- // Construct Wikipedia search URL that redirects to the article
- const searchUrl = `https://en.wikipedia.org/wiki/Special:Search?go=Go&search=${encodeURIComponent(query)}`;
-
- // Use the server tool to fetch the article
- const result = await app.callServerTool({
- name: "get-first-degree-links",
- arguments: { url: searchUrl },
- });
-
- const response = result.structuredContent as unknown as ToolResponse;
- if (response && response.page) {
- // Clear existing graph and start fresh with this article
- graphData.nodes = [];
- graphData.links = [];
- initialUrl = response.page.url;
- addNode(response.page.url, response.page.title, "default", {
- x: 0,
- y: 0,
- });
- graph.warmupTicks(100);
- handleToolResultData(result);
- graph.centerAt(0, 0, 500);
-
- return {
- content: [
- {
- type: "text" as const,
- text: `Navigated to article: ${response.page.title}`,
- },
- ],
- structuredContent: {
- success: true,
- article: response.page,
- linksFound: response.links?.length ?? 0,
- },
- };
- }
-
- return {
- content: [
- { type: "text" as const, text: `Could not find article for: ${query}` },
- ],
- structuredContent: {
- success: false,
- error: "Article not found",
- },
- };
- },
-);
-
-// Tool: Get information about the currently displayed article
-app.registerTool(
- "get-current-article",
- {
- title: "Get Current Article",
- description:
- "Get information about the currently selected or initial article in the graph",
- },
- async () => {
- const currentUrl = selectedNodeUrl || initialUrl;
-
- if (!currentUrl) {
- return {
- content: [
- { type: "text" as const, text: "No article is currently selected" },
- ],
- structuredContent: {
- hasSelection: false,
- article: null,
- },
- };
- }
-
- const node = graphData.nodes.find((n) => n.url === currentUrl);
-
- if (!node) {
- return {
- content: [
- {
- type: "text" as const,
- text: "Selected article not found in graph",
- },
- ],
- structuredContent: {
- hasSelection: false,
- article: null,
- },
- };
- }
-
- return {
- content: [
- {
- type: "text" as const,
- text: `Current article: ${node.title}\nURL: ${node.url}\nState: ${node.state}`,
- },
- ],
- structuredContent: {
- hasSelection: true,
- article: {
- url: node.url,
- title: node.title,
- state: node.state,
- isExpanded: node.state === "expanded",
- hasError: node.state === "error",
- errorMessage: node.errorMessage,
- },
- },
- };
- },
-);
-
-// Tool: Highlight a specific node in the graph
-app.registerTool(
- "highlight-node",
- {
- title: "Highlight Node",
- description:
- "Highlight and center on a specific node in the graph by title or URL",
- inputSchema: z.object({
- identifier: z
- .string()
- .describe("The title or URL of the node to highlight"),
- }),
- },
- async (args) => {
- const { identifier } = args as { identifier: string };
- const lowerIdentifier = identifier.toLowerCase();
-
- // Find node by title (case-insensitive partial match) or exact URL
- const node = graphData.nodes.find(
- (n) =>
- n.url === identifier || n.title.toLowerCase().includes(lowerIdentifier),
- );
-
- if (!node) {
- return {
- content: [
- { type: "text" as const, text: `Node not found: ${identifier}` },
- ],
- structuredContent: {
- success: false,
- error: "Node not found in graph",
- availableNodes: graphData.nodes.map((n) => n.title),
- },
- };
- }
-
- // Center on the node and select it
- selectedNodeUrl = node.url;
- if (node.x !== undefined && node.y !== undefined) {
- graph.centerAt(node.x, node.y, 500);
- graph.zoom(2, 500);
- }
-
- return {
- content: [
- { type: "text" as const, text: `Highlighted node: ${node.title}` },
- ],
- structuredContent: {
- success: true,
- node: {
- url: node.url,
- title: node.title,
- state: node.state,
- },
- },
- };
- },
-);
-
-// Tool: Expand a node to show its linked pages
-app.registerTool(
- "expand-node",
- {
- title: "Expand Node",
- description:
- "Expand a node to fetch and display all Wikipedia pages it links to. This is the core way to explore the graph.",
- inputSchema: z.object({
- identifier: z.string().describe("The title or URL of the node to expand"),
- }),
- },
- async (args) => {
- const { identifier } = args as { identifier: string };
- const lowerIdentifier = identifier.toLowerCase();
-
- // Find node by title (case-insensitive partial match) or exact URL
- const node = graphData.nodes.find(
- (n) =>
- n.url === identifier || n.title.toLowerCase().includes(lowerIdentifier),
- );
-
- if (!node) {
- return {
- content: [
- { type: "text" as const, text: `Node not found: ${identifier}` },
- ],
- structuredContent: {
- success: false,
- error: "Node not found in graph",
- availableNodes: graphData.nodes.map((n) => n.title),
- },
- };
- }
-
- if (node.state === "expanded") {
- return {
- content: [
- {
- type: "text" as const,
- text: `Node "${node.title}" is already expanded`,
- },
- ],
- structuredContent: {
- success: true,
- alreadyExpanded: true,
- node: { url: node.url, title: node.title },
- },
- };
- }
-
- if (node.state === "error") {
- return {
- content: [
- {
- type: "text" as const,
- text: `Node "${node.title}" has an error: ${node.errorMessage}`,
- },
- ],
- structuredContent: {
- success: false,
- error: node.errorMessage,
- },
- };
- }
-
- try {
- // Fetch the linked pages using the server tool
- const result = await app.callServerTool({
- name: "get-first-degree-links",
- arguments: { url: node.url },
- });
-
- graph.warmupTicks(0);
- handleToolResultData(result);
-
- const response = result.structuredContent as unknown as ToolResponse;
- const linksAdded = response?.links?.length ?? 0;
-
- // Center on the expanded node
- if (node.x !== undefined && node.y !== undefined) {
- graph.centerAt(node.x, node.y, 500);
- }
-
- return {
- content: [
- {
- type: "text" as const,
- text: `Expanded "${node.title}" - found ${linksAdded} linked articles`,
- },
- ],
- structuredContent: {
- success: true,
- node: { url: node.url, title: node.title },
- linksAdded,
- },
- };
- } catch (e) {
- setNodeState(node.url, "error", "Request failed");
- updateGraph();
- return {
- content: [
- {
- type: "text" as const,
- text: `Failed to expand "${node.title}": ${e instanceof Error ? e.message : String(e)}`,
- },
- ],
- structuredContent: {
- success: false,
- error: String(e),
- },
- };
- }
- },
-);
-
-// Tool: Get list of currently visible nodes in the graph
-app.registerTool(
- "get-visible-nodes",
- {
- title: "Get Visible Nodes",
- description: "Get a list of all nodes currently visible in the graph",
- },
- async () => {
- const nodes = graphData.nodes.map((n) => ({
- url: n.url,
- title: n.title,
- state: n.state,
- isExpanded: n.state === "expanded",
- hasError: n.state === "error",
- }));
-
- const expandedCount = nodes.filter((n) => n.isExpanded).length;
- const errorCount = nodes.filter((n) => n.hasError).length;
-
- return {
- content: [
- {
- type: "text" as const,
- text: `Graph contains ${nodes.length} nodes:\n${nodes.map((n) => `- ${n.title} (${n.state})`).join("\n")}`,
- },
- ],
- structuredContent: {
- totalNodes: nodes.length,
- expandedNodes: expandedCount,
- errorNodes: errorCount,
- nodes,
- },
- };
- },
-);
-
// Connect to host
app.connect().then(() => {
const ctx = app.getHostContext();
diff --git a/examples/wiki-explorer-server/tsconfig.json b/examples/wiki-explorer-server/tsconfig.json
index b028ff5..535267b 100644
--- a/examples/wiki-explorer-server/tsconfig.json
+++ b/examples/wiki-explorer-server/tsconfig.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/wiki-explorer-server/tsconfig.server.json b/examples/wiki-explorer-server/tsconfig.server.json
index 27eb28f..05ddd8e 100644
--- a/examples/wiki-explorer-server/tsconfig.server.json
+++ b/examples/wiki-explorer-server/tsconfig.server.json
@@ -1,6 +1,5 @@
{
"compilerOptions": {
- "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/package-lock.json b/package-lock.json
index 0189ca2..54d7d69 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,25 +1,19 @@
{
"name": "@modelcontextprotocol/ext-apps",
- "version": "2.0.0",
+ "version": "1.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/ext-apps",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"workspaces": [
"examples/*"
],
- "dependencies": {
- "@standard-schema/spec": "^1.1.0"
- },
"devDependencies": {
"@boneskull/typedoc-plugin-mermaid": "^0.2.0",
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/core": "2.0.0",
- "@modelcontextprotocol/sdk": "1.30.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"@playwright/test": "1.57.0",
"@types/bun": "^1.3.2",
"@types/node": "20.19.27",
@@ -34,7 +28,6 @@
"electron-to-chromium": "1.5.267",
"esbuild": "^0.25.12",
"express": "^5.1.0",
- "ext-apps-v1": "npm:@modelcontextprotocol/ext-apps@1.7.5",
"husky": "^9.1.7",
"nodemon": "^3.1.0",
"playwright": "1.57.0",
@@ -42,29 +35,24 @@
"prettier": "^3.6.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "sharp": "^0.35.3",
+ "sharp": "^0.34.5",
"ts-to-zod": "^5.1.0",
"tsx": "^4.21.0",
"typedoc": "^0.28.14",
"typedoc-github-theme": "^0.4.0",
"typescript": "^5.9.3",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
- "@modelcontextprotocol/client": "^2.0.0",
- "@modelcontextprotocol/core": "^2.0.0",
- "@modelcontextprotocol/server": "^2.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "zod": "^4.2.0"
+ "zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
- "@modelcontextprotocol/server": {
- "optional": true
- },
"react": {
"optional": true
},
@@ -75,13 +63,13 @@
},
"examples/basic-host": {
"name": "@modelcontextprotocol/ext-apps-basic-host",
- "version": "2.0.0",
+ "version": "1.4.0",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"devDependencies": {
"@types/express": "^5.0.0",
@@ -95,7 +83,8 @@
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"vite": "^6.0.0",
- "vite-plugin-singlefile": "^2.3.0"
+ "vite-plugin-singlefile": "^2.3.0",
+ "vitest": "^3.2.4"
}
},
"examples/basic-host/node_modules/@types/node": {
@@ -117,18 +106,15 @@
},
"examples/basic-server-preact": {
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"preact": "^10.0.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-server-basic-preact": "dist/index.js"
@@ -164,19 +150,16 @@
},
"examples/basic-server-react": {
"name": "@modelcontextprotocol/server-basic-react",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-server-basic-react": "dist/index.js"
@@ -214,18 +197,15 @@
},
"examples/basic-server-solid": {
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"solid-js": "1.9.10",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-server-basic-solid": "dist/index.js"
@@ -239,7 +219,7 @@
"typescript": "^5.9.3",
"vite": "^6.0.0",
"vite-plugin-singlefile": "^2.3.0",
- "vite-plugin-solid": "^2.11.12"
+ "vite-plugin-solid": "^2.0.0"
}
},
"examples/basic-server-solid/node_modules/@types/node": {
@@ -261,18 +241,15 @@
},
"examples/basic-server-svelte": {
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"svelte": "^5.0.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-server-basic-svelte": "dist/index.js"
@@ -308,17 +285,14 @@
},
"examples/basic-server-vanillajs": {
"name": "@modelcontextprotocol/server-basic-vanillajs",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-server-basic-vanillajs": "dist/index.js"
@@ -353,18 +327,15 @@
},
"examples/basic-server-vue": {
"name": "@modelcontextprotocol/server-basic-vue",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"vue": "^3.5.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-server-basic-vue": "dist/index.js"
@@ -400,17 +371,15 @@
},
"examples/budget-allocator-server": {
"name": "@modelcontextprotocol/server-budget-allocator",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-budget-allocator-server": "dist/index.js"
@@ -445,18 +414,16 @@
},
"examples/cohort-heatmap-server": {
"name": "@modelcontextprotocol/server-cohort-heatmap",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-cohort-heatmap-server": "dist/index.js"
@@ -494,17 +461,15 @@
},
"examples/customer-segmentation-server": {
"name": "@modelcontextprotocol/server-customer-segmentation",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-customer-segmentation-server": "dist/index.js"
@@ -539,14 +504,12 @@
},
"examples/debug-server": {
"name": "@modelcontextprotocol/server-debug",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
- "zod": "^4.2.0"
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
+ "zod": "^4.1.13"
},
"bin": {
"mcp-server-debug": "dist/index.js"
@@ -582,18 +545,15 @@
"license": "MIT"
},
"examples/integration-server": {
- "version": "2.0.0",
+ "version": "1.4.0",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-integration-server": "dist/index.js"
@@ -628,63 +588,16 @@
"dev": true,
"license": "MIT"
},
- "examples/lazy-auth-server": {
- "name": "@modelcontextprotocol/server-lazy-auth",
- "version": "2.0.0",
- "license": "MIT",
- "dependencies": {
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
- "cors": "^2.8.5",
- "express": "^5.1.0",
- "jose": "^6.0.0",
- "zod": "^4.2.0"
- },
- "bin": {
- "mcp-server-lazy-auth": "dist/index.js"
- },
- "devDependencies": {
- "@types/cors": "^2.8.19",
- "@types/express": "^5.0.0",
- "@types/node": "22.10.0",
- "concurrently": "^9.2.1",
- "cross-env": "^10.1.0",
- "typescript": "^5.9.3",
- "vite": "^6.0.0",
- "vite-plugin-singlefile": "^2.3.0"
- }
- },
- "examples/lazy-auth-server/node_modules/@types/node": {
- "version": "22.10.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz",
- "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.20.0"
- }
- },
- "examples/lazy-auth-server/node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
- "dev": true,
- "license": "MIT"
- },
"examples/map-server": {
"name": "@modelcontextprotocol/server-map",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-map-server": "dist/index.js"
@@ -719,19 +632,16 @@
},
"examples/pdf-server": {
"name": "@modelcontextprotocol/server-pdf",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@cantoo/pdf-lib": "^2.6.5",
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
+ "pdf-lib": "^1.17.1",
"pdfjs-dist": "^5.0.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-pdf-server": "dist/index.js"
@@ -766,23 +676,21 @@
},
"examples/qr-server": {
"name": "@modelcontextprotocol/server-qr",
- "version": "2.0.0",
+ "version": "1.4.0",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^2.0.0"
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0"
}
},
"examples/quickstart": {
"name": "@modelcontextprotocol/quickstart",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
- "express": "^5.1.0",
- "zod": "^4.2.0"
+ "express": "^5.1.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
@@ -808,27 +716,26 @@
},
"examples/say-server": {
"name": "@modelcontextprotocol/server-say",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^2.0.0"
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0"
}
},
"examples/scenario-modeler-server": {
"name": "@modelcontextprotocol/server-scenario-modeler",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-scenario-modeler-server": "dist/index.js"
@@ -866,16 +773,14 @@
},
"examples/shadertoy-server": {
"name": "@modelcontextprotocol/server-shadertoy",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-shadertoy-server": "dist/index.js"
@@ -910,17 +815,15 @@
},
"examples/sheet-music-server": {
"name": "@modelcontextprotocol/server-sheet-music",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"abcjs": "^6.4.4",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-sheet-music-server": "dist/index.js"
@@ -955,18 +858,16 @@
},
"examples/system-monitor-server": {
"name": "@modelcontextprotocol/server-system-monitor",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"chart.js": "^4.4.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "systeminformation": "^5.31.6",
- "zod": "^4.2.0"
+ "systeminformation": "^5.31.5",
+ "zod": "^4.1.13"
},
"bin": {
"mcp-system-monitor-server": "dist/index.js"
@@ -992,6 +893,32 @@
"undici-types": "~6.20.0"
}
},
+ "examples/system-monitor-server/node_modules/systeminformation": {
+ "version": "5.31.5",
+ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.5.tgz",
+ "integrity": "sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==",
+ "license": "MIT",
+ "os": [
+ "darwin",
+ "linux",
+ "win32",
+ "freebsd",
+ "openbsd",
+ "netbsd",
+ "sunos",
+ "android"
+ ],
+ "bin": {
+ "systeminformation": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "Buy me a coffee",
+ "url": "https://www.buymeacoffee.com/systeminfo"
+ }
+ },
"examples/system-monitor-server/node_modules/undici-types": {
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
@@ -1001,20 +928,17 @@
},
"examples/threejs-server": {
"name": "@modelcontextprotocol/server-threejs",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"three": "^0.181.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-threejs-server": "dist/index.js"
@@ -1053,16 +977,14 @@
},
"examples/transcript-server": {
"name": "@modelcontextprotocol/server-transcript",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-transcript-server": "dist/index.js"
@@ -1098,17 +1020,14 @@
},
"examples/video-resource-server": {
"name": "@modelcontextprotocol/server-video-resource",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-video-resource-server": "dist/index.js"
@@ -1143,18 +1062,15 @@
},
"examples/wiki-explorer-server": {
"name": "@modelcontextprotocol/server-wiki-explorer",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/express": "2.0.0",
- "@modelcontextprotocol/ext-apps": "^2.0.0",
- "@modelcontextprotocol/node": "2.0.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^1.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"cheerio": "^1.0.0",
"cors": "^2.8.5",
"express": "^5.1.0",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"bin": {
"mcp-wiki-explorer-server": "dist/index.js"
@@ -1189,13 +1105,13 @@
"license": "MIT"
},
"node_modules/@babel/code-frame": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
+ "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.28.5",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -1204,9 +1120,9 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
+ "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1214,21 +1130,21 @@
}
},
"node_modules/@babel/core": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
+ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-compilation-targets": "^7.29.7",
- "@babel/helper-module-transforms": "^7.29.7",
- "@babel/helpers": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7",
+ "@babel/code-frame": "^7.28.6",
+ "@babel/generator": "^7.28.6",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -1255,14 +1171,14 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
- "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
+ "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -1285,14 +1201,14 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
@@ -1312,9 +1228,9 @@
}
},
"node_modules/@babel/helper-globals": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1322,29 +1238,29 @@
}
},
"node_modules/@babel/helper-module-imports": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7",
- "@babel/traverse": "^7.29.7"
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -1364,27 +1280,27 @@
}
},
"node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1392,26 +1308,26 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7"
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
- "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
+ "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.7"
+ "@babel/types": "^7.28.6"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1505,33 +1421,33 @@
}
},
"node_modules/@babel/template": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7"
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
- "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
+ "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-globals": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7",
+ "@babel/code-frame": "^7.28.6",
+ "@babel/generator": "^7.28.6",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.6",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6",
"debug": "^4.3.1"
},
"engines": {
@@ -1539,13 +1455,13 @@
}
},
"node_modules/@babel/types": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
- "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
+ "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
"license": "MIT",
"dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7"
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
@@ -1570,21 +1486,6 @@
}
}
},
- "node_modules/@cantoo/pdf-lib": {
- "version": "2.6.5",
- "resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.6.5.tgz",
- "integrity": "sha512-3eMHEaqKHt/G/q+6QjT06A3lz0S/a8x3+myiSN7FNeL3uWcedO0lpfs6TWofa4C03Z1wz3tWeHoa4CsI7DrTSA==",
- "license": "MIT",
- "dependencies": {
- "@pdf-lib/standard-fonts": "^1.0.0",
- "@pdf-lib/upng": "^1.0.1",
- "color": "^4.2.3",
- "crypto-js": "^4.2.0",
- "node-html-better-parser": ">=1.4.0",
- "pako": "^1.0.11",
- "tslib": ">=2"
- }
- },
"node_modules/@clack/core": {
"version": "1.0.0-alpha.4",
"resolved": "https://registry.npmjs.org/@clack/core/-/core-1.0.0-alpha.4.tgz",
@@ -1616,9 +1517,9 @@
"license": "Apache-2.0"
},
"node_modules/@emnapi/runtime": {
- "version": "1.11.2",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz",
- "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==",
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
+ "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -2090,9 +1991,9 @@
}
},
"node_modules/@hono/node-server": {
- "version": "1.19.14",
- "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz",
- "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==",
+ "version": "1.19.11",
+ "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.11.tgz",
+ "integrity": "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==",
"license": "MIT",
"engines": {
"node": ">=18.14.1"
@@ -2102,9 +2003,9 @@
}
},
"node_modules/@img/colour": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
- "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
+ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2112,9 +2013,9 @@
}
},
"node_modules/@img/sharp-darwin-arm64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
- "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
+ "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
"cpu": [
"arm64"
],
@@ -2125,19 +2026,19 @@
"darwin"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.3.2"
+ "@img/sharp-libvips-darwin-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-darwin-x64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
- "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
+ "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
"cpu": [
"x64"
],
@@ -2148,39 +2049,19 @@
"darwin"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-freebsd-wasm32": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
- "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "dependencies": {
- "@img/sharp-wasm32": "0.35.3"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
+ "@img/sharp-libvips-darwin-x64": "1.2.4"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
- "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
+ "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
"cpu": [
"arm64"
],
@@ -2195,9 +2076,9 @@
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
- "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
+ "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
"cpu": [
"x64"
],
@@ -2212,9 +2093,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
- "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
+ "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
"cpu": [
"arm"
],
@@ -2229,9 +2110,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
- "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
+ "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
"cpu": [
"arm64"
],
@@ -2246,9 +2127,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
- "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
+ "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
"cpu": [
"ppc64"
],
@@ -2263,9 +2144,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
- "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
+ "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
"cpu": [
"riscv64"
],
@@ -2280,9 +2161,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
- "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
+ "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
"cpu": [
"s390x"
],
@@ -2297,9 +2178,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
- "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
+ "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
"cpu": [
"x64"
],
@@ -2314,9 +2195,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
- "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
+ "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
"cpu": [
"arm64"
],
@@ -2331,9 +2212,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
- "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
+ "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
"cpu": [
"x64"
],
@@ -2348,9 +2229,9 @@
}
},
"node_modules/@img/sharp-linux-arm": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
- "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
+ "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
"cpu": [
"arm"
],
@@ -2361,19 +2242,19 @@
"linux"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.3.2"
+ "@img/sharp-libvips-linux-arm": "1.2.4"
}
},
"node_modules/@img/sharp-linux-arm64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
- "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
+ "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
"cpu": [
"arm64"
],
@@ -2384,19 +2265,19 @@
"linux"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.3.2"
+ "@img/sharp-libvips-linux-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-ppc64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
- "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
+ "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
"cpu": [
"ppc64"
],
@@ -2407,19 +2288,19 @@
"linux"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-ppc64": "1.3.2"
+ "@img/sharp-libvips-linux-ppc64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-riscv64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
- "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
+ "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
"cpu": [
"riscv64"
],
@@ -2430,19 +2311,19 @@
"linux"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-riscv64": "1.3.2"
+ "@img/sharp-libvips-linux-riscv64": "1.2.4"
}
},
"node_modules/@img/sharp-linux-s390x": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
- "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
+ "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
"cpu": [
"s390x"
],
@@ -2453,19 +2334,19 @@
"linux"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.3.2"
+ "@img/sharp-libvips-linux-s390x": "1.2.4"
}
},
"node_modules/@img/sharp-linux-x64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
- "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
+ "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
"cpu": [
"x64"
],
@@ -2476,19 +2357,19 @@
"linux"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.3.2"
+ "@img/sharp-libvips-linux-x64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
- "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
+ "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
"cpu": [
"arm64"
],
@@ -2499,19 +2380,19 @@
"linux"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
- "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
+ "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
"cpu": [
"x64"
],
@@ -2522,56 +2403,39 @@
"linux"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.3.2"
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
}
},
"node_modules/@img/sharp-wasm32": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
- "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/runtime": "^1.11.1"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-webcontainers-wasm32": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
- "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
+ "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
"cpu": [
"wasm32"
],
"dev": true,
- "license": "Apache-2.0",
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
- "@img/sharp-wasm32": "0.35.3"
+ "@emnapi/runtime": "^1.7.0"
},
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
- "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
+ "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
"cpu": [
"arm64"
],
@@ -2582,16 +2446,16 @@
"win32"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
- "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
+ "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
"cpu": [
"ia32"
],
@@ -2602,16 +2466,16 @@
"win32"
],
"engines": {
- "node": "^20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
- "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
+ "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
"cpu": [
"x64"
],
@@ -2622,7 +2486,7 @@
"win32"
],
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
@@ -2679,93 +2543,64 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
- "node_modules/@modelcontextprotocol/client": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz",
- "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==",
+ "node_modules/@modelcontextprotocol/ext-apps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.0.tgz",
+ "integrity": "sha512-d5vGKBhWkRoa3xlKOynF8kd+sTSY2D3QSjTMCs46/ddOUOSn5e/E0SaShixFm7H7mNlj4uY0RuU0jAsPM/0qwA==",
+ "hasInstallScript": true,
"license": "MIT",
- "dependencies": {
- "@modelcontextprotocol/core": "2.0.0",
- "cross-spawn": "^7.0.5",
- "eventsource": "^3.0.2",
- "eventsource-parser": "^3.0.0",
- "jose": "^6.1.3",
- "pkce-challenge": "^5.0.0",
- "zod": "^4.2.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/@modelcontextprotocol/core": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
- "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
- "license": "MIT",
- "dependencies": {
- "zod": "^4.2.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
- "node_modules/@modelcontextprotocol/express": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0.tgz",
- "integrity": "sha512-Snlr8j9FR9LcVvEJPF7qJ7d5zTL4Bes2dk7RcacN9eSZ7OLohwxqhEvWu1+UxELyScgLbLLOdeVEGdwKI1iwVQ==",
- "license": "MIT",
- "dependencies": {
- "cors": "^2.8.5"
- },
- "engines": {
- "node": ">=20"
+ "workspaces": [
+ "examples/*"
+ ],
+ "optionalDependencies": {
+ "@oven/bun-darwin-aarch64": "^1.2.21",
+ "@oven/bun-darwin-x64": "^1.2.21",
+ "@oven/bun-darwin-x64-baseline": "^1.2.21",
+ "@oven/bun-linux-aarch64": "^1.2.21",
+ "@oven/bun-linux-aarch64-musl": "^1.2.21",
+ "@oven/bun-linux-x64": "^1.2.21",
+ "@oven/bun-linux-x64-baseline": "^1.2.21",
+ "@oven/bun-linux-x64-musl": "^1.2.21",
+ "@oven/bun-linux-x64-musl-baseline": "^1.2.21",
+ "@oven/bun-windows-x64": "^1.2.21",
+ "@oven/bun-windows-x64-baseline": "^1.2.21",
+ "@rollup/rollup-darwin-arm64": "^4.53.3",
+ "@rollup/rollup-darwin-x64": "^4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "^4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "^4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "^4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "^4.53.3"
},
"peerDependencies": {
- "@modelcontextprotocol/server": "^2.0.0",
- "express": "^4.18.0 || ^5.0.0"
+ "@modelcontextprotocol/sdk": "^1.24.0",
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
+ "zod": "^3.25.0 || ^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
}
},
- "node_modules/@modelcontextprotocol/ext-apps": {
- "resolved": "",
- "link": true
- },
"node_modules/@modelcontextprotocol/ext-apps-basic-host": {
"resolved": "examples/basic-host",
"link": true
},
- "node_modules/@modelcontextprotocol/node": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz",
- "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==",
- "license": "MIT",
- "dependencies": {
- "@hono/node-server": "^1.19.9"
- },
- "engines": {
- "node": ">=20"
- },
- "peerDependencies": {
- "@modelcontextprotocol/server": "^2.0.0",
- "hono": "^4.11.4"
- },
- "peerDependenciesMeta": {
- "hono": {
- "optional": true
- }
- }
- },
"node_modules/@modelcontextprotocol/quickstart": {
"resolved": "examples/quickstart",
"link": true
},
"node_modules/@modelcontextprotocol/sdk": {
- "version": "1.30.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
- "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
- "dev": true,
+ "version": "1.29.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz",
+ "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==",
"license": "MIT",
"dependencies": {
- "@hono/node-server": "^1.19.9 || ^2.0.5",
+ "@hono/node-server": "^1.19.9",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
@@ -2799,19 +2634,6 @@
}
}
},
- "node_modules/@modelcontextprotocol/server": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
- "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
- "license": "MIT",
- "dependencies": {
- "@modelcontextprotocol/core": "2.0.0",
- "zod": "^4.2.0"
- },
- "engines": {
- "node": ">=20"
- }
- },
"node_modules/@modelcontextprotocol/server-basic-preact": {
"resolved": "examples/basic-server-preact",
"link": true
@@ -2852,10 +2674,6 @@
"resolved": "examples/debug-server",
"link": true
},
- "node_modules/@modelcontextprotocol/server-lazy-auth": {
- "resolved": "examples/lazy-auth-server",
- "link": true
- },
"node_modules/@modelcontextprotocol/server-map": {
"resolved": "examples/map-server",
"link": true
@@ -3185,9 +3003,9 @@
}
},
"node_modules/@oclif/core/node_modules/brace-expansion": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
- "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3217,7 +3035,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3231,7 +3048,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3245,7 +3061,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3259,7 +3074,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3273,7 +3087,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3287,7 +3100,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3301,7 +3113,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3315,7 +3126,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3329,7 +3139,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3357,7 +3166,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3371,7 +3179,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3531,7 +3338,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3545,7 +3351,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3615,7 +3420,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3741,7 +3545,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3797,7 +3600,6 @@
"cpu": [
"arm64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3839,7 +3641,6 @@
"cpu": [
"x64"
],
- "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3895,16 +3696,10 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@standard-schema/spec": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
- "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
- "license": "MIT"
- },
"node_modules/@sveltejs/acorn-typescript": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz",
- "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz",
+ "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==",
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
@@ -4023,6 +3818,17 @@
"bun-types": "1.3.6"
}
},
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
"node_modules/@types/connect": {
"version": "3.4.38",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
@@ -4043,6 +3849,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/dom-speech-recognition": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.7.tgz",
@@ -4206,6 +4019,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.58.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
+ "integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==",
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
"node_modules/@typescript/vfs": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@typescript/vfs/-/vfs-1.6.2.tgz",
@@ -4254,6 +4080,131 @@
"vue": "^3.2.25"
}
},
+ "node_modules/@vitest/expect": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+ "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+ "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "3.2.4",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.17"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/mocker/node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+ "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "3.2.4",
+ "pathe": "^2.0.3",
+ "strip-literal": "^3.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+ "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+ "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyspy": "^4.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+ "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "3.2.4",
+ "loupe": "^3.1.4",
+ "tinyrainbow": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/@vue/compiler-core": {
"version": "3.5.27",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.27.tgz",
@@ -4407,9 +4358,9 @@
}
},
"node_modules/acorn": {
- "version": "8.16.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
- "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -4419,10 +4370,9 @@
}
},
"node_modules/ajv": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
- "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
- "dev": true,
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -4439,7 +4389,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
@@ -4538,6 +4487,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/async": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
@@ -4555,9 +4514,9 @@
}
},
"node_modules/babel-plugin-jsx-dom-expressions": {
- "version": "0.40.6",
- "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.6.tgz",
- "integrity": "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ==",
+ "version": "0.40.3",
+ "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.3.tgz",
+ "integrity": "sha512-5HOwwt0BYiv/zxl7j8Pf2bGL6rDXfV6nUhLs8ygBX+EFJXzBPHM/euj9j/6deMZ6wa52Wb2PBaAV5U/jKwIY1w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4595,17 +4554,17 @@
}
},
"node_modules/babel-preset-solid": {
- "version": "1.9.12",
- "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz",
- "integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==",
+ "version": "1.9.10",
+ "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.10.tgz",
+ "integrity": "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "babel-plugin-jsx-dom-expressions": "^0.40.6"
+ "babel-plugin-jsx-dom-expressions": "^0.40.3"
},
"peerDependencies": {
"@babel/core": "^7.0.0",
- "solid-js": "^1.9.12"
+ "solid-js": "^1.9.10"
},
"peerDependenciesMeta": {
"solid-js": {
@@ -4621,16 +4580,13 @@
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
- "version": "2.11.1",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.1.tgz",
- "integrity": "sha512-HYXq73DDpCtNzOmrFsm9eSwCvWCql0RzqjpDzXN9EadiLJ4DNat0nsZ/Bzmy+Ud12mb4/zKDY0cQ805ZzN+i0A==",
+ "version": "2.9.18",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.18.tgz",
+ "integrity": "sha512-e23vBV1ZLfjb9apvfPk4rHVu2ry6RIr2Wfs+O324okSidrX7pTAnEJPCh/O5BtRlr7QtZI7ktOP3vsqr7Z5XoA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
- "baseline-browser-mapping": "dist/cli.cjs"
- },
- "engines": {
- "node": ">=6.0.0"
+ "baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/bezier-js": {
@@ -4658,20 +4614,20 @@
}
},
"node_modules/body-parser": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
- "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
+ "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
- "content-type": "^2.0.0",
+ "content-type": "^1.0.5",
"debug": "^4.4.3",
- "http-errors": "^2.0.1",
- "iconv-lite": "^0.7.2",
+ "http-errors": "^2.0.0",
+ "iconv-lite": "^0.7.0",
"on-finished": "^2.4.1",
- "qs": "^6.15.2",
- "raw-body": "^3.0.2",
- "type-is": "^2.1.0"
+ "qs": "^6.14.1",
+ "raw-body": "^3.0.1",
+ "type-is": "^2.0.1"
},
"engines": {
"node": ">=18"
@@ -4681,19 +4637,6 @@
"url": "https://opencollective.com/express"
}
},
- "node_modules/body-parser/node_modules/content-type": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
- "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
"node_modules/body-parser/node_modules/iconv-lite": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
@@ -4717,9 +4660,9 @@
"license": "ISC"
},
"node_modules/brace-expansion": {
- "version": "1.1.16",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
- "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4741,9 +4684,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.7",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
- "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
@@ -4761,11 +4704,11 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.10.44",
- "caniuse-lite": "^1.0.30001806",
- "electron-to-chromium": "^1.5.393",
- "node-releases": "^2.0.51",
- "update-browserslist-db": "^1.2.3"
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
@@ -4774,34 +4717,6 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
- "node_modules/browserslist/node_modules/caniuse-lite": {
- "version": "1.0.30001806",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
- "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
- },
- "node_modules/browserslist/node_modules/electron-to-chromium": {
- "version": "1.5.395",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.395.tgz",
- "integrity": "sha512-7zt9Aw+SrmxLWLN0zhaTWZQiCdryLVrYTq5R7iZakLvi2UQPYMMsROYV/2qVCzMeCiSXHwKOU+sZ4zOVVlrtKA==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/bun": {
"version": "1.3.10",
"resolved": "https://registry.npmjs.org/bun/-/bun-1.3.10.tgz",
@@ -4856,6 +4771,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/cac": {
+ "version": "6.7.14",
+ "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+ "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -4919,6 +4844,23 @@
"node": ">=12"
}
},
+ "node_modules/chai": {
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+ "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "assertion-error": "^2.0.1",
+ "check-error": "^2.1.1",
+ "deep-eql": "^5.0.1",
+ "loupe": "^3.1.0",
+ "pathval": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -4961,6 +4903,16 @@
"pnpm": ">=8"
}
},
+ "node_modules/check-error": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz",
+ "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 16"
+ }
+ },
"node_modules/cheerio": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz",
@@ -5154,23 +5106,11 @@
"node": ">=6"
}
},
- "node_modules/color": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
- "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
- "license": "MIT",
- "dependencies": {
- "color-convert": "^2.0.1",
- "color-string": "^1.9.0"
- },
- "engines": {
- "node": ">=12.5.0"
- }
- },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -5183,18 +5123,9 @@
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
"license": "MIT"
},
- "node_modules/color-string": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
- "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
- "license": "MIT",
- "dependencies": {
- "color-name": "^1.0.0",
- "simple-swizzle": "^0.2.2"
- }
- },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
@@ -5210,15 +5141,15 @@
"license": "MIT"
},
"node_modules/concurrently": {
- "version": "9.2.4",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz",
- "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==",
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
+ "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "4.1.2",
"rxjs": "7.8.2",
- "shell-quote": "1.9.0",
+ "shell-quote": "1.8.3",
"supports-color": "8.1.1",
"tree-kill": "1.2.2",
"yargs": "17.7.2"
@@ -5326,12 +5257,6 @@
"node": ">= 8"
}
},
- "node_modules/crypto-js": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
- "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
- "license": "MIT"
- },
"node_modules/css-select": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
@@ -5618,6 +5543,16 @@
}
}
},
+ "node_modules/deep-eql": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+ "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
@@ -5648,9 +5583,9 @@
}
},
"node_modules/devalue": {
- "version": "5.8.1",
- "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz",
- "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==",
+ "version": "5.6.4",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz",
+ "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==",
"license": "MIT"
},
"node_modules/dom-serializer": {
@@ -5823,6 +5758,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
@@ -5913,20 +5855,13 @@
"license": "MIT"
},
"node_modules/esrap": {
- "version": "2.2.11",
- "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz",
- "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==",
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz",
+ "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==",
"license": "MIT",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.4.15"
- },
- "peerDependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15",
"@typescript-eslint/types": "^8.2.0"
- },
- "peerDependenciesMeta": {
- "@typescript-eslint/types": {
- "optional": true
- }
}
},
"node_modules/estree-walker": {
@@ -5972,6 +5907,16 @@
"node": ">=18.0.0"
}
},
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/express": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
@@ -6016,14 +5961,12 @@
}
},
"node_modules/express-rate-limit": {
- "version": "8.7.0",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
- "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
- "dev": true,
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
+ "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
"license": "MIT",
"dependencies": {
- "debug": "^4.4.3",
- "ip-address": "^10.2.0"
+ "ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
@@ -6035,49 +5978,16 @@
"express": ">= 4.11"
}
},
- "node_modules/ext-apps-v1": {
- "name": "@modelcontextprotocol/ext-apps",
- "version": "1.7.5",
- "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.5.tgz",
- "integrity": "sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==",
- "dev": true,
- "license": "MIT",
- "workspaces": [
- "examples/*"
- ],
- "dependencies": {
- "@standard-schema/spec": "^1.1.0"
- },
- "engines": {
- "node": ">=20"
- },
- "peerDependencies": {
- "@modelcontextprotocol/sdk": "^1.29.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "zod": "^3.25.0 || ^4.0.0"
- },
- "peerDependenciesMeta": {
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- }
- }
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.1.7",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
- "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
- "dev": true,
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
@@ -6108,9 +6018,9 @@
}
},
"node_modules/filelist/node_modules/brace-expansion": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
- "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6411,9 +6321,9 @@
}
},
"node_modules/hono": {
- "version": "4.12.31",
- "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.31.tgz",
- "integrity": "sha512-zJIHFrl6bq3RDd2YusFNCDlM8qUprxKswyi/OPzPyzKDdyBXDqWx8bZlZ7R+saTdSTatUmb3O7K4SspGPaEOQg==",
+ "version": "4.12.9",
+ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz",
+ "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"
@@ -6423,6 +6333,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz",
"integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/htmlparser2": {
@@ -6552,10 +6463,9 @@
}
},
"node_modules/ip-address": {
- "version": "10.7.0",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
- "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
- "dev": true,
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -6570,12 +6480,6 @@
"node": ">= 0.10"
}
},
- "node_modules/is-arrayish": {
- "version": "0.3.4",
- "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz",
- "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
- "license": "MIT"
- },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -6752,14 +6656,12 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "dev": true,
"license": "MIT"
},
"node_modules/json-schema-typed": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz",
"integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==",
- "dev": true,
"license": "BSD-2-Clause"
},
"node_modules/json5": {
@@ -6819,20 +6721,10 @@
}
},
"node_modules/linkify-it": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
- "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
"dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/puzrin"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/markdown-it"
- }
- ],
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
@@ -7017,6 +6909,13 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
+ "node_modules/loupe": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+ "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -7044,25 +6943,15 @@
}
},
"node_modules/markdown-it": {
- "version": "14.3.0",
- "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz",
- "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==",
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz",
+ "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==",
"dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/puzrin"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/markdown-it"
- }
- ],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
- "entities": "^4.5.0",
- "linkify-it": "^5.0.2",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
@@ -7203,9 +7092,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"funding": [
{
"type": "github",
@@ -7229,15 +7118,6 @@
"node": ">= 0.6"
}
},
- "node_modules/node-html-better-parser": {
- "version": "1.5.8",
- "resolved": "https://registry.npmjs.org/node-html-better-parser/-/node-html-better-parser-1.5.8.tgz",
- "integrity": "sha512-t/wAKvaTSKco43X+yf9+76RiMt18MtMmzd4wc7rKj+fWav6DV4ajDEKdWlLzSE8USDF5zr/06uGj0Wr/dGAFtw==",
- "license": "MIT",
- "dependencies": {
- "html-entities": "^2.3.2"
- }
- },
"node_modules/node-html-parser": {
"version": "6.1.13",
"resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz",
@@ -7250,14 +7130,11 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.51",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
- "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
+ "license": "MIT"
},
"node_modules/nodemon": {
"version": "3.1.11",
@@ -7474,6 +7351,41 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+ "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.16"
+ }
+ },
+ "node_modules/pdf-lib": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
+ "integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
+ "license": "MIT",
+ "dependencies": {
+ "@pdf-lib/standard-fonts": "^1.0.0",
+ "@pdf-lib/upng": "^1.0.1",
+ "pako": "^1.0.11",
+ "tslib": "^1.11.1"
+ }
+ },
+ "node_modules/pdf-lib/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
"node_modules/pdfjs-dist": {
"version": "5.4.530",
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.530.tgz",
@@ -7562,9 +7474,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.15",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
- "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -7581,7 +7493,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.12",
+ "nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -7646,9 +7558,9 @@
}
},
"node_modules/qs": {
- "version": "6.15.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
- "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
+ "version": "6.15.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz",
+ "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.1.0"
@@ -7758,7 +7670,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -7882,9 +7793,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "version": "7.7.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
@@ -7967,53 +7878,48 @@
"license": "ISC"
},
"node_modules/sharp": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
- "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
+ "version": "0.34.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
+ "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
"dev": true,
+ "hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
- "@img/colour": "^1.1.0",
+ "@img/colour": "^1.0.0",
"detect-libc": "^2.1.2",
- "semver": "^7.8.5"
+ "semver": "^7.7.3"
},
"engines": {
- "node": ">=20.9.0"
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.35.3",
- "@img/sharp-darwin-x64": "0.35.3",
- "@img/sharp-freebsd-wasm32": "0.35.3",
- "@img/sharp-libvips-darwin-arm64": "1.3.2",
- "@img/sharp-libvips-darwin-x64": "1.3.2",
- "@img/sharp-libvips-linux-arm": "1.3.2",
- "@img/sharp-libvips-linux-arm64": "1.3.2",
- "@img/sharp-libvips-linux-ppc64": "1.3.2",
- "@img/sharp-libvips-linux-riscv64": "1.3.2",
- "@img/sharp-libvips-linux-s390x": "1.3.2",
- "@img/sharp-libvips-linux-x64": "1.3.2",
- "@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
- "@img/sharp-libvips-linuxmusl-x64": "1.3.2",
- "@img/sharp-linux-arm": "0.35.3",
- "@img/sharp-linux-arm64": "0.35.3",
- "@img/sharp-linux-ppc64": "0.35.3",
- "@img/sharp-linux-riscv64": "0.35.3",
- "@img/sharp-linux-s390x": "0.35.3",
- "@img/sharp-linux-x64": "0.35.3",
- "@img/sharp-linuxmusl-arm64": "0.35.3",
- "@img/sharp-linuxmusl-x64": "0.35.3",
- "@img/sharp-webcontainers-wasm32": "0.35.3",
- "@img/sharp-win32-arm64": "0.35.3",
- "@img/sharp-win32-ia32": "0.35.3",
- "@img/sharp-win32-x64": "0.35.3"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
+ "@img/sharp-darwin-arm64": "0.34.5",
+ "@img/sharp-darwin-x64": "0.34.5",
+ "@img/sharp-libvips-darwin-arm64": "1.2.4",
+ "@img/sharp-libvips-darwin-x64": "1.2.4",
+ "@img/sharp-libvips-linux-arm": "1.2.4",
+ "@img/sharp-libvips-linux-arm64": "1.2.4",
+ "@img/sharp-libvips-linux-ppc64": "1.2.4",
+ "@img/sharp-libvips-linux-riscv64": "1.2.4",
+ "@img/sharp-libvips-linux-s390x": "1.2.4",
+ "@img/sharp-libvips-linux-x64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
+ "@img/sharp-linux-arm": "0.34.5",
+ "@img/sharp-linux-arm64": "0.34.5",
+ "@img/sharp-linux-ppc64": "0.34.5",
+ "@img/sharp-linux-riscv64": "0.34.5",
+ "@img/sharp-linux-s390x": "0.34.5",
+ "@img/sharp-linux-x64": "0.34.5",
+ "@img/sharp-linuxmusl-arm64": "0.34.5",
+ "@img/sharp-linuxmusl-x64": "0.34.5",
+ "@img/sharp-wasm32": "0.34.5",
+ "@img/sharp-win32-arm64": "0.34.5",
+ "@img/sharp-win32-ia32": "0.34.5",
+ "@img/sharp-win32-x64": "0.34.5"
}
},
"node_modules/shebang-command": {
@@ -8038,9 +7944,9 @@
}
},
"node_modules/shell-quote": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
- "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -8122,6 +8028,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
@@ -8145,15 +8058,6 @@
"kolorist": "^1.6.0"
}
},
- "node_modules/simple-swizzle": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
- "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
- "license": "MIT",
- "dependencies": {
- "is-arrayish": "^0.3.1"
- }
- },
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
@@ -8272,6 +8176,13 @@
"node": ">=16"
}
},
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -8281,6 +8192,13 @@
"node": ">= 0.8"
}
},
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -8345,6 +8263,26 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
+ "node_modules/strip-literal": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz",
+ "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^9.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+ "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
@@ -8362,23 +8300,23 @@
}
},
"node_modules/svelte": {
- "version": "5.56.1",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.1.tgz",
- "integrity": "sha512-eArsJmvl3xZVuTYD852PzIEdg2wgDdIZ1NEsIPbzAukHwi284B18No4nK2rCO9AwsWUDza4Cjvmoa4HaojTl5g==",
+ "version": "5.55.1",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.1.tgz",
+ "integrity": "sha512-QjvU7EFemf6mRzdMGlAFttMWtAAVXrax61SZYHdkD6yoVGQ89VeyKfZD4H1JrV1WLmJBxWhFch9H6ig/87VGjw==",
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
- "@sveltejs/acorn-typescript": "^1.0.10",
+ "@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
"aria-query": "5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
- "devalue": "^5.8.1",
+ "devalue": "^5.6.4",
"esm-env": "^1.2.1",
- "esrap": "^2.2.9",
+ "esrap": "^2.2.4",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@@ -8388,32 +8326,6 @@
"node": ">=18"
}
},
- "node_modules/systeminformation": {
- "version": "5.31.7",
- "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.31.7.tgz",
- "integrity": "sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw==",
- "license": "MIT",
- "os": [
- "darwin",
- "linux",
- "win32",
- "freebsd",
- "openbsd",
- "netbsd",
- "sunos",
- "android"
- ],
- "bin": {
- "systeminformation": "lib/cli.js"
- },
- "engines": {
- "node": ">=8.0.0"
- },
- "funding": {
- "type": "Buy me a coffee",
- "url": "https://www.buymeacoffee.com/systeminfo"
- }
- },
"node_modules/text-camel-case": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/text-camel-case/-/text-camel-case-1.2.9.tgz",
@@ -8632,6 +8544,13 @@
"integrity": "sha512-k/CjiZ80bYss6Qs7/ex1TBlPD11whT9oKfT8oTGiHa34W4JRd1NiH/Tr1DbHWQ2/vMUypxksLnF2CfmlmM5XFQ==",
"license": "MIT"
},
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
@@ -8639,6 +8558,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyexec": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+ "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -8687,6 +8613,36 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/tinypool": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+ "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+ "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tinyspy": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz",
+ "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -8786,6 +8742,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
"license": "0BSD"
},
"node_modules/tsutils": {
@@ -9329,34 +9286,17 @@
}
},
"node_modules/type-is": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
- "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
"dependencies": {
- "content-type": "^2.0.0",
+ "content-type": "^1.0.5",
"media-typer": "^1.1.0",
"mime-types": "^3.0.0"
},
"engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "node_modules/type-is/node_modules/content-type": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
- "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "node": ">= 0.6"
}
},
"node_modules/typedoc": {
@@ -9397,9 +9337,9 @@
}
},
"node_modules/typedoc/node_modules/brace-expansion": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
- "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
+ "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9451,9 +9391,9 @@
"license": "MIT"
},
"node_modules/undici": {
- "version": "7.28.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
- "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
+ "version": "7.24.7",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz",
+ "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==",
"license": "MIT",
"engines": {
"node": ">=20.18.1"
@@ -9516,9 +9456,9 @@
}
},
"node_modules/vite": {
- "version": "6.4.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz",
- "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==",
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9590,6 +9530,29 @@
}
}
},
+ "node_modules/vite-node": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+ "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cac": "^6.7.14",
+ "debug": "^4.4.1",
+ "es-module-lexer": "^1.7.0",
+ "pathe": "^2.0.3",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ },
+ "bin": {
+ "vite-node": "vite-node.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/vite-plugin-singlefile": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.0.tgz",
@@ -9608,9 +9571,9 @@
}
},
"node_modules/vite-plugin-solid": {
- "version": "2.11.12",
- "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.12.tgz",
- "integrity": "sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==",
+ "version": "2.11.10",
+ "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.11.10.tgz",
+ "integrity": "sha512-Yr1dQybmtDtDAHkii6hXuc1oVH9CPcS/Zb2jN/P36qqcrkNnVPsMTzQ06jyzFPFjj3U1IYKMVt/9ZqcwGCEbjw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9624,7 +9587,7 @@
"peerDependencies": {
"@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.*",
"solid-js": "^1.7.2",
- "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
},
"peerDependenciesMeta": {
"@testing-library/jest-dom": {
@@ -9633,9 +9596,9 @@
}
},
"node_modules/vite-prerender-plugin": {
- "version": "0.5.13",
- "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.13.tgz",
- "integrity": "sha512-IKSpYkzDBsKAxa05naRbj7GvNVMSdww/Z/E89oO3xndz+gWnOBOKOAbEXv7qDhktY/j3vHgJmoV1pPzqU2tx9g==",
+ "version": "0.5.12",
+ "resolved": "https://registry.npmjs.org/vite-prerender-plugin/-/vite-prerender-plugin-0.5.12.tgz",
+ "integrity": "sha512-EiwhbMn+flg14EysbLTmZSzq8NGTxhytgK3bf4aGRF1evWLGwZiHiUJ1KZDvbxgKbMf2pG6fJWGEa3UZXOnR1g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -9647,7 +9610,7 @@
"stack-trace": "^1.0.0-pre2"
},
"peerDependencies": {
- "vite": "5.x || 6.x || 7.x || 8.x"
+ "vite": "5.x || 6.x || 7.x"
}
},
"node_modules/vite/node_modules/fdir": {
@@ -9701,6 +9664,92 @@
}
}
},
+ "node_modules/vitest": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/expect": "3.2.4",
+ "@vitest/mocker": "3.2.4",
+ "@vitest/pretty-format": "^3.2.4",
+ "@vitest/runner": "3.2.4",
+ "@vitest/snapshot": "3.2.4",
+ "@vitest/spy": "3.2.4",
+ "@vitest/utils": "3.2.4",
+ "chai": "^5.2.0",
+ "debug": "^4.4.1",
+ "expect-type": "^1.2.1",
+ "magic-string": "^0.30.17",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.2",
+ "std-env": "^3.9.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.2",
+ "tinyglobby": "^0.2.14",
+ "tinypool": "^1.1.1",
+ "tinyrainbow": "^2.0.0",
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+ "vite-node": "3.2.4",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@types/debug": "^4.1.12",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "@vitest/browser": "3.2.4",
+ "@vitest/ui": "3.2.4",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@types/debug": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest/node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/vue": {
"version": "3.5.27",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.27.tgz",
@@ -9759,6 +9808,23 @@
"node": ">= 8"
}
},
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/widest-line": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
@@ -9904,13 +9970,12 @@
}
},
"node_modules/zod-to-json-schema": {
- "version": "3.25.2",
- "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
- "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
- "dev": true,
+ "version": "3.25.1",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz",
+ "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==",
"license": "ISC",
"peerDependencies": {
- "zod": "^3.25.28 || ^4"
+ "zod": "^3.25 || ^4"
}
}
}
diff --git a/package.json b/package.json
index c86efa9..7b92e3c 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,10 @@
"name": "@modelcontextprotocol/ext-apps",
"repository": {
"type": "git",
- "url": "git+https://github.com/modelcontextprotocol/ext-apps.git"
+ "url": "https://github.com/modelcontextprotocol/ext-apps"
},
"homepage": "https://github.com/modelcontextprotocol/ext-apps",
- "version": "2.0.0",
+ "version": "1.4.0",
"license": "MIT",
"description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.",
"type": "module",
@@ -54,7 +54,6 @@
"prepack": "npm run build",
"build:all": "npm run examples:build",
"test": "bun test src examples",
- "test:dependency-isolation": "npm run build && node scripts/check-dependency-isolation.mjs",
"test:e2e": "playwright test",
"test:e2e:update": "playwright test --update-snapshots",
"test:e2e:ui": "playwright test --ui",
@@ -72,17 +71,12 @@
"prettier": "prettier -u \"**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --check",
"prettier:fix": "prettier -u \"**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --write",
"check:versions": "node scripts/check-versions.mjs",
- "bump": "node scripts/bump-version.mjs",
"update-lock:docker": "rm -rf node_modules package-lock.json examples/*/node_modules && docker run --rm --platform linux/amd64 -v $(pwd):/work -w /work -e HOME=/tmp node:latest npm i --registry=https://registry.npmjs.org/ --ignore-scripts && rm -rf node_modules examples/*/node_modules && npm i --registry=https://registry.npmjs.org/"
},
"author": "Olivier Chafik",
- "//interop": "The @modelcontextprotocol/sdk 1.x and ext-apps-v1 devDependencies exist only for src/interop-v1.test.ts, which runs the published 1.7.5 package against this build. The library does not use them.",
"devDependencies": {
"@boneskull/typedoc-plugin-mermaid": "^0.2.0",
- "@modelcontextprotocol/client": "2.0.0",
- "@modelcontextprotocol/core": "2.0.0",
- "@modelcontextprotocol/sdk": "1.30.0",
- "@modelcontextprotocol/server": "2.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"@playwright/test": "1.57.0",
"@types/bun": "^1.3.2",
"@types/node": "20.19.27",
@@ -97,7 +91,6 @@
"electron-to-chromium": "1.5.267",
"esbuild": "^0.25.12",
"express": "^5.1.0",
- "ext-apps-v1": "npm:@modelcontextprotocol/ext-apps@1.7.5",
"husky": "^9.1.7",
"nodemon": "^3.1.0",
"playwright": "1.57.0",
@@ -105,26 +98,21 @@
"prettier": "^3.6.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
- "sharp": "^0.35.3",
+ "sharp": "^0.34.5",
"ts-to-zod": "^5.1.0",
"tsx": "^4.21.0",
"typedoc": "^0.28.14",
"typedoc-github-theme": "^0.4.0",
"typescript": "^5.9.3",
- "zod": "^4.2.0"
+ "zod": "^4.1.13"
},
"peerDependencies": {
- "@modelcontextprotocol/client": "^2.0.0",
- "@modelcontextprotocol/core": "^2.0.0",
- "@modelcontextprotocol/server": "^2.0.0",
+ "@modelcontextprotocol/sdk": "^1.29.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "zod": "^4.2.0"
+ "zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
- "@modelcontextprotocol/server": {
- "optional": true
- },
"react": {
"optional": true
},
@@ -137,8 +125,5 @@
"seroval-plugins": "1.4.2",
"solid-js": "1.9.10",
"@types/node": "20.19.27"
- },
- "dependencies": {
- "@standard-schema/spec": "^1.1.0"
}
}
diff --git a/playwright.config.ts b/playwright.config.ts
index ecf1e13..8485b00 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -30,7 +30,6 @@ export default defineConfig({
...devices["Desktop Chrome"],
// Use default Chromium everywhere for consistent screenshot rendering
// Run `npm run test:e2e:docker` locally for CI-identical results
- ...(process.env.PW_CHANNEL ? { channel: process.env.PW_CHANNEL } : {}),
},
},
],
@@ -47,10 +46,6 @@ export default defineConfig({
env: {
...process.env,
EXAMPLE: process.env.EXAMPLE ?? "",
- // Let pdf-server fetch from the http://127.0.0.1 range-counting fixture
- // (validateUrl rejects loopback HTTP unless this is set). Scoped to this
- // server's check only — does not touch Node's TLS verification.
- PDF_SERVER_ALLOW_LOOPBACK_HTTP: "1",
},
},
// Snapshot configuration
diff --git a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md
index 5a473f0..02d09a4 100644
--- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md
+++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md
@@ -77,28 +77,13 @@ Before writing any code, analyze the server's existing tools and determine which
## Step 2: Add Dependencies
```bash
-npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 zod@^4.2.0
+npm install @modelcontextprotocol/ext-apps
npm install -D vite vite-plugin-singlefile
```
-Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React), and `@modelcontextprotocol/node` / `@modelcontextprotocol/express` if the server uses HTTP transports.
+Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React).
-ext-apps 2.x requires the split base MCP SDK packages at `^2.0.0`
-(`@modelcontextprotocol/core` comes in transitively). Existing servers that
-still import `@modelcontextprotocol/sdk` v1 must migrate those imports and
-handler schemas before adding the App integration:
-
-| SDK v1 (`@modelcontextprotocol/sdk`) | SDK v2 |
-|---|---|
-| `sdk/server/mcp.js` (`McpServer`) | `@modelcontextprotocol/server` |
-| `sdk/server/streamableHttp.js` (`StreamableHTTPServerTransport`) | `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` |
-| Express wiring by hand | `createMcpExpressApp` from `@modelcontextprotocol/express` |
-| `sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` |
-| `sdk/types.js` types (`CallToolResult`, …) | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` |
-| `sdk/types.js` zod schemas (`CallToolResultSchema`, …) | `@modelcontextprotocol/core` |
-| Raw zod shapes: `inputSchema: { q: z.string() }` | `inputSchema: z.object({ q: z.string() })` |
-| `extra.signal` in tool callbacks | `extra.mcpReq.signal` |
-| `setRequestHandler(SomeRequestSchema, handler)` | `setRequestHandler("some/method", { params: ParamsSchema }, handler)` (the 2-arg form is only for spec methods such as `"tools/call"`) |
+Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
## Step 3: Set Up the Build Pipeline
@@ -161,14 +146,10 @@ Transform plain MCP tools into App tools with UI.
**Before** (plain MCP tool):
```typescript
-server.registerTool(
- "my-tool",
- { inputSchema: z.object({ param: z.string() }) },
- async ({ param }) => {
- const data = await fetchData(param);
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
- },
-);
+server.tool("my-tool", { param: z.string() }, async (args) => {
+ const data = await fetchData(args.param);
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
+});
```
**After** (App tool with UI):
@@ -177,22 +158,17 @@ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@model
const resourceUri = "ui://my-tool/mcp-app.html";
-registerAppTool(
- server,
- "my-tool",
- {
- description: "Shows data with an interactive UI",
- inputSchema: z.object({ param: z.string() }),
- _meta: { ui: { resourceUri } },
- },
- async ({ param }) => {
- const data = await fetchData(param);
- return {
- content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts
- structuredContent: { data }, // structured data for the UI
- };
- },
-);
+registerAppTool(server, "my-tool", {
+ description: "Shows data with an interactive UI",
+ inputSchema: { param: z.string() },
+ _meta: { ui: { resourceUri } },
+}, async (args) => {
+ const data = await fetchData(args.param);
+ return {
+ content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts
+ structuredContent: { data }, // structured data for the UI
+ };
+});
```
Key guidance:
@@ -211,21 +187,17 @@ import path from "node:path";
const resourceUri = "ui://my-tool/mcp-app.html";
-registerAppResource(
- server,
- "My Tool UI",
- resourceUri,
- {},
- async () => {
- const html = await fs.readFile(
- path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
- "utf-8",
- );
- return {
- contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
- };
- },
-);
+registerAppResource(server, {
+ uri: resourceUri,
+ name: "My Tool UI",
+ mimeType: RESOURCE_MIME_TYPE,
+}, async () => {
+ const html = await fs.readFile(
+ path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
+ "utf-8",
+ );
+ return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
+});
```
If multiple tools share the same UI, they can reference the same `resourceUri` and the same resource registration.
@@ -299,32 +271,25 @@ registerAppTool(server, "poll-data", {
});
```
-The UI calls these via
-`app.callServerTool({ name: "poll-data", arguments: {} })`.
+The UI calls these via `app.callServerTool("poll-data", {})`.
### CSP Configuration
If the UI needs to load external resources (fonts, APIs, CDNs), declare the domains:
```typescript
-registerAppResource(server, "My Tool UI", resourceUri, {}, async () => ({
- contents: [
- {
- uri: resourceUri,
- mimeType: RESOURCE_MIME_TYPE,
- text: html,
- _meta: {
- ui: {
- csp: {
- connectDomains: ["https://api.example.com"], // fetch/XHR targets
- resourceDomains: ["https://cdn.example.com"], // scripts, styles, images
- frameDomains: ["https://embed.example.com"], // nested iframes
- },
- },
- },
+registerAppResource(server, {
+ uri: resourceUri,
+ name: "My Tool UI",
+ mimeType: RESOURCE_MIME_TYPE,
+ _meta: {
+ ui: {
+ connectDomains: ["api.example.com"], // fetch/XHR targets
+ resourceDomains: ["cdn.example.com"], // scripts, styles, images
+ frameDomains: ["embed.example.com"], // nested iframes
},
- ],
-}));
+ },
+}, async () => { /* ... */ });
```
### Streaming Partial Input
@@ -361,14 +326,7 @@ server.server.oninitialized = () => {
}, appToolHandler);
} else {
// Text-only client — register plain tool
- server.registerTool(
- "my-tool",
- {
- description: "Shows data",
- inputSchema: z.object({ param: z.string() }),
- },
- plainToolHandler,
- );
+ server.tool("my-tool", "Shows data", { param: z.string() }, plainToolHandler);
}
};
```
diff --git a/plugins/mcp-apps/skills/convert-web-app/SKILL.md b/plugins/mcp-apps/skills/convert-web-app/SKILL.md
index 8fb7fa0..5db2d94 100644
--- a/plugins/mcp-apps/skills/convert-web-app/SKILL.md
+++ b/plugins/mcp-apps/skills/convert-web-app/SKILL.md
@@ -115,22 +115,19 @@ Create a new MCP server with tool and resource registration. This wraps the exis
### Dependencies
```bash
-npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 zod@^4.2.0
+npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
npm install -D tsx vite vite-plugin-singlefile
```
-Use `npm install` so the package manager resolves versions; ext-apps 2.x needs
-the split base MCP SDK packages at `^2.0.0` (`@modelcontextprotocol/core` comes
-in transitively). Do not add the legacy `@modelcontextprotocol/sdk` v1 package
-or substitute unpublished local packages.
+Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
### Server Code
Create `server.ts`:
```typescript
-import { McpServer } from "@modelcontextprotocol/server";
-import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
import fs from "node:fs/promises";
import path from "node:path";
@@ -143,9 +140,7 @@ const resourceUri = "ui://my-app/mcp-app.html";
// Register the tool — inputSchema maps to the app's data sources
registerAppTool(server, "show-app", {
description: "Displays the app with the given parameters",
- inputSchema: z.object({
- query: z.string().describe("The search query"),
- }),
+ inputSchema: { query: z.string().describe("The search query") },
_meta: { ui: { resourceUri } },
}, async (args) => {
// Process args server-side if needed
@@ -156,29 +151,19 @@ registerAppTool(server, "show-app", {
});
// Register the HTML resource
-registerAppResource(
- server,
- "My App UI",
- resourceUri,
- {},
- async () => {
- const html = await fs.readFile(
- path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
- "utf-8",
- );
- return {
- contents: [
- {
- uri: resourceUri,
- mimeType: RESOURCE_MIME_TYPE,
- text: html,
- // Add CSP domains from Step 2 if needed:
- // _meta: { ui: { csp: { connectDomains: ["https://api.example.com"], resourceDomains: ["https://cdn.example.com"] } } },
- },
- ],
- };
- },
-);
+registerAppResource(server, {
+ uri: resourceUri,
+ name: "My App UI",
+ mimeType: RESOURCE_MIME_TYPE,
+ // Add CSP domains from Step 2 if needed:
+ // _meta: { ui: { connectDomains: ["api.example.com"], resourceDomains: ["cdn.example.com"] } },
+}, async () => {
+ const html = await fs.readFile(
+ path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
+ "utf-8",
+ );
+ return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
+});
// Start the server
const transport = new StdioServerTransport();
@@ -325,10 +310,7 @@ const data = await fetch("/api/data").then(r => r.json());
// After (hybrid):
async function fetchData(): Promise {
if (isMcpApp) {
- const result = await app.callServerTool({
- name: "fetch-data",
- arguments: {},
- });
+ const result = await app.callServerTool("fetch-data", {});
return result.structuredContent;
}
return fetch("/api/data").then(r => r.json());
@@ -455,8 +437,7 @@ registerAppTool(server, "refresh-data", {
});
```
-The UI calls these via
-`app.callServerTool({ name: "refresh-data", arguments: {} })`.
+The UI calls these via `app.callServerTool("refresh-data", {})`.
### Streaming Partial Input
diff --git a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md
index d73dc90..5bd1219 100644
--- a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md
+++ b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md
@@ -110,14 +110,11 @@ See `/tmp/mcp-ext-apps/docs/patterns.md` for detailed recipes:
**Always** use `npm install` to add dependencies rather than manually writing version numbers:
```bash
-npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 @modelcontextprotocol/node@^2.0.0 @modelcontextprotocol/express@^2.0.0 zod@^4.2.0 express cors
+npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod express cors
npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors
```
-ext-apps 2.x requires the split base MCP SDK packages (`@modelcontextprotocol/client`,
-`server`, `node`, `express`) at `^2.0.0`; `@modelcontextprotocol/core` comes in
-transitively. Do not add the legacy `@modelcontextprotocol/sdk` v1 package or
-substitute unpublished local packages.
+This lets npm resolve the latest compatible versions. **Never** specify version numbers from memory.
### TypeScript Server Execution
diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs
deleted file mode 100644
index 65c878f..0000000
--- a/scripts/bump-version.mjs
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/usr/bin/env node
-/**
- * Bump the version in the root package.json and sync all workspace packages
- * to the same version.
- *
- * Usage:
- * node scripts/bump-version.mjs patch
- * node scripts/bump-version.mjs minor
- * node scripts/bump-version.mjs major
- * node scripts/bump-version.mjs 1.4.0
- * node scripts/bump-version.mjs prerelease --preid=beta
- *
- * Writes the new version to stdout (logs go to stderr).
- */
-
-import { execSync } from "node:child_process";
-import { readFileSync } from "node:fs";
-
-const args = process.argv.slice(2);
-if (!args[0]) {
- console.error(
- "Usage: node scripts/bump-version.mjs [--preid=]",
- );
- process.exit(1);
-}
-
-const exec = (cmd) =>
- execSync(cmd, { stdio: ["inherit", "pipe", "inherit"] })
- .toString()
- .trim();
-
-const pkgName = JSON.parse(readFileSync("package.json", "utf-8")).name;
-
-const newVersion = exec(
- `npm version ${args.join(" ")} --no-git-tag-version`,
-).replace(/^v/, "");
-exec(`npm pkg set version=${newVersion} --workspaces`);
-
-// Keep workspace dependency ranges compatible (needed on major bumps)
-const [major, minor] = newVersion.split(".");
-exec(`npm pkg set "dependencies.${pkgName}=^${major}.${minor}.0" --workspaces`);
-
-// Sync package-lock.json so `npm ci` doesn't reject the release PR
-exec("npm install --package-lock-only --ignore-scripts");
-
-console.error(`Bumped root + workspaces to ${newVersion}`);
-console.log(newVersion);
diff --git a/scripts/check-dependency-isolation.mjs b/scripts/check-dependency-isolation.mjs
deleted file mode 100644
index 3809224..0000000
--- a/scripts/check-dependency-isolation.mjs
+++ /dev/null
@@ -1,209 +0,0 @@
-import { execFileSync } from "node:child_process";
-import {
- mkdtempSync,
- mkdirSync,
- readdirSync,
- readFileSync,
- rmSync,
- writeFileSync,
-} from "node:fs";
-import { tmpdir } from "node:os";
-import { join, resolve } from "node:path";
-
-const root = resolve(import.meta.dirname, "..");
-const packageJson = JSON.parse(
- readFileSync(join(root, "package.json"), "utf8"),
-);
-const client = "@modelcontextprotocol/client";
-const server = "@modelcontextprotocol/server";
-
-// App and AppBridge extend the client package's Protocol class, so client is
-// a required peer for every consumer. The server helpers are only needed by
-// server authors, so server stays optional: View-only consumers must not have
-// it installed or bundled.
-for (const role of [client, server]) {
- if (!packageJson.peerDependencies?.[role]) {
- throw new Error(`${role} must remain a peer dependency`);
- }
-}
-if (packageJson.peerDependenciesMeta?.[client]?.optional) {
- throw new Error(`${client} must be a required peer dependency`);
-}
-if (packageJson.peerDependenciesMeta?.[server]?.optional !== true) {
- throw new Error(`${server} must be an optional peer dependency`);
-}
-
-/**
- * Exact version for a synthetic consumer dependency. Read from
- * devDependencies so the consumers exercise the same SDK version the
- * repository tests against; the check would silently drift if a range were
- * allowed here.
- */
-function exactDevDependency(name) {
- const version = packageJson.devDependencies?.[name];
- if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version ?? "")) {
- throw new Error(
- `devDependencies["${name}"] must be an exact version, got ${JSON.stringify(version)}`,
- );
- }
- return version;
-}
-
-/** Exact version of a package as installed in this repository's node_modules. */
-function installedVersion(name) {
- return JSON.parse(
- readFileSync(join(root, "node_modules", name, "package.json"), "utf8"),
- ).version;
-}
-
-const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-"));
-try {
- // Minimal environment: nothing inherited from the caller's npm config
- // (registry overrides, auth tokens, npm_config_* set by an outer `npm run`)
- // can leak into the synthetic consumers.
- const npmEnvironment = {
- PATH: process.env.PATH ?? process.env.Path,
- HOME: process.env.HOME ?? process.env.USERPROFILE,
- npm_config_cache: join(temporaryRoot, "npm-cache"),
- };
- const packDestination = join(temporaryRoot, "pack");
- mkdirSync(packDestination);
- // `npm pack` runs the package's `prepare` script even with --ignore-scripts
- // (pacote's directory fetcher), and its output can pollute stdout, so do not
- // rely on `--json`: locate the tarball on disk instead.
- execFileSync(
- "npm",
- ["pack", "--ignore-scripts", "--pack-destination", packDestination],
- { cwd: root, stdio: "pipe", env: npmEnvironment },
- );
- const tarballs = readdirSync(packDestination).filter((name) =>
- name.endsWith(".tgz"),
- );
- if (tarballs.length !== 1) {
- throw new Error(`expected exactly one tarball, found ${tarballs}`);
- }
- const tarball = join(packDestination, tarballs[0]);
-
- const consumers = [
- {
- // View / host author: ext-apps + client (+ react for the hooks entry).
- name: "app-only",
- dependencies: {
- "@types/node": exactDevDependency("@types/node"),
- "@types/react": installedVersion("@types/react"),
- [client]: exactDevDependency(client),
- "@modelcontextprotocol/ext-apps": `file:${tarball}`,
- react: installedVersion("react"),
- },
- absent: server,
- // server must be neither installed (it is an optional peer) nor bundled.
- mustNotInstall: true,
- entry:
- 'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; console.log(App, AppBridge, useApp);',
- },
- {
- // Server author: ext-apps + server. npm auto-installs client as a
- // required peer (its types back the shared wire types), but the server
- // entry must not pull it into a runtime bundle.
- name: "server-only",
- dependencies: {
- "@types/node": exactDevDependency("@types/node"),
- "@modelcontextprotocol/ext-apps": `file:${tarball}`,
- [server]: exactDevDependency(server),
- },
- absent: client,
- mustNotInstall: false,
- entry:
- 'import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"; console.log(registerAppTool);',
- },
- ];
-
- for (const consumer of consumers) {
- const directory = join(temporaryRoot, consumer.name);
- mkdirSync(directory);
- writeFileSync(
- join(directory, "package.json"),
- JSON.stringify({
- private: true,
- type: "module",
- dependencies: consumer.dependencies,
- }),
- );
- execFileSync(
- "npm",
- [
- "install",
- "--ignore-scripts",
- "--package-lock=false",
- "--no-audit",
- "--no-fund",
- ],
- { cwd: directory, stdio: "inherit", env: npmEnvironment },
- );
-
- if (consumer.mustNotInstall) {
- const absentPath = join(
- directory,
- "node_modules",
- ...consumer.absent.split("/"),
- "package.json",
- );
- try {
- readFileSync(absentPath);
- throw new Error(
- `${consumer.name} unexpectedly installed ${consumer.absent}`,
- );
- } catch (error) {
- if (error?.code !== "ENOENT") throw error;
- }
- }
-
- writeFileSync(join(directory, "entry.ts"), consumer.entry);
- writeFileSync(
- join(directory, "tsconfig.json"),
- JSON.stringify({
- compilerOptions: {
- jsx: "react-jsx",
- lib: ["ES2020", "DOM"],
- module: "ESNext",
- moduleResolution: "bundler",
- noEmit: true,
- skipLibCheck: false,
- strict: true,
- target: "ES2020",
- },
- include: ["entry.ts"],
- }),
- );
- execFileSync(
- process.execPath,
- [join(root, "node_modules", "typescript", "bin", "tsc")],
- { cwd: directory, stdio: "inherit" },
- );
-
- const metafile = join(directory, "bundle-meta.json");
- execFileSync(
- join(root, "node_modules", ".bin", "esbuild"),
- [
- "entry.ts",
- "--bundle",
- "--platform=browser",
- "--outfile=bundle.js",
- `--metafile=${metafile}`,
- ],
- { cwd: directory, stdio: "inherit" },
- );
- const bundleInputs = Object.keys(
- JSON.parse(readFileSync(metafile, "utf8")).inputs,
- ).join("\n");
- if (bundleInputs.includes(`/node_modules/${consumer.absent}/`)) {
- throw new Error(
- `${consumer.name} unexpectedly bundled ${consumer.absent}`,
- );
- }
- }
-} finally {
- rmSync(temporaryRoot, { recursive: true, force: true });
-}
-
-console.log("Role peer dependency isolation checks passed.");
diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts
index 0ff9f04..109f352 100644
--- a/scripts/generate-schemas.ts
+++ b/scripts/generate-schemas.ts
@@ -12,7 +12,7 @@
*
* ts-to-zod generates `import { z } from "zod"`. We rewrite this to
* `import { z } from "zod/v4"` because the generated schemas compose with
- * schemas imported from `@modelcontextprotocol/core`, which the SDK
+ * schemas imported from `@modelcontextprotocol/sdk/types.js`, which the SDK
* constructs via `zod/v4`. Mixing schemas from the v3 and v4 APIs at runtime
* fails with errors like `keyValidator._parse is not a function` (v3 internals
* calling into v4 objects, or vice versa). The `zod/v4` subpath is exported by
@@ -22,12 +22,10 @@
*
* **Problem**: ts-to-zod cannot resolve types imported from external packages.
* When it encounters types like `ContentBlock`, `CallToolResult`, `Implementation`,
- * `RequestId`, and `Tool` (imported by spec.types.ts from
- * `@modelcontextprotocol/client`), it generates `z.any()` as a placeholder.
+ * `RequestId`, and `Tool` from `@modelcontextprotocol/sdk`, it generates `z.any()`
+ * as a placeholder.
*
- * **Solution**: Import the corresponding Zod schemas from
- * `@modelcontextprotocol/core` (the role-neutral package both client and
- * server re-export) and remove the z.any() placeholders.
+ * **Solution**: Import the schemas from MCP SDK and remove the z.any() placeholders.
*
* ### 3. Index Signatures (`z.record().and()` → `z.object().passthrough()`)
*
@@ -195,7 +193,7 @@ function postProcess(content: string): string {
`import { z } from "zod/v4";
import {
${mcpImports},
-} from "@modelcontextprotocol/core";`,
+} from "@modelcontextprotocol/sdk/types.js";`,
);
// 2. Remove z.any() placeholders for external types (now imported from MCP SDK)
diff --git a/scripts/link-self.mjs b/scripts/link-self.mjs
index c1f79cc..41051bf 100644
--- a/scripts/link-self.mjs
+++ b/scripts/link-self.mjs
@@ -5,12 +5,10 @@
* so examples always type-check against the latest local types.
* See: https://github.com/npm/feedback/discussions/774
*/
-import { cpSync, existsSync, lstatSync } from "fs";
+import { cpSync, existsSync } from "fs";
const target = "node_modules/@modelcontextprotocol/ext-apps";
if (!existsSync(target)) process.exit(0);
-// If target is a symlink (e.g. to the repo root), dist is already current.
-if (lstatSync(target).isSymbolicLink()) process.exit(0);
cpSync("dist", `${target}/dist`, { recursive: true });
cpSync("package.json", `${target}/package.json`);
diff --git a/scripts/typedoc-plugin-base-sdk-links.mjs b/scripts/typedoc-plugin-base-sdk-links.mjs
deleted file mode 100644
index 6098918..0000000
--- a/scripts/typedoc-plugin-base-sdk-links.mjs
+++ /dev/null
@@ -1,16 +0,0 @@
-/**
- * Resolve module-local links inherited from the base MCP SDK declarations.
- *
- * TypeDoc's externalSymbolLinkMappings handles package-qualified symbols, but
- * the base SDK writes this particular reference as `index.inputRequired`.
- */
-
-const BASE_SDK_DOCS = "https://ts.sdk.modelcontextprotocol.io/v2/";
-
-/** @param {import("typedoc").Application} app */
-export function load(app) {
- app.converter.addUnknownSymbolResolver((ref) => {
- const name = ref.symbolReference?.path?.map((part) => part.path).join(".");
- if (name === "index.inputRequired") return BASE_SDK_DOCS;
- });
-}
diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx
index f2523f2..6c810d6 100644
--- a/specification/draft/apps.mdx
+++ b/specification/draft/apps.mdx
@@ -289,7 +289,6 @@ When `_meta.ui` is present on **both**, the content-item value takes precedence.
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
media-src 'self' data:;
- object-src 'none';
connect-src 'none';
```
@@ -507,32 +506,16 @@ If the Host is a web page, it MUST wrap the View and communicate with it through
### Standard MCP Messages
-UI iframes can use the following subset of standard MCP protocol messages.
-
-Note that `tools/call` and `tools/list` flow **bidirectionally**:
-- **App → Host → Server**: Apps call server tools (requires host `serverTools` capability)
-- **Host → App**: Host calls app-registered tools (requires app `tools` capability)
+UI iframes can use the following subset of standard MCP protocol messages:
**Tools:**
-- `tools/call` - Execute a tool (bidirectional)
- - **App → Host**: Call server tool via host proxy
- - **Host → App**: Call app-registered tool
-- `tools/list` - List available tools (bidirectional)
- - **App → Host**: List server tools
- - **Host → App**: List app-registered tools
-- `notifications/tools/list_changed` - Notify when tool list changes (bidirectional)
- - **Server → Host → App**: Server tools changed
- - **App → Host**: App-registered tools changed
+- `tools/call` - Execute a tool on the MCP server
**Resources:**
- `resources/read` - Read resource content
-**Sampling:**
-
-- `sampling/createMessage` - Request an LLM completion from the host (uses the standard MCP [`CreateMessageRequest`](https://modelcontextprotocol.io/specification/2025-11-25/client/sampling) / `CreateMessageResult` types, including SEP-1577 `tools` / `toolChoice` / `tool_use` content blocks). The host has full discretion over model selection and SHOULD apply rate limiting, cost controls, and user approval (human-in-the-loop). Apps MUST check `hostCapabilities.sampling` before sending this request, and `hostCapabilities.sampling.tools` before including `tools` in the request params.
-
**Notifications:**
- `notifications/message` - Log messages to host
@@ -548,8 +531,8 @@ When the View sends an `ui/initialize` request to the Host, it MUST include its
```typescript
interface McpUiAppCapabilities {
- /** Experimental features keyed by identifier. */
- experimental?: Record;
+ /** Experimental features (structure TBD). */
+ experimental?: {};
/** App exposes MCP-style tools that the host can call. */
tools?: {
/** App supports tools/list_changed notifications. */
@@ -660,25 +643,9 @@ Example:
They describe the features and capabilities that the Host supports.
```typescript
-/** Content block modalities that the host supports for a given capability. */
-interface SupportedContentBlockModalities {
- /** Host supports text content blocks. */
- text?: {};
- /** Host supports image content blocks. */
- image?: {};
- /** Host supports audio content blocks. */
- audio?: {};
- /** Host supports resource content blocks. */
- resource?: {};
- /** Host supports resource link content blocks. */
- resourceLink?: {};
- /** Host supports structured content. */
- structuredContent?: {};
-}
-
interface HostCapabilities {
- /** Experimental features keyed by identifier. */
- experimental?: Record;
+ /** Experimental features (structure TBD). */
+ experimental?: {};
/** Host supports opening external URLs. */
openLinks?: {};
/** Host supports file downloads via ui/download-file. */
@@ -716,21 +683,6 @@ interface HostCapabilities {
baseUriDomains?: string[];
};
};
- /**
- * Host accepts context updates (ui/update-model-context) to be included in
- * the model's context for future turns.
- */
- updateModelContext?: SupportedContentBlockModalities;
- /** Host supports receiving content messages (ui/message) from the view. */
- message?: SupportedContentBlockModalities;
- /**
- * Host supports LLM sampling (sampling/createMessage) from the view.
- * Mirrors MCP ClientCapabilities.sampling so hosts can pass it through.
- */
- sampling?: {
- /** Host supports tool use via `tools` and `toolChoice` params (SEP-1577). */
- tools?: {};
- };
}
```
@@ -1236,95 +1188,6 @@ Host behavior:
- If multiple updates are received before the next user message, Host SHOULD only send the last update to the model
- MAY display context updates to the user
-#### Requests (Host → App)
-
-When Apps declare the `tools` capability, the Host can send standard MCP tool requests to the App:
-
-`tools/call` - Execute an App-registered tool
-
-```typescript
-// Request (Host → App)
-{
- jsonrpc: "2.0",
- id: 1,
- method: "tools/call",
- params: {
- name: string, // Name of app-registered tool to execute
- arguments?: object // Tool arguments (validated against inputSchema)
- }
-}
-
-// Success Response (App → Host)
-{
- jsonrpc: "2.0",
- id: 1,
- result: {
- content: Array, // Result for model context
- structuredContent?: object, // Optional structured data for UI
- isError?: boolean, // True if tool execution failed
- _meta?: object // Optional metadata
- }
-}
-
-// Error Response
-{
- jsonrpc: "2.0",
- id: 1,
- error: {
- code: number,
- message: string
- }
-}
-```
-
-**App Behavior:**
-- Apps MUST implement `oncalltool` handler if they declare `tools` capability
-- Apps SHOULD validate tool names and arguments
-- Apps MAY use `app.registerTool()` SDK helper for automatic validation
-- Apps SHOULD return `isError: true` for tool execution failures
-
-`tools/list` - List App-registered tools
-
-```typescript
-// Request (Host → App)
-{
- jsonrpc: "2.0",
- id: 2,
- method: "tools/list",
- params: {
- cursor?: string // Optional pagination cursor
- }
-}
-
-// Response (App → Host)
-{
- jsonrpc: "2.0",
- id: 2,
- result: {
- tools: Array, // List of available tools
- nextCursor?: string // Pagination cursor if more tools exist
- }
-}
-```
-
-**Tool Structure** (identical to [core MCP `Tool`](https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool)):
-```typescript
-interface Tool {
- name: string; // Unique tool identifier
- title?: string; // Display name
- description?: string; // Human-readable description
- inputSchema: object; // JSON Schema for arguments
- outputSchema?: object; // JSON Schema for structuredContent
- annotations?: ToolAnnotations; // MCP tool annotations (e.g., readOnlyHint)
- _meta?: object;
-}
-```
-
-**App Behavior:**
-- Apps MUST implement `onlisttools` handler if they declare `tools` capability
-- Apps SHOULD return complete tool metadata including schemas
-- Apps MAY filter tools based on context or permissions
-
#### Notifications (Host → View)
`ui/notifications/tool-input` - Host MUST send this notification with the complete tool arguments after the View's initialize request completes.
@@ -1736,446 +1599,6 @@ This pattern enables interactive, self-updating views.
Note: Tools with `visibility: ["app"]` are hidden from the agent but remain callable by apps via `tools/call`. This enables UI-only interactions (refresh buttons, form submissions) without exposing implementation details to the model. See the Visibility section under Resource Discovery for details.
-### App-Provided Tools
-
-Apps can register their own tools that hosts and agents can call, making apps **introspectable and accessible** to the model. This complements the existing capability where apps call server tools (via host proxy).
-
-#### Motivation: Semantic Introspection
-
-Without tool registration, apps are opaque to the model:
-- The host renders the app in a sandboxed iframe and has no guaranteed access to its internals — apps may embed further cross-origin iframes (via `frameDomains`), and even where same-origin, DOM access is not part of the host↔app contract
-- The model therefore cannot query app state or discover what operations the app supports
-
-With tool registration, apps expose a semantic interface:
-- The model discovers available operations via `tools/list`
-- The model queries app state via tools (e.g., `get_board_state`)
-- The model executes actions via tools (e.g., `make_move`)
-- Apps return structured data rather than relying on the host to interpret rendered output
-
-This is *pull*-based and complements the *push*-based [`ui/update-model-context`](#uiupdate-model-context) request already defined in this spec (and analogous mechanisms elsewhere, e.g. OAI Apps SDK's [`setWidgetState()`](https://developers.openai.com/apps-sdk/reference/sdk#setwidgetstate)). Push lets an app proactively keep the model's context current; pull lets the agent query and command the app on demand. Apps may use either or both.
-
-#### App Tool Registration
-
-Apps register tools using the SDK's `registerTool()` method:
-
-```typescript
-import { App } from '@modelcontextprotocol/ext-apps';
-import { z } from 'zod';
-
-const app = new App(
- { name: "TicTacToe", version: "1.0.0" },
- { tools: { listChanged: true } } // Declare tool capability
-);
-
-// Register a tool with schema validation
-const moveTool = app.registerTool(
- "tictactoe_move",
- {
- description: "Make a move in the tic-tac-toe game",
- inputSchema: z.object({
- position: z.number().int().min(0).max(8),
- player: z.enum(['X', 'O'])
- }),
- outputSchema: z.object({
- board: z.array(z.string()).length(9),
- winner: z.enum(['X', 'O', 'draw', null]).nullable()
- }),
- annotations: {
- readOnlyHint: false // This tool has side effects
- }
- },
- async (params) => {
- // Validate and execute move
- const newBoard = makeMove(params.position, params.player);
- const winner = checkWinner(newBoard);
-
- return {
- content: [{
- type: "text",
- text: `Move made at position ${params.position}`
- }],
- structuredContent: {
- board: newBoard,
- winner
- }
- };
- }
-);
-
-await app.connect(new PostMessageTransport(window.parent));
-```
-
-**Registration Options:**
-
-| Field | Type | Required | Description |
-|-------|------|----------|-------------|
-| `name` | string | Yes | Unique tool identifier |
-| `description` | string | No | Human-readable description for agent |
-| `inputSchema` | [Standard Schema](https://standardschema.dev/) | No | Validates arguments; serialized to JSON Schema for `tools/list` |
-| `outputSchema` | [Standard Schema](https://standardschema.dev/) | No | Validates `structuredContent` |
-| `annotations` | ToolAnnotations | No | MCP tool hints (e.g., `readOnlyHint`) |
-| `_meta` | object | No | Custom metadata |
-
-Apps can also implement tool handling manually without the SDK:
-
-```javascript
-app.oncalltool = async (params, extra) => {
- if (params.name === "tictactoe_move") {
- // Manual validation
- if (typeof params.arguments?.position !== 'number') {
- throw new Error("Invalid position");
- }
-
- // Execute tool
- const newBoard = makeMove(params.arguments.position, params.arguments.player);
-
- return {
- content: [{ type: "text", text: "Move made" }],
- structuredContent: { board: newBoard }
- };
- }
-
- throw new Error(`Unknown tool: ${params.name}`);
-};
-
-app.onlisttools = async () => {
- return {
- tools: [
- {
- name: "tictactoe_move",
- description: "Make a move in the game",
- inputSchema: {
- type: "object",
- properties: {
- position: { type: "number", minimum: 0, maximum: 8 },
- player: { type: "string", enum: ["X", "O"] }
- },
- required: ["position", "player"]
- }
- }
- ]
- };
-};
-```
-
-#### Tool Lifecycle
-
-Registered tools support dynamic lifecycle management:
-
-**Enable/Disable:**
-
-```typescript
-const tool = app.registerTool("my_tool", config, callback);
-
-// Disable tool (hide from tools/list)
-tool.disable();
-
-// Re-enable tool
-tool.enable();
-```
-
-When a tool is disabled/enabled, the app automatically sends `notifications/tools/list_changed` (if the app declared `listChanged: true` capability).
-
-**Update:**
-
-```typescript
-// Update tool description or schema
-tool.update({
- description: "New description",
- inputSchema: newSchema
-});
-```
-
-Updates also trigger `notifications/tools/list_changed`.
-
-**Remove:**
-
-```typescript
-// Permanently remove tool
-tool.remove();
-```
-
-#### Long-Running App Tools
-
-App tools are bounded by the app's render lifecycle. For work that may outlive a single render, app `tools/call` MAY be task-augmented per [core MCP Tasks](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks); hosts proxy the `tasks/*` methods to the app exactly as they proxy `tools/call`. Because the iframe can be torn down at any time, apps SHOULD delegate long-lived work to the server (via `callServerTool()`) and return the server's task handle, so the host can continue polling the server after teardown.
-
-#### Schema Validation
-
-The SDK validates input and output against any [Standard Schema](https://standardschema.dev/)–compatible library (Zod, ArkType, Valibot, …). Examples below use Zod:
-
-**Input Validation:**
-
-```typescript
-app.registerTool(
- "search",
- {
- inputSchema: z.object({
- query: z.string().min(1).max(100),
- limit: z.number().int().positive().default(10)
- })
- },
- async (params) => {
- // params.query is guaranteed to be a string (1-100 chars)
- // params.limit is guaranteed to be a positive integer (default 10)
- return performSearch(params.query, params.limit);
- }
-);
-```
-
-If the host sends invalid arguments, the tool automatically returns an error before the callback is invoked.
-
-**Output Validation:**
-
-```typescript
-app.registerTool(
- "get_status",
- {
- outputSchema: z.object({
- status: z.enum(['ready', 'busy', 'error']),
- timestamp: z.string().datetime()
- })
- },
- async () => {
- return {
- content: [{ type: "text", text: "Status retrieved" }],
- structuredContent: {
- status: 'ready',
- timestamp: new Date().toISOString()
- }
- };
- }
-);
-```
-
-If the callback returns data that doesn't match `outputSchema`, the tool returns an error.
-
-#### Complete Example: Introspectable Tic-Tac-Toe
-
-This example demonstrates how apps expose semantic interfaces through tools:
-
-```typescript
-import { App } from '@modelcontextprotocol/ext-apps';
-import { z } from 'zod';
-
-// Game state
-let board: Array<'X' | 'O' | null> = Array(9).fill(null);
-let currentPlayer: 'X' | 'O' = 'X';
-let moveHistory: number[] = [];
-
-const app = new App(
- { name: "TicTacToe", version: "1.0.0" },
- { tools: { listChanged: true } }
-);
-
-// Agent can query semantic state directly
-app.registerTool(
- "get_board_state",
- {
- description: "Get current game state including board, current player, and winner",
- outputSchema: z.object({
- board: z.array(z.enum(['X', 'O', null])).length(9),
- currentPlayer: z.enum(['X', 'O']),
- winner: z.enum(['X', 'O', 'draw', null]).nullable(),
- moveHistory: z.array(z.number())
- })
- },
- async () => {
- return {
- content: [{
- type: "text",
- text: `Board: ${board.map(c => c || '-').join('')}, Player: ${currentPlayer}`
- }],
- structuredContent: {
- board,
- currentPlayer,
- winner: checkWinner(board),
- moveHistory
- }
- };
- }
-);
-
-// Agent can execute moves
-app.registerTool(
- "make_move",
- {
- description: "Place a piece at the specified position",
- inputSchema: z.object({
- position: z.number().int().min(0).max(8)
- }),
- annotations: { readOnlyHint: false }
- },
- async ({ position }) => {
- if (board[position] !== null) {
- return {
- content: [{ type: "text", text: "Position already taken" }],
- isError: true
- };
- }
-
- board[position] = currentPlayer;
- moveHistory.push(position);
- const winner = checkWinner(board);
- currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
-
- return {
- content: [{
- type: "text",
- text: `Player ${board[position]} moved to position ${position}` +
- (winner ? `. ${winner} wins!` : '')
- }],
- structuredContent: {
- board,
- currentPlayer,
- winner,
- moveHistory
- }
- };
- }
-);
-
-// Agent can reset game
-app.registerTool(
- "reset_game",
- {
- description: "Reset the game board to initial state",
- annotations: { readOnlyHint: false }
- },
- async () => {
- board = Array(9).fill(null);
- currentPlayer = 'X';
- moveHistory = [];
-
- return {
- content: [{ type: "text", text: "Game reset" }],
- structuredContent: { board, currentPlayer, moveHistory }
- };
- }
-);
-
-await app.connect(new PostMessageTransport(window.parent));
-
-function checkWinner(board: Array<'X' | 'O' | null>): 'X' | 'O' | 'draw' | null {
- const lines = [
- [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
- [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns
- [0, 4, 8], [2, 4, 6] // diagonals
- ];
-
- for (const [a, b, c] of lines) {
- if (board[a] && board[a] === board[b] && board[a] === board[c]) {
- return board[a];
- }
- }
-
- return board.every(cell => cell !== null) ? 'draw' : null;
-}
-```
-
-**Agent Interaction:**
-
-```typescript
-// 1. Discover available operations
-const { tools } = await bridge.sendListTools({});
-// → ["get_board_state", "make_move", "reset_game"]
-
-// 2. Query semantic state (not visual/DOM)
-const state = await bridge.sendCallTool({
- name: "get_board_state",
- arguments: {}
-});
-// → { board: [null, null, null, ...], currentPlayer: 'X', winner: null }
-
-// 3. Execute actions based on semantic understanding
-if (state.structuredContent.board[4] === null) {
- await bridge.sendCallTool({
- name: "make_move",
- arguments: { position: 4 }
- });
-}
-
-// 4. Query updated state
-const newState = await bridge.sendCallTool({
- name: "get_board_state",
- arguments: {}
-});
-// → { board: [null, null, null, null, 'X', null, ...], currentPlayer: 'O', ... }
-```
-
-The agent interacts with the app through semantic operations rather than visual interpretation.
-
-#### Tool Flow Directions
-
-**Existing Flow (unchanged): App → Host → Server**
-
-Apps call server tools (proxied by host):
-
-```typescript
-// App calls server tool
-const result = await app.callServerTool("get_weather", { location: "NYC" });
-```
-
-Requires host `serverTools` capability.
-
-**New Flow: Host/Agent → App**
-
-Host/Agent calls app-registered tools:
-
-```typescript
-// Host calls app tool
-const result = await bridge.sendCallTool({
- name: "tictactoe_move",
- arguments: { position: 4 }
-});
-```
-
-Requires app `tools` capability.
-
-**Key Distinction:**
-
-| Aspect | Server Tools | App Tools |
-|--------|-------------|-----------|
-| **Lifetime** | Persistent (server process) | Ephemeral (while app loaded) |
-| **Source** | MCP Server | App JavaScript |
-| **Trust** | Trusted | Sandboxed (untrusted) |
-| **Discovery** | Server `tools/list` | App `tools/list` (when app declares capability) |
-| **When Available** | Always | Only while app is loaded |
-
-#### Use Cases
-
-**Introspection:** Agent queries app state semantically — the app's DOM is not exposed to the host or model
-
-**Voice mode:** Agent drives app interactions programmatically based on voice commands
-
-**Accessibility:** Structured state and operations more accessible than visual rendering
-
-**Complex workflows:** Agent discovers available operations and coordinates multi-step interactions
-
-**Stateful apps:** Apps expose operations (move, reset, query) rather than pushing state updates via messages
-
-#### Security Implications
-
-App tools run in **sandboxed iframes** (untrusted). See Security Implications section for detailed mitigations.
-
-Key considerations:
-- App tools could provide misleading descriptions
-- Tool namespacing needed to avoid conflicts with server tools
-- Resource limits (max tools, execution timeouts)
-- Audit trail for app tool invocations
-- User confirmation for tools with side effects
-
-#### Relation to WebMCP
-
-This feature is inspired by [WebMCP](https://github.com/webmachinelearning/webmcp) (W3C incubation), which proposes allowing web pages to register JavaScript functions as tools via `navigator.modelContext.registerTool()`.
-
-Key differences:
-- **WebMCP**: General web pages, browser API, manifest-based discovery
-- **This spec**: MCP Apps, standard MCP messages, capability-based negotiation
-
-Similar to WebMCP but without turning the App (embedded page) into an MCP server - apps register tools within the App/Host architecture.
-
-See [ext-apps#35](https://github.com/modelcontextprotocol/ext-apps/issues/35) for discussion.
-
### Client\<\>Server Capability Negotiation
Clients and servers negotiate MCP Apps support through the standard MCP extensions capability mechanism (defined in SEP-1724).
@@ -2246,104 +1669,15 @@ if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
- Tools MUST return meaningful content array even when UI is available
- Servers MAY register different tool variants based on host capabilities
-#### App (Guest UI) Capabilities
-
-Apps advertise their capabilities in the `ui/initialize` request to the host. When an app supports tool registration, it includes the `tools` capability:
-
-```json
-{
- "method": "ui/initialize",
- "params": {
- "appInfo": {
- "name": "TicTacToe",
- "version": "1.0.0"
- },
- "appCapabilities": {
- "tools": {
- "listChanged": true
- }
- }
- }
-}
-```
-
-The host responds with its own capabilities, including support for proxying server tools:
-
-```json
-{
- "result": {
- "hostInfo": {
- "name": "claude-desktop",
- "version": "1.0.0"
- },
- "hostCapabilities": {
- "serverTools": {
- "listChanged": true
- },
- "openLinks": {},
- "logging": {}
- }
- }
-}
-```
-
-**App Capability: `tools`**
-
-When present, the app can register tools that the host and agent can call.
-
-- `listChanged` (boolean, optional): If `true`, the app will send `notifications/tools/list_changed` when tools are added, removed, or modified
-
-**Host Capability: `serverTools`**
-
-When present, the host can proxy calls from the app to MCP server tools.
-
-- `listChanged` (boolean, optional): If `true`, the host will send `notifications/tools/list_changed` when server tools change
-
-These capabilities are independent - an app can have one, both, or neither.
-
-**TypeScript Types:**
-
-```typescript
-interface McpUiAppCapabilities {
- tools?: {
- listChanged?: boolean;
- };
-}
-
-interface McpUiHostCapabilities {
- serverTools?: {
- listChanged?: boolean;
- };
- openLinks?: {};
- logging?: {};
-}
-```
-
### Extensibility
-This specification defines the Minimum Viable Product (MVP) for MCP Apps.
-
-**Included in MVP:**
-
-- **App-Provided Tools:** Apps can register tools via `app.registerTool()` that agents can call
- - Bidirectional tool flow (Apps consume server tools AND provide app tools)
- - Full lifecycle management (enable/disable/update/remove)
- - Schema validation via [Standard Schema](https://standardschema.dev/) (Zod, ArkType, Valibot, …)
- - Tool list change notifications
+This specification defines the Minimum Viable Product (MVP) for MCP Apps. Future extensions may include:
**Content Types (deferred from MVP):**
- `externalUrl`: Embed external web applications (e.g., `text/uri-list`)
-**Advanced Tool Features (future extensions):**
-
-- Tool namespacing standards and conventions
-- Standardized permission model specifications
-- Tool categories/tags for organization
-- Cross-app tool composition
-- Tool marketplace/discovery mechanisms
-
-**Other Advanced Features (see Future Considerations):**
+**Advanced Features (see Future Considerations):**
- Support multiple UI resources in a tool response
- State persistence and restoration
@@ -2381,7 +1715,7 @@ This proposal synthesizes feedback from the UI CWG and MCP-UI community, host im
- Reuses existing MCP infrastructure (type definitions, error handling, timeouts)
- UI developers can use standard MCP SDK (`@modelcontextprotocol/sdk`) or alternatives
-- Automatic compatibility with core MCP features (Tasks for long-running calls, sampling, etc.)
+- Automatic compatibility with future MCP features (long-running tools, sampling, etc.)
- Better auditability through structured JSON-RPC messages
- Reduces maintenance burden (no parallel protocol to evolve)
@@ -2443,37 +1777,6 @@ This proposal synthesizes feedback from the UI CWG and MCP-UI community, host im
- **Boolean `private` flag:** Simpler but less flexible; doesn't express model-only tools.
- **Flat `ui/visibility` key:** Rejected in favor of nested structure for consistency with future `_meta.ui` fields.
-#### 6. App Tool Registration Support
-
-**Decision:** Enable Apps to register tools using standard MCP `tools/call` and `tools/list` messages, making tools flow bidirectionally between Apps and Hosts.
-
-**Rationale:**
-
-- **Semantic introspection:** Apps expose state and operations in a structured, machine-readable form. Hosts have no guaranteed DOM access into an app (which may itself nest cross-origin frames via `frameDomains`), so a tool surface is the only reliable introspection path
-- **Protocol reuse:** Reuses existing MCP tool infrastructure (`tools/call`, `tools/list`, `notifications/tools/list_changed`) instead of inventing new message types
-- **WebMCP alignment:** Brings WebMCP's vision of "JavaScript functions as tools" to MCP Apps while staying MCP-native
-- **Agent-driven interaction:** Agents can pull app state and issue commands on demand, complementing push-style [`ui/update-model-context`](#uiupdate-model-context)
-- **Bidirectional symmetry:** Apps act as both MCP clients (calling server tools) and MCP servers (providing app tools), creating clean architectural symmetry
-- **Use case coverage:** Enables interactive games, stateful forms, complex workflows, and reusable widgets
-
-**Alternatives considered:**
-
-- **Custom app-action API:** Rejected because it would duplicate MCP's existing tool infrastructure and create parallel protocol semantics. Using standard `tools/call` means automatic compatibility with future MCP features and better ecosystem integration.
-- **Server-side proxy tools:** Apps could expose operations by having the server register proxy tools that communicate back to the app. Rejected because it doesn't leverage the app's JavaScript execution environment, adds unnecessary round-trips, and couples app functionality to server implementation.
-- **Resources instead of tools:** Apps could expose state via `resources/read` rather than tools. Rejected because resources have wrong semantics (passive data retrieval vs. active operations), don't support parameters well, and don't convey operational intent.
-
-**Security implications:**
-
-Apps are forward-deployed emanations of server tools, running in the client context. Hosts should consider how to handle tool call approval:
-
-- Per-app-instance approval (confirm each time a specific app instance calls a tool)
-- Per-server approval (approve all apps from a trusted server)
-- Per-tool approval (approve based on tool semantics and annotations)
-- Clear attribution showing which app instance is calling a tool
-- Audit trails for app tool calls
-
-See [Security Implications: App-Provided Tools Security](#5-app-provided-tools-security) for detailed considerations.
-
### Backward Compatibility
The proposal builds on the existing core protocol. There are no incompatibilities.
@@ -2566,83 +1869,6 @@ const allowAttribute = allowList.join(' ');
- Host SHOULD warn users when UI requires external domain access
- Host MAY implement global domain allowlists/blocklists
-#### 5. App-Provided Tools Security
-
-Apps can register their own tools that agents can call. Apps are forward-deployed emanations of server tools, running in the client context. Hosts need to decide how to handle approval for app tool calls.
-
-**Approval Considerations:**
-
-App-provided tools introduce additional approval considerations:
-
-- **Tool description accuracy:** Apps may describe tools in ways that don't fully capture side effects
-- **Namespace conflicts:** Apps could register tools with names conflicting with server tools
-- **Resource consumption:** Apps could register many tools or implement slow callbacks
-- **Data validation:** Tool results should match declared schemas
-- **Semantic clarity:** Tool operations should be clear from their descriptions
-
-**Approval Granularity:**
-
-Hosts have discretion in how they handle app tool call approval:
-
-1. **Per-app-instance approval:** Confirm each time a specific app instance's tool is called
-2. **Per-server approval:** Trust all apps from servers the user has approved
-3. **Per-tool approval:** Approve based on tool annotations (e.g., `readOnlyHint`)
-4. **Hybrid approaches:** Combine strategies (e.g., auto-approve read-only tools from trusted servers)
-
-**Host Protections:**
-
-Hosts SHOULD implement the following protections for app-provided tools:
-
-1. **Clear Attribution:**
- - Display tool source in agent's tool list (e.g., "Tool from TicTacToe App")
- - Visually distinguish app tools from server tools in UI
- - Show app name and version in tool call confirmations
-
-2. **User Confirmation:**
- - Require explicit user approval for tools with `readOnlyHint: false`
- - Consider auto-approving tools with `readOnlyHint: true` after review
- - Implement per-app permission settings (always allow, always deny, ask)
-
-3. **Namespace Management:**
- - Recommend or enforce tool name prefixes (e.g., `app:move`, `tictactoe:move`)
- - Prevent apps from registering tool names that conflict with server tools
- - Document namespace conventions for app developers
-
-4. **Resource Limits:**
- - Limit maximum number of tools per app (recommended: 50)
- - Enforce execution timeouts for tool callbacks (recommended: 30 seconds)
- - Limit tool result sizes (recommended: 10 MB)
- - Throttle `tools/list_changed` notifications to prevent spam
-
-5. **Audit Trail:**
- - Log all app tool registrations with timestamps
- - Log all app tool calls with arguments and results
- - Provide audit interface for users to review app tool activity
-
-6. **Result Validation:**
- - Validate tool results match declared schemas
- - Sanitize result content before displaying to user or agent
- - Reject results that appear malicious (e.g., phishing content)
-
-**Permission Model:**
-
-Hosts MAY implement different permission levels based on tool annotations:
-
-| Annotation | Recommended Permission | Example |
-|---------------------|------------------------|-------------------|
-| `readOnlyHint: true`| Auto-approve (with caution) | `get_board_state()` |
-| `readOnlyHint: false` | User confirmation required | `make_move()` |
-| No annotation | User confirmation required (safe default) | Any tool |
-
-**App Tool Lifecycle:**
-
-App tools MUST be tied to the app's lifecycle:
-
-- Tools become available once the app advertises the `tools` capability in `ui/initialize` and the host issues `tools/list`; subsequent changes are signaled via `notifications/tools/list_changed`
-- Tools automatically disappear when app iframe is torn down
-- Hosts MUST NOT persist app tool registrations across sessions
-- Calling a tool from a closed app MUST return an error
-
### Other risks
- **Social engineering:** UI can still display misleading content. Hosts should clearly indicate sandboxed UI boundaries.
diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts
index 47ad623..b613451 100644
--- a/src/app-bridge.examples.ts
+++ b/src/app-bridge.examples.ts
@@ -7,11 +7,15 @@
* @module
*/
-import { Client, type Transport } from "@modelcontextprotocol/client";
-import type {
- CreateMessageRequest,
- CreateMessageResult,
-} from "@modelcontextprotocol/server";
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
+import {
+ CallToolResult,
+ CallToolResultSchema,
+ ListResourcesResultSchema,
+ ReadResourceResultSchema,
+ ListPromptsResultSchema,
+} from "@modelcontextprotocol/sdk/types.js";
import { AppBridge, PostMessageTransport } from "./app-bridge.js";
import type { McpUiDisplayMode } from "./types.js";
@@ -217,34 +221,13 @@ function AppBridge_oncalltool_forwardToServer(
bridge.oncalltool = async (params, extra) => {
return mcpClient.request(
{ method: "tools/call", params },
- { signal: extra.mcpReq.signal },
+ CallToolResultSchema,
+ { signal: extra.signal },
);
};
//#endregion AppBridge_oncalltool_forwardToServer
}
-/**
- * Example: Forward sampling requests to your LLM provider.
- */
-function AppBridge_oncreatesamplingmessage_forwardToLlm(
- bridge: AppBridge,
- myLlmProvider: {
- complete: (
- params: CreateMessageRequest["params"],
- opts: { signal: AbortSignal },
- ) => Promise;
- },
-) {
- //#region AppBridge_oncreatesamplingmessage_forwardToLlm
- bridge.oncreatesamplingmessage = async (params, extra) => {
- // Apply rate limiting, user approval, cost controls here
- return await myLlmProvider.complete(params, {
- signal: extra.mcpReq.signal,
- });
- };
- //#endregion AppBridge_oncreatesamplingmessage_forwardToLlm
-}
-
/**
* Example: Forward list resources requests to the MCP server.
*/
@@ -256,7 +239,8 @@ function AppBridge_onlistresources_returnResources(
bridge.onlistresources = async (params, extra) => {
return mcpClient.request(
{ method: "resources/list", params },
- { signal: extra.mcpReq.signal },
+ ListResourcesResultSchema,
+ { signal: extra.signal },
);
};
//#endregion AppBridge_onlistresources_returnResources
@@ -273,7 +257,8 @@ function AppBridge_onreadresource_returnResource(
bridge.onreadresource = async (params, extra) => {
return mcpClient.request(
{ method: "resources/read", params },
- { signal: extra.mcpReq.signal },
+ ReadResourceResultSchema,
+ { signal: extra.signal },
);
};
//#endregion AppBridge_onreadresource_returnResource
@@ -290,7 +275,8 @@ function AppBridge_onlistprompts_returnPrompts(
bridge.onlistprompts = async (params, extra) => {
return mcpClient.request(
{ method: "prompts/list", params },
- { signal: extra.mcpReq.signal },
+ ListPromptsResultSchema,
+ { signal: extra.signal },
);
};
//#endregion AppBridge_onlistprompts_returnPrompts
@@ -436,10 +422,10 @@ async function AppBridge_sendToolResult_afterExecution(
args: Record,
) {
//#region AppBridge_sendToolResult_afterExecution
- const result = await mcpClient.request({
- method: "tools/call",
- params: { name: "get_weather", arguments: args },
- });
+ const result = await mcpClient.request(
+ { method: "tools/call", params: { name: "get_weather", arguments: args } },
+ CallToolResultSchema,
+ );
bridge.sendToolResult(result);
//#endregion AppBridge_sendToolResult_afterExecution
}
diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts
index a87b0e7..4761d76 100644
--- a/src/app-bridge.test.ts
+++ b/src/app-bridge.test.ts
@@ -1,19 +1,26 @@
-import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
-import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
-import { Server, type ServerCapabilities } from "@modelcontextprotocol/server";
-import { z } from "zod/v4";
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
+import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
+import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
+import {
+ EmptyResultSchema,
+ ListPromptsResultSchema,
+ ListResourcesResultSchema,
+ ListResourceTemplatesResultSchema,
+ PromptListChangedNotificationSchema,
+ ReadResourceResultSchema,
+ ResourceListChangedNotificationSchema,
+ ToolListChangedNotificationSchema,
+} from "@modelcontextprotocol/sdk/types.js";
-import { App } from "./app.js";
-import { LATEST_PROTOCOL_VERSION } from "./types.js";
+import { App } from "./app";
import {
AppBridge,
- buildAllowAttribute,
getToolUiResourceUri,
isToolVisibilityModelOnly,
isToolVisibilityAppOnly,
- McpUiOpenLinkResultSchema,
type McpUiHostCapabilities,
-} from "./app-bridge.js";
+} from "./app-bridge";
/** Wait for pending microtasks to complete */
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
@@ -24,27 +31,17 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
*/
function createMockClient(
serverCapabilities: ServerCapabilities = {},
-): Pick<
- Client,
- | "getServerCapabilities"
- | "request"
- | "notification"
- | "setNotificationHandler"
-> {
+): Pick {
return {
getServerCapabilities: () => serverCapabilities,
request: async () => ({}) as never,
notification: async () => {},
- setNotificationHandler: () => {},
};
}
const testHostInfo = { name: "TestHost", version: "1.0.0" };
const testAppInfo = { name: "TestApp", version: "1.0.0" };
const testHostCapabilities: McpUiHostCapabilities = {
- experimental: {
- "com.example/host-extension": { version: 1 },
- },
openLinks: {},
serverTools: {},
logging: {},
@@ -85,53 +82,6 @@ describe("App <-> AppBridge integration", () => {
expect(initializedFired).toBe(true);
});
- it("sends only the Apps handshake on the wire and gates the Apps-ready callback", async () => {
- const methods: string[] = [];
- let releaseAppsInitialized!: () => void;
- let reachedAppsInitialized!: () => void;
- const appsInitializedGate = new Promise((resolve) => {
- releaseAppsInitialized = resolve;
- });
- const reachedGate = new Promise((resolve) => {
- reachedAppsInitialized = resolve;
- });
- const originalSend = appTransport.send.bind(appTransport);
- appTransport.send = async (message, options) => {
- if ("method" in message) methods.push(message.method);
- if (
- "method" in message &&
- message.method === "ui/notifications/initialized"
- ) {
- reachedAppsInitialized();
- await appsInitializedGate;
- }
- return originalSend(message, options);
- };
-
- let singularCalls = 0;
- let listenerCalls = 0;
- bridge.oninitialized = () => singularCalls++;
- bridge.addEventListener("initialized", () => listenerCalls++);
-
- await bridge.connect(bridgeTransport);
- const connecting = app.connect(appTransport);
- await reachedGate;
-
- expect(methods).toEqual([
- "ui/initialize",
- "ui/notifications/initialized",
- ]);
- expect(bridge.getAppVersion()).toEqual(testAppInfo);
- expect(singularCalls).toBe(0);
- expect(listenerCalls).toBe(0);
-
- releaseAppsInitialized();
- await connecting;
-
- expect(singularCalls).toBe(1);
- expect(listenerCalls).toBe(1);
- });
-
it("App receives host info and capabilities after connect", async () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@@ -141,16 +91,10 @@ describe("App <-> AppBridge integration", () => {
const hostCaps = app.getHostCapabilities();
expect(hostCaps).toEqual(testHostCapabilities);
- expect(bridge.getCapabilities()).toEqual(testHostCapabilities);
});
it("Bridge receives app info and capabilities after initialization", async () => {
- const appCapabilities = {
- experimental: {
- "com.example/app-extension": { version: 1 },
- },
- tools: { listChanged: true },
- };
+ const appCapabilities = { tools: { listChanged: true } };
app = new App(testAppInfo, appCapabilities, { autoResize: false });
await bridge.connect(bridgeTransport);
@@ -570,7 +514,7 @@ describe("App <-> AppBridge integration", () => {
};
await app.connect(appTransport);
- expect(
+ await expect(
app.updateModelContext({
content: [{ type: "text", text: "Test" }],
}),
@@ -667,30 +611,6 @@ describe("App <-> AppBridge integration", () => {
expect(result.isError).toBe(true);
});
-
- it("rejects invalid params for a custom ui request", async () => {
- bridge.onopenlink = async () => ({});
- await app.connect(appTransport);
-
- await expect(
- app.request(
- {
- method: "ui/open-link",
- params: { url: 42 },
- },
- McpUiOpenLinkResultSchema,
- ),
- ).rejects.toThrow();
- });
-
- it("rejects invalid results from a custom ui handler", async () => {
- bridge.onopenlink = async () => ({ isError: "yes" }) as never;
- await app.connect(appTransport);
-
- await expect(
- app.openLink({ url: "https://example.com" }),
- ).rejects.toThrow();
- });
});
describe("deprecated method aliases", () => {
@@ -727,7 +647,7 @@ describe("App <-> AppBridge integration", () => {
// Attempting to connect again with a different transport should throw
const [, secondBridgeTransport] = InMemoryTransport.createLinkedPair();
- expect(bridge.connect(secondBridgeTransport)).rejects.toThrow(
+ await expect(bridge.connect(secondBridgeTransport)).rejects.toThrow(
"AppBridge is already connected",
);
});
@@ -738,7 +658,7 @@ describe("App <-> AppBridge integration", () => {
// Attempting to connect again should throw
const [secondAppTransport] = InMemoryTransport.createLinkedPair();
- expect(app.connect(secondAppTransport)).rejects.toThrow(
+ await expect(app.connect(secondAppTransport)).rejects.toThrow(
"App is already connected",
);
});
@@ -748,7 +668,7 @@ describe("App <-> AppBridge integration", () => {
await app.connect(appTransport);
// Should throw regardless of whether it's the same or a different transport
- expect(bridge.connect(bridgeTransport)).rejects.toThrow(
+ await expect(bridge.connect(bridgeTransport)).rejects.toThrow(
"AppBridge is already connected",
);
});
@@ -760,1069 +680,15 @@ describe("App <-> AppBridge integration", () => {
await app.connect(appTransport);
// Bridge can send ping via the protocol's request method
- const result = await bridge.request({ method: "ping", params: {} });
+ const result = await bridge.request(
+ { method: "ping", params: {} },
+ EmptyResultSchema,
+ );
expect(result).toEqual({});
});
});
- describe("App tool registration", () => {
- beforeEach(async () => {
- app = new App(
- testAppInfo,
- { tools: { listChanged: true } },
- { autoResize: false },
- );
- await bridge.connect(bridgeTransport);
- });
-
- it("registerTool creates a registered tool", async () => {
- const InputSchema = z.object({ name: z.string() });
- const OutputSchema = z.object({ greeting: z.string() });
-
- const tool = app.registerTool(
- "greet",
- {
- title: "Greet User",
- description: "Greets a user by name",
- inputSchema: InputSchema,
- outputSchema: OutputSchema,
- },
- async (args: any) => ({
- content: [{ type: "text" as const, text: `Hello, ${args.name}!` }],
- structuredContent: { greeting: `Hello, ${args.name}!` },
- }),
- );
-
- expect(tool.title).toBe("Greet User");
- expect(tool.description).toBe("Greets a user by name");
- expect(tool.enabled).toBe(true);
- });
-
- it("registered tool can be enabled and disabled", async () => {
- await app.connect(appTransport);
-
- const tool = app.registerTool(
- "test-tool",
- {
- description: "Test tool",
- },
- async (_extra: any) => ({ content: [] }),
- );
-
- expect(tool.enabled).toBe(true);
-
- tool.disable();
- expect(tool.enabled).toBe(false);
-
- tool.enable();
- expect(tool.enabled).toBe(true);
- });
-
- it("registered tool can be updated", async () => {
- await app.connect(appTransport);
-
- const tool = app.registerTool(
- "test-tool",
- {
- description: "Original description",
- },
- async (_extra: any) => ({ content: [] }),
- );
-
- expect(tool.description).toBe("Original description");
-
- tool.update({ description: "Updated description" });
- expect(tool.description).toBe("Updated description");
- });
-
- it("registered tool can be removed", async () => {
- await app.connect(appTransport);
-
- const tool = app.registerTool(
- "test-tool",
- {
- description: "Test tool",
- },
- async (_extra: any) => ({ content: [] }),
- );
-
- tool.remove();
- // Tool should no longer be registered (internal check)
- });
-
- it("registerTool throws on duplicate name", () => {
- app.registerTool("dup", {}, async () => ({ content: [] }));
- expect(() =>
- app.registerTool("dup", {}, async () => ({ content: [] })),
- ).toThrow(/already registered/);
- });
-
- it("enable/disable/update/remove pre-connect do not throw", () => {
- const tool = app.registerTool("t", {}, async () => ({ content: [] }));
- expect(() => tool.disable()).not.toThrow();
- expect(() => tool.enable()).not.toThrow();
- expect(() => tool.update({ description: "x" })).not.toThrow();
- expect(() => tool.remove()).not.toThrow();
- });
-
- it("callback without inputSchema receives extra as first arg", async () => {
- await app.connect(appTransport);
- let receivedExtra: any;
- app.registerTool("noargs", {}, async (extra: any) => {
- receivedExtra = extra;
- return { content: [] };
- });
- await bridge.callTool({ name: "noargs", arguments: {} });
- expect(receivedExtra).toBeDefined();
- expect(receivedExtra.mcpReq.signal).toBeInstanceOf(AbortSignal);
- });
-
- it("isError result skips output schema validation", async () => {
- await app.connect(appTransport);
- app.registerTool(
- "errs",
- { outputSchema: z.object({ ok: z.boolean() }) },
- async () => ({
- content: [{ type: "text" as const, text: "boom" }],
- isError: true,
- }),
- );
- const res = await bridge.callTool({ name: "errs", arguments: {} });
- expect(res.isError).toBe(true);
- expect(res.structuredContent).toBeUndefined();
- });
-
- it("stale handle remove() does not delete a re-registered tool", async () => {
- const t1 = app.registerTool("phoenix", {}, async () => ({ content: [] }));
- t1.remove();
- app.registerTool("phoenix", {}, async () => ({ content: [] }));
- t1.remove();
- await app.connect(appTransport);
- const list = await bridge.listTools({});
- expect(list.tools.map((t) => t.name)).toContain("phoenix");
- });
-
- it("host omitting arguments defaults to empty object", async () => {
- await app.connect(appTransport);
- let received: unknown;
- app.registerTool(
- "noargs2",
- { inputSchema: z.object({}) },
- async (args) => {
- received = args;
- return { content: [] };
- },
- );
- await bridge.callTool({ name: "noargs2" });
- expect(received).toEqual({});
- });
-
- it("update({inputSchema}) is honored by handler validation", async () => {
- await app.connect(appTransport);
- const tool = app.registerTool(
- "evolving",
- { inputSchema: z.object({ a: z.string() }) },
- async (args: any) => ({
- content: [{ type: "text" as const, text: JSON.stringify(args) }],
- }),
- );
- expect(
- bridge.callTool({ name: "evolving", arguments: { a: 123 } }),
- ).rejects.toThrow(/Invalid input/);
- tool.update({ inputSchema: z.object({ a: z.number() }) });
- const result = await bridge.callTool({
- name: "evolving",
- arguments: { a: 123 },
- });
- expect(result.content[0]).toEqual({ type: "text", text: '{"a":123}' });
- });
-
- it("tool throws error when disabled and called", async () => {
- await app.connect(appTransport);
-
- const tool = app.registerTool(
- "test-tool",
- {
- description: "Test tool",
- },
- async (_extra: any) => ({ content: [] }),
- );
-
- tool.disable();
-
- const mockExtra = {
- signal: new AbortController().signal,
- requestId: "test",
- sendNotification: async () => {},
- sendRequest: async () => ({}),
- } as any;
-
- expect((tool.handler as any)(mockExtra)).rejects.toThrow(
- "Tool test-tool is disabled",
- );
- });
-
- it("tool validates input schema", async () => {
- const InputSchema = z.object({ name: z.string() });
-
- const tool = app.registerTool(
- "greet",
- {
- inputSchema: InputSchema,
- },
- async (args: any) => ({
- content: [{ type: "text" as const, text: `Hello, ${args.name}!` }],
- }),
- );
-
- // Create a mock RequestHandlerExtra
- const mockExtra = {
- signal: new AbortController().signal,
- requestId: "test",
- sendNotification: async () => {},
- sendRequest: async () => ({}),
- } as any;
-
- // Valid input should work
- expect(
- (tool.handler as any)({ name: "Alice" }, mockExtra),
- ).resolves.toBeDefined();
-
- // Invalid input should fail
- expect(
- (tool.handler as any)({ invalid: "field" }, mockExtra),
- ).rejects.toThrow("Invalid input for tool greet");
- });
-
- it("tool validates output schema", async () => {
- const OutputSchema = z.object({ greeting: z.string() });
-
- const tool = app.registerTool(
- "greet",
- {
- outputSchema: OutputSchema,
- },
- async (_extra: any) => ({
- content: [{ type: "text" as const, text: "Hello!" }],
- structuredContent: { greeting: "Hello!" },
- }),
- );
-
- // Create a mock RequestHandlerExtra
- const mockExtra = {
- signal: new AbortController().signal,
- requestId: "test",
- sendNotification: async () => {},
- sendRequest: async () => ({}),
- } as any;
-
- // Valid output should work
- expect((tool.handler as any)(mockExtra)).resolves.toBeDefined();
- });
-
- it("tool enable/disable/update/remove trigger sendToolListChanged", async () => {
- await app.connect(appTransport);
-
- const tool = app.registerTool(
- "test-tool",
- {
- description: "Test tool",
- },
- async (_extra: any) => ({ content: [] }),
- );
-
- // The methods should not throw when connected
- expect(() => tool.disable()).not.toThrow();
- expect(() => tool.enable()).not.toThrow();
- expect(() => tool.update({ description: "Updated" })).not.toThrow();
- expect(() => tool.remove()).not.toThrow();
- });
- });
-
- describe("AppBridge -> App tool requests", () => {
- beforeEach(async () => {
- await bridge.connect(bridgeTransport);
- });
-
- it("bridge.callTool calls app.oncalltool handler", async () => {
- // App needs tool capabilities to handle tool calls
- const appCapabilities = { tools: {} };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- const receivedCalls: unknown[] = [];
-
- app.oncalltool = async (params) => {
- receivedCalls.push(params);
- return {
- content: [{ type: "text", text: `Executed: ${params.name}` }],
- };
- };
-
- await app.connect(appTransport);
-
- const result = await bridge.callTool({
- name: "test-tool",
- arguments: { foo: "bar" },
- });
-
- expect(receivedCalls).toHaveLength(1);
- expect(receivedCalls[0]).toMatchObject({
- name: "test-tool",
- arguments: { foo: "bar" },
- });
- expect(result.content).toEqual([
- { type: "text", text: "Executed: test-tool" },
- ]);
- });
-
- it("bridge.listTools calls app.onlisttools handler", async () => {
- // App needs tool capabilities to handle tool list requests
- const appCapabilities = { tools: {} };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- const receivedCalls: unknown[] = [];
-
- app.onlisttools = async (params, _extra) => {
- receivedCalls.push(params);
- return {
- tools: [
- {
- name: "tool1",
- description: "First tool",
- inputSchema: { type: "object", properties: {} },
- },
- {
- name: "tool2",
- description: "Second tool",
- inputSchema: { type: "object", properties: {} },
- },
- {
- name: "tool3",
- description: "Third tool",
- inputSchema: { type: "object", properties: {} },
- },
- ],
- };
- };
-
- await app.connect(appTransport);
-
- const result = await bridge.listTools({});
-
- expect(receivedCalls).toHaveLength(1);
- expect(result.tools).toHaveLength(3);
- expect(result.tools[0].name).toBe("tool1");
- expect(result.tools[1].name).toBe("tool2");
- expect(result.tools[2].name).toBe("tool3");
- });
- });
-
- describe("App tool capabilities", () => {
- it("App with tool capabilities can handle tool calls", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- const receivedCalls: unknown[] = [];
- app.oncalltool = async (params) => {
- receivedCalls.push(params);
- return {
- content: [{ type: "text", text: "Success" }],
- };
- };
-
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- await bridge.callTool({
- name: "test-tool",
- arguments: {},
- });
-
- expect(receivedCalls).toHaveLength(1);
- });
-
- it("registered tool is invoked via oncalltool", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- const tool = app.registerTool(
- "greet",
- {
- description: "Greets user",
- inputSchema: z.object({ name: z.string() }),
- },
- async (args: any) => ({
- content: [{ type: "text" as const, text: `Hello, ${args.name}!` }],
- }),
- );
-
- app.oncalltool = async (params, extra) => {
- if (params.name === "greet") {
- return await (tool.handler as any)(params.arguments || {}, extra);
- }
- throw new Error(`Unknown tool: ${params.name}`);
- };
-
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- const result = await bridge.callTool({
- name: "greet",
- arguments: { name: "Alice" },
- });
-
- expect(result.content).toEqual([{ type: "text", text: "Hello, Alice!" }]);
- });
- });
-
- describe("Automatic request handlers", () => {
- beforeEach(async () => {
- await bridge.connect(bridgeTransport);
- });
-
- describe("oncalltool automatic handler", () => {
- it("automatically calls registered tool without manual oncalltool setup", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- // Register a tool
- app.registerTool(
- "greet",
- {
- description: "Greets user",
- inputSchema: z.object({ name: z.string() }),
- },
- async (args: any) => ({
- content: [{ type: "text" as const, text: `Hello, ${args.name}!` }],
- }),
- );
-
- await app.connect(appTransport);
-
- // Call the tool through bridge - should work automatically
- const result = await bridge.callTool({
- name: "greet",
- arguments: { name: "Bob" },
- });
-
- expect(result.content).toEqual([{ type: "text", text: "Hello, Bob!" }]);
- });
-
- it("throws error when calling non-existent tool", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- // Register a tool to initialize handlers
- app.registerTool("existing-tool", {}, async (_args: any) => ({
- content: [],
- }));
-
- await app.connect(appTransport);
-
- // Try to call a tool that doesn't exist
- expect(
- bridge.callTool({
- name: "nonexistent",
- arguments: {},
- }),
- ).rejects.toThrow("Tool nonexistent not found");
- });
-
- it("handles multiple registered tools correctly", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- // Register multiple tools
- app.registerTool(
- "add",
- {
- description: "Add two numbers",
- inputSchema: z.object({ a: z.number(), b: z.number() }),
- },
- async (args: any) => ({
- content: [
- {
- type: "text" as const,
- text: `Result: ${args.a + args.b}`,
- },
- ],
- structuredContent: { result: args.a + args.b },
- }),
- );
-
- app.registerTool(
- "multiply",
- {
- description: "Multiply two numbers",
- inputSchema: z.object({ a: z.number(), b: z.number() }),
- },
- async (args: any) => ({
- content: [
- {
- type: "text" as const,
- text: `Result: ${args.a * args.b}`,
- },
- ],
- structuredContent: { result: args.a * args.b },
- }),
- );
-
- await app.connect(appTransport);
-
- // Call first tool
- const addResult = await bridge.callTool({
- name: "add",
- arguments: { a: 5, b: 3 },
- });
- expect(addResult.content).toEqual([
- { type: "text", text: "Result: 8" },
- ]);
-
- // Call second tool
- const multiplyResult = await bridge.callTool({
- name: "multiply",
- arguments: { a: 5, b: 3 },
- });
- expect(multiplyResult.content).toEqual([
- { type: "text", text: "Result: 15" },
- ]);
- });
-
- it("respects tool enable/disable state", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- const tool = app.registerTool(
- "test-tool",
- {
- description: "Test tool",
- },
- async (_args: any) => ({
- content: [{ type: "text" as const, text: "Success" }],
- }),
- );
-
- await app.connect(appTransport);
-
- // Should work when enabled
- expect(
- bridge.callTool({ name: "test-tool", arguments: {} }),
- ).resolves.toBeDefined();
-
- // Disable tool
- tool.disable();
-
- // Should throw when disabled
- expect(
- bridge.callTool({ name: "test-tool", arguments: {} }),
- ).rejects.toThrow("Tool test-tool is disabled");
- });
-
- it("validates input schema through automatic handler", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- app.registerTool(
- "strict-tool",
- {
- description: "Requires specific input",
- inputSchema: z.object({
- required: z.string(),
- optional: z.number().optional(),
- }) as any,
- },
- async (args: any) => ({
- content: [{ type: "text" as const, text: `Got: ${args.required}` }],
- }),
- );
-
- await app.connect(appTransport);
-
- // Valid input should work
- expect(
- bridge.callTool({
- name: "strict-tool",
- arguments: { required: "hello" },
- }),
- ).resolves.toBeDefined();
-
- // Invalid input should fail
- expect(
- bridge.callTool({
- name: "strict-tool",
- arguments: { wrong: "field" },
- }),
- ).rejects.toThrow("Invalid input for tool strict-tool");
- });
-
- it("validates output schema through automatic handler", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- app.registerTool(
- "validated-output",
- {
- description: "Has output validation",
- outputSchema: z.object({
- status: z.enum(["success", "error"]),
- }) as any,
- },
- async (_args: any) => ({
- content: [{ type: "text" as const, text: "Done" }],
- structuredContent: { status: "success" },
- }),
- );
-
- await app.connect(appTransport);
-
- // Valid output should work
- const result = await bridge.callTool({
- name: "validated-output",
- arguments: {},
- });
- expect(result).toBeDefined();
- });
-
- it("works after tool is removed and re-registered", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- const tool = app.registerTool(
- "dynamic-tool",
- {},
- async (_args: any) => ({
- content: [{ type: "text" as const, text: "Version 1" }],
- }),
- );
-
- await app.connect(appTransport);
-
- // First version
- let result = await bridge.callTool({
- name: "dynamic-tool",
- arguments: {},
- });
- expect(result.content).toEqual([{ type: "text", text: "Version 1" }]);
-
- // Remove tool
- tool.remove();
-
- // Should fail after removal
- expect(
- bridge.callTool({ name: "dynamic-tool", arguments: {} }),
- ).rejects.toThrow("Tool dynamic-tool not found");
-
- // Re-register with different behavior
- app.registerTool("dynamic-tool", {}, async (_args: any) => ({
- content: [{ type: "text" as const, text: "Version 2" }],
- }));
-
- // Should work with new version
- result = await bridge.callTool({
- name: "dynamic-tool",
- arguments: {},
- });
- expect(result.content).toEqual([{ type: "text", text: "Version 2" }]);
- });
- });
-
- describe("onlisttools automatic handler", () => {
- it("automatically returns list of registered tool names", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- // Register some tools
- app.registerTool("tool1", {}, async (_args: any) => ({
- content: [],
- }));
- app.registerTool("tool2", {}, async (_args: any) => ({
- content: [],
- }));
- app.registerTool("tool3", {}, async (_args: any) => ({
- content: [],
- }));
-
- await app.connect(appTransport);
-
- const result = await bridge.listTools({});
-
- expect(result.tools).toHaveLength(3);
- expect(result.tools.map((t) => t.name)).toContain("tool1");
- expect(result.tools.map((t) => t.name)).toContain("tool2");
- expect(result.tools.map((t) => t.name)).toContain("tool3");
- });
-
- it("emits core MCP Tool fields (title, outputSchema only when provided)", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- app.registerTool(
- "with-output",
- {
- title: "With Output",
- description: "has structured output",
- outputSchema: z.object({ ok: z.boolean() }),
- },
- async () => ({
- content: [],
- structuredContent: { ok: true },
- }),
- );
- app.registerTool(
- "no-output",
- { description: "no structured output" },
- async () => ({ content: [] }),
- );
-
- await app.connect(appTransport);
- const result = await bridge.listTools({});
- const byName = Object.fromEntries(result.tools.map((t) => [t.name, t]));
-
- expect(byName["with-output"].title).toBe("With Output");
- expect(byName["with-output"].inputSchema).toBeDefined();
- expect(byName["with-output"].outputSchema).toBeDefined();
- // outputSchema is optional in core MCP — omitted when not declared
- expect(byName["no-output"]).not.toHaveProperty("outputSchema");
- expect(byName["no-output"].inputSchema).toBeDefined();
- });
-
- it("accepts any Standard Schema implementation, not only zod", async () => {
- // Hand-rolled StandardSchemaWithJSON — proves registerTool has no
- // zod-specific runtime path. Any library implementing the spec
- // (ArkType, Valibot, …) works the same way.
- type Point = { x: number; y: number };
- const PointSchema = {
- "~standard": {
- version: 1 as const,
- vendor: "test",
- types: undefined as
- | undefined
- | { readonly input: Point; readonly output: Point },
- validate: (v: unknown) =>
- typeof v === "object" &&
- v !== null &&
- typeof (v as any).x === "number" &&
- typeof (v as any).y === "number"
- ? { value: v as { x: number; y: number } }
- : { issues: [{ message: "expected {x:number,y:number}" }] },
- jsonSchema: {
- input: () => ({
- type: "object",
- properties: { x: { type: "number" }, y: { type: "number" } },
- required: ["x", "y"],
- }),
- output: () => ({
- type: "object",
- properties: { x: { type: "number" }, y: { type: "number" } },
- }),
- },
- },
- };
-
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
- app.registerTool(
- "translate",
- { inputSchema: PointSchema, outputSchema: PointSchema },
- async ({ x, y }) => ({
- content: [],
- structuredContent: { x: x + 1, y: y + 1 },
- }),
- );
- await app.connect(appTransport);
-
- const list = await bridge.listTools({});
- expect(list.tools[0].inputSchema).toEqual({
- type: "object",
- properties: { x: { type: "number" }, y: { type: "number" } },
- required: ["x", "y"],
- });
- expect(list.tools[0].outputSchema).toBeDefined();
-
- const ok = await bridge.callTool({
- name: "translate",
- arguments: { x: 1, y: 2 },
- });
- expect(ok.structuredContent).toEqual({ x: 2, y: 3 });
-
- expect(
- bridge.callTool({ name: "translate", arguments: { x: "bad" } }),
- ).rejects.toThrow(/Invalid input for tool translate/);
- });
-
- it("rejects listTools when a tool schema does not implement Standard JSON Schema", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
- app.registerTool(
- "broken",
- {
- inputSchema: {
- "~standard": {
- version: 1 as const,
- vendor: "mystery",
- validate: () => ({ value: {} }),
- },
- },
- },
- async () => ({ content: [] }),
- );
- await app.connect(appTransport);
-
- expect(bridge.listTools({})).rejects.toThrow(
- /does not implement Standard JSON Schema/,
- );
- });
-
- it("returns empty list when no tools registered", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- // Register a tool to ensure handlers are initialized
- const dummyTool = app.registerTool("dummy", {}, async () => ({
- content: [],
- }));
-
- await app.connect(appTransport);
-
- // Remove the tool after connecting
- dummyTool.remove();
-
- const result = await bridge.listTools({});
-
- expect(result.tools).toEqual([]);
- });
-
- it("updates list when tools are added", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- await app.connect(appTransport);
-
- // Register then remove a tool to initialize handlers
- const dummy = app.registerTool("init", {}, async () => ({
- content: [],
- }));
- dummy.remove();
-
- // Initially no tools
- let result = await bridge.listTools({});
- expect(result.tools).toEqual([]);
-
- // Add a tool
- app.registerTool("new-tool", {}, async (_args: any) => ({
- content: [],
- }));
-
- // Should now include the new tool
- result = await bridge.listTools({});
- expect(result.tools.map((t) => t.name)).toEqual(["new-tool"]);
-
- // Add another tool
- app.registerTool("another-tool", {}, async (_args: any) => ({
- content: [],
- }));
-
- // Should now include both tools
- result = await bridge.listTools({});
- expect(result.tools).toHaveLength(2);
- expect(result.tools.map((t) => t.name)).toContain("new-tool");
- expect(result.tools.map((t) => t.name)).toContain("another-tool");
- });
-
- it("updates list when tools are removed", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- const tool1 = app.registerTool("tool1", {}, async (_args: any) => ({
- content: [],
- }));
- const tool2 = app.registerTool("tool2", {}, async (_args: any) => ({
- content: [],
- }));
- app.registerTool("tool3", {}, async (_args: any) => ({
- content: [],
- }));
-
- await app.connect(appTransport);
-
- // Initially all three tools
- let result = await bridge.listTools({});
- expect(result.tools).toHaveLength(3);
-
- // Remove one tool
- tool2.remove();
-
- // Should now have two tools
- result = await bridge.listTools({});
- expect(result.tools).toHaveLength(2);
- expect(result.tools.map((t) => t.name)).toContain("tool1");
- expect(result.tools.map((t) => t.name)).toContain("tool3");
- expect(result.tools.map((t) => t.name)).not.toContain("tool2");
-
- // Remove another tool
- tool1.remove();
-
- // Should now have one tool
- result = await bridge.listTools({});
- expect(result.tools.map((t) => t.name)).toEqual(["tool3"]);
- });
-
- it("only includes enabled tools in list", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- app.registerTool("enabled-tool", {}, async (_args: any) => ({
- content: [],
- }));
- const tool2 = app.registerTool(
- "disabled-tool",
- {},
- async (_args: any) => ({
- content: [],
- }),
- );
-
- await app.connect(appTransport);
-
- // Disable one tool after connecting
- tool2.disable();
-
- const result = await bridge.listTools({});
-
- // Only enabled tool should be in the list
- expect(result.tools).toHaveLength(1);
- expect(result.tools.map((t) => t.name)).toContain("enabled-tool");
- expect(result.tools.map((t) => t.name)).not.toContain("disabled-tool");
- });
- });
-
- describe("Integration: automatic handlers with tool lifecycle", () => {
- it("handles complete tool lifecycle: register -> call -> update -> call -> remove", async () => {
- const appCapabilities = { tools: { listChanged: true } };
- app = new App(testAppInfo, appCapabilities, { autoResize: false });
-
- await app.connect(appTransport);
-
- // Register tool
- const tool = app.registerTool(
- "counter",
- {
- description: "A counter tool",
- },
- async (_args: any) => ({
- content: [{ type: "text" as const, text: "Count: 1" }],
- structuredContent: { count: 1 },
- }),
- );
-
- // List should include the tool
- let listResult = await bridge.listTools({});
- expect(listResult.tools.map((t) => t.name)).toContain("counter");
-
- // Call the tool
- let callResult = await bridge.callTool({
- name: "counter",
- arguments: {},
- });
- expect(callResult.content).toEqual([
- { type: "text", text: "Count: 1" },
- ]);
-
- // Update tool description
- tool.update({ description: "An updated counter tool" });
-
- // Should still be callable
- callResult = await bridge.callTool({
- name: "counter",
- arguments: {},
- });
- expect(callResult).toBeDefined();
-
- // Remove tool
- tool.remove();
-
- // Should no longer be in list
- listResult = await bridge.listTools({});
- expect(listResult.tools.map((t) => t.name)).not.toContain("counter");
-
- // Should no longer be callable
- expect(
- bridge.callTool({ name: "counter", arguments: {} }),
- ).rejects.toThrow("Tool counter not found");
- });
-
- it("multiple apps can have separate tool registries", async () => {
- const appCapabilities = { tools: { listChanged: true } };
-
- // Create two separate apps
- const app1 = new App(
- { name: "App1", version: "1.0.0" },
- appCapabilities,
- { autoResize: false },
- );
- const app2 = new App(
- { name: "App2", version: "1.0.0" },
- appCapabilities,
- { autoResize: false },
- );
-
- // Create separate transports for each app
- const [app1Transport, bridge1Transport] =
- InMemoryTransport.createLinkedPair();
- const [app2Transport, bridge2Transport] =
- InMemoryTransport.createLinkedPair();
-
- const bridge1 = new AppBridge(
- createMockClient() as Client,
- testHostInfo,
- testHostCapabilities,
- );
- const bridge2 = new AppBridge(
- createMockClient() as Client,
- testHostInfo,
- testHostCapabilities,
- );
-
- // Register different tools in each app
- app1.registerTool("app1-tool", {}, async (_args: any) => ({
- content: [{ type: "text" as const, text: "From App1" }],
- }));
-
- app2.registerTool("app2-tool", {}, async (_args: any) => ({
- content: [{ type: "text" as const, text: "From App2" }],
- }));
-
- await bridge1.connect(bridge1Transport);
- await bridge2.connect(bridge2Transport);
- await app1.connect(app1Transport);
- await app2.connect(app2Transport);
-
- // Each app should only see its own tools
- const list1 = await bridge1.listTools({});
- expect(list1.tools.map((t) => t.name)).toEqual(["app1-tool"]);
-
- const list2 = await bridge2.listTools({});
- expect(list2.tools.map((t) => t.name)).toEqual(["app2-tool"]);
-
- // Each app should only be able to call its own tools
- expect(
- bridge1.callTool({ name: "app1-tool", arguments: {} }),
- ).resolves.toBeDefined();
-
- expect(
- bridge1.callTool({ name: "app2-tool", arguments: {} }),
- ).rejects.toThrow("Tool app2-tool not found");
-
- // Clean up
- await app1Transport.close();
- await bridge1Transport.close();
- await app2Transport.close();
- await bridge2Transport.close();
- });
- });
- });
-
describe("AppBridge without MCP client (manual handlers)", () => {
let app: App;
let bridge: AppBridge;
@@ -1871,43 +737,6 @@ describe("App <-> AppBridge integration", () => {
expect(result.content).toEqual(resultContent);
});
- it("oncreatesamplingmessage setter registers handler for sampling/createMessage requests", async () => {
- // Re-create bridge with sampling capability so App's capability check passes
- bridge = new AppBridge(null, testHostInfo, {
- ...testHostCapabilities,
- sampling: { tools: {} },
- });
-
- const receivedParams: unknown[] = [];
- bridge.oncreatesamplingmessage = async (params) => {
- receivedParams.push(params);
- return {
- role: "assistant",
- content: { type: "text", text: "Hello from the model" },
- model: "test-model",
- stopReason: "endTurn",
- };
- };
-
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- expect(app.getHostCapabilities()?.sampling?.tools).toEqual({});
-
- const result = await app.createSamplingMessage({
- messages: [{ role: "user", content: { type: "text", text: "Hi" } }],
- maxTokens: 50,
- });
-
- expect(receivedParams).toHaveLength(1);
- expect(receivedParams[0]).toMatchObject({ maxTokens: 50 });
- expect(result.model).toEqual("test-model");
- expect(result.content).toEqual({
- type: "text",
- text: "Hello from the model",
- });
- });
-
it("ondownloadfile setter registers handler for ui/download-file requests", async () => {
const downloadParams = {
contents: [
@@ -1942,7 +771,7 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
- expect(
+ await expect(
// @ts-expect-error intentionally testing wrong usage
app.callServerTool("my_tool"),
).rejects.toThrow(
@@ -1951,269 +780,6 @@ describe("App <-> AppBridge integration", () => {
);
});
- describe("pre-handshake guard (claude-ai-mcp#149)", () => {
- const guardMsg =
- /called before connect\(\) completed the ui\/initialize handshake/;
- let warnSpy: ReturnType>;
-
- beforeEach(() => {
- warnSpy = spyOn(console, "warn").mockImplementation(() => {});
- });
- afterEach(() => {
- warnSpy.mockRestore();
- });
-
- const warnings = () => warnSpy.mock.calls.map((c) => String(c[0]));
-
- it("callServerTool warns when called before connect() completes", async () => {
- bridge.oncalltool = async () => ({ content: [] });
- await bridge.connect(bridgeTransport);
-
- const connecting = app.connect(appTransport);
- // Handshake is in flight; _initializedSent is still false.
- await app.callServerTool({ name: "t", arguments: {} }).catch(() => {});
-
- const appSide = warnings().filter((m) => guardMsg.test(m));
- expect(appSide).toHaveLength(1);
- expect(appSide[0]).toContain("App.callServerTool()");
-
- await connecting;
- });
-
- it("callServerTool does not warn after connect() resolves", async () => {
- bridge.oncalltool = async () => ({ content: [] });
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- await app.callServerTool({ name: "t", arguments: {} });
-
- expect(warnings()).toEqual([]);
- });
-
- it("sendMessage and readServerResource also warn before handshake", async () => {
- await app.sendMessage({ role: "user", content: [] }).catch(() => {});
- await app.readServerResource({ uri: "test://r" }).catch(() => {});
-
- const appSide = warnings().filter((m) => guardMsg.test(m));
- expect(appSide).toHaveLength(2);
- expect(appSide[0]).toContain("App.sendMessage()");
- expect(appSide[1]).toContain("App.readServerResource()");
- });
-
- it("throws instead of warning when strict: true", async () => {
- const strictApp = new App(
- testAppInfo,
- {},
- { autoResize: false, strict: true },
- );
-
- await expect(
- strictApp.callServerTool({ name: "t", arguments: {} }),
- ).rejects.toThrow(guardMsg);
- expect(warnings()).toEqual([]);
- });
-
- describe("late handler registration", () => {
- const lateMsg =
- /handler registered after connect\(\) completed the ui\/initialize handshake/;
-
- it("warns when ontoolresult is set after connect() resolves", async () => {
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- app.ontoolresult = () => {};
-
- expect(warnings()).toHaveLength(1);
- expect(warnings()[0]).toMatch(lateMsg);
- expect(warnings()[0]).toContain('"toolresult"');
- });
-
- it("warns when addEventListener('toolinput', …) is called after connect()", async () => {
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- app.addEventListener("toolinput", () => {});
-
- expect(warnings()).toHaveLength(1);
- expect(warnings()[0]).toContain('"toolinput"');
- });
-
- it("does not warn for handlers set before connect()", async () => {
- app.ontoolinput = () => {};
- app.addEventListener("toolresult", () => {});
-
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- expect(warnings()).toEqual([]);
- });
-
- it("does not warn for hostcontextchanged (repeating event)", async () => {
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- app.onhostcontextchanged = () => {};
- app.addEventListener("hostcontextchanged", () => {});
-
- expect(warnings()).toEqual([]);
- });
-
- it("does not warn when clearing a handler (set to undefined)", async () => {
- app.ontoolinput = () => {};
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- app.ontoolinput = undefined;
-
- expect(warnings()).toEqual([]);
- });
-
- it("does not warn on re-registration when a handler existed pre-connect", async () => {
- const h = () => {};
- app.addEventListener("toolresult", h);
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- // React-style: useEffect cleanup removes, dep change re-adds.
- app.removeEventListener("toolresult", h);
- app.addEventListener("toolresult", () => {});
-
- expect(warnings().filter((m) => lateMsg.test(m))).toEqual([]);
- });
-
- it("warns once for the first late registration, not on subsequent ones", async () => {
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- app.addEventListener("toolinput", () => {});
- app.addEventListener("toolinput", () => {});
- app.addEventListener("toolinput", () => {});
-
- const late = warnings().filter((m) => lateMsg.test(m));
- expect(late).toHaveLength(1);
- expect(late[0]).toContain('"toolinput"');
- });
-
- it("tracks first-handler per event independently", async () => {
- app.addEventListener("toolinput", () => {});
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- app.addEventListener("toolinput", () => {}); // had pre-connect handler → silent
- app.addEventListener("toolresult", () => {}); // first reg, late → warns
-
- const late = warnings().filter((m) => lateMsg.test(m));
- expect(late).toHaveLength(1);
- expect(late[0]).toContain('"toolresult"');
- });
-
- it("throws instead of warning when strict: true", async () => {
- const [strictAppT, strictBridgeT] =
- InMemoryTransport.createLinkedPair();
- const strictBridge = new AppBridge(
- createMockClient() as Client,
- testHostInfo,
- testHostCapabilities,
- );
- const strictApp = new App(
- testAppInfo,
- {},
- { autoResize: false, strict: true },
- );
- // Pre-connect registration for toolcancelled — later swap must not throw.
- strictApp.addEventListener("toolcancelled", () => {});
-
- await strictBridge.connect(strictBridgeT);
- await strictApp.connect(strictAppT);
-
- expect(() => {
- strictApp.ontoolresult = () => {};
- }).toThrow(lateMsg);
- expect(() => {
- strictApp.addEventListener("toolinput", () => {});
- }).toThrow(lateMsg);
- // Swapping a handler that existed pre-connect is allowed under strict.
- expect(() => {
- strictApp.addEventListener("toolcancelled", () => {});
- }).not.toThrow();
- expect(warnings().filter((m) => lateMsg.test(m))).toEqual([]);
- });
- });
-
- it("AppBridge warns on a second ui/initialize (View double-mount)", async () => {
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
- expect(warnings()).toEqual([]);
-
- // Simulate a second View instance re-running the handshake.
- appTransport.send({
- jsonrpc: "2.0",
- id: 99,
- method: "ui/initialize",
- params: {
- protocolVersion: LATEST_PROTOCOL_VERSION,
- appInfo: testAppInfo,
- appCapabilities: {},
- },
- });
- await flush();
-
- const doubleInit = warnings().filter((m) =>
- /second ui\/initialize/.test(m),
- );
- expect(doubleInit).toHaveLength(1);
- });
-
- it("close() stops further notification delivery (StrictMode cleanup relies on this)", async () => {
- const received: unknown[] = [];
- app.addEventListener("toolresult", (r) => received.push(r));
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- await bridge.sendToolResult({
- content: [{ type: "text", text: "before" }],
- });
- expect(received).toHaveLength(1);
-
- await app.close();
-
- await bridge
- .sendToolResult({ content: [{ type: "text", text: "after" }] })
- .catch(() => {});
- expect(received).toHaveLength(1);
- });
-
- it("AppBridge warns on tools/call from a View that skipped the handshake", async () => {
- bridge.oncalltool = async () => ({ content: [] });
- await bridge.connect(bridgeTransport);
-
- // Simulate a hand-rolled View (no SDK, no handshake) sending tools/call.
- await appTransport.start();
- appTransport.send({
- jsonrpc: "2.0",
- id: 1,
- method: "tools/call",
- params: { name: "t", arguments: {} },
- });
- await flush();
-
- expect(warnings()).toHaveLength(1);
- expect(warnings()[0]).toContain(
- "received 'tools/call' before ui/notifications/initialized",
- );
- });
-
- it("AppBridge does not warn after initialized is received", async () => {
- bridge.oncalltool = async () => ({ content: [] });
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- await app.callServerTool({ name: "t", arguments: {} });
-
- expect(warnings()).toEqual([]);
- });
- });
-
it("onlistresources setter registers handler for resources/list requests", async () => {
const requestParams = {};
const resources = [{ uri: "test://resource", name: "Test" }];
@@ -2228,10 +794,10 @@ describe("App <-> AppBridge integration", () => {
await app.connect(appTransport);
// App sends resources/list request via the protocol's request method
- const result = await app.request({
- method: "resources/list",
- params: requestParams,
- });
+ const result = await app.request(
+ { method: "resources/list", params: requestParams },
+ ListResourcesResultSchema,
+ );
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams);
@@ -2272,10 +838,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
- const result = await app.request({
- method: "resources/read",
- params: requestParams,
- });
+ const result = await app.request(
+ { method: "resources/read", params: requestParams },
+ ReadResourceResultSchema,
+ );
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams);
@@ -2323,10 +889,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
- const result = await app.request({
- method: "resources/templates/list",
- params: requestParams,
- });
+ const result = await app.request(
+ { method: "resources/templates/list", params: requestParams },
+ ListResourceTemplatesResultSchema,
+ );
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams);
@@ -2346,10 +912,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
- const result = await app.request({
- method: "prompts/list",
- params: requestParams,
- });
+ const result = await app.request(
+ { method: "prompts/list", params: requestParams },
+ ListPromptsResultSchema,
+ );
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams);
@@ -2358,13 +924,9 @@ describe("App <-> AppBridge integration", () => {
it("sendToolListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
- bridge.oncalltool = async () => ({ content: [] });
- app.setNotificationHandler(
- "notifications/tools/list_changed",
- (notification) => {
- receivedNotifications.push(notification.params);
- },
- );
+ app.setNotificationHandler(ToolListChangedNotificationSchema, (n) => {
+ receivedNotifications.push(n.params);
+ });
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@@ -2377,13 +939,9 @@ describe("App <-> AppBridge integration", () => {
it("sendResourceListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
- bridge.onlistresources = async () => ({ resources: [] });
- app.setNotificationHandler(
- "notifications/resources/list_changed",
- (notification) => {
- receivedNotifications.push(notification.params);
- },
- );
+ app.setNotificationHandler(ResourceListChangedNotificationSchema, (n) => {
+ receivedNotifications.push(n.params);
+ });
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@@ -2396,13 +954,9 @@ describe("App <-> AppBridge integration", () => {
it("sendPromptListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
- bridge.onlistprompts = async () => ({ prompts: [] });
- app.setNotificationHandler(
- "notifications/prompts/list_changed",
- (notification) => {
- receivedNotifications.push(notification.params);
- },
- );
+ app.setNotificationHandler(PromptListChangedNotificationSchema, (n) => {
+ receivedNotifications.push(n.params);
+ });
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@@ -2415,184 +969,6 @@ describe("App <-> AppBridge integration", () => {
});
});
-describe("AppBridge outer v2 Client proxy", () => {
- let outerClient: Client;
- let outerServer: Server;
- let outerClientTransport: InMemoryTransport;
- let outerServerTransport: InMemoryTransport;
- let outerInitializeCount: number;
-
- beforeEach(async () => {
- [outerClientTransport, outerServerTransport] =
- InMemoryTransport.createLinkedPair();
- outerInitializeCount = 0;
- const originalSend = outerClientTransport.send.bind(outerClientTransport);
- outerClientTransport.send = async (message, options) => {
- if ("method" in message && message.method === "initialize") {
- outerInitializeCount++;
- }
- return originalSend(message, options);
- };
-
- outerServer = new Server(
- { name: "ActualServer", version: "1.0.0" },
- {
- capabilities: {
- tools: { listChanged: true },
- resources: { listChanged: true },
- prompts: { listChanged: true },
- },
- supportedProtocolVersions: ["2025-11-25"],
- },
- );
- outerServer.setRequestHandler("tools/call", async (request) => ({
- content: [
- {
- type: "text",
- text: `outer:${request.params.name}`,
- },
- ],
- }));
-
- outerClient = new Client(
- { name: "HostOuterClient", version: "1.0.0" },
- {
- capabilities: {},
- versionNegotiation: { mode: "legacy" },
- supportedProtocolVersions: ["2025-11-25"],
- },
- );
- await outerServer.connect(outerServerTransport);
- await outerClient.connect(outerClientTransport);
- });
-
- afterEach(async () => {
- await outerClient.close().catch(() => {});
- await outerServer.close().catch(() => {});
- });
-
- it("proxies through the outer Client without reconnecting it", async () => {
- const bridge = new AppBridge(
- outerClient,
- testHostInfo,
- testHostCapabilities,
- );
- const app = new App(testAppInfo, {}, { autoResize: false });
- let [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair();
-
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
- expect(
- await app.callServerTool({ name: "first", arguments: {} }),
- ).toMatchObject({
- content: [{ type: "text", text: "outer:first" }],
- });
- expect(outerInitializeCount).toBe(1);
-
- await app.close();
- await bridge.close();
- [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair();
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- expect(
- await app.callServerTool({ name: "second", arguments: {} }),
- ).toMatchObject({
- content: [{ type: "text", text: "outer:second" }],
- });
- expect(outerInitializeCount).toBe(1);
- expect(outerClient.getServerVersion()).toMatchObject({
- name: "ActualServer",
- version: "1.0.0",
- });
-
- await app.close();
- await bridge.close();
- });
-
- it("propagates View cancellation to the actual outer Server", async () => {
- let started!: () => void;
- let aborted!: () => void;
- const startedPromise = new Promise((resolve) => {
- started = resolve;
- });
- const abortedPromise = new Promise((resolve) => {
- aborted = resolve;
- });
- outerServer.setRequestHandler("tools/call", async (_request, context) => {
- started();
- await new Promise((resolve) => {
- context.mcpReq.signal.addEventListener(
- "abort",
- () => {
- aborted();
- resolve();
- },
- { once: true },
- );
- });
- return { content: [] };
- });
-
- const bridge = new AppBridge(
- outerClient,
- testHostInfo,
- testHostCapabilities,
- );
- const app = new App(testAppInfo, {}, { autoResize: false });
- const [appTransport, bridgeTransport] =
- InMemoryTransport.createLinkedPair();
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
-
- const controller = new AbortController();
- const result = app.callServerTool(
- { name: "cancel-me", arguments: {} },
- { signal: controller.signal },
- );
- await startedPromise;
- controller.abort("test cancellation");
- await abortedPromise;
- await expect(result).rejects.toThrow();
-
- await app.close();
- await bridge.close();
- });
-
- it("forwards outer list-changed notifications to the App", async () => {
- const bridge = new AppBridge(
- outerClient,
- testHostInfo,
- testHostCapabilities,
- );
- const app = new App(testAppInfo, {}, { autoResize: false });
- const [appTransport, bridgeTransport] =
- InMemoryTransport.createLinkedPair();
- const received: string[] = [];
- app.setNotificationHandler("notifications/tools/list_changed", () => {
- received.push("tools");
- });
- app.setNotificationHandler("notifications/resources/list_changed", () => {
- received.push("resources");
- });
- app.setNotificationHandler("notifications/prompts/list_changed", () => {
- received.push("prompts");
- });
-
- await bridge.connect(bridgeTransport);
- await app.connect(appTransport);
- await outerServer.sendToolListChanged();
- await outerServer.sendResourceListChanged();
- await outerServer.sendPromptListChanged();
- await flush();
-
- expect(received).toEqual(["tools", "resources", "prompts"]);
-
- await app.close();
- await bridge.close();
- });
-});
-
describe("getToolUiResourceUri", () => {
describe("new nested format (_meta.ui.resourceUri)", () => {
it("extracts resourceUri from _meta.ui.resourceUri", () => {
@@ -2948,7 +1324,7 @@ describe("isToolVisibilityAppOnly", () => {
expect(a).toEqual([]);
});
- it("App merges hostcontext before listeners fire", async () => {
+ it("App.onEventDispatch merges hostcontext before listeners fire", async () => {
let seen: unknown;
app.addEventListener("hostcontextchanged", () => {
seen = app.getHostContext();
@@ -2993,140 +1369,32 @@ describe("isToolVisibilityAppOnly", () => {
testHostInfo,
testHostCapabilities,
);
- const params = z.object({});
- bridge2.setRequestHandler("test/method", { params }, () => ({}));
- expect(() => {
- bridge2.setRequestHandler("test/method", { params }, () => ({}));
- }).toThrow(/already registered/);
- });
-
- it("direct setRequestHandler cannot silently replace an on* host handler", () => {
- const bridge2 = new AppBridge(
- createMockClient() as Client,
- testHostInfo,
- testHostCapabilities,
+ bridge2.setRequestHandler(
+ // @ts-expect-error — exercising throw path with raw schema
+ { shape: { method: { value: "test/method" } } },
+ () => ({}),
);
- bridge2.onopenlink = async () => ({});
expect(() => {
bridge2.setRequestHandler(
- "ui/open-link",
- { params: z.object({}) },
+ // @ts-expect-error — exercising throw path with raw schema
+ { shape: { method: { value: "test/method" } } },
() => ({}),
);
}).toThrow(/already registered/);
});
it("direct setNotificationHandler throws for event-mapped methods", () => {
- const bridge2 = new AppBridge(
- createMockClient() as Client,
- testHostInfo,
- testHostCapabilities,
- );
- bridge2.onsizechange = () => {};
+ const app2 = new App(testAppInfo, {}, { autoResize: false });
+ app2.addEventListener("toolinput", () => {});
expect(() => {
- bridge2.setNotificationHandler(
- "ui/notifications/size-changed",
- { params: z.object({}) },
+ app2.setNotificationHandler(
+ // @ts-expect-error — exercising throw path with raw schema
+ {
+ shape: { method: { value: "ui/notifications/tool-input" } },
+ },
() => {},
);
}).toThrow(/already registered/);
});
-
- it("removeRequestHandler releases the method so an on* setter can re-register", () => {
- const bridge2 = new AppBridge(
- createMockClient() as Client,
- testHostInfo,
- testHostCapabilities,
- );
- bridge2.removeRequestHandler("tools/call");
- expect(() => {
- bridge2.setRequestHandler("tools/call", async () => ({ content: [] }));
- }).not.toThrow();
- expect(() => {
- bridge2.oncalltool = async () => ({ content: [] });
- }).not.toThrow();
- });
-
- it("oncreatesamplingmessage has a getter, replace semantics, and a replace warning", () => {
- const bridge2 = new AppBridge(
- createMockClient() as Client,
- testHostInfo,
- testHostCapabilities,
- );
- expect(bridge2.oncreatesamplingmessage).toBeUndefined();
- const first = async () => ({
- role: "assistant" as const,
- content: { type: "text" as const, text: "" },
- model: "m",
- });
- bridge2.oncreatesamplingmessage = first;
- expect(bridge2.oncreatesamplingmessage).toBe(first);
-
- const warn = spyOn(console, "warn").mockImplementation(() => {});
- try {
- expect(() => {
- bridge2.oncreatesamplingmessage = first;
- }).not.toThrow();
- expect(warn).toHaveBeenCalledWith(
- expect.stringContaining("oncreatesamplingmessage handler replaced"),
- );
- } finally {
- warn.mockRestore();
- }
-
- bridge2.oncreatesamplingmessage = undefined;
- expect(bridge2.oncreatesamplingmessage).toBeUndefined();
- });
- });
-});
-
-describe("buildAllowAttribute", () => {
- describe("returns empty string", () => {
- it("when permissions is undefined", () => {
- expect(buildAllowAttribute(undefined)).toBe("");
- });
-
- it("when permissions object is empty", () => {
- expect(buildAllowAttribute({})).toBe("");
- });
- });
-
- describe("returns a single permission directive", () => {
- it("when only camera is set", () => {
- expect(buildAllowAttribute({ camera: {} })).toBe("camera");
- });
-
- it("when only microphone is set", () => {
- expect(buildAllowAttribute({ microphone: {} })).toBe("microphone");
- });
-
- it("when only geolocation is set", () => {
- expect(buildAllowAttribute({ geolocation: {} })).toBe("geolocation");
- });
-
- it("when only clipboardWrite is set, maps to clipboard-write", () => {
- expect(buildAllowAttribute({ clipboardWrite: {} })).toBe(
- "clipboard-write",
- );
- });
- });
-
- describe("returns multiple directives joined with '; '", () => {
- it("when camera and microphone are set", () => {
- expect(buildAllowAttribute({ camera: {}, microphone: {} })).toBe(
- "camera; microphone",
- );
- });
-
- it("when all permissions are set", () => {
- expect(
- buildAllowAttribute({
- camera: {},
- microphone: {},
- geolocation: {},
- clipboardWrite: {},
- }),
- ).toBe("camera; microphone; geolocation; clipboard-write");
- });
});
});
diff --git a/src/app-bridge.ts b/src/app-bridge.ts
index 1ac4db4..7712587 100644
--- a/src/app-bridge.ts
+++ b/src/app-bridge.ts
@@ -1,53 +1,53 @@
+import { Client } from "@modelcontextprotocol/sdk/client/index.js";
+import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import {
- Protocol,
- type BaseContext,
- type Client,
- type ProtocolOptions,
- type CallToolRequest,
- type CallToolResult,
- type CreateMessageRequest,
- type CreateMessageResult,
- type CreateMessageResultWithTools,
- type EmptyResult,
- type Implementation,
- type JSONRPCRequest,
- type ListPromptsRequest,
- type ListPromptsResult,
- type ListResourcesRequest,
- type ListResourcesResult,
- type ListResourceTemplatesRequest,
- type ListResourceTemplatesResult,
- type ListToolsRequest,
- type LoggingMessageNotification,
- type PingRequest,
- type PromptListChangedNotification,
- type ReadResourceRequest,
- type ReadResourceResult,
- type RequestOptions,
- type ResourceListChangedNotification,
- type Result,
- type Tool,
- type ToolListChangedNotification,
- type Transport,
-} from "@modelcontextprotocol/client";
-import {
- EmptyResultSchema,
+ CallToolRequest,
+ CallToolRequestSchema,
+ CallToolResult,
+ CallToolResultSchema,
+ EmptyResult,
+ Implementation,
+ ListPromptsRequest,
+ ListPromptsRequestSchema,
+ ListPromptsResult,
+ ListPromptsResultSchema,
+ ListResourcesRequest,
+ ListResourcesRequestSchema,
+ ListResourcesResult,
+ ListResourcesResultSchema,
+ ListResourceTemplatesRequest,
+ ListResourceTemplatesRequestSchema,
+ ListResourceTemplatesResult,
+ ListResourceTemplatesResultSchema,
+ ListToolsRequest,
+ ListToolsRequestSchema,
+ ListToolsResultSchema,
+ LoggingMessageNotification,
LoggingMessageNotificationSchema,
-} from "@modelcontextprotocol/core";
-import { EventDispatcher, MethodRegistry } from "./events.js";
+ PingRequest,
+ PingRequestSchema,
+ PromptListChangedNotification,
+ PromptListChangedNotificationSchema,
+ ReadResourceRequest,
+ ReadResourceRequestSchema,
+ ReadResourceResult,
+ ReadResourceResultSchema,
+ ResourceListChangedNotification,
+ ResourceListChangedNotificationSchema,
+ Tool,
+ ToolListChangedNotification,
+ ToolListChangedNotificationSchema,
+} from "@modelcontextprotocol/sdk/types.js";
import {
- toModernArgs,
- type LegacyNotificationHandlerSetter,
- type LegacyRequestHandlerSetter,
-} from "./legacy-handlers.js";
-import type { ZodLiteral, ZodObject, ZodType } from "zod/v4";
-
-type MethodSchema = ZodObject<{
- method: ZodLiteral;
- params: ZodType;
-}>;
+ ProtocolOptions,
+ RequestOptions,
+} from "@modelcontextprotocol/sdk/shared/protocol.js";
+import { ProtocolWithEvents } from "./events";
import {
+ type AppNotification,
+ type AppRequest,
+ type AppResult,
type McpUiSandboxResourceReadyNotification,
type McpUiSizeChangedNotification,
type McpUiToolCancelledNotification,
@@ -66,19 +66,15 @@ import {
McpUiInitializeRequest,
McpUiInitializeRequestSchema,
McpUiInitializeResult,
- McpUiInitializeResultSchema,
McpUiMessageRequest,
McpUiMessageRequestSchema,
McpUiMessageResult,
- McpUiMessageResultSchema,
McpUiOpenLinkRequest,
McpUiOpenLinkRequestSchema,
McpUiOpenLinkResult,
- McpUiOpenLinkResultSchema,
McpUiDownloadFileRequest,
McpUiDownloadFileRequestSchema,
McpUiDownloadFileResult,
- McpUiDownloadFileResultSchema,
McpUiResourceTeardownRequest,
McpUiResourceTeardownResultSchema,
McpUiRequestTeardownNotification,
@@ -89,16 +85,13 @@ import {
McpUiRequestDisplayModeRequest,
McpUiRequestDisplayModeRequestSchema,
McpUiRequestDisplayModeResult,
- McpUiRequestDisplayModeResultSchema,
McpUiResourcePermissions,
McpUiToolMeta,
-} from "./types.js";
-export * from "./types.js";
-export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants.js";
-import { RESOURCE_URI_META_KEY } from "./constants.js";
-
-type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void;
-export { PostMessageTransport } from "./message-transport.js";
+} from "./types";
+export * from "./types";
+export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app";
+import { RESOURCE_URI_META_KEY } from "./app";
+export { PostMessageTransport } from "./message-transport";
/**
* Extract UI resource URI from tool metadata.
@@ -205,10 +198,10 @@ export function buildAllowAttribute(
*
* @property hostContext - Optional initial host context to provide to the view
*
- * @see `ProtocolOptions` from @modelcontextprotocol/client for available options
+ * @see `ProtocolOptions` from @modelcontextprotocol/sdk for available options
* @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure
*/
-export type HostOptions = Omit & {
+export type HostOptions = ProtocolOptions & {
hostContext?: McpUiHostContext;
};
@@ -223,12 +216,15 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION];
/**
* Extra metadata passed to request handlers.
*
- * This type represents the additional context (`BaseContext`) provided by the
- * base MCP SDK `Protocol` when handling requests.
+ * This type represents the additional context provided by the `Protocol` class
+ * when handling requests, including abort signals and session information.
+ * It is extracted from the MCP SDK's request handler signature.
*
* @internal
*/
-type RequestHandlerExtra = BaseContext;
+type RequestHandlerExtra = Parameters<
+ Parameters[1]
+>[1];
/**
* Maps DOM-style event names to their notification `params` types.
@@ -248,11 +244,11 @@ export type AppBridgeEventMap = {
/**
* Host-side bridge for communicating with a single View ({@link app!App `App`}).
*
- * `AppBridge` extends the base MCP SDK's public `Protocol` class and acts as a
- * proxy between the host application and a view running in an iframe. When a
- * separate MCP client is provided to the constructor, it automatically forwards
- * MCP server capabilities (tools, resources, prompts) to the view. It also
- * handles the Apps initialization handshake.
+ * `AppBridge` extends the MCP SDK's `Protocol` class and acts as a proxy between
+ * the host application and a view running in an iframe. When an MCP client
+ * is provided to the constructor, it automatically forwards MCP server capabilities
+ * (tools, resources, prompts) to the view. It also handles the initialization
+ * handshake.
*
* ## Architecture
*
@@ -302,125 +298,17 @@ export type AppBridgeEventMap = {
* await bridge.connect(transport);
* ```
*/
-export class AppBridge extends Protocol {
+export class AppBridge extends ProtocolWithEvents<
+ AppRequest,
+ AppNotification,
+ AppResult,
+ AppBridgeEventMap
+> {
private _appCapabilities?: McpUiAppCapabilities;
private _hostContext: McpUiHostContext = {};
private _appInfo?: Implementation;
- private _initializedReceived = false;
- private readonly _registeredEvents = new Set();
- private readonly _events = new EventDispatcher();
- private readonly _methods = new MethodRegistry();
- // ── Handler registration with double-set protection ─────────────────
- //
- // The base SDK `Protocol` silently replaces an existing handler. The four
- // overrides below restore the v1 behaviour: a direct `setRequestHandler` /
- // `setNotificationHandler` for a method that already has a handler throws,
- // so a stray registration cannot silently replace a host's `on*` handler
- // (for example the URL allow-listing in `onopenlink`) or disconnect
- // `addEventListener` listeners. They are arrow-function class fields rather
- // than prototype methods so that `Protocol`'s constructor — which registers
- // its own ping/cancelled/progress handlers before our fields initialize —
- // hits the base implementation and skips tracking.
-
- /**
- * Registers a request handler. Throws if a handler for the same method has
- * already been registered — use the `on*` setter for replace semantics.
- *
- * @throws {Error} if a handler for this method is already registered.
- */
- override setRequestHandler: LegacyRequestHandlerSetter<
- Protocol["setRequestHandler"]
- > = (...args: unknown[]) => {
- const [method, ...rest] = toModernArgs("request", args) ?? args;
- this._methods.claim(method as string, "setRequestHandler");
- (super.setRequestHandler as unknown as UntypedHandlerSetter).call(
- this,
- method,
- ...rest,
- );
- };
-
- /**
- * Registers a notification handler. Throws if a handler for the same method
- * has already been registered — use the `on*` setter (replace semantics) or
- * `addEventListener` (multi-listener) for mapped events.
- *
- * @throws {Error} if a handler for this method is already registered.
- */
- override setNotificationHandler: LegacyNotificationHandlerSetter<
- Protocol["setNotificationHandler"]
- > = (...args: unknown[]) => {
- const [method, ...rest] = toModernArgs("notification", args) ?? args;
- this._methods.claim(method as string, "setNotificationHandler");
- (super.setNotificationHandler as unknown as UntypedHandlerSetter).call(
- this,
- method,
- ...rest,
- );
- };
-
- override removeRequestHandler: Protocol["removeRequestHandler"] =
- (method: string) => {
- this._methods.release(method);
- super.removeRequestHandler(method);
- };
-
- override removeNotificationHandler: Protocol["removeNotificationHandler"] =
- (method: string) => {
- this._methods.release(method);
- super.removeNotificationHandler(method);
- };
-
- /**
- * Register a request handler with replace semantics, bypassing the
- * double-set protection of {@link setRequestHandler `setRequestHandler`}.
- * Used by the `on*` request-handler setters.
- */
- protected replaceRequestHandler: Protocol["setRequestHandler"] =
- (method: string, ...rest: unknown[]) => {
- this._methods.replace(method);
- (super.setRequestHandler as unknown as UntypedHandlerSetter).call(
- this,
- method,
- ...rest,
- );
- };
-
- /**
- * The base MCP SDK calls this hook for every standard and custom request
- * handler registration. AppBridge uses it to preserve the existing warning
- * when a View calls host methods before `ui/notifications/initialized`,
- * without adding overload-forwarding adapters around `setRequestHandler`.
- * `ui/initialize` and `ping` are exempt because they are valid before the
- * Apps-ready gate. This wraps registered callbacks, not JSON-RPC dispatch.
- *
- * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149}
- */
- protected override _wrapHandler(
- method: string,
- handler: (request: JSONRPCRequest, context: BaseContext) => Promise,
- ): (request: JSONRPCRequest, context: BaseContext) => Promise {
- const wrapped = super._wrapHandler(method, handler);
- if (method === "ui/initialize" || method === "ping") {
- return wrapped;
- }
- return async (request, context) => {
- if (!this._initializedReceived) {
- console.warn(
- `[ext-apps] AppBridge received '${method}' before ` +
- `ui/notifications/initialized. The View is calling host ` +
- `methods before completing the handshake; it should await ` +
- `app.connect() first.`,
- );
- }
- return wrapped(request, context);
- };
- }
-
- private readonly eventSchemas: {
- [K in keyof AppBridgeEventMap]: MethodSchema;
- } = {
+ protected readonly eventSchemas = {
sizechange: McpUiSizeChangedNotificationSchema,
sandboxready: McpUiSandboxProxyReadyNotificationSchema,
initialized: McpUiInitializedNotificationSchema,
@@ -428,72 +316,6 @@ export class AppBridge extends Protocol {
loggingmessage: LoggingMessageNotificationSchema,
};
- protected setEventHandler(
- event: K,
- handler: ((params: AppBridgeEventMap[K]) => void) | undefined,
- ): void {
- this._ensureEventSlot(event);
- this._events.setHandler(event, handler);
- }
-
- protected getEventHandler(
- event: K,
- ): ((params: AppBridgeEventMap[K]) => void) | undefined {
- return this._events.getHandler(event);
- }
-
- addEventListener(
- event: K,
- handler: (params: AppBridgeEventMap[K]) => void,
- ): void {
- this._ensureEventSlot(event);
- this._events.addEventListener(event, handler);
- }
-
- removeEventListener(
- event: K,
- handler: (params: AppBridgeEventMap[K]) => void,
- ): void {
- this._events.removeEventListener(event, handler);
- }
-
- private _ensureEventSlot(event: K): void {
- if (this._registeredEvents.has(event)) return;
- const schema = this.eventSchemas[event];
- if (!schema) throw new Error(`Unknown event: ${String(event)}`);
-
- const dispatch = (params: AppBridgeEventMap[K]) => {
- if (event === "initialized") this._initializedReceived = true;
- this._events.dispatch(event, params);
- };
-
- if (event === "loggingmessage") {
- this.setNotificationHandler("notifications/message", (notification) => {
- dispatch(notification.params as AppBridgeEventMap[K]);
- });
- } else {
- this.setNotificationHandler(
- schema.shape.method.value,
- { params: schema.shape.params },
- (params) => dispatch(params as AppBridgeEventMap[K]),
- );
- }
- this._registeredEvents.add(event);
- }
-
- protected warnIfRequestHandlerReplaced(
- name: string,
- previous: unknown,
- next: unknown,
- ): void {
- if (previous && next) {
- console.warn(
- `[MCP Apps] ${name} handler replaced. ` +
- `Previous handler will no longer be called.`,
- );
- }
- }
-
/**
* Create a new AppBridge instance.
*
@@ -502,8 +324,8 @@ export class AppBridge extends Protocol {
* between the View and the server. When `null`, you must register handlers
* manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters.
* @param _hostInfo - Host application identification (name and version)
- * @param _hostCapabilities - Features and capabilities the host supports
- * @param options - Configuration options (`ProtocolOptions` from the base MCP SDK plus `hostContext`)
+ * @param _capabilities - Features and capabilities the host supports
+ * @param options - Configuration options (inherited from Protocol)
*
* @example With MCP client (automatic forwarding)
* ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient"
@@ -530,27 +352,18 @@ export class AppBridge extends Protocol {
constructor(
private _client: Client | null,
private _hostInfo: Implementation,
- private _hostCapabilities: McpUiHostCapabilities,
+ private _capabilities: McpUiHostCapabilities,
options?: HostOptions,
) {
super(options);
- this._methods.replace("notifications/cancelled");
- this._methods.replace("notifications/progress");
-
- this._ensureEventSlot("initialized");
this._hostContext = options?.hostContext || {};
- this.replaceRequestHandler(
- "ui/initialize",
- {
- params: McpUiInitializeRequestSchema.shape.params,
- result: McpUiInitializeResultSchema,
- },
- (params) => this._onAppsInitialize(params),
+ this.setRequestHandler(McpUiInitializeRequestSchema, (request) =>
+ this._oninitialize(request),
);
- this.replaceRequestHandler("ping", (request, extra) => {
+ this.setRequestHandler(PingRequestSchema, (request, extra) => {
this.onping?.(request.params, extra);
return {};
});
@@ -558,47 +371,14 @@ export class AppBridge extends Protocol {
// Default handler for requestDisplayMode - returns current mode from host context.
// Hosts can override this by setting bridge.onrequestdisplaymode = ...
this.replaceRequestHandler(
- "ui/request-display-mode",
- {
- params: McpUiRequestDisplayModeRequestSchema.shape.params,
- result: McpUiRequestDisplayModeResultSchema,
- },
- () => {
+ McpUiRequestDisplayModeRequestSchema,
+ (request) => {
const currentMode = this._hostContext.displayMode ?? "inline";
return { mode: currentMode };
},
);
}
- /** @internal */
- protected buildContext(ctx: BaseContext): BaseContext {
- return ctx;
- }
-
- /**
- * Verify that the view supports the capability required for the given request method.
- * @internal
- */
- protected assertCapabilityForMethod(_method: string): void {
- // Host-to-view requests are not capability-gated.
- }
-
- /**
- * Verify that a request handler is registered and supported for the given method.
- * @internal
- */
- protected assertRequestHandlerCapability(_method: string): void {
- // Host handler registration is not capability-gated.
- }
-
- /**
- * Verify that the host supports the capability required for the given notification method.
- * @internal
- */
- protected assertNotificationCapability(_method: string): void {
- // Host notifications are not capability-gated.
- }
-
/**
* Get the view's capabilities discovered during initialization.
*
@@ -722,7 +502,8 @@ export class AppBridge extends Protocol {
* ```typescript
* bridge.onsandboxready = async () => {
* const resource = await mcpClient.request(
- * { method: "resources/read", params: { uri: "ui://my-app" } }
+ * { method: "resources/read", params: { uri: "ui://my-app" } },
+ * ReadResourceResultSchema
* );
*
* bridge.sendSandboxResourceReady({
@@ -769,13 +550,13 @@ export class AppBridge extends Protocol {
* @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}.
*/
get oninitialized():
- | ((params?: McpUiInitializedNotification["params"]) => void)
+ | ((params: McpUiInitializedNotification["params"]) => void)
| undefined {
return this.getEventHandler("initialized");
}
set oninitialized(
callback:
- | ((params?: McpUiInitializedNotification["params"]) => void)
+ | ((params: McpUiInitializedNotification["params"]) => void)
| undefined,
) {
this.setEventHandler("initialized", callback);
@@ -833,14 +614,10 @@ export class AppBridge extends Protocol {
this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback);
this._onmessage = callback;
this.replaceRequestHandler(
- "ui/message",
- {
- params: McpUiMessageRequestSchema.shape.params,
- result: McpUiMessageResultSchema,
- },
- async (params, extra) => {
+ McpUiMessageRequestSchema,
+ async (request, extra) => {
if (!this._onmessage) throw new Error("No onmessage handler set");
- return this._onmessage(params, extra);
+ return this._onmessage(request.params, extra);
},
);
}
@@ -906,14 +683,10 @@ export class AppBridge extends Protocol {
this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback);
this._onopenlink = callback;
this.replaceRequestHandler(
- "ui/open-link",
- {
- params: McpUiOpenLinkRequestSchema.shape.params,
- result: McpUiOpenLinkResultSchema,
- },
- async (params, extra) => {
+ McpUiOpenLinkRequestSchema,
+ async (request, extra) => {
if (!this._onopenlink) throw new Error("No onopenlink handler set");
- return this._onopenlink(params, extra);
+ return this._onopenlink(request.params, extra);
},
);
}
@@ -982,15 +755,11 @@ export class AppBridge extends Protocol {
);
this._ondownloadfile = callback;
this.replaceRequestHandler(
- "ui/download-file",
- {
- params: McpUiDownloadFileRequestSchema.shape.params,
- result: McpUiDownloadFileResultSchema,
- },
- async (params, extra) => {
+ McpUiDownloadFileRequestSchema,
+ async (request, extra) => {
if (!this._ondownloadfile)
throw new Error("No ondownloadfile handler set");
- return this._ondownloadfile(params, extra);
+ return this._ondownloadfile(request.params, extra);
},
);
}
@@ -1086,15 +855,11 @@ export class AppBridge extends Protocol {
);
this._onrequestdisplaymode = callback;
this.replaceRequestHandler(
- "ui/request-display-mode",
- {
- params: McpUiRequestDisplayModeRequestSchema.shape.params,
- result: McpUiRequestDisplayModeResultSchema,
- },
- async (params, extra) => {
+ McpUiRequestDisplayModeRequestSchema,
+ async (request, extra) => {
if (!this._onrequestdisplaymode)
throw new Error("No onrequestdisplaymode handler set");
- return this._onrequestdisplaymode(params, extra);
+ return this._onrequestdisplaymode(request.params, extra);
},
);
}
@@ -1187,15 +952,11 @@ export class AppBridge extends Protocol {
);
this._onupdatemodelcontext = callback;
this.replaceRequestHandler(
- "ui/update-model-context",
- {
- params: McpUiUpdateModelContextRequestSchema.shape.params,
- result: EmptyResultSchema,
- },
- async (params, extra) => {
+ McpUiUpdateModelContextRequestSchema,
+ async (request, extra) => {
if (!this._onupdatemodelcontext)
throw new Error("No onupdatemodelcontext handler set");
- return this._onupdatemodelcontext(params, extra);
+ return this._onupdatemodelcontext(request.params, extra);
},
);
}
@@ -1217,13 +978,14 @@ export class AppBridge extends Protocol {
* bridge.oncalltool = async (params, extra) => {
* return mcpClient.request(
* { method: "tools/call", params },
- * { signal: extra.mcpReq.signal },
+ * CallToolResultSchema,
+ * { signal: extra.signal },
* );
* };
* ```
*
- * @see `CallToolRequest` from @modelcontextprotocol/client for the request type
- * @see `CallToolResult` from @modelcontextprotocol/client for the result type
+ * @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type
+ * @see `CallToolResult` from @modelcontextprotocol/sdk for the result type
*/
private _oncalltool?: (
params: CallToolRequest["params"],
@@ -1242,71 +1004,11 @@ export class AppBridge extends Protocol {
) {
this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback);
this._oncalltool = callback;
- this.replaceRequestHandler("tools/call", async (request, extra) => {
- if (!this._oncalltool) throw new Error("No oncalltool handler set");
- return this._oncalltool(request.params, extra);
- });
- }
-
- /**
- * Register a handler for LLM sampling requests from the view.
- *
- * The view sends standard MCP `sampling/createMessage` requests to obtain
- * LLM completions via the host's model connection. The host has full
- * discretion over which model to use and SHOULD apply rate limiting,
- * cost controls, and user approval (human-in-the-loop) before sampling.
- *
- * Hosts that register this handler SHOULD advertise `sampling` (and
- * `sampling.tools` if tool-calling is supported) in
- * {@link McpUiHostCapabilities `McpUiHostCapabilities`}.
- *
- * @param callback - Handler that receives `CreateMessageRequest` params and
- * returns a `CreateMessageResult` (or `CreateMessageResultWithTools` when
- * `params.tools` was provided)
- * - `params` - Standard MCP sampling params (messages, maxTokens, tools, etc.)
- * - `extra` - Request metadata (abort signal, session info)
- *
- * @example Forward to your LLM provider
- * ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm"
- * bridge.oncreatesamplingmessage = async (params, extra) => {
- * // Apply rate limiting, user approval, cost controls here
- * return await myLlmProvider.complete(params, {
- * signal: extra.mcpReq.signal,
- * });
- * };
- * ```
- *
- * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type
- * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types
- */
- private _oncreatesamplingmessage?: (
- params: CreateMessageRequest["params"],
- extra: RequestHandlerExtra,
- ) => Promise;
- get oncreatesamplingmessage() {
- return this._oncreatesamplingmessage;
- }
- set oncreatesamplingmessage(
- callback:
- | ((
- params: CreateMessageRequest["params"],
- extra: RequestHandlerExtra,
- ) => Promise)
- | undefined,
- ) {
- this.warnIfRequestHandlerReplaced(
- "oncreatesamplingmessage",
- this._oncreatesamplingmessage,
- callback,
- );
- this._oncreatesamplingmessage = callback;
this.replaceRequestHandler(
- "sampling/createMessage",
+ CallToolRequestSchema,
async (request, extra) => {
- if (!this._oncreatesamplingmessage) {
- throw new Error("No oncreatesamplingmessage handler set");
- }
- return this._oncreatesamplingmessage(request.params, extra);
+ if (!this._oncalltool) throw new Error("No oncalltool handler set");
+ return this._oncalltool(request.params, extra);
},
);
}
@@ -1323,12 +1025,12 @@ export class AppBridge extends Protocol {
* @example
* ```typescript
* // In your MCP client notification handler:
- * mcpClient.setNotificationHandler("notifications/tools/list_changed", () => {
+ * mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => {
* bridge.sendToolListChanged();
* });
* ```
*
- * @see `ToolListChangedNotification` from @modelcontextprotocol/client for the notification type
+ * @see `ToolListChangedNotification` from @modelcontextprotocol/sdk for the notification type
*/
sendToolListChanged(params: ToolListChangedNotification["params"] = {}) {
return this.notification({
@@ -1354,13 +1056,14 @@ export class AppBridge extends Protocol {
* bridge.onlistresources = async (params, extra) => {
* return mcpClient.request(
* { method: "resources/list", params },
- * { signal: extra.mcpReq.signal },
+ * ListResourcesResultSchema,
+ * { signal: extra.signal },
* );
* };
* ```
*
- * @see `ListResourcesRequest` from @modelcontextprotocol/client for the request type
- * @see `ListResourcesResult` from @modelcontextprotocol/client for the result type
+ * @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type
+ * @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type
*/
private _onlistresources?: (
params: ListResourcesRequest["params"],
@@ -1383,11 +1086,14 @@ export class AppBridge extends Protocol {
callback,
);
this._onlistresources = callback;
- this.replaceRequestHandler("resources/list", async (request, extra) => {
- if (!this._onlistresources)
- throw new Error("No onlistresources handler set");
- return this._onlistresources(request.params, extra);
- });
+ this.replaceRequestHandler(
+ ListResourcesRequestSchema,
+ async (request, extra) => {
+ if (!this._onlistresources)
+ throw new Error("No onlistresources handler set");
+ return this._onlistresources(request.params, extra);
+ },
+ );
}
/**
@@ -1407,13 +1113,14 @@ export class AppBridge extends Protocol {
* bridge.onlistresourcetemplates = async (params, extra) => {
* return mcpClient.request(
* { method: "resources/templates/list", params },
- * { signal: extra.mcpReq.signal }
+ * ListResourceTemplatesResultSchema,
+ * { signal: extra.signal }
* );
* };
* ```
*
- * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client for the request type
- * @see `ListResourceTemplatesResult` from @modelcontextprotocol/client for the result type
+ * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/sdk for the request type
+ * @see `ListResourceTemplatesResult` from @modelcontextprotocol/sdk for the result type
*/
private _onlistresourcetemplates?: (
params: ListResourceTemplatesRequest["params"],
@@ -1437,7 +1144,7 @@ export class AppBridge extends Protocol {
);
this._onlistresourcetemplates = callback;
this.replaceRequestHandler(
- "resources/templates/list",
+ ListResourceTemplatesRequestSchema,
async (request, extra) => {
if (!this._onlistresourcetemplates)
throw new Error("No onlistresourcetemplates handler set");
@@ -1463,13 +1170,14 @@ export class AppBridge extends Protocol {
* bridge.onreadresource = async (params, extra) => {
* return mcpClient.request(
* { method: "resources/read", params },
- * { signal: extra.mcpReq.signal },
+ * ReadResourceResultSchema,
+ * { signal: extra.signal },
* );
* };
* ```
*
- * @see `ReadResourceRequest` from @modelcontextprotocol/client for the request type
- * @see `ReadResourceResult` from @modelcontextprotocol/client for the result type
+ * @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type
+ * @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type
*/
private _onreadresource?: (
params: ReadResourceRequest["params"],
@@ -1492,11 +1200,14 @@ export class AppBridge extends Protocol {
callback,
);
this._onreadresource = callback;
- this.replaceRequestHandler("resources/read", async (request, extra) => {
- if (!this._onreadresource)
- throw new Error("No onreadresource handler set");
- return this._onreadresource(request.params, extra);
- });
+ this.replaceRequestHandler(
+ ReadResourceRequestSchema,
+ async (request, extra) => {
+ if (!this._onreadresource)
+ throw new Error("No onreadresource handler set");
+ return this._onreadresource(request.params, extra);
+ },
+ );
}
/**
@@ -1511,12 +1222,12 @@ export class AppBridge extends Protocol {
* @example
* ```typescript
* // In your MCP client notification handler:
- * mcpClient.setNotificationHandler("notifications/resources/list_changed", () => {
+ * mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
* bridge.sendResourceListChanged();
* });
* ```
*
- * @see `ResourceListChangedNotification` from @modelcontextprotocol/client for the notification type
+ * @see `ResourceListChangedNotification` from @modelcontextprotocol/sdk for the notification type
*/
sendResourceListChanged(
params: ResourceListChangedNotification["params"] = {},
@@ -1544,13 +1255,14 @@ export class AppBridge extends Protocol {
* bridge.onlistprompts = async (params, extra) => {
* return mcpClient.request(
* { method: "prompts/list", params },
- * { signal: extra.mcpReq.signal },
+ * ListPromptsResultSchema,
+ * { signal: extra.signal },
* );
* };
* ```
*
- * @see `ListPromptsRequest` from @modelcontextprotocol/client for the request type
- * @see `ListPromptsResult` from @modelcontextprotocol/client for the result type
+ * @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type
+ * @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type
*/
private _onlistprompts?: (
params: ListPromptsRequest["params"],
@@ -1573,10 +1285,14 @@ export class AppBridge extends Protocol {
callback,
);
this._onlistprompts = callback;
- this.replaceRequestHandler("prompts/list", async (request, extra) => {
- if (!this._onlistprompts) throw new Error("No onlistprompts handler set");
- return this._onlistprompts(request.params, extra);
- });
+ this.replaceRequestHandler(
+ ListPromptsRequestSchema,
+ async (request, extra) => {
+ if (!this._onlistprompts)
+ throw new Error("No onlistprompts handler set");
+ return this._onlistprompts(request.params, extra);
+ },
+ );
}
/**
@@ -1591,12 +1307,12 @@ export class AppBridge extends Protocol {
* @example
* ```typescript
* // In your MCP client notification handler:
- * mcpClient.setNotificationHandler("notifications/prompts/list_changed", () => {
+ * mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => {
* bridge.sendPromptListChanged();
* });
* ```
*
- * @see `PromptListChangedNotification` from @modelcontextprotocol/client for the notification type
+ * @see `PromptListChangedNotification` from @modelcontextprotocol/sdk for the notification type
*/
sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) {
return this.notification({
@@ -1605,6 +1321,46 @@ export class AppBridge extends Protocol {
});
}
+ /**
+ * Verify that the guest supports the capability required for the given request method.
+ * @internal
+ */
+ assertCapabilityForMethod(method: AppRequest["method"]): void {
+ // TODO
+ }
+
+ /**
+ * Verify that a request handler is registered and supported for the given method.
+ * @internal
+ */
+ assertRequestHandlerCapability(method: AppRequest["method"]): void {
+ // TODO
+ }
+
+ /**
+ * Verify that the host supports the capability required for the given notification method.
+ * @internal
+ */
+ assertNotificationCapability(method: AppNotification["method"]): void {
+ // TODO
+ }
+
+ /**
+ * Verify that task creation is supported for the given request method.
+ * @internal
+ */
+ protected assertTaskCapability(_method: string): void {
+ throw new Error("Tasks are not supported in MCP Apps");
+ }
+
+ /**
+ * Verify that task handler is supported for the given method.
+ * @internal
+ */
+ protected assertTaskHandlerCapability(_method: string): void {
+ throw new Error("Task handlers are not supported in MCP Apps");
+ }
+
/**
* Get the host capabilities passed to the constructor.
*
@@ -1613,29 +1369,20 @@ export class AppBridge extends Protocol {
* @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure
*/
getCapabilities(): McpUiHostCapabilities {
- return this._hostCapabilities;
+ return this._capabilities;
}
/**
* Handle the ui/initialize request from the guest.
* @internal
*/
- private async _onAppsInitialize(
- params: McpUiInitializeRequest["params"],
+ private async _oninitialize(
+ request: McpUiInitializeRequest,
): Promise {
- const requestedVersion = params.protocolVersion;
+ const requestedVersion = request.params.protocolVersion;
- if (this._appInfo !== undefined) {
- console.warn(
- "[ext-apps] AppBridge received a second ui/initialize. The View may " +
- "be double-mounting (e.g. React StrictMode in dev) without closing " +
- "the previous App instance. Responding normally; the latest " +
- "appInfo/appCapabilities replace the previous values.",
- );
- }
-
- this._appCapabilities = params.appCapabilities;
- this._appInfo = params.appInfo;
+ this._appCapabilities = request.params.appCapabilities;
+ this._appInfo = request.params.appInfo;
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(
requestedVersion,
@@ -1798,10 +1545,10 @@ export class AppBridge extends Protocol {
*
* @example
* ```ts source="./app-bridge.examples.ts#AppBridge_sendToolResult_afterExecution"
- * const result = await mcpClient.request({
- * method: "tools/call",
- * params: { name: "get_weather", arguments: args },
- * });
+ * const result = await mcpClient.request(
+ * { method: "tools/call", params: { name: "get_weather", arguments: args } },
+ * CallToolResultSchema,
+ * );
* bridge.sendToolResult(result);
* ```
*
@@ -1927,7 +1674,11 @@ export class AppBridge extends Protocol {
* @returns Promise resolving to the tool call result
*/
callTool(params: CallToolRequest["params"], options?: RequestOptions) {
- return this.request({ method: "tools/call", params }, options);
+ return this.request(
+ { method: "tools/call", params },
+ CallToolResultSchema,
+ options,
+ );
}
/**
@@ -1940,7 +1691,11 @@ export class AppBridge extends Protocol {
* @returns Promise resolving to the list of tools
*/
listTools(params: ListToolsRequest["params"], options?: RequestOptions) {
- return this.request({ method: "tools/list", params }, options);
+ return this.request(
+ { method: "tools/list", params },
+ ListToolsResultSchema,
+ options,
+ );
}
/**
@@ -2003,7 +1758,6 @@ export class AppBridge extends Protocol {
"AppBridge is already connected. Call close() before connecting again.",
);
}
- this._initializedReceived = false;
if (this._client) {
// When a client was passed to the constructor, automatically forward
// MCP requests/notifications between the view and the server
@@ -2016,12 +1770,13 @@ export class AppBridge extends Protocol {
this.oncalltool = async (params, extra) => {
return this._client!.request(
{ method: "tools/call", params },
- { signal: extra.mcpReq.signal },
+ CallToolResultSchema,
+ { signal: extra.signal },
);
};
if (serverCapabilities.tools.listChanged) {
this._client.setNotificationHandler(
- "notifications/tools/list_changed",
+ ToolListChangedNotificationSchema,
(n) => this.sendToolListChanged(n.params),
);
}
@@ -2030,24 +1785,27 @@ export class AppBridge extends Protocol {
this.onlistresources = async (params, extra) => {
return this._client!.request(
{ method: "resources/list", params },
- { signal: extra.mcpReq.signal },
+ ListResourcesResultSchema,
+ { signal: extra.signal },
);
};
this.onlistresourcetemplates = async (params, extra) => {
return this._client!.request(
{ method: "resources/templates/list", params },
- { signal: extra.mcpReq.signal },
+ ListResourceTemplatesResultSchema,
+ { signal: extra.signal },
);
};
this.onreadresource = async (params, extra) => {
return this._client!.request(
{ method: "resources/read", params },
- { signal: extra.mcpReq.signal },
+ ReadResourceResultSchema,
+ { signal: extra.signal },
);
};
if (serverCapabilities.resources.listChanged) {
this._client.setNotificationHandler(
- "notifications/resources/list_changed",
+ ResourceListChangedNotificationSchema,
(n) => this.sendResourceListChanged(n.params),
);
}
@@ -2056,21 +1814,23 @@ export class AppBridge extends Protocol {
this.onlistprompts = async (params, extra) => {
return this._client!.request(
{ method: "prompts/list", params },
- { signal: extra.mcpReq.signal },
+ ListPromptsResultSchema,
+ { signal: extra.signal },
);
};
if (serverCapabilities.prompts.listChanged) {
this._client.setNotificationHandler(
- "notifications/prompts/list_changed",
+ PromptListChangedNotificationSchema,
(n) => this.sendPromptListChanged(n.params),
);
}
}
}
- // Protocol.connect only wires the transport — no MCP handshake happens on
- // the iframe channel. The View drives ui/initialize;
- // ui/notifications/initialized remains the public View-ready signal.
+ // MCP-UI specific handlers are registered by the host component
+ // after the proxy is created. The standard MCP initialization
+ // (via oninitialized callback set in constructor) handles the ready signal.
+
return super.connect(transport);
}
}
diff --git a/src/app.examples.ts b/src/app.examples.ts
index 7d475ef..a0a5820 100644
--- a/src/app.examples.ts
+++ b/src/app.examples.ts
@@ -7,8 +7,11 @@
* @module
*/
-import type { SamplingMessage, Tool } from "@modelcontextprotocol/client";
-import type { McpServer, ToolCallback } from "@modelcontextprotocol/server";
+import type { Tool } from "@modelcontextprotocol/sdk/types.js";
+import type {
+ McpServer,
+ ToolCallback,
+} from "@modelcontextprotocol/sdk/server/mcp.js";
import {
App,
PostMessageTransport,
@@ -301,21 +304,9 @@ function App_onlisttools_returnTools(app: App) {
app.onlisttools = async (params, extra) => {
return {
tools: [
- {
- name: "greet",
- description: "Greet the user",
- inputSchema: { type: "object" as const },
- },
- {
- name: "calculate",
- description: "Perform a calculation",
- inputSchema: { type: "object" as const },
- },
- {
- name: "format",
- description: "Format text",
- inputSchema: { type: "object" as const },
- },
+ { name: "greet", inputSchema: { type: "object" as const } },
+ { name: "calculate", inputSchema: { type: "object" as const } },
+ { name: "format", inputSchema: { type: "object" as const } },
],
};
};
@@ -343,54 +334,6 @@ async function App_callServerTool_fetchWeather(app: App) {
//#endregion App_callServerTool_fetchWeather
}
-/**
- * Example: Simple LLM completion via host sampling.
- */
-async function App_createSamplingMessage_simple(app: App) {
- //#region App_createSamplingMessage_simple
- const result = await app.createSamplingMessage({
- messages: [
- {
- role: "user",
- content: { type: "text", text: "Summarize this in one line." },
- },
- ],
- maxTokens: 100,
- });
- console.log(result.content);
- //#endregion App_createSamplingMessage_simple
-}
-
-/**
- * Example: Agentic loop with tools (requires host sampling.tools capability).
- */
-async function App_createSamplingMessage_withTools(
- app: App,
- messages: SamplingMessage[],
-) {
- //#region App_createSamplingMessage_withTools
- if (!app.getHostCapabilities()?.sampling?.tools) return;
-
- const result = await app.createSamplingMessage({
- messages,
- maxTokens: 1024,
- tools: [
- {
- name: "get_weather",
- description: "Get the current weather",
- inputSchema: {
- type: "object",
- properties: { city: { type: "string" } },
- },
- },
- ],
- });
- if (result.stopReason === "toolUse") {
- // result.content may be an array containing tool_use blocks
- }
- //#endregion App_createSamplingMessage_withTools
-}
-
/**
* Example: Read a video resource and play it.
*/
diff --git a/src/app.test.ts b/src/app.test.ts
deleted file mode 100644
index 8420e81..0000000
--- a/src/app.test.ts
+++ /dev/null
@@ -1,381 +0,0 @@
-import { afterEach, describe, expect, it } from "bun:test";
-import {
- InMemoryTransport,
- Server,
- type JSONRPCMessage,
-} from "@modelcontextprotocol/server";
-import { z } from "zod/v4";
-
-import { App } from "./app.js";
-import {
- LATEST_PROTOCOL_VERSION,
- McpUiInitializeRequestSchema,
- McpUiInitializeResultSchema,
- McpUiInitializedNotificationSchema,
-} from "./types.js";
-
-type ConnectedPair = {
- app: App;
- server: Server;
- sentByApp: JSONRPCMessage[];
- appsInitialized: Promise;
-};
-
-async function connectPair(
- app = new App(
- { name: "view-app", version: "1.0.0" },
- { tools: { listChanged: true } },
- { autoResize: false },
- ),
-): Promise {
- const server = new Server(
- { name: "inner-mcp-server", version: "0.0.0" },
- {
- capabilities: { resources: {}, tools: {} },
- },
- );
- const [appTransport, serverTransport] = InMemoryTransport.createLinkedPair();
- const sentByApp: JSONRPCMessage[] = [];
- const send = appTransport.send.bind(appTransport);
- appTransport.send = async (message, options) => {
- sentByApp.push(message);
- await send(message, options);
- };
-
- let resolveAppsInitialized!: () => void;
- const appsInitialized = new Promise((resolve) => {
- resolveAppsInitialized = resolve;
- });
-
- server.setRequestHandler(
- "ui/initialize",
- {
- params: McpUiInitializeRequestSchema.shape.params,
- result: McpUiInitializeResultSchema,
- },
- (params) => {
- expect(params.protocolVersion).toBe(LATEST_PROTOCOL_VERSION);
- return {
- protocolVersion: LATEST_PROTOCOL_VERSION,
- hostCapabilities: { openLinks: {} },
- hostInfo: { name: "apps-host", version: "2.0.0" },
- hostContext: { theme: "dark" as const, locale: "en-US" },
- };
- },
- );
- server.setNotificationHandler(
- "ui/notifications/initialized",
- { params: McpUiInitializedNotificationSchema.shape.params },
- () => resolveAppsInitialized(),
- );
- server.setRequestHandler("tools/call", (request) => ({
- content: [{ type: "text", text: `called ${request.params.name}` }],
- }));
- server.setRequestHandler("resources/read", (request) => ({
- contents: [{ uri: request.params.uri, text: "resource body" }],
- }));
- server.setRequestHandler("resources/list", () => ({
- resources: [{ uri: "test://resource", name: "Test resource" }],
- }));
- server.setRequestHandler("sampling/createMessage", () => ({
- model: "test-model",
- role: "assistant",
- content: { type: "text", text: "sampled" },
- }));
-
- await server.connect(serverTransport);
- await app.connect(appTransport);
- await appsInitialized;
-
- return { app, server, sentByApp, appsInitialized };
-}
-
-const connected: Array> = [];
-
-afterEach(async () => {
- await Promise.all(
- connected
- .splice(0)
- .flatMap(({ app, server }) => [app.close(), server.close()]),
- );
-});
-
-describe("App base MCP SDK v2 Protocol migration", () => {
- it("performs only the Apps handshake — no MCP initialize on the wire", async () => {
- const pair = await connectPair();
- connected.push(pair);
-
- const lifecycleMethods = pair.sentByApp
- .filter(
- (message): message is JSONRPCMessage & { method: string } =>
- "method" in message,
- )
- .map((message) => message.method)
- .filter((method) =>
- [
- "initialize",
- "notifications/initialized",
- "ui/initialize",
- "ui/notifications/initialized",
- ].includes(method),
- );
-
- // The filter above includes the core MCP lifecycle methods, so this
- // equality also proves no `initialize`/`notifications/initialized` was sent
- // — the wire contract deployed v1 hosts depend on.
- expect(lifecycleMethods).toEqual([
- "ui/initialize",
- "ui/notifications/initialized",
- ]);
- });
-
- it("uses ui/initialize as the authority for Apps host state", async () => {
- const pair = await connectPair();
- connected.push(pair);
-
- expect(pair.app.getHostVersion()).toEqual({
- name: "apps-host",
- version: "2.0.0",
- });
- expect(pair.app.getHostCapabilities()).toEqual({ openLinks: {} });
- expect(pair.app.getHostContext()).toEqual({
- theme: "dark",
- locale: "en-US",
- });
- });
-
- it("preserves registered App tools with the base MCP SDK v2 Client handler context", async () => {
- const pair = await connectPair();
- connected.push(pair);
- let receivedSignal: AbortSignal | undefined;
-
- pair.app.registerTool(
- "greet",
- { description: "Greets a person" },
- async (extra) => {
- receivedSignal = extra.mcpReq.signal;
- return { content: [{ type: "text", text: "hello" }] };
- },
- );
-
- const listed = await pair.server.request({
- method: "tools/list",
- params: {},
- });
- const result = await pair.server.request({
- method: "tools/call",
- params: { name: "greet", arguments: {} },
- });
-
- expect(listed.tools.map((tool) => tool.name)).toEqual(["greet"]);
- expect(result.content).toEqual([{ type: "text", text: "hello" }]);
- expect(receivedSignal).toBeInstanceOf(AbortSignal);
- });
-
- it("uses base MCP SDK v2 method-keyed sends for standard MCP requests", async () => {
- const pair = await connectPair();
- connected.push(pair);
-
- const toolResult = await pair.app.callServerTool({
- name: "refresh",
- arguments: {},
- });
- const resource = await pair.app.readServerResource({
- uri: "test://resource",
- });
- const resources = await pair.app.listServerResources();
- const sample = await pair.app.createSamplingMessage({
- messages: [{ role: "user", content: { type: "text", text: "hello" } }],
- maxTokens: 20,
- });
-
- expect(toolResult.content).toEqual([
- { type: "text", text: "called refresh" },
- ]);
- expect(resource.contents[0]).toEqual({
- uri: "test://resource",
- text: "resource body",
- });
- expect(resources.resources.map(({ uri }) => uri)).toEqual([
- "test://resource",
- ]);
- expect(sample.content).toEqual({ type: "text", text: "sampled" });
- });
-
- it("preserves reconnect behavior after close", async () => {
- const first = await connectPair();
- await first.app.close();
- await first.server.close();
-
- const second = await connectPair(first.app);
- connected.push(second);
-
- expect(second.app.getHostVersion()).toEqual({
- name: "apps-host",
- version: "2.0.0",
- });
- });
-
- it("preserves auto-resize setup after Apps initialization", async () => {
- const app = new App(
- { name: "view-app", version: "1.0.0" },
- {},
- { autoResize: true },
- );
- let setupCalls = 0;
- app.setupSizeChangedNotifications = () => {
- setupCalls += 1;
- return () => {};
- };
-
- const pair = await connectPair(app);
- connected.push(pair);
-
- expect(setupCalls).toBe(1);
- });
-
- it("preserves strict late-handler timing errors", async () => {
- const app = new App(
- { name: "view-app", version: "1.0.0" },
- {},
- { autoResize: false, strict: true },
- );
- const pair = await connectPair(app);
- connected.push(pair);
-
- expect(() => {
- app.ontoolinput = () => {};
- }).toThrow(/handler registered after connect/);
- });
-
- it("direct setRequestHandler throws when a handler is already registered", async () => {
- const pair = await connectPair();
- connected.push(pair);
- const params = z.object({});
- let handledBy = 0;
-
- pair.app.setRequestHandler("test/method", { params }, () => {
- handledBy = 1;
- return {};
- });
- expect(() => {
- pair.app.setRequestHandler("test/method", { params }, () => {
- handledBy = 2;
- return {};
- });
- }).toThrow(/already registered/);
-
- await pair.server.request(
- { method: "test/method", params: {} },
- z.object({}),
- );
- expect(handledBy).toBe(1);
- });
-
- it("direct setRequestHandler throws for methods owned by the App itself", async () => {
- const pair = await connectPair();
- connected.push(pair);
- expect(() => {
- pair.app.setRequestHandler("ping", () => ({}));
- }).toThrow(/already registered/);
- });
-
- it("removeRequestHandler releases the method for re-registration", async () => {
- const pair = await connectPair();
- connected.push(pair);
- const params = z.object({});
- let handledBy = 0;
-
- pair.app.setRequestHandler("test/method", { params }, () => {
- handledBy = 1;
- return {};
- });
- pair.app.removeRequestHandler("test/method");
- pair.app.setRequestHandler("test/method", { params }, () => {
- handledBy = 2;
- return {};
- });
-
- await pair.server.request(
- { method: "test/method", params: {} },
- z.object({}),
- );
- expect(handledBy).toBe(2);
- });
-
- it("direct setNotificationHandler throws when a handler is already registered", async () => {
- const pair = await connectPair();
- connected.push(pair);
- const calls: number[] = [];
- const params = z.object({});
-
- pair.app.setNotificationHandler("test/notification", { params }, () => {
- calls.push(1);
- });
- expect(() => {
- pair.app.setNotificationHandler("test/notification", { params }, () => {
- calls.push(2);
- });
- }).toThrow(/already registered/);
- await pair.server.notification({
- method: "test/notification",
- params: {},
- });
-
- expect(calls).toEqual([1]);
- });
-
- it("direct setNotificationHandler throws for event-mapped methods (listener first)", () => {
- const app = new App(
- { name: "view-app", version: "1.0.0" },
- {},
- { autoResize: false },
- );
- app.addEventListener("toolinput", () => {});
- expect(() => {
- app.setNotificationHandler(
- "ui/notifications/tool-input",
- { params: z.object({}) },
- () => {},
- );
- }).toThrow(/already registered/);
- });
-
- it("event registration throws when a direct handler already owns the method", () => {
- const app = new App(
- { name: "view-app", version: "1.0.0" },
- {},
- { autoResize: false },
- );
- app.setNotificationHandler(
- "ui/notifications/tool-input",
- { params: z.object({}) },
- () => {},
- );
- expect(() => {
- app.ontoolinput = () => {};
- }).toThrow(/already registered/);
- expect(() => {
- app.addEventListener("toolinput", () => {});
- }).toThrow(/already registered/);
- });
-
- it("on* request setters keep replace semantics", async () => {
- const pair = await connectPair();
- connected.push(pair);
- let handledBy = 0;
- pair.app.onteardown = async () => {
- handledBy = 1;
- return {};
- };
- pair.app.onteardown = async () => {
- handledBy = 2;
- return {};
- };
- await pair.server.request(
- { method: "ui/resource-teardown", params: {} },
- z.object({}),
- );
- expect(handledBy).toBe(2);
- });
-});
diff --git a/src/app.ts b/src/app.ts
index 89c6c35..f5f97b3 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,45 +1,31 @@
import {
- Protocol,
- type BaseContext,
- type ProtocolOptions,
type RequestOptions,
- type CallToolRequest,
- type CallToolResult,
- type CreateMessageRequest,
- type CreateMessageResult,
- type CreateMessageResultWithTools,
- type Implementation,
- type ListResourcesRequest,
- type ListResourcesResult,
- type ListToolsRequest,
- type ListToolsResult,
- type LoggingMessageNotification,
- type ReadResourceRequest,
- type ReadResourceResult,
- type Tool,
- type ToolAnnotations,
- type ToolListChangedNotification,
- type Transport,
-} from "@modelcontextprotocol/client";
-import { EmptyResultSchema } from "@modelcontextprotocol/core";
-export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js";
-import { EventDispatcher, MethodRegistry } from "./events.js";
-import {
- toModernArgs,
- type LegacyNotificationHandlerSetter,
- type LegacyRequestHandlerSetter,
-} from "./legacy-handlers.js";
-export type {
- LegacyMethodSchema,
- LegacyNotificationHandler,
- LegacyNotificationHandlerSetter,
- LegacyRequestHandler,
- LegacyRequestHandlerExtra,
- LegacyRequestHandlerSetter,
-} from "./legacy-handlers.js";
-export { EventDispatcher } from "./events.js";
+ ProtocolOptions,
+} from "@modelcontextprotocol/sdk/shared/protocol.js";
-import { PostMessageTransport } from "./message-transport.js";
+import {
+ CallToolRequest,
+ CallToolRequestSchema,
+ CallToolResult,
+ CallToolResultSchema,
+ EmptyResultSchema,
+ Implementation,
+ ListResourcesRequest,
+ ListResourcesResult,
+ ListResourcesResultSchema,
+ ListToolsRequest,
+ ListToolsRequestSchema,
+ ListToolsResult,
+ LoggingMessageNotification,
+ PingRequestSchema,
+ ReadResourceRequest,
+ ReadResourceResult,
+ ReadResourceResultSchema,
+} from "@modelcontextprotocol/sdk/types.js";
+import { AppNotification, AppRequest, AppResult } from "./types";
+import { ProtocolWithEvents } from "./events";
+export { ProtocolWithEvents };
+import { PostMessageTransport } from "./message-transport";
import {
LATEST_PROTOCOL_VERSION,
McpUiAppCapabilities,
@@ -60,7 +46,6 @@ import {
McpUiResourceTeardownRequest,
McpUiResourceTeardownRequestSchema,
McpUiResourceTeardownResult,
- McpUiResourceTeardownResultSchema,
McpUiRequestTeardownNotification,
McpUiSizeChangedNotification,
McpUiToolCancelledNotification,
@@ -73,60 +58,93 @@ import {
McpUiToolResultNotificationSchema,
McpUiRequestDisplayModeRequest,
McpUiRequestDisplayModeResultSchema,
-} from "./types.js";
-import {
- StandardSchemaV1,
- standardSchemaToJsonSchema,
- validateStandardSchema,
-} from "./standard-schema.js";
-import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4";
+} from "./types";
+import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
-type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void;
-
-type MethodSchema = ZodObject<{
- method: ZodLiteral;
- params: ZodType;
-}>;
-
-function mergeAppCapabilities(
- base: McpUiAppCapabilities,
- additional: McpUiAppCapabilities,
-): McpUiAppCapabilities {
- const result = { ...base, ...additional };
- if (additional.experimental !== undefined) {
- result.experimental = {
- ...base.experimental,
- ...additional.experimental,
- };
- }
- if (additional.tools !== undefined) {
- result.tools = { ...base.tools, ...additional.tools };
- }
- return result;
-}
-
-export type {
- StandardSchemaV1,
- StandardSchemaWithJSON,
-} from "./standard-schema.js";
-
-export { PostMessageTransport } from "./message-transport.js";
-export * from "./types.js";
+export { PostMessageTransport } from "./message-transport";
+export * from "./types";
export {
applyHostStyleVariables,
applyHostFonts,
getDocumentTheme,
applyDocumentTheme,
-} from "./styles.js";
+} from "./styles";
+
+/**
+ * Metadata key for associating a UI resource URI with a tool.
+ *
+ * MCP servers include this key in tool definition metadata (via `tools/list`)
+ * to indicate which UI resource should be displayed when the tool is called.
+ * When hosts see a tool with this metadata, they fetch and render the
+ * corresponding {@link App `App`}.
+ *
+ * **Note**: This constant is provided for reference and backwards compatibility.
+ * Server developers should use {@link server-helpers!registerAppTool `registerAppTool`}
+ * with the `_meta.ui.resourceUri` format instead. Host developers must check both
+ * formats for compatibility.
+ *
+ * @example Modern format (server-side, not in Apps)
+ * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_modernFormat"
+ * // Preferred: Use registerAppTool with nested ui.resourceUri
+ * registerAppTool(
+ * server,
+ * "weather",
+ * {
+ * description: "Get weather forecast",
+ * _meta: {
+ * ui: { resourceUri: "ui://weather/forecast" },
+ * },
+ * },
+ * handler,
+ * );
+ * ```
+ *
+ * @example Legacy format (deprecated, for backwards compatibility)
+ * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_legacyFormat"
+ * // Deprecated: Direct use of RESOURCE_URI_META_KEY
+ * server.registerTool(
+ * "weather",
+ * {
+ * description: "Get weather forecast",
+ * _meta: {
+ * [RESOURCE_URI_META_KEY]: "ui://weather/forecast",
+ * },
+ * },
+ * handler,
+ * );
+ * ```
+ *
+ * @example How hosts check for this metadata (must support both formats)
+ * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_hostSide"
+ * // Hosts should check both modern and legacy formats
+ * const meta = tool._meta;
+ * const uiMeta = meta?.ui as McpUiToolMeta | undefined;
+ * const legacyUri = meta?.[RESOURCE_URI_META_KEY] as string | undefined;
+ * const uiUri = uiMeta?.resourceUri ?? legacyUri;
+ * if (typeof uiUri === "string" && uiUri.startsWith("ui://")) {
+ * // Fetch the resource and display the UI
+ * }
+ * ```
+ */
+export const RESOURCE_URI_META_KEY = "ui/resourceUri";
+
+/**
+ * MIME type for MCP UI resources.
+ *
+ * Identifies HTML content as an MCP App UI resource.
+ *
+ * Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources.
+ */
+export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app";
/**
* Options for configuring {@link App `App`} behavior.
*
- * Extends `ProtocolOptions` from the base MCP SDK with `App`-specific configuration.
+ * Extends `ProtocolOptions` from the MCP SDK with `App`-specific configuration.
*
- * @see `ProtocolOptions` from @modelcontextprotocol/client for inherited options
+ * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options
*/
-export type AppOptions = Omit & {
+type AppOptions = ProtocolOptions & {
/**
* Automatically report size changes to the host using `ResizeObserver`.
*
@@ -137,103 +155,18 @@ export type AppOptions = Omit & {
* @default true
*/
autoResize?: boolean;
- /**
- * Throw on detected misuse instead of logging a console warning.
- *
- * Currently this affects calling host-bound methods (e.g.
- * {@link App.callServerTool `callServerTool`}, {@link App.sendMessage `sendMessage`})
- * before {@link App.connect `connect`} has completed the `ui/initialize`
- * handshake. With `strict: false` (default) a `console.warn` is emitted;
- * with `strict: true` an `Error` is thrown.
- *
- * @remarks Throwing will become the default in a future release.
- * @default false
- */
- strict?: boolean;
- /**
- * Allow code paths that require CSP `unsafe-eval` (e.g. `new Function()`).
- *
- * Views typically run under a strict CSP without `unsafe-eval`. Zod's JIT
- * object parser uses `new Function()` and throws on the first message parse
- * under such a policy. By default (`allowUnsafeEval: false`) the
- * {@link App `App`} constructor sets `z.config({ jitless: true })` so the
- * SDK works out of the box under the spec's default CSP. Set
- * `allowUnsafeEval: true` to skip that and keep the faster JIT path when
- * the host's CSP permits `unsafe-eval`.
- *
- * @default false
- */
- allowUnsafeEval?: boolean;
};
-type RequestHandlerExtra = BaseContext;
-
-/**
- * Result of an app-registered tool callback. When `Out` is provided,
- * `structuredContent` is required and typed (unless `isError: true`).
- */
-export type AppToolResult<
- Out extends StandardSchemaV1 | undefined = undefined,
-> = Out extends StandardSchemaV1
- ?
- | (CallToolResult & {
- structuredContent: StandardSchemaV1.InferOutput;
- isError?: false;
- })
- | (CallToolResult & { isError: true })
- : CallToolResult;
-
-/**
- * Callback for an app-registered tool. When `In` is provided, `args` is the
- * validated/parsed input; when `In` is `undefined`, the callback receives only
- * `extra`. When `Out` is provided, the return's `structuredContent` is typed.
- *
- * Mirrors `ToolCallback` from `@modelcontextprotocol/server` but is
- * parameterized over {@link StandardSchemaV1} instead of zod, so any
- * Standard-Schema-compatible library (Zod, ArkType, Valibot, …) can be used.
- */
-export type AppToolCallback<
- In extends StandardSchemaV1 | undefined = undefined,
- Out extends StandardSchemaV1 | undefined = undefined,
-> = In extends StandardSchemaV1
- ? (
- args: StandardSchemaV1.InferOutput,
- extra: RequestHandlerExtra,
- ) => AppToolResult | Promise>
- : (
- extra: RequestHandlerExtra,
- ) => AppToolResult | Promise>;
-
-/**
- * Handle returned by {@link App.registerTool}. Mirrors `RegisteredTool` from
- * `@modelcontextprotocol/server` but stores
- * {@link StandardSchemaV1} schemas.
- */
-export type RegisteredAppTool = {
- title?: string;
- description?: string;
- inputSchema?: StandardSchemaV1;
- outputSchema?: StandardSchemaV1;
- annotations?: ToolAnnotations;
- _meta?: Record;
- enabled: boolean;
- enable(): void;
- disable(): void;
- remove(): void;
- update(updates: Partial>): void;
- /** @internal */
- handler: (
- args: unknown,
- extra: RequestHandlerExtra,
- ) => Promise;
-};
+type RequestHandlerExtra = Parameters<
+ Parameters[1]
+>[1];
/**
* Maps DOM-style event names to their notification `params` types.
*
- * Used by {@link App `App`} and its composed {@link EventDispatcher} to
- * provide type-safe `addEventListener` / `removeEventListener` and singular
- * `on*` handler support.
+ * Used by {@link App `App`} (which extends {@link ProtocolWithEvents `ProtocolWithEvents`})
+ * to provide type-safe `addEventListener` / `removeEventListener` and
+ * singular `on*` handler support.
*/
export type AppEventMap = {
toolinput: McpUiToolInputNotification["params"];
@@ -247,9 +180,9 @@ export type AppEventMap = {
* Main class for MCP Apps to communicate with their host.
*
* The `App` class provides a framework-agnostic way to build interactive MCP Apps
- * that run inside host applications. It extends the base MCP SDK's public
- * `Protocol` class, composes an {@link EventDispatcher}, and adds the Apps
- * handshake, lifecycle, and high-level APIs.
+ * that run inside host applications. It extends the MCP SDK's `Protocol` class and
+ * handles the connection lifecycle, initialization handshake, and bidirectional
+ * communication with the host.
*
* ## Architecture
*
@@ -264,16 +197,16 @@ export type AppEventMap = {
* 3. **Interactive**: Send requests, receive notifications, call tools
* 4. **Teardown**: Host sends teardown request before unmounting
*
- * ## Protocol and event methods
+ * ## Inherited Methods
*
- * From the base MCP SDK's `Protocol`, `App` provides:
+ * As a subclass of {@link ProtocolWithEvents `ProtocolWithEvents`}, `App` inherits:
* - `setRequestHandler()` - Register handlers for requests from host
* - `setNotificationHandler()` - Register handlers for notifications from host
- *
- * From its composed {@link EventDispatcher}, `App` also provides:
* - `addEventListener()` - Append a listener for a notification event (multi-listener)
* - `removeEventListener()` - Remove a previously added listener
*
+ * @see {@link ProtocolWithEvents `ProtocolWithEvents`} for the DOM-model event system
+ *
* ## Notification Setters (DOM-model `on*` handlers)
*
* For common notifications, the `App` class provides getter/setter properties
@@ -302,118 +235,17 @@ export type AppEventMap = {
* await app.connect();
* ```
*/
-export class App extends Protocol {
+export class App extends ProtocolWithEvents<
+ AppRequest,
+ AppNotification,
+ AppResult,
+ AppEventMap
+> {
private _hostCapabilities?: McpUiHostCapabilities;
private _hostInfo?: Implementation;
private _hostContext?: McpUiHostContext;
- private _registeredTools: { [name: string]: RegisteredAppTool } = {};
- private _initializedSent = false;
- private readonly _registeredEvents = new Set();
- private readonly _events = new EventDispatcher();
- private readonly _methods = new MethodRegistry();
- // ── Handler registration with double-set protection ─────────────────
- //
- // The base SDK `Protocol` silently replaces an existing handler. The four
- // overrides below restore the v1 behaviour: a direct `setRequestHandler` /
- // `setNotificationHandler` for a method that already has a handler throws,
- // so a stray registration cannot silently disconnect `on*` handlers or
- // `addEventListener` listeners. They are arrow-function class fields rather
- // than prototype methods so that `Protocol`'s constructor — which registers
- // its own ping/cancelled/progress handlers before our fields initialize —
- // hits the base implementation and skips tracking.
-
- /**
- * Registers a request handler. Throws if a handler for the same method has
- * already been registered — use the `on*` setter for replace semantics.
- *
- * @throws {Error} if a handler for this method is already registered.
- */
- override setRequestHandler: LegacyRequestHandlerSetter<
- Protocol["setRequestHandler"]
- > = (...args: unknown[]) => {
- const [method, ...rest] = toModernArgs("request", args) ?? args;
- this._methods.claim(method as string, "setRequestHandler");
- (super.setRequestHandler as unknown as UntypedHandlerSetter).call(
- this,
- method,
- ...rest,
- );
- };
-
- /**
- * Registers a notification handler. Throws if a handler for the same method
- * has already been registered — use the `on*` setter (replace semantics) or
- * `addEventListener` (multi-listener) for mapped events.
- *
- * @throws {Error} if a handler for this method is already registered.
- */
- override setNotificationHandler: LegacyNotificationHandlerSetter<
- Protocol["setNotificationHandler"]
- > = (...args: unknown[]) => {
- const [method, ...rest] = toModernArgs("notification", args) ?? args;
- this._methods.claim(method as string, "setNotificationHandler");
- (super.setNotificationHandler as unknown as UntypedHandlerSetter).call(
- this,
- method,
- ...rest,
- );
- };
-
- override removeRequestHandler: Protocol["removeRequestHandler"] =
- (method: string) => {
- this._methods.release(method);
- super.removeRequestHandler(method);
- };
-
- override removeNotificationHandler: Protocol["removeNotificationHandler"] =
- (method: string) => {
- this._methods.release(method);
- super.removeNotificationHandler(method);
- };
-
- /**
- * Register a request handler with replace semantics, bypassing the
- * double-set protection of {@link setRequestHandler `setRequestHandler`}.
- * Used by the `on*` request-handler setters.
- */
- protected replaceRequestHandler: Protocol["setRequestHandler"] =
- (method: string, ...rest: unknown[]) => {
- this._methods.replace(method);
- (super.setRequestHandler as unknown as UntypedHandlerSetter).call(
- this,
- method,
- ...rest,
- );
- };
-
- /**
- * Warn if a host-bound method is called before {@link connect `connect`} has
- * completed the `ui/initialize` → `ui/notifications/initialized` handshake.
- *
- * Calling these methods early can race the handshake on strict hosts and
- * leave the iframe permanently hidden. See
- * {@link https://github.com/anthropics/claude-ai-mcp/issues/61 claude-ai-mcp#61} /
- * {@link https://github.com/anthropics/claude-ai-mcp/issues/149 #149}.
- *
- * @remarks This will become a thrown `Error` in a future minor release.
- */
- private _assertInitialized(method: string): void {
- if (this._initializedSent) return;
- const msg =
- `[ext-apps] App.${method}() called before connect() completed the ` +
- `ui/initialize handshake. Await app.connect() before calling this ` +
- `method, or move data loading to an ontoolresult handler.`;
- if (this.options?.strict) {
- throw new Error(msg);
- }
- // TODO(next-minor): make `strict: true` the default.
- console.warn(`${msg}. This will throw in a future release.`);
- }
-
- private readonly eventSchemas: {
- [K in keyof AppEventMap]: MethodSchema;
- } = {
+ protected readonly eventSchemas = {
toolinput: McpUiToolInputNotificationSchema,
toolinputpartial: McpUiToolInputPartialNotificationSchema,
toolresult: McpUiToolResultNotificationSchema,
@@ -421,106 +253,12 @@ export class App extends Protocol {
hostcontextchanged: McpUiHostContextChangedNotificationSchema,
};
- /**
- * Events the host typically sends once, shortly after the handshake.
- * Registering a handler for one of these *after* {@link connect `connect`}
- * resolves risks missing the notification entirely.
- */
- private static readonly ONE_SHOT_EVENTS: ReadonlySet =
- new Set(["toolinput", "toolinputpartial", "toolresult", "toolcancelled"]);
-
- /**
- * One-shot events that have had at least one handler registered (via `on*`
- * setter or `addEventListener`) at any point. Once an event is in this set,
- * subsequent late registrations are not flagged — only the *first* handler
- * matters for the missed-notification race, and re-registration (e.g. React
- * `useEffect` cleanup → re-add on dep change) is a legitimate pattern.
- */
- private readonly _everHadListener = new Set();
-
- /**
- * Warn (or throw under `strict`) when the *first* handler for a one-shot
- * event is registered after the `ui/initialize` → `ui/notifications/initialized`
- * handshake has completed. The host may have already fired the notification
- * by then. Subsequent registrations for the same event are not flagged.
- *
- * Mirrors {@link _assertInitialized `_assertInitialized`} (the outbound-side guard).
- */
- private _assertHandlerTiming(event: keyof AppEventMap): void {
- if (!App.ONE_SHOT_EVENTS.has(event) || this._everHadListener.has(event)) {
- return;
- }
- this._everHadListener.add(event);
- if (!this._initializedSent) return;
- const msg =
- `[ext-apps] "${String(event)}" handler registered after connect() ` +
- `completed the ui/initialize handshake. The host may have already sent ` +
- `this notification. Register handlers before calling app.connect().`;
- if (this.options?.strict) {
- throw new Error(msg);
- }
- console.warn(msg);
- }
-
- protected setEventHandler(
+ protected override onEventDispatch(
event: K,
- handler: ((params: AppEventMap[K]) => void) | undefined,
+ params: AppEventMap[K],
): void {
- if (handler) this._assertHandlerTiming(event);
- this._ensureEventSlot(event);
- this._events.setHandler(event, handler);
- }
-
- addEventListener(
- event: K,
- handler: (params: AppEventMap[K]) => void,
- ): void {
- this._assertHandlerTiming(event);
- this._ensureEventSlot(event);
- this._events.addEventListener(event, handler);
- }
-
- removeEventListener(
- event: K,
- handler: (params: AppEventMap[K]) => void,
- ): void {
- this._events.removeEventListener(event, handler);
- }
-
- private _ensureEventSlot(event: K): void {
- if (this._registeredEvents.has(event)) return;
- const schema = this.eventSchemas[event];
- if (!schema) throw new Error(`Unknown event: ${String(event)}`);
- this.setNotificationHandler(
- schema.shape.method.value,
- { params: schema.shape.params },
- (params) => {
- const eventParams = params as AppEventMap[K];
- if (event === "hostcontextchanged") {
- this._hostContext = { ...this._hostContext, ...eventParams };
- }
- this._events.dispatch(event, eventParams);
- },
- );
- this._registeredEvents.add(event);
- }
-
- protected getEventHandler(
- event: K,
- ): ((params: AppEventMap[K]) => void) | undefined {
- return this._events.getHandler(event);
- }
-
- protected warnIfRequestHandlerReplaced(
- name: string,
- previous: unknown,
- next: unknown,
- ): void {
- if (previous && next) {
- console.warn(
- `[MCP Apps] ${name} handler replaced. ` +
- `Previous handler will no longer be called.`,
- );
+ if (event === "hostcontextchanged") {
+ this._hostContext = { ...this._hostContext, ...params };
}
}
@@ -528,7 +266,7 @@ export class App extends Protocol {
* Create a new MCP App instance.
*
* @param _appInfo - App identification (name and version)
- * @param _appCapabilities - Features and capabilities this app provides
+ * @param _capabilities - Features and capabilities this app provides
* @param options - Configuration options including `autoResize` behavior
*
* @example
@@ -542,208 +280,22 @@ export class App extends Protocol {
*/
constructor(
private _appInfo: Implementation,
- private _appCapabilities: McpUiAppCapabilities = {},
+ private _capabilities: McpUiAppCapabilities = {},
private options: AppOptions = { autoResize: true },
) {
super(options);
- // Claim the handlers the base Protocol constructor installed so a direct
- // setNotificationHandler cannot silently replace them either.
- this._methods.replace("notifications/cancelled");
- this._methods.replace("notifications/progress");
- if (!options.allowUnsafeEval) {
- z.config({ jitless: true });
- }
-
- this.replaceRequestHandler("ping", (request) => {
+ this.setRequestHandler(PingRequestSchema, (request) => {
console.log("Received ping:", request.params);
return {};
});
- // Eagerly register hostcontextchanged so the owner-side context merge runs
- // even if the user never assigns a handler or adds a listener.
+ // Eagerly register the hostcontextchanged event slot so that
+ // onEventDispatch (which merges into _hostContext) fires even if the
+ // user never assigns onhostcontextchanged or calls addEventListener.
this.setEventHandler("hostcontextchanged", undefined);
}
- /** @internal */
- protected buildContext(ctx: BaseContext): BaseContext {
- return ctx;
- }
-
- private registerAppCapabilities(capabilities: McpUiAppCapabilities): void {
- if (this.transport) {
- throw new Error(
- "Cannot register capabilities after transport is established",
- );
- }
- this._appCapabilities = mergeAppCapabilities(
- this._appCapabilities,
- capabilities,
- );
- }
-
- registerTool<
- OutputArgs extends undefined | StandardSchemaV1 = undefined,
- InputArgs extends undefined | StandardSchemaV1 = undefined,
- >(
- name: string,
- config: {
- title?: string;
- description?: string;
- inputSchema?: InputArgs;
- outputSchema?: OutputArgs;
- annotations?: ToolAnnotations;
- _meta?: Record;
- },
- cb: AppToolCallback,
- ): RegisteredAppTool {
- if (this._registeredTools[name]) {
- throw new Error(`Tool ${name} is already registered`);
- }
- const app = this;
- const notify = () => {
- if (app._initializedSent && app._appCapabilities.tools?.listChanged) {
- void app.sendToolListChanged();
- }
- };
- // Arity is fixed at registration time even if inputSchema is later
- // update()d, since cb's signature can't change.
- const cbTakesArgs = config.inputSchema !== undefined;
- const registeredTool: RegisteredAppTool = {
- title: config.title,
- description: config.description,
- inputSchema: config.inputSchema,
- outputSchema: config.outputSchema,
- annotations: config.annotations,
- _meta: config._meta,
- enabled: true,
- enable(): void {
- this.enabled = true;
- notify();
- },
- disable(): void {
- this.enabled = false;
- notify();
- },
- update(updates) {
- Object.assign(this, updates);
- notify();
- },
- remove() {
- if (app._registeredTools[name] !== registeredTool) return;
- delete app._registeredTools[name];
- notify();
- },
- handler: async (rawArgs, extra) => {
- if (!registeredTool.enabled) {
- throw new Error(`Tool ${name} is disabled`);
- }
- let result: CallToolResult;
- if (cbTakesArgs) {
- const schema = registeredTool.inputSchema;
- const parsedArgs = schema
- ? await validateStandardSchema(
- schema,
- rawArgs ?? {},
- `Invalid input for tool ${name}: `,
- )
- : (rawArgs ?? {});
- result = await (
- cb as AppToolCallback
- )(parsedArgs, extra);
- } else {
- result = await (cb as AppToolCallback)(
- extra,
- );
- }
- if (registeredTool.outputSchema && !result.isError) {
- result.structuredContent = (await validateStandardSchema(
- registeredTool.outputSchema,
- result.structuredContent,
- `Invalid output for tool ${name}: `,
- )) as CallToolResult["structuredContent"];
- }
- return result;
- },
- };
-
- this._registeredTools[name] = registeredTool;
-
- // Auto-register tools capability so setRequestHandler's capability check
- // passes. Mirrors McpServer.registerTool behavior — callers shouldn't need
- // to declare { tools: {} } in the constructor just to use registerTool.
- // Only do this pre-connect; post-connect the capability was already
- // advertised (or wasn't) and can't change.
- if (!this._appCapabilities.tools && !this.transport) {
- this.registerAppCapabilities({ tools: { listChanged: true } });
- }
-
- this.ensureToolHandlersInitialized();
- notify();
- return registeredTool;
- }
-
- private _toolHandlersInitialized = false;
- private ensureToolHandlersInitialized(): void {
- if (this._toolHandlersInitialized) {
- return;
- }
- this._toolHandlersInitialized = true;
-
- this.oncalltool = async (params, extra) => {
- const tool = this._registeredTools[params.name];
- if (!tool) {
- throw new Error(`Tool ${params.name} not found`);
- }
- return tool.handler(params.arguments, extra);
- };
- this.onlisttools = async (_params, _extra) => {
- const tools: Tool[] = await Promise.all(
- Object.entries(this._registeredTools)
- .filter(([_, tool]) => tool.enabled)
- .map(async ([name, tool]) => {
- const result: Tool = {
- name,
- title: tool.title,
- description: tool.description,
- inputSchema: (tool.inputSchema
- ? await standardSchemaToJsonSchema(tool.inputSchema, "input")
- : {
- type: "object" as const,
- properties: {},
- }) as Tool["inputSchema"],
- };
- // outputSchema is optional in core MCP — only emit when the app
- // provided one, otherwise hosts would assume structuredContent.
- if (tool.outputSchema) {
- result.outputSchema = (await standardSchemaToJsonSchema(
- tool.outputSchema,
- "output",
- )) as Tool["outputSchema"];
- }
- if (tool.annotations) {
- result.annotations = tool.annotations;
- }
- if (tool._meta) {
- result._meta = tool._meta;
- }
- return result;
- }),
- );
- return { tools };
- };
- }
-
- async sendToolListChanged(
- params: ToolListChangedNotification["params"] = {},
- ): Promise {
- this._assertInitialized("sendToolListChanged");
- await this.notification({
- method: "notifications/tools/list_changed",
- params,
- });
- }
-
/**
* Get the host's capabilities discovered during initialization.
*
@@ -1007,9 +559,9 @@ export class App extends Protocol {
* without replacing.
*
* Notification params are automatically merged into the internal host context
- * before any handler or listener fires. This means
- * {@link getHostContext `getHostContext`} will return the updated values even
- * before your callback runs.
+ * via {@link onEventDispatch `onEventDispatch`} before any handler or listener
+ * fires. This means {@link getHostContext `getHostContext`} will return the
+ * updated values even before your callback runs.
*
* Register handlers before calling {@link connect `connect`} to avoid missing notifications.
*
@@ -1088,14 +640,10 @@ export class App extends Protocol {
this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback);
this._onteardown = callback;
this.replaceRequestHandler(
- "ui/resource-teardown",
- {
- params: McpUiResourceTeardownRequestSchema.shape.params,
- result: McpUiResourceTeardownResultSchema,
- },
- (params, extra) => {
+ McpUiResourceTeardownRequestSchema,
+ (request, extra) => {
if (!this._onteardown) throw new Error("No onteardown handler set");
- return this._onteardown(params, extra);
+ return this._onteardown(request.params, extra);
},
);
}
@@ -1145,7 +693,7 @@ export class App extends Protocol {
) {
this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback);
this._oncalltool = callback;
- this.replaceRequestHandler("tools/call", (request, extra) => {
+ this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => {
if (!this._oncalltool) throw new Error("No oncalltool handler set");
return this._oncalltool(request.params, extra);
});
@@ -1173,21 +721,9 @@ export class App extends Protocol