From 05b1f22cd0831ebc42a4f79802e115f2a8b991ba Mon Sep 17 00:00:00 2001 From: Den Delimarsky <53200638+localden@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:54:05 +0000 Subject: [PATCH 001/140] Add SECURITY.md with GitHub Security Advisories guidance --- SECURITY.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..5029242 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +Thank you for helping keep the Model Context Protocol and its ecosystem secure. + +## Reporting Security Issues + +If you discover a security vulnerability in this repository, please report it through +the [GitHub Security Advisory process](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) +for this repository. + +Please **do not** report security vulnerabilities through public GitHub issues, discussions, +or pull requests. + +## What to Include + +To help us triage and respond quickly, please include: + +- A description of the vulnerability +- Steps to reproduce the issue +- The potential impact +- Any suggested fixes (optional) From b4642b433871eb7c5174e286b401ad068e5c3c1f Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Tue, 24 Feb 2026 11:36:59 -0600 Subject: [PATCH 002/140] Use `apps.extensions.modelcontextprotocol.io` domain for docs URLs Update all documentation links from `modelcontextprotocol.github.io/ext-apps` to the new custom domain across README.md, docs/, and example READMEs. Add `cname` to the `peaceiris/actions-gh-pages` deploy action in `.github/workflows/docs.yml` so the CNAME file persists across deploys. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/docs.yml | 1 + README.md | 20 +++++++++---------- docs/quickstart.md | 2 +- examples/basic-server-preact/README.md | 6 +++--- examples/basic-server-react/README.md | 6 +++--- examples/basic-server-solid/README.md | 6 +++--- examples/basic-server-svelte/README.md | 6 +++--- examples/basic-server-vanillajs/README.md | 8 ++++---- examples/basic-server-vue/README.md | 6 +++--- examples/budget-allocator-server/README.md | 6 +++--- examples/cohort-heatmap-server/README.md | 6 +++--- .../customer-segmentation-server/README.md | 6 +++--- examples/integration-server/README.md | 4 ++-- examples/qr-server/README.md | 2 +- examples/scenario-modeler-server/README.md | 6 +++--- examples/shadertoy-server/README.md | 6 +++--- examples/sheet-music-server/README.md | 4 ++-- examples/system-monitor-server/README.md | 6 +++--- examples/wiki-explorer-server/README.md | 6 +++--- 19 files changed, 57 insertions(+), 56 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f6875a3..7232571 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,3 +26,4 @@ jobs: publish_dir: ./docs publish_branch: gh-pages keep_files: true + cname: apps.extensions.modelcontextprotocol.io diff --git a/README.md b/README.md index 9427f4b..73125c4 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@

Why · - Quickstart + Quickstart · - API Docs + API Docs · Spec · @@ -24,7 +24,7 @@ npm version npm downloads GitHub stars - API Documentation + API Documentation

@@ -122,7 +122,7 @@ npm install -S @modelcontextprotocol/ext-apps ``` **New here?** Start with the -[Quickstart Guide](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html) +[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html) to build your first MCP App. ## Using the SDK @@ -133,10 +133,10 @@ UI metadata. | Package | Purpose | Docs | | ------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -| `@modelcontextprotocol/ext-apps` | Build interactive Views (App class, PostMessageTransport) | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/app.html) | -| `@modelcontextprotocol/ext-apps/react` | React hooks for Views (useApp, useHostStyles, etc.) | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/_modelcontextprotocol_ext-apps_react.html) | -| `@modelcontextprotocol/ext-apps/app-bridge` | Embed and communicate with Views in your chat client | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/app-bridge.html) | -| `@modelcontextprotocol/ext-apps/server` | Register tools and resources on your MCP server | [API Docs →](https://modelcontextprotocol.github.io/ext-apps/api/modules/server.html) | +| `@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.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) @@ -243,8 +243,8 @@ point your client at a local build: ## Resources -- [Quickstart Guide](https://modelcontextprotocol.github.io/ext-apps/api/documents/Quickstart.html) -- [API Documentation](https://modelcontextprotocol.github.io/ext-apps/api/) +- [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)) - [SEP-1865 Discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865) diff --git a/docs/quickstart.md b/docs/quickstart.md index 3fb1198..c850f8c 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -472,4 +472,4 @@ You've built your first MCP App! - **Continue learning**: The [`basic-server-vanillajs`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) example builds on this quickstart with host communication, theming, and lifecycle handlers - **React version**: Compare with [`basic-server-react`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for a React-based UI - **Other frameworks**: See also [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue), [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte), [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact), and [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) examples -- **API reference**: See the full [API documentation](https://modelcontextprotocol.github.io/ext-apps/api/) +- **API reference**: See the full [API documentation](https://apps.extensions.modelcontextprotocol.io/api/) diff --git a/examples/basic-server-preact/README.md b/examples/basic-server-preact/README.md index 1f929f1..6a75ddb 100644 --- a/examples/basic-server-preact/README.md +++ b/examples/basic-server-preact/README.md @@ -47,8 +47,8 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w ## Overview - Tool registration with a linked UI resource -- Preact UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class -- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink) +- Preact UI using the [`App`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) class +- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink) ## Key Files @@ -70,4 +70,4 @@ npm run dev ## Build System -This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-react/README.md b/examples/basic-server-react/README.md index 07d235f..d2f2e0d 100644 --- a/examples/basic-server-react/README.md +++ b/examples/basic-server-react/README.md @@ -49,8 +49,8 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w ## Overview - Tool registration with a linked UI resource -- React UI using the [`useApp()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook -- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink) +- React UI using the [`useApp()`](https://apps.extensions.modelcontextprotocol.io/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook +- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink) ## Key Files @@ -72,4 +72,4 @@ npm run dev ## Build System -This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-solid/README.md b/examples/basic-server-solid/README.md index cb12859..43811a2 100644 --- a/examples/basic-server-solid/README.md +++ b/examples/basic-server-solid/README.md @@ -47,8 +47,8 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w ## Overview - Tool registration with a linked UI resource -- Solid UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class -- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink) +- Solid UI using the [`App`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) class +- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink) ## Key Files @@ -70,4 +70,4 @@ npm run dev ## Build System -This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-svelte/README.md b/examples/basic-server-svelte/README.md index aa548f1..ec6a0d2 100644 --- a/examples/basic-server-svelte/README.md +++ b/examples/basic-server-svelte/README.md @@ -47,8 +47,8 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w ## Overview - Tool registration with a linked UI resource -- Svelte 5 UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class -- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink) +- Svelte 5 UI using the [`App`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) class +- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink) ## Key Files @@ -70,4 +70,4 @@ npm run dev ## Build System -This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-vanillajs/README.md b/examples/basic-server-vanillajs/README.md index 5168f20..6445d8c 100644 --- a/examples/basic-server-vanillajs/README.md +++ b/examples/basic-server-vanillajs/README.md @@ -47,9 +47,9 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w ## Overview - Tool registration with a linked UI resource -- Vanilla JS UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class directly -- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink) -- Theme integration via [`applyDocumentTheme()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyDocumentTheme.html), [`applyHostStyleVariables()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostStyleVariables.html), and [`applyHostFonts()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/app.applyHostFonts.html) +- Vanilla JS UI using the [`App`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) class directly +- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink) +- Theme integration via [`applyDocumentTheme()`](https://apps.extensions.modelcontextprotocol.io/api/functions/app.applyDocumentTheme.html), [`applyHostStyleVariables()`](https://apps.extensions.modelcontextprotocol.io/api/functions/app.applyHostStyleVariables.html), and [`applyHostFonts()`](https://apps.extensions.modelcontextprotocol.io/api/functions/app.applyHostFonts.html) ## Key Files @@ -71,4 +71,4 @@ npm run dev ## Build System -This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/basic-server-vue/README.md b/examples/basic-server-vue/README.md index 9f4e89b..c484f5b 100644 --- a/examples/basic-server-vue/README.md +++ b/examples/basic-server-vue/README.md @@ -47,8 +47,8 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w ## Overview - Tool registration with a linked UI resource -- Vue 3 UI using the [`App`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html) class -- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink) +- Vue 3 UI using the [`App`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html) class +- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink) ## Key Files @@ -70,4 +70,4 @@ npm run dev ## Build System -This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://modelcontextprotocol.github.io/ext-apps/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. +This example bundles into a single HTML file using Vite with `vite-plugin-singlefile` — see [`vite.config.ts`](vite.config.ts). This allows all UI content to be served as a single MCP resource. Alternatively, MCP apps can load external resources by defining [`_meta.ui.csp.resourceDomains`](https://apps.extensions.modelcontextprotocol.io/api/interfaces/app.McpUiResourceCsp.html#resourcedomains) in the UI resource metadata. diff --git a/examples/budget-allocator-server/README.md b/examples/budget-allocator-server/README.md index c2a3e0d..d4da76f 100644 --- a/examples/budget-allocator-server/README.md +++ b/examples/budget-allocator-server/README.md @@ -4,9 +4,9 @@ An interactive budget allocation tool demonstrating real-time data visualization - - - + + +
InitialOver budgetTech-heavyInitialOver budgetTech-heavy
diff --git a/examples/cohort-heatmap-server/README.md b/examples/cohort-heatmap-server/README.md index e8a948b..c011933 100644 --- a/examples/cohort-heatmap-server/README.md +++ b/examples/cohort-heatmap-server/README.md @@ -4,9 +4,9 @@ A demo MCP App that displays cohort retention data as an interactive heatmap, sh - - - + + +
Initial viewHover stateLow retention hoverInitial viewHover stateLow retention hover
diff --git a/examples/customer-segmentation-server/README.md b/examples/customer-segmentation-server/README.md index da115a8..8f5cc23 100644 --- a/examples/customer-segmentation-server/README.md +++ b/examples/customer-segmentation-server/README.md @@ -4,9 +4,9 @@ A demo MCP App that displays customer data as an interactive scatter/bubble char - - - + + +
Account age vs revenueTickets, NPS, engagementAge, engagement, revenueAccount age vs revenueTickets, NPS, engagementAge, engagement, revenue
diff --git a/examples/integration-server/README.md b/examples/integration-server/README.md index ffa1d4e..d66ed74 100644 --- a/examples/integration-server/README.md +++ b/examples/integration-server/README.md @@ -7,8 +7,8 @@ An MCP App example used for E2E integration testing. This example demonstrates all App SDK communication APIs and is used by the E2E test suite to verify host-app interactions: - Tool registration with a linked UI resource -- React UI using the [`useApp()`](https://modelcontextprotocol.github.io/ext-apps/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook -- App communication APIs: [`callServerTool`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#callservertool), [`sendMessage`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendmessage), [`sendLog`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#sendlog), [`openLink`](https://modelcontextprotocol.github.io/ext-apps/api/classes/app.App.html#openlink) +- React UI using the [`useApp()`](https://apps.extensions.modelcontextprotocol.io/api/functions/_modelcontextprotocol_ext-apps_react.useApp.html) hook +- App communication APIs: [`callServerTool`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#callservertool), [`sendMessage`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendmessage), [`sendLog`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#sendlog), [`openLink`](https://apps.extensions.modelcontextprotocol.io/api/classes/app.App.html#openlink) ## Key Files diff --git a/examples/qr-server/README.md b/examples/qr-server/README.md index 9348bbc..bf792e7 100644 --- a/examples/qr-server/README.md +++ b/examples/qr-server/README.md @@ -2,7 +2,7 @@ A minimal Python MCP server that generates customizable QR codes with an interactive view UI. -![Screenshot](https://modelcontextprotocol.github.io/ext-apps/screenshots/qr-server/screenshot.png) +![Screenshot](https://apps.extensions.modelcontextprotocol.io/screenshots/qr-server/screenshot.png) ## MCP Client Configuration diff --git a/examples/scenario-modeler-server/README.md b/examples/scenario-modeler-server/README.md index c934150..8b4c18e 100644 --- a/examples/scenario-modeler-server/README.md +++ b/examples/scenario-modeler-server/README.md @@ -4,9 +4,9 @@ A React-based demo MCP App that lets users adjust SaaS business parameters and s - - - + + +
Compare toBootstrapped growthVC rocketshipCompare toBootstrapped growthVC rocketship
diff --git a/examples/shadertoy-server/README.md b/examples/shadertoy-server/README.md index bcc9f1b..e1390f6 100644 --- a/examples/shadertoy-server/README.md +++ b/examples/shadertoy-server/README.md @@ -4,9 +4,9 @@ A demo MCP App that renders [ShaderToy](https://www.shadertoy.com/)-compatible G - - - + + +
GradientKaleidoscopeKaleidoscopeGradientKaleidoscopeKaleidoscope
diff --git a/examples/sheet-music-server/README.md b/examples/sheet-music-server/README.md index 6c8b770..556b726 100644 --- a/examples/sheet-music-server/README.md +++ b/examples/sheet-music-server/README.md @@ -4,8 +4,8 @@ A demo MCP App that renders [ABC notation](https://en.wikipedia.org/wiki/ABC_not - - + +
Twinkle, Twinkle Little StarPlaying on repeatTwinkle, Twinkle Little StarPlaying on repeat
diff --git a/examples/system-monitor-server/README.md b/examples/system-monitor-server/README.md index 6b79794..a754e60 100644 --- a/examples/system-monitor-server/README.md +++ b/examples/system-monitor-server/README.md @@ -4,9 +4,9 @@ A demo MCP App that displays real-time OS metrics with a stacked area chart for - - - + + +
Initial stateCPU data accumulatedExtended CPU historyInitial stateCPU data accumulatedExtended CPU history
diff --git a/examples/wiki-explorer-server/README.md b/examples/wiki-explorer-server/README.md index 5bf4241..cc91b23 100644 --- a/examples/wiki-explorer-server/README.md +++ b/examples/wiki-explorer-server/README.md @@ -4,9 +4,9 @@ Visualizes Wikipedia link graphs using a force-directed layout. Explore how Wiki - - - + + +
ZoomedPop-upExpanded graphZoomedPop-upExpanded graph
From 627ead3805a1a880a6026c8a292e541c703679f2 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Thu, 26 Feb 2026 04:00:23 +0000 Subject: [PATCH 003/140] fix examples servers' npm run start:stdio (#507) * fix examples servers' npm run start:stdio * rm mistakenly added file --- examples/budget-allocator-server/package.json | 4 ++-- examples/cohort-heatmap-server/package.json | 4 ++-- examples/customer-segmentation-server/package.json | 4 ++-- examples/integration-server/package.json | 4 ++-- examples/map-server/package.json | 4 ++-- examples/pdf-server/package.json | 2 ++ examples/scenario-modeler-server/package.json | 4 ++-- examples/shadertoy-server/package.json | 2 +- examples/system-monitor-server/package.json | 4 ++-- examples/threejs-server/package.json | 4 ++-- examples/wiki-explorer-server/package.json | 4 ++-- 11 files changed, 21 insertions(+), 19 deletions(-) diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index ed111c4..4c3f1d1 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -17,10 +17,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "prepublishOnly": "npm run build", "serve": "bun --watch main.ts" diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 21b560b..34f5f66 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -17,10 +17,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "prepublishOnly": "npm run build", "serve": "bun --watch main.ts" diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index fb3c6d4..e040cd9 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -17,10 +17,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "prepublishOnly": "npm run build", "serve": "bun --watch main.ts" diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 3ef3b3c..8602108 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -7,10 +7,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "serve": "bun --watch main.ts" }, diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 54037f8..17b3933 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -17,10 +17,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "prepublishOnly": "npm run build", "serve": "bun --watch main.ts" diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index beac872..56cb7c8 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node --external pdfjs-dist && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --external pdfjs-dist --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index af91923..c0f05c9 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -17,10 +17,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "prepublishOnly": "npm run build", "serve": "bun --watch main.ts" diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 7829dba..6caa5fa 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -19,7 +19,7 @@ "serve": "bun --watch main.ts", "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 0806b0b..4abc469 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -17,10 +17,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "prepublishOnly": "npm run build", "serve": "bun --watch main.ts" diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index d984170..4a6971b 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -17,10 +17,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "prepublishOnly": "npm run build", "serve": "bun --watch main.ts" diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 4eedb14..6b19632 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -17,10 +17,10 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve:http": "bun --watch main.ts", - "serve:stdio": "bun server.ts --stdio", + "serve:stdio": "bun main.ts --stdio", "start": "npm run start:http", "start:http": "cross-env NODE_ENV=development npm run build && npm run serve:http", - "start:stdio": "cross-env NODE_ENV=development npm run build && npm run serve:stdio", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve:http\"", "prepublishOnly": "npm run build", "serve": "bun --watch main.ts" From 30f79b9e3f2525a95a997fce2e5839ab0dd4da96 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Sat, 28 Feb 2026 00:42:11 +0000 Subject: [PATCH 004/140] ignore client roots by default in pdf-server (#510) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * security: ignore client roots by default in pdf-server When the pdf-server is started with --stdio, MCP clients may advertise roots that refer to directories on the *client's* file system. Because the server resolves those paths locally, accepting them by default would give the remote client access to arbitrary directories on the server's machine. This commit makes client roots opt-in via the --use-client-roots flag. Without the flag, the server logs a notice and skips roots setup entirely. The createServer() function now accepts a CreateServerOptions object with a `useClientRoots` boolean (defaults to false). https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * Enable client roots by default for HTTP, keep off for stdio HTTP mode serves a local client so roots are safe. Stdio mode may have a remote client whose roots would resolve against the server's filesystem, so roots stay off unless --use-client-roots is passed. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * fix: correct transport-aware defaults for useClientRoots stdio = local client (e.g. Claude Desktop) → auto-enable roots HTTP = remote client → ignore roots unless --use-client-roots passed The previous commit had the logic inverted. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * refactor: inline useClientRoots per transport branch Remove the intermediate effectiveUseClientRoots variable — just pass `true` in the stdio branch and the flag value in the HTTP branch. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw * docs: update README to match transport-aware client roots defaults Stdio always enables roots (client is local); HTTP ignores them by default. The previous README text had these reversed. https://claude.ai/code/session_014ohk5NMEPe8TBKpqp4ZRSw --------- Co-authored-by: Claude --- examples/pdf-server/README.md | 20 +++++++++++- examples/pdf-server/main.ts | 23 ++++++++++--- examples/pdf-server/server.test.ts | 25 ++++++++++++++ examples/pdf-server/server.ts | 52 +++++++++++++++++++++++------- 4 files changed, 103 insertions(+), 17 deletions(-) diff --git a/examples/pdf-server/README.md b/examples/pdf-server/README.md index b73622c..5b518d5 100644 --- a/examples/pdf-server/README.md +++ b/examples/pdf-server/README.md @@ -149,9 +149,27 @@ bun examples/pdf-server/main.ts ./local.pdf https://arxiv.org/pdf/2401.00001.pdf bun examples/pdf-server/main.ts --stdio ./papers/ ``` +## Security: Client Roots + +MCP clients may advertise **roots** — `file://` URIs pointing to directories on the client's file system. The server uses these to allow access to local files under those directories. + +- **Stdio mode** (`--stdio`): Client roots are **always enabled** — the client is typically on the same machine (e.g. Claude Desktop), so the roots are safe. +- **HTTP mode** (default): Client roots are **ignored** by default — the client may be remote, and its roots would be resolved against the server's filesystem. To opt in, pass `--use-client-roots`: + +```bash +# Trust that the HTTP client is local and its roots are safe +bun examples/pdf-server/main.ts --use-client-roots +``` + +When roots are ignored the server logs: + +``` +[pdf-server] Client roots are ignored (default for remote transports). Pass --use-client-roots to allow the client to expose local directories. +``` + ## Allowed Sources -- **Local files**: Must be passed as CLI arguments +- **Local files**: Must be passed as CLI arguments (or via client roots when enabled) - **Remote URLs**: arxiv.org, biorxiv.org, medrxiv.org, chemrxiv.org, zenodo.org, osf.io, hal.science, ssrn.com, and more ## Tools diff --git a/examples/pdf-server/main.ts b/examples/pdf-server/main.ts index ddf26c2..2ff4979 100644 --- a/examples/pdf-server/main.ts +++ b/examples/pdf-server/main.ts @@ -89,14 +89,21 @@ export async function startStdioServer( await createServer().connect(new StdioServerTransport()); } -function parseArgs(): { urls: string[]; stdio: boolean } { +function parseArgs(): { + urls: string[]; + stdio: boolean; + useClientRoots: boolean; +} { const args = process.argv.slice(2); const urls: string[] = []; let stdio = false; + let useClientRoots = false; for (const arg of args) { if (arg === "--stdio") { stdio = true; + } else if (arg === "--use-client-roots") { + useClientRoots = true; } else if (!arg.startsWith("-")) { // Convert local paths to file:// URLs, normalize arxiv URLs let url = arg; @@ -113,11 +120,15 @@ function parseArgs(): { urls: string[]; stdio: boolean } { } } - return { urls: urls.length > 0 ? urls : [DEFAULT_PDF], stdio }; + return { + urls: urls.length > 0 ? urls : [DEFAULT_PDF], + stdio, + useClientRoots, + }; } async function main() { - const { urls, stdio } = parseArgs(); + const { urls, stdio, useClientRoots } = parseArgs(); // Register local files in whitelist for (const url of urls) { @@ -141,9 +152,11 @@ async function main() { console.error(`[pdf-server] Ready (${urls.length} URL(s) configured)`); if (stdio) { - await startStdioServer(createServer); + // stdio → client is local (e.g. Claude Desktop), roots are safe + await startStdioServer(() => createServer({ useClientRoots: true })); } else { - await startStreamableHTTPServer(createServer); + // HTTP → client is remote, only honour roots with explicit opt-in + await startStreamableHTTPServer(() => createServer({ useClientRoots })); } } diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index dd5ac44..4ea1ecf 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; import path from "node:path"; import { createPdfCache, + createServer, validateUrl, isAncestorDir, allowedLocalFiles, @@ -421,3 +422,27 @@ describe("isAncestorDir", () => { ); }); }); + +describe("createServer useClientRoots option", () => { + it("should not set up roots handlers by default", () => { + const server = createServer(); + // When useClientRoots is false (default), oninitialized should NOT + // be overridden by our roots logic. + expect(server.server.oninitialized).toBeUndefined(); + server.close(); + }); + + it("should not set up roots handlers when useClientRoots is false", () => { + const server = createServer({ useClientRoots: false }); + expect(server.server.oninitialized).toBeUndefined(); + server.close(); + }); + + it("should set up roots handlers when useClientRoots is true", () => { + const server = createServer({ useClientRoots: true }); + // When useClientRoots is true, oninitialized should be set to + // the roots refresh handler. + expect(server.server.oninitialized).toBeFunction(); + server.close(); + }); +}); diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index ccc400f..0f835b7 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -443,19 +443,49 @@ async function refreshRoots(server: Server): Promise { // MCP Server Factory // ============================================================================= -export function createServer(): McpServer { +export interface CreateServerOptions { + /** + * Whether to honour MCP roots sent by the client. + * + * When a server is exposed over HTTP, the connecting client is + * typically remote and may advertise `roots` that refer to + * directories on the **client's** file system. Because the server + * resolves those paths locally, accepting them by default would give + * the remote client access to arbitrary directories on the + * **server's** machine. + * + * For stdio the client is typically local (e.g. Claude Desktop on the + * same machine), so roots are safe and enabled by default. + * + * Set this to `true` for HTTP only when you trust the client, or + * pass the `--use-client-roots` CLI flag. + * + * @default false + */ + useClientRoots?: boolean; +} + +export function createServer(options: CreateServerOptions = {}): McpServer { + const { useClientRoots = false } = options; const server = new McpServer({ name: "PDF Server", version: "2.0.0" }); - // Fetch roots on initialization and subscribe to changes - server.server.oninitialized = () => { - refreshRoots(server.server); - }; - server.server.setNotificationHandler( - RootsListChangedNotificationSchema, - async () => { - await refreshRoots(server.server); - }, - ); + if (useClientRoots) { + // Fetch roots on initialization and subscribe to changes + server.server.oninitialized = () => { + refreshRoots(server.server); + }; + server.server.setNotificationHandler( + RootsListChangedNotificationSchema, + async () => { + await refreshRoots(server.server); + }, + ); + } else { + console.error( + "[pdf-server] Client roots are ignored (default for remote transports). " + + "Pass --use-client-roots to allow the client to expose local directories.", + ); + } // Create session-local cache (isolated per server instance) const { readPdfRange } = createPdfCache(); From 134c71c29c8e25bdd4f21cb1401828759403f70d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 6 Mar 2026 13:16:42 +0000 Subject: [PATCH 005/140] Add MCP client configuration examples to CONTRIBUTING.md (#508) * docs: restore full MCP client JSON configs to CONTRIBUTING.md Reinstate the copy-pasteable JSON configurations for running all examples with MCP clients (npx-based and local development), which were removed from README.md in 2af02ba. Placed in CONTRIBUTING.md as a better home for contributor-facing setup instructions. https://claude.ai/code/session_014pjt7bE4ckHns5QFPkdWQp * Use start:stdio in local dev config, add missing scripts to 9 examples Update the local development JSON config in CONTRIBUTING.md to use `npm --silent run start:stdio` instead of manual bash commands. Add `serve:stdio` and `start:stdio` scripts to the 9 examples that were missing them (basic-server-*, sheet-music, transcript, video-resource, debug) to match the pattern already in place for the other 11 showcase examples. https://claude.ai/code/session_014pjt7bE4ckHns5QFPkdWQp * style: auto-fix prettier formatting --------- Co-authored-by: Claude Co-authored-by: github-actions[bot] --- CONTRIBUTING.md | 459 +++++++++++++++++++ examples/basic-server-preact/package.json | 2 + examples/basic-server-react/package.json | 2 + examples/basic-server-solid/package.json | 2 + examples/basic-server-svelte/package.json | 2 + examples/basic-server-vanillajs/package.json | 2 + examples/basic-server-vue/package.json | 2 + examples/debug-server/package.json | 2 + examples/sheet-music-server/package.json | 2 + examples/transcript-server/package.json | 2 + examples/video-resource-server/package.json | 2 + 11 files changed, 479 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3793622..2813b2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,465 @@ Or build and run examples: npm run examples:start ``` +### With MCP Clients + +To use these examples with MCP clients that support the stdio transport (such as Claude Desktop or VS Code), add this MCP server configuration to your client's settings: + +

+MCP client configuration for all examples (using stdio) + +```json +{ + "mcpServers": { + "basic-react": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-basic-react", + "--stdio" + ] + }, + "basic-vanillajs": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-basic-vanillajs", + "--stdio" + ] + }, + "basic-vue": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-basic-vue", + "--stdio" + ] + }, + "basic-svelte": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-basic-svelte", + "--stdio" + ] + }, + "basic-preact": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-basic-preact", + "--stdio" + ] + }, + "basic-solid": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-basic-solid", + "--stdio" + ] + }, + "budget-allocator": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-budget-allocator", + "--stdio" + ] + }, + "cohort-heatmap": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-cohort-heatmap", + "--stdio" + ] + }, + "customer-segmentation": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-customer-segmentation", + "--stdio" + ] + }, + "map": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-map", + "--stdio" + ] + }, + "pdf": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-pdf", + "--stdio" + ] + }, + "scenario-modeler": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-scenario-modeler", + "--stdio" + ] + }, + "shadertoy": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-shadertoy", + "--stdio" + ] + }, + "sheet-music": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-sheet-music", + "--stdio" + ] + }, + "system-monitor": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-system-monitor", + "--stdio" + ] + }, + "threejs": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-threejs", + "--stdio" + ] + }, + "transcript": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-transcript", + "--stdio" + ] + }, + "video-resource": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-video-resource", + "--stdio" + ] + }, + "wiki-explorer": { + "command": "npx", + "args": [ + "-y", + "--silent", + "--registry=https://registry.npmjs.org/", + "@modelcontextprotocol/server-wiki-explorer", + "--stdio" + ] + }, + "qr": { + "command": "uv", + "args": [ + "run", + "/path/to/ext-apps/examples/qr-server/server.py", + "--stdio" + ] + }, + "say": { + "command": "uv", + "args": [ + "run", + "--default-index", + "https://pypi.org/simple", + "https://raw.githubusercontent.com/modelcontextprotocol/ext-apps/refs/heads/main/examples/say-server/server.py", + "--stdio" + ] + } + } +} +``` + +
+ +> [!NOTE] +> The `qr` server requires cloning the repository first. See [qr-server README](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) for details. + +### Local Development + +To test local modifications with MCP clients, first clone and install the repository: + +```bash +git clone https://github.com/modelcontextprotocol/ext-apps.git +cd ext-apps +npm install +``` + +Then configure your MCP client to build and run the local server. Replace `~/code/ext-apps` with your actual clone path. + +Most example servers have a `start:stdio` script that builds and launches in stdio mode. Use `npm --silent run start:stdio` for these: + +
+MCP client configuration for local development (all examples) + +```json +{ + "mcpServers": { + "basic-react": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/basic-server-react", + "run", + "start:stdio" + ] + }, + "basic-vanillajs": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/basic-server-vanillajs", + "run", + "start:stdio" + ] + }, + "basic-vue": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/basic-server-vue", + "run", + "start:stdio" + ] + }, + "basic-svelte": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/basic-server-svelte", + "run", + "start:stdio" + ] + }, + "basic-preact": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/basic-server-preact", + "run", + "start:stdio" + ] + }, + "basic-solid": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/basic-server-solid", + "run", + "start:stdio" + ] + }, + "budget-allocator": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/budget-allocator-server", + "run", + "start:stdio" + ] + }, + "cohort-heatmap": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/cohort-heatmap-server", + "run", + "start:stdio" + ] + }, + "customer-segmentation": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/customer-segmentation-server", + "run", + "start:stdio" + ] + }, + "map": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/map-server", + "run", + "start:stdio" + ] + }, + "pdf": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/pdf-server", + "run", + "start:stdio" + ] + }, + "scenario-modeler": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/scenario-modeler-server", + "run", + "start:stdio" + ] + }, + "shadertoy": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/shadertoy-server", + "run", + "start:stdio" + ] + }, + "sheet-music": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/sheet-music-server", + "run", + "start:stdio" + ] + }, + "system-monitor": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/system-monitor-server", + "run", + "start:stdio" + ] + }, + "threejs": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/threejs-server", + "run", + "start:stdio" + ] + }, + "transcript": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/transcript-server", + "run", + "start:stdio" + ] + }, + "video-resource": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/video-resource-server", + "run", + "start:stdio" + ] + }, + "wiki-explorer": { + "command": "npm", + "args": [ + "--silent", + "--prefix", + "~/code/ext-apps/examples/wiki-explorer-server", + "run", + "start:stdio" + ] + }, + "qr": { + "command": "bash", + "args": [ + "-c", + "uv run ~/code/ext-apps/examples/qr-server/server.py --stdio" + ] + }, + "say": { + "command": "bash", + "args": [ + "-c", + "uv run --index https://pypi.org/simple ~/code/ext-apps/examples/say-server/server.py --stdio" + ] + } + } +} +``` + +
+ +This configuration rebuilds each server on launch, ensuring your local changes are picked up. + ## Testing ### Unit Tests diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 166b75f..32fb1a0 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index 0df2201..9732266 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -27,7 +27,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index b8115cb..d31b8e3 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 2f6a7b6..2f57cfc 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 44d3fdb..eb0b24a 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index e77102b..ea7e2b4 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index ec2a644..7f36b3a 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -27,7 +27,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 54ea185..d7a45a2 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index e72c109..c920285 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index cf64132..77c34a0 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -17,7 +17,9 @@ "build": "tsc --noEmit && cross-env INPUT=mcp-app.html vite build && tsc -p tsconfig.server.json && bun build server.ts --outdir dist --target node && bun build main.ts --outfile dist/index.js --target node --external \"./server.js\" --banner \"#!/usr/bin/env node\"", "watch": "cross-env INPUT=mcp-app.html vite build --watch", "serve": "bun --watch main.ts", + "serve:stdio": "bun main.ts --stdio", "start": "cross-env NODE_ENV=development npm run build && npm run serve", + "start:stdio": "cross-env NODE_ENV=development npm run build 1>&2 && npm run serve:stdio", "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"", "prepublishOnly": "npm run build" }, From 26928b862203b2f2bc0e30ed13082d3d4290199a Mon Sep 17 00:00:00 2001 From: Mel Ludowise Date: Fri, 6 Mar 2026 08:30:13 -0500 Subject: [PATCH 006/140] Fix autoResize reporting viewport height when content is taller (#525) fit-content clamps to the containing block's available size. For , that's the iframe viewport, so when content exceeds the viewport height the SDK reported the viewport height instead of the true content height, causing internal scrolling instead of iframe growth. max-content has no available-space constraint and correctly reports the intrinsic content height in both directions (shrink and grow). Width stays fit-content to preserve responsive wrapping. --- src/app.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app.ts b/src/app.ts index 8e133d6..fb952a2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1119,13 +1119,14 @@ export class App extends Protocol { scheduled = false; const html = document.documentElement; - // Measure actual content size by temporarily setting html to fit-content. - // This shrinks html to fit body (including body margins), giving us the - // true minimum size needed by the content. + // Measure actual content size by temporarily overriding html sizing. + // Width uses fit-content so content wraps at the host-provided width. + // Height uses max-content because fit-content would clamp to the viewport + // height when content is taller than the iframe, causing internal scrolling. const originalWidth = html.style.width; const originalHeight = html.style.height; html.style.width = "fit-content"; - html.style.height = "fit-content"; + html.style.height = "max-content"; const rect = html.getBoundingClientRect(); html.style.width = originalWidth; html.style.height = originalHeight; From b8a1349c071032be6e7908d5cdd238855f426b94 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Fri, 6 Mar 2026 13:44:57 +0000 Subject: [PATCH 007/140] docs: use cd-based MCP config in CONTRIBUTING local dev examples (#526) Switch local development MCP server config from `npm --prefix` to `bash -c "cd DIR && npm --silent run start:stdio"`. The --prefix form caused connection instability in Claude Desktop (reconnect loops with orphan builds competing on the same dist/). Also updated example paths from ~/code/ext-apps to ~/src/ext-apps. --- CONTRIBUTING.md | 179 +++++++++++++++++------------------------------- 1 file changed, 61 insertions(+), 118 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2813b2d..35c73ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -277,9 +277,9 @@ cd ext-apps npm install ``` -Then configure your MCP client to build and run the local server. Replace `~/code/ext-apps` with your actual clone path. +Then configure your MCP client to build and run the local server. Replace `~/src/ext-apps` with your actual clone path. -Most example servers have a `start:stdio` script that builds and launches in stdio mode. Use `npm --silent run start:stdio` for these: +Most example servers have a `start:stdio` script that builds and launches in stdio mode:
MCP client configuration for local development (all examples) @@ -288,207 +288,150 @@ Most example servers have a `start:stdio` script that builds and launches in std { "mcpServers": { "basic-react": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/basic-server-react", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/basic-server-react && npm --silent run start:stdio" ] }, "basic-vanillajs": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/basic-server-vanillajs", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/basic-server-vanillajs && npm --silent run start:stdio" ] }, "basic-vue": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/basic-server-vue", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/basic-server-vue && npm --silent run start:stdio" ] }, "basic-svelte": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/basic-server-svelte", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/basic-server-svelte && npm --silent run start:stdio" ] }, "basic-preact": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/basic-server-preact", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/basic-server-preact && npm --silent run start:stdio" ] }, "basic-solid": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/basic-server-solid", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/basic-server-solid && npm --silent run start:stdio" ] }, "budget-allocator": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/budget-allocator-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/budget-allocator-server && npm --silent run start:stdio" ] }, "cohort-heatmap": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/cohort-heatmap-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/cohort-heatmap-server && npm --silent run start:stdio" ] }, "customer-segmentation": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/customer-segmentation-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/customer-segmentation-server && npm --silent run start:stdio" ] }, "map": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/map-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/map-server && npm --silent run start:stdio" ] }, "pdf": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/pdf-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/pdf-server && npm --silent run start:stdio" ] }, "scenario-modeler": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/scenario-modeler-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/scenario-modeler-server && npm --silent run start:stdio" ] }, "shadertoy": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/shadertoy-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/shadertoy-server && npm --silent run start:stdio" ] }, "sheet-music": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/sheet-music-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/sheet-music-server && npm --silent run start:stdio" ] }, "system-monitor": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/system-monitor-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/system-monitor-server && npm --silent run start:stdio" ] }, "threejs": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/threejs-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/threejs-server && npm --silent run start:stdio" ] }, "transcript": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/transcript-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/transcript-server && npm --silent run start:stdio" ] }, "video-resource": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/video-resource-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/video-resource-server && npm --silent run start:stdio" ] }, "wiki-explorer": { - "command": "npm", + "command": "bash", "args": [ - "--silent", - "--prefix", - "~/code/ext-apps/examples/wiki-explorer-server", - "run", - "start:stdio" + "-c", + "cd ~/src/ext-apps/examples/wiki-explorer-server && npm --silent run start:stdio" ] }, "qr": { "command": "bash", "args": [ "-c", - "uv run ~/code/ext-apps/examples/qr-server/server.py --stdio" + "uv run ~/src/ext-apps/examples/qr-server/server.py --stdio" ] }, "say": { "command": "bash", "args": [ "-c", - "uv run --index https://pypi.org/simple ~/code/ext-apps/examples/say-server/server.py --stdio" + "uv run --index https://pypi.org/simple ~/src/ext-apps/examples/say-server/server.py --stdio" ] } } From 76f2de7b19ba14918e15085fdc3ae150d7c6c87d Mon Sep 17 00:00:00 2001 From: Den Delimarsky <53200638+localden@users.noreply.github.com> Date: Fri, 6 Mar 2026 05:45:15 -0800 Subject: [PATCH 008/140] Add Security documentation section and SEO improvements (#524) * Add Security docs section, SEO improvements, and doc site enhancements - Add Authorization guide covering per-server and per-tool OAuth patterns - Add CSP & CORS guide extracted from Patterns doc - Group both under a new Security section in the documentation nav - Replace CSP & CORS section in Patterns with cross-reference to new guide - Rename doc site title from package name to 'MCP Apps' - Rename 'Modules' nav section to 'API Documentation' - Ensure Security section appears before API Documentation in TOC - Add SEO plugin with JSON-LD structured data (TechArticle/WebPage) - Add per-page meta descriptions from frontmatter - Add description frontmatter to all documentation pages - Add hostedBaseUrl for sitemap.xml generation and canonical URLs - Normalize document page slugs to lowercase hyphenated format - Update compressed navigation and search index data to match new slugs * Add favicons with light/dark mode support - Download favicons from modelcontextprotocol.io (light + dark variants) - Inject favicon link tags into every page with correct relative paths - Support prefers-color-scheme media queries for light/dark mode - Include apple-touch-icon for iOS - Copy favicon assets to output directory during doc build * Fix HTML sanitization in SEO plugin with proper parser Replace regex-based HTML tag stripping with htmlparser2 for extracting plain-text descriptions from rendered pages. * Add custom theme to align with modelcontextprotocol.io styling - Import Inter and JetBrains Mono fonts (matching MCP site) - Override light mode colors: pure white backgrounds, softer gray borders - Override dark mode colors: match MCP site dark background (#0f1117) - Taller header (3.5rem) with backdrop-blur frosted glass effect - Sidebar: adjusted spacing, font size, and hover/active states - Content: increased line-height (1.6), more heading breathing room - Code blocks: JetBrains Mono, subtle border, more padding - Fix CSS load order: custom.css now loads after theme stylesheets * Align sidebar nav styling with modelcontextprotocol.io - Active item: tinted blue background with blue text (matching MCP's primary-color tinted active state) - Non-active items: muted gray text, subtle hover background - Larger border-radius (0.75rem/rounded-xl) matching MCP sidebar - Group headers: semibold with slight letter spacing - Proper dark mode variants for all states * Fix sidebar active item: dark gray background with bold text * Make sidebar active item more prominent: bolder weight, stronger background * Highlight current page in sidebar nav, normalize code font size Inject a small script via the SEO plugin that marks the active sidebar link with a "current" class (TypeDoc doesn't do this natively for document pages). Uses a MutationObserver since the sidebar is populated asynchronously. Pathname comparison strips .html extensions to support clean-URL servers. Also bumps code/pre font-size from 0.85em to 1em so code snippets match the surrounding body text size. * Split styling into dedicated plugin, fix auth terminology, improve breadcrumbs Extract CSS cascade fix, breadcrumb labels, and sidebar nav highlighting from the SEO plugin into a new typedoc-plugin-mcpstyle.mjs to keep concerns separated. Fix authorization doc: use "authorization" consistently instead of mixing with "authentication", link to MCP spec and RFC 9728 for Protected Resource Metadata, remove redundant spec link paragraph. Rename "Documents" nav section to "Getting Started" and add explicit group frontmatter to all doc pages. Breadcrumbs now show the section name instead of duplicating the page title. --- docs/agent-skills.md | 2 + docs/authorization.md | 211 ++++++++++++++++++ docs/csp-cors.md | 64 ++++++ docs/favicons/apple-touch-icon.png | Bin 0 -> 7947 bytes docs/favicons/favicon-16x16.png | Bin 0 -> 298 bytes docs/favicons/favicon-32x32.png | Bin 0 -> 3669 bytes docs/favicons/favicon-dark-16x16.png | Bin 0 -> 298 bytes docs/favicons/favicon-dark-32x32.png | Bin 0 -> 519 bytes docs/favicons/favicon-dark.ico | Bin 0 -> 5166 bytes docs/favicons/favicon.ico | Bin 0 -> 1275 bytes docs/mcp-theme.css | 189 ++++++++++++++++ docs/migrate_from_openai_apps.md | 2 + docs/overview.md | 2 + docs/patterns.md | 59 +---- docs/quickstart.md | 2 + docs/testing-mcp-apps.md | 2 + scripts/typedoc-plugin-mcpstyle.mjs | 53 +++++ scripts/typedoc-plugin-seo.mjs | 315 +++++++++++++++++++++++++++ typedoc.config.mjs | 16 +- 19 files changed, 860 insertions(+), 57 deletions(-) create mode 100644 docs/authorization.md create mode 100644 docs/csp-cors.md create mode 100644 docs/favicons/apple-touch-icon.png create mode 100644 docs/favicons/favicon-16x16.png create mode 100644 docs/favicons/favicon-32x32.png create mode 100644 docs/favicons/favicon-dark-16x16.png create mode 100644 docs/favicons/favicon-dark-32x32.png create mode 100644 docs/favicons/favicon-dark.ico create mode 100644 docs/favicons/favicon.ico create mode 100644 docs/mcp-theme.css create mode 100644 scripts/typedoc-plugin-mcpstyle.mjs create mode 100644 scripts/typedoc-plugin-seo.mjs diff --git a/docs/agent-skills.md b/docs/agent-skills.md index 36e1f98..d27ad71 100644 --- a/docs/agent-skills.md +++ b/docs/agent-skills.md @@ -1,5 +1,7 @@ --- title: Agent Skills +group: Getting Started +description: Use Agent Skills to build, migrate, and extend MCP Apps with AI coding agents. Install skills that scaffold new apps, convert OpenAI Apps, and add UI to existing servers. --- # Agent Skills diff --git a/docs/authorization.md b/docs/authorization.md new file mode 100644 index 0000000..59fc26c --- /dev/null +++ b/docs/authorization.md @@ -0,0 +1,211 @@ +--- +title: Authorization +group: Security +description: Learn how to protect MCP App tools with OAuth authorization, including per-server and per-tool auth patterns, token verification, and UI-initiated auth escalation. +--- + +# Authorization + +MCP Apps can protect tools behind OAuth-based authorization, as defined in the [MCP specification](https://modelcontextprotocol.io/specification/latest/basic/authorization). There are two approaches: + +- **Per-server authorization** — The entire MCP server requires authorization at connection time. Every request must include a valid token, regardless of which tool is being called. This is the simpler model when all tools are sensitive. +- **Per-tool authorization** — Only specific tools require authorization. Public tools work without a token, and the OAuth flow is triggered only when the user calls a protected tool. This lets you mix public and protected tools in the same server. + +## Shared setup + +Regardless of which approach you choose, you need OAuth discovery metadata and token verification. These are the same for both. + +### OAuth discovery metadata + +The MCP specification requires servers to implement [authorization server discovery](https://modelcontextprotocol.io/specification/latest/basic/authorization#authorization-server-discovery) so clients know how to obtain authorization. Two well-known endpoints are needed: + +**[Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728)** (`/.well-known/oauth-protected-resource`) — describes the resource server and identifies which authorization server(s) can issue tokens for it. The MCP SDK's `mcpAuthRouter` handles this automatically. + +**Authorization Server Metadata** (`/.well-known/oauth-authorization-server`) — advertises the authorization and token endpoints, supported scopes, and whether [Client ID Metadata Documents](https://modelcontextprotocol.io/specification/latest/basic/authorization#client-id-metadata-documents) (CIMD) is supported: + +```ts +app.get("/.well-known/oauth-authorization-server", (_req, res) => { + res.json({ + ...oauthMetadata, + client_id_metadata_document_supported: true, + }); +}); +``` + +Setting `client_id_metadata_document_supported: true` tells MCP clients to use CIMD instead of [Dynamic Client Registration](https://modelcontextprotocol.io/specification/latest/basic/authorization#dynamic-client-registration) (DCR). With CIMD, the `client_id` is a URL that serves the client's metadata document, removing the need for a registration endpoint. See [Client Registration Approaches](https://modelcontextprotocol.io/specification/latest/basic/authorization#client-registration-approaches) in the spec for the full list of options and priority order. + +### Token verification + +Verify access tokens as JWTs against the identity provider's JWKS endpoint. The `jose` library handles key fetching and caching: + +```ts +import { createRemoteJWKSet, jwtVerify } from "jose"; + +const JWKS = createRemoteJWKSet(new URL(`${IDP_DOMAIN}/.well-known/jwks.json`)); + +const { payload } = await jwtVerify(token, JWKS, { + issuer: IDP_DOMAIN, +}); +``` + +MCP servers must validate that tokens were issued specifically for them — see [Token Handling](https://modelcontextprotocol.io/specification/latest/basic/authorization#token-handling) and [Access Token Privilege Restriction](https://modelcontextprotocol.io/specification/latest/basic/authorization#access-token-privilege-restriction) in the spec for the full requirements. + +## Per-server authorization + +With per-server authorization, every request to the `/mcp` endpoint must include a valid Bearer token. Any unauthorized request receives HTTP `401`, and the host must complete the OAuth flow before the client can use any tools. This is the right choice when all tools are sensitive and there's no value in allowing unauthorized access. + +The TypeScript MCP SDK supports this out of the box via `mcpAuthRouter` and `ProxyOAuthServerProvider` — no custom HTTP handler logic is needed. See the [MCP SDK documentation](https://github.com/modelcontextprotocol/typescript-sdk) for setup details. + +## Per-tool authorization + +With per-tool authorization, the `/mcp` endpoint handler inspects the raw JSON-RPC request body, checks whether any message targets a protected tool, and only enforces authorization for those calls. Public tools pass through without a token. + +### How it works + +1. The server maintains a set of tool names that require authorization +2. When a JSON-RPC request arrives at the `/mcp` endpoint, the server inspects the request body to determine if any message is a `tools/call` targeting a protected tool +3. If a protected tool is being called and no valid Bearer token is present, the server returns HTTP `401` with a [`WWW-Authenticate` header](https://modelcontextprotocol.io/specification/latest/basic/authorization#protected-resource-metadata-discovery-requirements) pointing to its [Protected Resource Metadata](https://datatracker.ietf.org/doc/html/rfc9728) +4. The MCP host (e.g., Claude Desktop) sees the `401`, discovers the authorization server via the metadata URL, runs the [OAuth flow](https://modelcontextprotocol.io/specification/latest/basic/authorization#authorization-flow-steps) with the user, and retries the request with the acquired token +5. On retry, the server verifies the token, extracts the user identity, and creates a per-request MCP server instance with that auth context +6. Unprotected tools pass through without any token check — they work for everyone + +This design means authorization is enforced at the HTTP boundary (as [required by the spec](https://modelcontextprotocol.io/specification/latest/basic/authorization#access-token-usage)), not as a tool-level error. The MCP server itself never sees unauthorized requests for protected tools. + +### Enforcing HTTP 401 + +The [MCP auth specification](https://modelcontextprotocol.io/specification/latest/basic/authorization#access-token-usage) requires protected resources to return HTTP `401` responses — not tool-level errors. + +Start by defining which tools require authorization. Then, in the `/mcp` endpoint handler, inspect the raw JSON-RPC request body, check whether any message targets a protected tool, and either verify the Bearer token or return `401` before the request ever reaches the MCP server: + +```ts +/** Tools that require a valid Bearer token — checked at the HTTP level for proper 401. */ +const PROTECTED_TOOLS = new Set(["get_account_balance", "manage_branch_admin"]); + +app.all("/mcp", async (req, res) => { + // Parse the JSON-RPC body — it may be a single message or a batch + const messages = Array.isArray(req.body) ? req.body : [req.body]; + + // Check if any message is a tools/call for a protected tool + const needsAuth = messages.some( + (msg: any) => + msg?.method === "tools/call" && PROTECTED_TOOLS.has(msg.params?.name), + ); + + // Extract and verify the Bearer token + let authInfo: AuthInfo | undefined; + const authHeader = req.headers.authorization; + + if (authHeader?.startsWith("Bearer ")) { + try { + const token = authHeader.slice(7); + const { payload } = await jwtVerify(token, JWKS, { + issuer: IDP_DOMAIN, + }); + authInfo = { token, sub: payload.sub as string }; + } catch { + if (needsAuth) { + return res + .status(401) + .set( + "WWW-Authenticate", + `Bearer resource_metadata="${resourceMetadataUrl}"`, + ) + .json({ + error: "invalid_token", + error_description: "The access token is invalid", + }); + } + } + } else if (needsAuth) { + return res + .status(401) + .set( + "WWW-Authenticate", + `Bearer resource_metadata="${resourceMetadataUrl}"`, + ) + .json({ + error: "invalid_token", + error_description: "Authorization required", + }); + } + + // Create a per-request MCP server with the auth context. + // authInfo is undefined for public tool calls, populated for + // authenticated requests — tool handlers use it to scope data + // to the authenticated user. + const server = createServer(authInfo); + // ... handle the request with transport +}); +``` + +The `WWW-Authenticate` header includes the [Protected Resource Metadata](https://modelcontextprotocol.io/specification/latest/basic/authorization#authorization-server-location) URL, which tells the client where to discover the authorization server. + +### Defence-in-depth in tool handlers + +Even though the HTTP layer enforces authorization, protected tool handlers should also verify `authInfo` as a defence-in-depth measure. If the HTTP layer is misconfigured or bypassed, the tool handler catches unauthorized access: + +```ts +registerAppTool( + server, + "get_account_balance", + { + description: "Get account balance", + inputSchema: { accountId: z.string() }, + }, + async ({ accountId }) => { + if (!authInfo) { + return { + isError: true, + content: [ + { + type: "text", + text: "Authorization required to access account data.", + }, + ], + }; + } + + const balance = await getBalance(authInfo.sub, accountId); + return { + content: [{ type: "text", text: `Balance: ${balance}` }], + }; + }, +); +``` + +### UI-initiated auth escalation + +A powerful pattern is mixing public and protected tools in the same app. The app loads with public data (no authorization required), and the OAuth flow is triggered only when the user performs a protected action. This is a practical application of the [step-up authorization flow](https://modelcontextprotocol.io/specification/latest/basic/authorization#step-up-authorization-flow) described in the spec: + +1. A public tool (e.g., `manage_branch`) loads the UI without requiring authorization +2. The user clicks a button that calls a protected tool via `app.callServerTool()` +3. The MCP host receives HTTP `401` and automatically runs the OAuth flow +4. After the user completes the OAuth flow, the host retries the tool call with the acquired token +5. The protected data appears in the UI + +```tsx +function BranchItem({ branch }: { branch: Branch }) { + const [adminData, setAdminData] = useState(null); + + async function handleManage() { + // This call may trigger the OAuth flow if the user + // hasn't been authorized yet — the host handles it + // transparently. + const result = await app.callServerTool({ + name: "manage_branch_admin", + arguments: { branch_id: branch.id }, + }); + setAdminData(result.structuredContent); + } + + return ( +
+ {branch.name} + + {adminData && } +
+ ); +} +``` + +This pattern keeps the initial experience fast (no login wall) while securing sensitive operations behind authorization. The host manages the entire OAuth flow — the app code simply calls the tool and handles the result. diff --git a/docs/csp-cors.md b/docs/csp-cors.md new file mode 100644 index 0000000..8978ab6 --- /dev/null +++ b/docs/csp-cors.md @@ -0,0 +1,64 @@ +--- +title: CSP and CORS +group: Security +description: Configure Content Security Policy and CORS for MCP Apps that make network requests from sandboxed iframes, including connectDomains, resourceDomains, and stable origin setup. +--- + +# CSP & CORS + +Unlike regular web apps, MCP Apps HTML is served as an MCP resource and runs in a sandboxed iframe with no same-origin server. Any app that makes network requests must configure Content Security Policy (CSP) and possibly CORS. + +**CSP** controls what the _browser_ allows. You must declare _all_ origins in {@link types!McpUiResourceMeta.csp `_meta.ui.csp`} ({@link types!McpUiResourceCsp `McpUiResourceCsp`}) — including `localhost` during development. Declare `connectDomains` for fetch/XHR/WebSocket requests and `resourceDomains` for scripts, stylesheets, images, and fonts. + +**CORS** controls what the _API server_ allows. Public APIs that respond with `Access-Control-Allow-Origin: *` or use API key authentication work without CORS configuration. For APIs that allowlist specific origins, use {@link types!McpUiResourceMeta.domain `_meta.ui.domain`} to give the app a stable origin that the API server can allowlist. The format is host-specific, so check each host's documentation for its supported format. + + +```ts source="../src/server/index.examples.ts#registerAppResource_withDomain" +// Computes a stable origin from an MCP server URL for hosting in Claude. +function computeAppDomainForClaude(mcpServerUrl: string): string { + const hash = crypto + .createHash("sha256") + .update(mcpServerUrl) + .digest("hex") + .slice(0, 32); + return `${hash}.claudemcpcontent.com`; +} + +const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp"); + +registerAppResource( + server, + "Company Dashboard", + "ui://dashboard/view.html", + { + description: "Internal dashboard with company data", + }, + async () => ({ + contents: [ + { + uri: "ui://dashboard/view.html", + mimeType: RESOURCE_MIME_TYPE, + text: dashboardHtml, + _meta: { + ui: { + // CSP: tell browser the app is allowed to make requests + csp: { + connectDomains: ["https://api.example.com"], + }, + // CORS: give app a stable origin for the API server to allowlist + // + // (Public APIs that use `Access-Control-Allow-Origin: *` or API + // key auth don't need this.) + domain: APP_DOMAIN, + }, + }, + }, + ], + }), +); +``` + +Note that `_meta.ui.csp` and `_meta.ui.domain` are set in the `contents[]` objects returned by the resource read callback, not in `registerAppResource()`'s config object. + +> [!NOTE] +> For full examples that configures CSP, see: [`examples/sheet-music-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/sheet-music-server) (`connectDomains`) and [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) (`connectDomains` and `resourceDomains`). diff --git a/docs/favicons/apple-touch-icon.png b/docs/favicons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2e10abc75b80580abe01aa38757747cc4e7a00e9 GIT binary patch literal 7947 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 literal 0 HcmV?d00001 diff --git a/docs/favicons/favicon-32x32.png b/docs/favicons/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..88f78f67cb3f42b3f781a972900d790e864db634 GIT binary patch literal 3669 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 literal 0 HcmV?d00001 diff --git a/docs/favicons/favicon-dark-32x32.png b/docs/favicons/favicon-dark-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..b861b127be45326f465905220ccc5862fee38e63 GIT binary patch literal 519 zcmV+i0{H!jP)kdg00001b5ch_0Itp) z=>Px$!AV3xRCwBz)xU2|VI0Tt&qbmUB51J~OvF|aKL)WN#BO6iVldcIq=la25%rQmHBRyxGv3sW!8#VZpM`LtscI? zcle4vOG=-y8?VGU<-0#@@IqiShQ%o}?w5HvQ1co4MamCf6^OH#a>-$*y)W}}pynn3 zUYB?|5V6x`{cbyI>0(@>VhX&CKzH+-Z$B_t%@+@h;D9L4l1Rn%AW47rl{ZNBU$N#6 ze%M~xF_zS0@x+PJdQZ}o(WsuT(#lBIB}r%M8H+Y&EInD5`~^_Yri|*+5|jV{002ov JPDHLkV1n(H>azd< literal 0 HcmV?d00001 diff --git a/docs/favicons/favicon-dark.ico b/docs/favicons/favicon-dark.ico new file mode 100644 index 0000000000000000000000000000000000000000..fd1f24d2a262983d60bccd025c547f46b1dd6ca1 GIT binary patch literal 5166 zcmbuB2{@Er`^V2S&#cTCV-T{8Wvn6l$ez6{k;qmwGcno6TDBGu32lU;Bt=E3C>2>s zi_$Jq4SrfxqbyU%`^>NFs{a4$df)$h{r=~fd9Kgg=RVIl-}^r2nS1`(JP(L3b8>Y8 z5Cj1icv^bn*d6b9J<*#UoBa{@tdY0^~^mJkoVpL8JY)|5rp|#&Qz?fL0*hMl5zb z6X7g`Ba;(ig?2u|lItRbj6w^Ui!2af86k)Lvj1v8*VTkh>7x zk(9_r7nte=EY-sdG z0Dfu~jsgsek{$uAaum23f-2U|V>h*WdAnBgpw%=oa+hVE! zU`!&hDf(?&R|-J$b^v4se%sUz13=CKpsDu{J<`Jc5}uF{yF_0a@B9C_p$d_osCZ5oO=!0F_MtHoxk*As z&V=xW0V045B!MhY1gbz2=mJAv1}uOra0JVMC-4FOU=3h`a1aH!AQ_~AEg&201Vvy! zCW7A)chDE;I|@b-QIaSH zlsZZu#X#AimZN-7!Kg4)9BLCP3srzRfT}{BLN%e#uQ^}Fc&b_F;6g~m?5Viq(4f_B)f}O3W#dY5r*Lh!hqzIk056JH!W-h9 z@B#Q}{8oH1z7F4te~5pBpCO17=!7K%PXddukx)peCbSTG38RD=5sHY0h?R(s$Oe%u zA_qk3MXrksihLCn6;%;67xfb5h;9}=AbLjhrs$C9H=+bln`loAA|?_Gh{uW7i2cN` zBoaxTWJ?MlC6Ee8wWLncOOim0DyApqD#jAa6e|~N5_>52iA*5V$#d`cbV7Uiu3PC{M6QGz9rC2>@uQ(}Y) zQ&p+#BwVHaJIx2~k)R1(Q+#tDA^0eeVNxqbXl(CerREku&)Kw{-G%T$y?J6B9 zT`1is{Z#t<0)+*33)l;C7t}9!xL{glp^U8zTP9DYLFS3fcUhXOqilriZrNtp7jkGh zZ8vCfYB^O#OWG>8G*tl>&9wo0W?=7Dye^kCleo{eE!C4_zp;Vz$VN6k4 z(N=MT;y%Sz#dk_nC2J*)(mtg&rS~)`njI~Yc7WDJ<15Q6yC^3rA5p%iJfotavPxyE zN`uM^RS{K&>U!0EsvW9)H6=B7wKTPpY6Elvok0($m(aWE)9M=Pe(E{u&FXJ8WHekg zHffyH7}O+b+GxgUR%t%g!fG+JBD4-^-PcBGn`nn?muvUxpma=iI68-P9xTEvGG7$E zsA^H4E>YJ`H%a%T?vS3eo}1oQy=J{n`fB(@TMb$a zz8Gp7G7ZZO9~+U3oQyJ!E*gy+>lm|*D~z9+P)*!Ta!fi*=1k2@xu*4|@66Q9Ld*`D z^)VzFD;W8Vo8}nvrRJIDSIoaHFhB>2pn5k7$o(PvGh5x!3c>3XK(UD=vFsy;gXYd%gBH@J{pY zUP)cKc4ghlDW9c2g+9+$X|76K)!|F_4f3t^o%VC^+vCSut-m^b^&NjXe~$mf09=4? zKuy48pkv_vz|kO+pzNTh!5YCS!ME1Pt%+Q7buD>q=-P83m=M2^x{%pW&(NyS$#t&l z4zA-f?U^Oa4=h{OKGr+76?+f+&3dc#d)B`VvkKcA_AcBeyf}P}W6vq$jBjw+P`P0` z!ZYG{#C)V*WJ44_DkSPsv_y17bXSaGOiIjySe@AHSYF(cxczZ_?s9HTJQN=se=$K4 zDW-Z7H50cd4kcM9l_v?3eUs0nP*S2(dNyir%-#5AlhdYSspwQ@YDb!K+LpATbi4E; z8BoT$jE+pz%&g4O&CZ)oY!TTKvE}a8#as7mo!I8LttCq#Yjf7fcIWLUv&FKx*?l<{ zIh8xmJ2*S;*?eaH?Og`D4(ysQSYL3j(6q3k2wN0g^mMn) z?h|_`dp7ME+3UWyX&-H0-oA>0aIsXaw5W8pEWGSV`O@-+ zgYpM+4^AIq9eP+{TTx%BP`R`6`(e)EXGffmG*zir6(7YOO*;Cz+PAv<7~@!Nja<#n znz`fA$9X5bPjuCq)z;R@*A<>bolHFW?o`mJ`==dFU#!=yKhhxGu=5N!lX&L+*^skO z8<#hBoMW7;KTkh@@B;NhUK7-`v5DUt-psq`fAPVkWtTcyELtvH*1LTCit?56t5R1B zTSZ#4TIbs~wM|}&y*Ab!);`j)w&Qu{>dwbqUS0RDFTZ~ChSQDCZoBTbn^rfk+*)$$ z(rw1=<~yc$ntDunF5ES_d*Pnxy{2BX-i!Cm@3%a#e9-#P=3)CIhetOayFR}2#N)|> zr>mYm>kI1RJ!3t4+aK9KK9Dpp^L+ClW-#xC_>1C~@-M4iX}xOTnewguZ!O(y{Ua``1Z;>r+4??2fiQu5c^?fEc+wn>41)f@5179OD0rmcu!SKNfTBP!1)A@IkOCIMAf*G+gmsFrAv2*d zSR9@pA}S705DcTxFtTiGDX^F<_WH1J&W8Ae#H8eu zjhnV*ZO_ixk(;+?@4o%TB?n5&s*YA4t2ut6wyyEq`3p_W7caGRbaq|8(S7sQ?T3#Z zKY7~stbbr=c;v6q*Kgjwdq4i=>%`>LwBXx!2*5w({8IFX9txrdg+|hV6Y7Cb$wILd zG)CC~D`Dq_3yq_y7;eE!F5P|PY&${K$eS;x1#?}(AIyX=$D{h zdgccJ5r&Y2!4zNveq_F_$^$wtt*L2BlwU(fRw7_wj$l~de0`gvtSS+ikU>>|!b1Gu$)o^ClJb-`PeTmB%deeE+ z_w#kL4Q-rD1N1#wbQ$VBY_doH+~Z)$klh7`6`4x2)YWtOjv8Gb5+C=ZcEKJ!nQo=q zq~<|{()J9GG^T()(zbn#TCjeX^a=FsUd~2>ehfD+Ho%l7pFexsR;Bg!`1Dx!NCR$G ztz;-<6pMfZg z>2T-$HFgo|wW(5#y#^glEXH7ZdQSZHjHxsq!D$}3zx0^yc)8nV;F^InX(*Kdbgq{p&<8 zhI7s^VBn~@JlrF$XlB8jZv27TtT05zr#Z}>vR29La37AbapU`z8qrVJ89>)8+o6&8 zCYx0K=&h|;f+I6Q!6`9aNlN4&<$9mDHl0(P88!ax!m#P)FN4e zxkAd_OvcMa{1@{e`^w7G;Ux!VJWA8!I0g!$*IpYspHQ}|1m}j8@-9r|-zcj&nVc-Acw(=M zzt7BOTVrVSaFHDjonZ1Xyji-EU&MK|0evfG>gJlintKAQc&+JNowBOUHdRUjO-x(q za6?w}_qxHmyva+ftESzmWEJDfE+eTOlaK@YI%xc zUnHYFn?m#51z!6H%4Z}Fx1rmjNMujGv9*X`?@(t$`2DUkcClxjV?T(b-+Z07PO2kQ z6om(|5rfC(&a|z6mD957Ynq85#m}cDzv19iZG`wI%YHNC{KH?*geZC0DwYy$NH#2+ z?25=%r?Vrw*1ZB^_35~jkE&Llem2Lm(ue}tlW9&*8d~*cgRjD_;6*2C=*!3@mv0g- zVyFPxOz!Mjk6OMFZ+*k~Lcgnm%i~ECBsF*=(JbfA@_mK7)i(3!RnQlW@$@CLZ8NJf zo&@A{y|6p4OQ=4_KYehsqqt(=TI0+lf4@dD&*O!s6`)Sl&DC8{%5B*e9zbat^jX+;&x$ZbVNYfB}8 Km!l*!-~TTe3wa;_ literal 0 HcmV?d00001 diff --git a/docs/favicons/favicon.ico b/docs/favicons/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b92e162a311e3dffc3e6b667e85308241af92890 GIT binary patch literal 1275 zcmex=DK+5#0R}-11_uTQW=16jCP7AK zLB{__7;+gHn3$MBPK5&wRyHPPc19Kf21aIPCKgr}pgdTTnURTwft8J2P}tBZkV8n( z$WcTou~1Z5ta0K(6cfOz1(D4Ji~PUEzymajNsw8P!Jffq#iA~zbu+>^CqDn47WJm! zLW_&L#QD&ShI4UAR@)OIM85~TU%2I#^UX^sJH=aN;`M7yZ_`eGn>$fb{w6FWAzkarV!ELiSH{w@GwW@lSTB&q2ncoYu zxVQ5BU4^Nf$s3>iQ!1)|{{D}L#T+4R2e$S5ceLIOUo%BAGH0iI$C-us-~Z|8_z8+G zV6OVz5qq`jez?!>X#d1rE{DGCmt#G1;pTxj36H|ft!EMvLd|b2ER_y?awp~3rMJ30 zbN2NtYkzUg(6UAQL+3TGX~Kr~yb-ge{*j)RSSbEG{`zT#Q@lT>#iw6iUHhKr%p0B7 znlAIbU23Z|u8K-5k+Lu6eOzvK$+mjS$&GP(+h#`Bx=6b9#O|zYdYW{(f8mX<*KFSO zhZTKZVsN^rXyeo0HZQ^zIqDDFnx$&|EPefFvgV2PzDXM=zY|i~xcbtiy)m7RF|HvY zAyE?=0_xQ{HkVh}Ey=U{w`$F2rCp0xEGXaO(_VdT_L&_EJG&L9Zki%LeQW%t<-x6Q zR@}KBI$4c%z5kX;k|IVf4Z#h5Y=?ddT>PyqH8($Mb%NB-yw*N;;qyA(Wm|eX6lO{s zwq&{Iqj37g-&J?smR#!55?OknVD38itmUE)G9-U|-d|d^l;y$k&Wh|KJ6aybZ2CQ6 z|NGZZ8&X3|Ui&?JE}Z%*Z<&|N)T7Dj3cl8yLkK3t?zzYUX>M^#Vvc4vcx+| z{YppBzGE}jyspo9`!YA6bav#D=byqZW`w&Pdu*M^ePh$qIUO!<=ISmu9%(u$tXlP2 zxAS~ynVz?8_F1RGiWKcRFPwTH66g@ h3 { + font-weight: 600; + font-size: 0.875rem; + letter-spacing: 0.01em; +} + +.site-menu { + padding: 1.25rem 0; +} + +@media (prefers-color-scheme: dark) { + .tsd-navigation a { + color: var(--color-text-aside); + } + + .tsd-navigation a:hover:not(.current) { + background-color: rgba(229, 231, 235, 0.05); + color: var(--color-text); + } + + .tsd-navigation a.current { + background-color: rgba(229, 231, 235, 0.15); + color: var(--color-text); + font-weight: 700; + } +} + +:root[data-theme='dark'] .tsd-navigation a { + color: var(--color-text-aside); +} + +:root[data-theme='dark'] .tsd-navigation a:hover:not(.current) { + background-color: rgba(229, 231, 235, 0.05); + color: var(--color-text); +} + +:root[data-theme='dark'] .tsd-navigation a.current { + background-color: rgba(229, 231, 235, 0.15); + color: var(--color-text); + font-weight: 700; +} + +/* ------------------------------------------------------------------ */ +/* Content area */ +/* ------------------------------------------------------------------ */ + +.tsd-typography { + line-height: 1.6; +} + +.tsd-panel.tsd-typography h1 { + margin-top: 0; + padding-bottom: 0.4em; +} + +.tsd-panel.tsd-typography h2 { + margin-top: 2rem; + padding-bottom: 0.3em; +} + +.tsd-panel.tsd-typography h3 { + margin-top: 1.75rem; +} + +/* ------------------------------------------------------------------ */ +/* Code blocks */ +/* ------------------------------------------------------------------ */ + +code, +pre { + font-family: var(--font-family-code); + font-size: 1em; +} + +.tsd-typography pre { + padding: 1rem 1.25rem; + border: 1px solid var(--color-accent); +} + +/* Inline code */ +.tsd-typography code:not(pre code) { + padding: 0.15em 0.35em; + font-size: 1em; +} diff --git a/docs/migrate_from_openai_apps.md b/docs/migrate_from_openai_apps.md index 6f5a1c3..d91281b 100644 --- a/docs/migrate_from_openai_apps.md +++ b/docs/migrate_from_openai_apps.md @@ -1,5 +1,7 @@ --- title: Migrate OpenAI App +group: Getting Started +description: Migrate from the OpenAI Apps SDK to MCP Apps SDK with concept mapping tables, API equivalents, and complete before/after code examples. --- # Migrating from OpenAI Apps SDK to MCP Apps SDK diff --git a/docs/overview.md b/docs/overview.md index 4de6a68..28f5b23 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,5 +1,7 @@ --- title: Overview +group: Getting Started +description: MCP Apps extends the Model Context Protocol to let MCP servers deliver interactive UIs — charts, forms, dashboards — rendered securely in iframes inside any compliant host. --- # MCP Apps Overview diff --git a/docs/patterns.md b/docs/patterns.md index 3a6302c..87a6d82 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -1,5 +1,7 @@ --- title: Patterns +group: Getting Started +description: Common patterns and recipes for building MCP Apps — polling, chunked data, binary resources, theming, fullscreen, model context, state persistence, and more. --- # MCP Apps Patterns @@ -298,62 +300,7 @@ videoEl.src = `data:${content.mimeType!};base64,${content.blob}`; ## Configuring CSP and CORS -Unlike regular web apps, MCP Apps HTML is served as an MCP resource and runs in a sandboxed iframe with no same-origin server. Any app that makes network requests must configure Content Security Policy (CSP) and possibly CORS. - -**CSP** controls what the _browser_ allows. You must declare _all_ origins in {@link types!McpUiResourceMeta.csp `_meta.ui.csp`} ({@link types!McpUiResourceCsp `McpUiResourceCsp`}) — including `localhost` during development. Declare `connectDomains` for fetch/XHR/WebSocket requests and `resourceDomains` for scripts, stylesheets, images, and fonts. - -**CORS** controls what the _API server_ allows. Public APIs that respond with `Access-Control-Allow-Origin: *` or use API key authentication work without CORS configuration. For APIs that allowlist specific origins, use {@link types!McpUiResourceMeta.domain `_meta.ui.domain`} to give the app a stable origin that the API server can allowlist. The format is host-specific, so check each host's documentation for its supported format. - - -```ts source="../src/server/index.examples.ts#registerAppResource_withDomain" -// Computes a stable origin from an MCP server URL for hosting in Claude. -function computeAppDomainForClaude(mcpServerUrl: string): string { - const hash = crypto - .createHash("sha256") - .update(mcpServerUrl) - .digest("hex") - .slice(0, 32); - return `${hash}.claudemcpcontent.com`; -} - -const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp"); - -registerAppResource( - server, - "Company Dashboard", - "ui://dashboard/view.html", - { - description: "Internal dashboard with company data", - }, - async () => ({ - contents: [ - { - uri: "ui://dashboard/view.html", - mimeType: RESOURCE_MIME_TYPE, - text: dashboardHtml, - _meta: { - ui: { - // CSP: tell browser the app is allowed to make requests - csp: { - connectDomains: ["https://api.example.com"], - }, - // CORS: give app a stable origin for the API server to allowlist - // - // (Public APIs that use `Access-Control-Allow-Origin: *` or API - // key auth don't need this.) - domain: APP_DOMAIN, - }, - }, - }, - ], - }), -); -``` - -Note that `_meta.ui.csp` and `_meta.ui.domain` are set in the `contents[]` objects returned by the resource read callback, not in `registerAppResource()`'s config object. - -> [!NOTE] -> For full examples that configures CSP, see: [`examples/sheet-music-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/sheet-music-server) (`connectDomains`) and [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) (`connectDomains` and `resourceDomains`). +See the dedicated [CSP & CORS](./csp-cors.md) guide in the Security section. ## Adapting to host context (theme, styling, fonts, and safe areas) diff --git a/docs/quickstart.md b/docs/quickstart.md index c850f8c..2714d4e 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -1,5 +1,7 @@ --- title: Quickstart +group: Getting Started +description: Build your first MCP App step by step — create an MCP server with an interactive View that renders inside Claude Desktop and other MCP hosts. --- # Build Your First MCP App diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md index 6bef13e..df1396c 100644 --- a/docs/testing-mcp-apps.md +++ b/docs/testing-mcp-apps.md @@ -1,5 +1,7 @@ --- title: Testing MCP Apps +group: Getting Started +description: Test MCP Apps locally with the basic-host reference implementation or in production hosts like Claude.ai and VS Code. --- # Test Your MCP App diff --git a/scripts/typedoc-plugin-mcpstyle.mjs b/scripts/typedoc-plugin-mcpstyle.mjs new file mode 100644 index 0000000..bcd40f7 --- /dev/null +++ b/scripts/typedoc-plugin-mcpstyle.mjs @@ -0,0 +1,53 @@ +/** + * TypeDoc plugin that applies MCP-specific styling tweaks. + * + * - Moves custom.css to load last so overrides win the cascade + * - Replaces breadcrumbs with the document group name (e.g. "Security") + * - Marks the current sidebar nav link for CSS highlighting + */ + +import { Renderer } from "typedoc"; + +/** + * TypeDoc plugin entry point. + * @param {import('typedoc').Application} app + */ +export function load(app) { + app.renderer.on(Renderer.EVENT_END_PAGE, (page) => { + if (!page.contents) return; + + // Move custom.css to load after all theme stylesheets so overrides win the cascade + const customCssLink = page.contents.match( + //, + ); + if (customCssLink) { + page.contents = page.contents.replace(customCssLink[0], ""); + page.contents = page.contents.replace( + "", + customCssLink[0] + "\n", + ); + } + + // For document pages, replace the breadcrumb with the group name + // (e.g. "Security", "Getting Started"). The page title is in the H1. + if (page.model?.isDocument?.() && page.model.frontmatter?.group) { + const group = String(page.model.frontmatter.group); + page.contents = page.contents.replace( + /