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/157] 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 494c73e7ca41ec6b7504f6861f78bd4bb659321a Mon Sep 17 00:00:00 2001
From: Anton Pidkuiko MacBook
Date: Thu, 19 Feb 2026 21:25:48 +0000
Subject: [PATCH 002/157] feat: add ui/download-file method for host-mediated
file downloads
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add a new ui/download-file JSON-RPC request that allows MCP App views to
request file downloads through the host. Since MCP Apps run in sandboxed
iframes where direct downloads are blocked (allow-downloads not set),
this provides a host-mediated mechanism for file exports.
Spec changes (draft):
- Add ui/download-file request to Requests (View → Host) section
- Add downloadFile capability to HostCapabilities
- Document host behavior requirements (confirmation dialog, filename
sanitization, base64 decoding, size limits)
SDK changes:
- Add McpUiDownloadFileRequest/Result interfaces to spec.types.ts
- Add downloadFile() method to App class
- Add ondownloadfile setter to AppBridge class
- Add DOWNLOAD_FILE_METHOD constant
- Generate Zod schemas
Example:
- Add download demo to integration-server (exports JSON with server time)
- Capability check: only shows download button when host advertises
downloadFile capability
Based on developer feedback requesting file export capabilities for
visualization tools, document editors, and data analysis apps.
---
examples/integration-server/src/mcp-app.tsx | 25 ++++++++
specification/draft/apps.mdx | 45 ++++++++++++++
src/app-bridge.ts | 57 ++++++++++++++++++
src/app.ts | 55 +++++++++++++++++
src/generated/schema.json | 67 +++++++++++++++++++++
src/generated/schema.test.ts | 20 ++++++
src/generated/schema.ts | 56 +++++++++++++++++
src/spec.types.ts | 42 +++++++++++++
src/types.ts | 9 +++
9 files changed, 376 insertions(+)
diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx
index ef280f3..8e4910a 100644
--- a/examples/integration-server/src/mcp-app.tsx
+++ b/examples/integration-server/src/mcp-app.tsx
@@ -137,6 +137,24 @@ function GetTimeAppInner({
log.info("Open link request", isError ? "rejected" : "accepted");
}, [app, linkUrl]);
+ const canDownload =
+ app.getHostCapabilities()?.downloadFile !== undefined;
+
+ const handleDownloadFile = useCallback(async () => {
+ const sampleContent = JSON.stringify(
+ { time: serverTime, exported: new Date().toISOString() },
+ null,
+ 2,
+ );
+ log.info("Requesting file download...");
+ const { isError } = await app.downloadFile({
+ filename: "export.json",
+ content: sampleContent,
+ mimeType: "application/json",
+ });
+ log.info("Download", isError ? "rejected" : "accepted");
+ }, [app, serverTime]);
+
return (
+
+ {canDownload && (
+
+
Export current server time as JSON file
+
+
+ )}
);
}
diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx
index cb73cab..d12ba12 100644
--- a/specification/draft/apps.mdx
+++ b/specification/draft/apps.mdx
@@ -648,6 +648,8 @@ interface HostCapabilities {
experimental?: {};
/** Host supports opening external URLs. */
openLinks?: {};
+ /** Host supports file downloads via ui/download-file. */
+ downloadFile?: {};
/** Host can proxy tool calls to the MCP server. */
serverTools?: {
/** Host supports tools/list_changed notifications. */
@@ -1013,6 +1015,49 @@ MCP Apps introduces additional JSON-RPC methods for UI-specific functionality:
Host SHOULD open the URL in the user's default browser or a new tab.
+`ui/download-file` - Request host to download a file
+
+```typescript
+// Request
+{
+ jsonrpc: "2.0",
+ id: 1,
+ method: "ui/download-file",
+ params: {
+ filename: string, // Suggested filename
+ content: string, // File content (text or base64-encoded)
+ mimeType: string, // MIME type (e.g. "image/svg+xml", "application/json")
+ encoding?: "utf-8" | "base64" // Content encoding, defaults to "utf-8"
+ }
+}
+
+// Success Response
+{
+ jsonrpc: "2.0",
+ id: 1,
+ result: {} // Empty result on success
+}
+
+// Error Response (if denied or failed)
+{
+ jsonrpc: "2.0",
+ id: 1,
+ error: {
+ code: -32000, // Implementation-defined error
+ message: "Download denied by user" | "Invalid content" | "Policy violation"
+ }
+}
+```
+
+MCP Apps run in sandboxed iframes where direct file downloads are blocked (`allow-downloads` is not set). `ui/download-file` provides a host-mediated mechanism for apps to offer file exports — useful for visualization tools (SVG/PNG export), document editors, data analysis tools, and any app that produces downloadable artifacts.
+
+Host behavior:
+* Host SHOULD show a confirmation dialog before initiating the download.
+* Host SHOULD use the `filename` as the suggested filename for the download.
+* For `encoding: "base64"`, host MUST decode the content from base64 before creating the file.
+* Host MAY reject the download based on security policy, file size limits, or user preferences.
+* Host SHOULD sanitize the filename to prevent path traversal.
+
`ui/message` - Send message content to the host's chat interface
```typescript
diff --git a/src/app-bridge.ts b/src/app-bridge.ts
index 6e1c021..de7d876 100644
--- a/src/app-bridge.ts
+++ b/src/app-bridge.ts
@@ -69,6 +69,9 @@ import {
McpUiOpenLinkRequest,
McpUiOpenLinkRequestSchema,
McpUiOpenLinkResult,
+ McpUiDownloadFileRequest,
+ McpUiDownloadFileRequestSchema,
+ McpUiDownloadFileResult,
McpUiResourceTeardownRequest,
McpUiResourceTeardownResultSchema,
McpUiSandboxProxyReadyNotification,
@@ -614,6 +617,60 @@ export class AppBridge extends Protocol<
);
}
+ /**
+ * Register a handler for file download requests from the View.
+ *
+ * The View sends `ui/download-file` requests when the user wants to
+ * download a file. The host should show a confirmation dialog and then
+ * trigger the download.
+ *
+ * @param callback - Handler that receives download params and returns a result
+ * - `params.filename` - Suggested filename for the download
+ * - `params.content` - File content (text or base64-encoded)
+ * - `params.mimeType` - MIME type of the file
+ * - `params.encoding` - Content encoding ("utf-8" or "base64", defaults to "utf-8")
+ * - `extra` - Request metadata (abort signal, session info)
+ * - Returns: `Promise` with optional `isError` flag
+ *
+ * @example
+ * ```ts
+ * bridge.ondownloadfile = async ({ filename, content, mimeType, encoding }, extra) => {
+ * const confirmed = await showDialog({
+ * message: `Download "${filename}"?`,
+ * buttons: ["Download", "Cancel"],
+ * });
+ * if (!confirmed) return { isError: true };
+ *
+ * const blob = encoding === "base64"
+ * ? new Blob([Uint8Array.from(atob(content), c => c.charCodeAt(0))], { type: mimeType })
+ * : new Blob([content], { type: mimeType });
+ * const url = URL.createObjectURL(blob);
+ * const link = document.createElement("a");
+ * link.href = url;
+ * link.download = filename;
+ * link.click();
+ * URL.revokeObjectURL(url);
+ * return {};
+ * };
+ * ```
+ *
+ * @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`} for the request type
+ * @see {@link McpUiDownloadFileResult `McpUiDownloadFileResult`} for the result type
+ */
+ set ondownloadfile(
+ callback: (
+ params: McpUiDownloadFileRequest["params"],
+ extra: RequestHandlerExtra,
+ ) => Promise,
+ ) {
+ this.setRequestHandler(
+ McpUiDownloadFileRequestSchema,
+ async (request, extra) => {
+ return callback(request.params, extra);
+ },
+ );
+ }
+
/**
* Register a handler for display mode change requests from the view.
*
diff --git a/src/app.ts b/src/app.ts
index 16f8da6..ebf7e81 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -33,6 +33,8 @@ import {
McpUiMessageResultSchema,
McpUiOpenLinkRequest,
McpUiOpenLinkResultSchema,
+ McpUiDownloadFileRequest,
+ McpUiDownloadFileResultSchema,
McpUiResourceTeardownRequest,
McpUiResourceTeardownRequestSchema,
McpUiResourceTeardownResult,
@@ -930,6 +932,59 @@ export class App extends Protocol {
/** @deprecated Use {@link openLink `openLink`} instead */
sendOpenLink: App["openLink"] = this.openLink;
+ /**
+ * Request the host to download a file.
+ *
+ * Since MCP Apps run in sandboxed iframes where direct downloads are blocked,
+ * this provides a host-mediated mechanism for file exports. The host will
+ * typically show a confirmation dialog before initiating the download.
+ *
+ * @param params - File content, filename, MIME type, and optional encoding
+ * @param options - Request options (timeout, etc.)
+ * @returns Result with `isError: true` if the host denied the request (e.g., user cancelled)
+ *
+ * @throws {Error} If the request times out or the connection is lost
+ *
+ * @example Download a JSON export
+ * ```ts
+ * const data = JSON.stringify({ items: selectedItems }, null, 2);
+ * const { isError } = await app.downloadFile({
+ * filename: "export.json",
+ * content: data,
+ * mimeType: "application/json",
+ * });
+ * if (isError) {
+ * console.warn("Download denied or cancelled");
+ * }
+ * ```
+ *
+ * @example Download binary content (base64)
+ * ```ts
+ * const { isError } = await app.downloadFile({
+ * filename: "image.png",
+ * content: base64EncodedPng,
+ * mimeType: "image/png",
+ * encoding: "base64",
+ * });
+ * ```
+ *
+ * @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`} for request structure
+ * @see {@link McpUiDownloadFileResult `McpUiDownloadFileResult`} for result structure
+ */
+ downloadFile(
+ params: McpUiDownloadFileRequest["params"],
+ options?: RequestOptions,
+ ) {
+ return this.request(
+ {
+ method: "ui/download-file",
+ params,
+ },
+ McpUiDownloadFileResultSchema,
+ options,
+ );
+ }
+
/**
* Request a change to the display mode.
*
diff --git a/src/generated/schema.json b/src/generated/schema.json
index d9e4b58..9cc87ae 100644
--- a/src/generated/schema.json
+++ b/src/generated/schema.json
@@ -81,6 +81,61 @@
],
"description": "Display mode for UI presentation."
},
+ "McpUiDownloadFileRequest": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "method": {
+ "type": "string",
+ "const": "ui/download-file"
+ },
+ "params": {
+ "type": "object",
+ "properties": {
+ "filename": {
+ "type": "string",
+ "description": "Suggested filename for the download."
+ },
+ "content": {
+ "type": "string",
+ "description": "File content — text or base64-encoded binary."
+ },
+ "mimeType": {
+ "type": "string",
+ "description": "MIME type of the file (e.g. \"image/svg+xml\", \"application/json\")."
+ },
+ "encoding": {
+ "description": "Content encoding. Defaults to \"utf-8\". Use \"base64\" for binary content.",
+ "anyOf": [
+ {
+ "type": "string",
+ "const": "utf-8"
+ },
+ {
+ "type": "string",
+ "const": "base64"
+ }
+ ]
+ }
+ },
+ "required": ["filename", "content", "mimeType"],
+ "additionalProperties": false
+ }
+ },
+ "required": ["method", "params"],
+ "additionalProperties": false
+ },
+ "McpUiDownloadFileResult": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "type": "object",
+ "properties": {
+ "isError": {
+ "description": "True if the download failed (e.g., user cancelled or host denied).",
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": {}
+ },
"McpUiHostCapabilities": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
@@ -97,6 +152,12 @@
"properties": {},
"additionalProperties": false
},
+ "downloadFile": {
+ "description": "Host supports file downloads via ui/download-file.",
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false
+ },
"serverTools": {
"description": "Host can proxy tool calls to the MCP server.",
"type": "object",
@@ -2443,6 +2504,12 @@
"properties": {},
"additionalProperties": false
},
+ "downloadFile": {
+ "description": "Host supports file downloads via ui/download-file.",
+ "type": "object",
+ "properties": {},
+ "additionalProperties": false
+ },
"serverTools": {
"description": "Host can proxy tool calls to the MCP server.",
"type": "object",
diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts
index 95ec2f2..4e7c336 100644
--- a/src/generated/schema.test.ts
+++ b/src/generated/schema.test.ts
@@ -35,6 +35,14 @@ export type McpUiOpenLinkResultSchemaInferredType = z.infer<
typeof generated.McpUiOpenLinkResultSchema
>;
+export type McpUiDownloadFileRequestSchemaInferredType = z.infer<
+ typeof generated.McpUiDownloadFileRequestSchema
+>;
+
+export type McpUiDownloadFileResultSchemaInferredType = z.infer<
+ typeof generated.McpUiDownloadFileResultSchema
+>;
+
export type McpUiMessageResultSchemaInferredType = z.infer<
typeof generated.McpUiMessageResultSchema
>;
@@ -179,6 +187,18 @@ expectType(
expectType(
{} as spec.McpUiOpenLinkResult,
);
+expectType(
+ {} as McpUiDownloadFileRequestSchemaInferredType,
+);
+expectType(
+ {} as spec.McpUiDownloadFileRequest,
+);
+expectType(
+ {} as McpUiDownloadFileResultSchemaInferredType,
+);
+expectType(
+ {} as spec.McpUiDownloadFileResult,
+);
expectType({} as McpUiMessageResultSchemaInferredType);
expectType({} as spec.McpUiMessageResult);
expectType(
diff --git a/src/generated/schema.ts b/src/generated/schema.ts
index 9c75c36..a0cd659 100644
--- a/src/generated/schema.ts
+++ b/src/generated/schema.ts
@@ -161,6 +161,57 @@ export const McpUiOpenLinkResultSchema = z
})
.passthrough();
+/**
+ * @description Request to download a file through the host.
+ *
+ * Sent from the View to the Host when the app wants to trigger a file download.
+ * Since MCP Apps run in sandboxed iframes where direct downloads are blocked,
+ * this provides a host-mediated mechanism for file exports.
+ * The host SHOULD show a confirmation dialog before initiating the download.
+ *
+ * @see {@link app!App.downloadFile `App.downloadFile`} for the method that sends this request
+ */
+export const McpUiDownloadFileRequestSchema = z.object({
+ method: z.literal("ui/download-file"),
+ params: z.object({
+ /** @description Suggested filename for the download. */
+ filename: z.string().describe("Suggested filename for the download."),
+ /** @description File content — text or base64-encoded binary. */
+ content: z
+ .string()
+ .describe("File content \u2014 text or base64-encoded binary."),
+ /** @description MIME type of the file (e.g. "image/svg+xml", "application/json"). */
+ mimeType: z
+ .string()
+ .describe(
+ 'MIME type of the file (e.g. "image/svg+xml", "application/json").',
+ ),
+ /** @description Content encoding. Defaults to "utf-8". Use "base64" for binary content. */
+ encoding: z
+ .union([z.literal("utf-8"), z.literal("base64")])
+ .optional()
+ .describe(
+ 'Content encoding. Defaults to "utf-8". Use "base64" for binary content.',
+ ),
+ }),
+});
+
+/**
+ * @description Result from a file download request.
+ * @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`}
+ */
+export const McpUiDownloadFileResultSchema = z
+ .object({
+ /** @description True if the download failed (e.g., user cancelled or host denied). */
+ isError: z
+ .boolean()
+ .optional()
+ .describe(
+ "True if the download failed (e.g., user cancelled or host denied).",
+ ),
+ })
+ .passthrough();
+
/**
* @description Result from sending a message.
* @see {@link McpUiMessageRequest `McpUiMessageRequest`}
@@ -476,6 +527,11 @@ export const McpUiHostCapabilitiesSchema = z.object({
.object({})
.optional()
.describe("Host supports opening external URLs."),
+ /** @description Host supports file downloads via ui/download-file. */
+ downloadFile: z
+ .object({})
+ .optional()
+ .describe("Host supports file downloads via ui/download-file."),
/** @description Host can proxy tool calls to the MCP server. */
serverTools: z
.object({
diff --git a/src/spec.types.ts b/src/spec.types.ts
index 469ca19..a1fa81c 100644
--- a/src/spec.types.ts
+++ b/src/spec.types.ts
@@ -168,6 +168,44 @@ export interface McpUiOpenLinkResult {
[key: string]: unknown;
}
+/**
+ * @description Request to download a file through the host.
+ *
+ * Sent from the View to the Host when the app wants to trigger a file download.
+ * Since MCP Apps run in sandboxed iframes where direct downloads are blocked,
+ * this provides a host-mediated mechanism for file exports.
+ * The host SHOULD show a confirmation dialog before initiating the download.
+ *
+ * @see {@link app!App.downloadFile `App.downloadFile`} for the method that sends this request
+ */
+export interface McpUiDownloadFileRequest {
+ method: "ui/download-file";
+ params: {
+ /** @description Suggested filename for the download. */
+ filename: string;
+ /** @description File content — text or base64-encoded binary. */
+ content: string;
+ /** @description MIME type of the file (e.g. "image/svg+xml", "application/json"). */
+ mimeType: string;
+ /** @description Content encoding. Defaults to "utf-8". Use "base64" for binary content. */
+ encoding?: "utf-8" | "base64";
+ };
+}
+
+/**
+ * @description Result from a file download request.
+ * @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`}
+ */
+export interface McpUiDownloadFileResult {
+ /** @description True if the download failed (e.g., user cancelled or host denied). */
+ isError?: boolean;
+ /**
+ * Index signature required for MCP SDK `Protocol` class compatibility.
+ * Note: The generated schema uses passthrough() to allow additional properties.
+ */
+ [key: string]: unknown;
+}
+
/**
* @description Request to send a message to the host's chat interface.
* @see {@link app!App.sendMessage `App.sendMessage`} for the method that sends this request
@@ -450,6 +488,8 @@ export interface McpUiHostCapabilities {
experimental?: {};
/** @description Host supports opening external URLs. */
openLinks?: {};
+ /** @description Host supports file downloads via ui/download-file. */
+ downloadFile?: {};
/** @description Host can proxy tool calls to the MCP server. */
serverTools?: {
/** @description Host supports tools/list_changed notifications. */
@@ -746,6 +786,8 @@ export interface McpUiToolMeta {
* ```
*/
export const OPEN_LINK_METHOD: McpUiOpenLinkRequest["method"] = "ui/open-link";
+export const DOWNLOAD_FILE_METHOD: McpUiDownloadFileRequest["method"] =
+ "ui/download-file";
export const MESSAGE_METHOD: McpUiMessageRequest["method"] = "ui/message";
export const SANDBOX_PROXY_READY_METHOD: McpUiSandboxProxyReadyNotification["method"] =
"ui/notifications/sandbox-proxy-ready";
diff --git a/src/types.ts b/src/types.ts
index a4770fd..739da6f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -13,6 +13,7 @@
export {
LATEST_PROTOCOL_VERSION,
OPEN_LINK_METHOD,
+ DOWNLOAD_FILE_METHOD,
MESSAGE_METHOD,
SANDBOX_PROXY_READY_METHOD,
SANDBOX_RESOURCE_READY_METHOD,
@@ -34,6 +35,8 @@ export {
type McpUiHostStyles,
type McpUiOpenLinkRequest,
type McpUiOpenLinkResult,
+ type McpUiDownloadFileRequest,
+ type McpUiDownloadFileResult,
type McpUiMessageRequest,
type McpUiMessageResult,
type McpUiUpdateModelContextRequest,
@@ -68,6 +71,7 @@ export {
import type {
McpUiInitializeRequest,
McpUiOpenLinkRequest,
+ McpUiDownloadFileRequest,
McpUiMessageRequest,
McpUiUpdateModelContextRequest,
McpUiResourceTeardownRequest,
@@ -83,6 +87,7 @@ import type {
McpUiSandboxProxyReadyNotification,
McpUiInitializeResult,
McpUiOpenLinkResult,
+ McpUiDownloadFileResult,
McpUiMessageResult,
McpUiResourceTeardownResult,
McpUiRequestDisplayModeResult,
@@ -96,6 +101,8 @@ export {
McpUiHostStylesSchema,
McpUiOpenLinkRequestSchema,
McpUiOpenLinkResultSchema,
+ McpUiDownloadFileRequestSchema,
+ McpUiDownloadFileResultSchema,
McpUiMessageRequestSchema,
McpUiMessageResultSchema,
McpUiUpdateModelContextRequestSchema,
@@ -159,6 +166,7 @@ import {
export type AppRequest =
| McpUiInitializeRequest
| McpUiOpenLinkRequest
+ | McpUiDownloadFileRequest
| McpUiMessageRequest
| McpUiUpdateModelContextRequest
| McpUiResourceTeardownRequest
@@ -207,6 +215,7 @@ export type AppNotification =
export type AppResult =
| McpUiInitializeResult
| McpUiOpenLinkResult
+ | McpUiDownloadFileResult
| McpUiMessageResult
| McpUiResourceTeardownResult
| McpUiRequestDisplayModeResult
From 375727164117800d2e1975a4a4c9259a28f14f21 Mon Sep 17 00:00:00 2001
From: Anton Pidkuiko MacBook
Date: Fri, 20 Feb 2026 12:00:35 +0000
Subject: [PATCH 003/157] refactor: use MCP resource types for ui/download-file
params
Replace custom {filename, content, mimeType, encoding} params with
standard MCP resource types: EmbeddedResource for inline content and
ResourceLink for host-resolved references.
New params shape:
params: { contents: (EmbeddedResource | ResourceLink)[] }
This reuses existing MCP primitives instead of inventing new ones,
and enables hosts to show resource 'bubbles' or resolve ResourceLink
URIs via resources/read on the MCP server.
---
examples/integration-server/server.ts | 24 +++
examples/integration-server/src/mcp-app.tsx | 38 +++-
scripts/generate-schemas.ts | 2 +
specification/draft/apps.mdx | 42 +++-
src/app-bridge.ts | 46 ++---
src/app.ts | 44 ++++-
src/generated/schema.json | 204 +++++++++++++++++---
src/generated/schema.test.ts | 20 +-
src/generated/schema.ts | 59 +++---
src/spec.types.ts | 12 +-
10 files changed, 366 insertions(+), 125 deletions(-)
diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts
index a44c007..e44689d 100644
--- a/examples/integration-server/server.ts
+++ b/examples/integration-server/server.ts
@@ -16,6 +16,7 @@ const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
: import.meta.dirname;
const RESOURCE_URI = "ui://get-time/mcp-app.html";
+const SAMPLE_DOWNLOAD_URI = "resource:///sample-report.txt";
/**
* Creates a new MCP server instance with tools and resources registered.
@@ -70,5 +71,28 @@ export function createServer(): McpServer {
},
);
+ // Sample downloadable resource — used to demo ResourceLink in ui/download-file
+ server.resource(
+ SAMPLE_DOWNLOAD_URI,
+ SAMPLE_DOWNLOAD_URI,
+ {
+ mimeType: "text/plain",
+ },
+ async (): Promise => {
+ const content = [
+ "Integration Test Server — Sample Report",
+ `Generated: ${new Date().toISOString()}`,
+ "",
+ "This file was downloaded via MCP ResourceLink.",
+ "The host resolved it by calling resources/read on the server.",
+ ].join("\n");
+ return {
+ contents: [
+ { uri: SAMPLE_DOWNLOAD_URI, mimeType: "text/plain", text: content },
+ ],
+ };
+ },
+ );
+
return server;
}
diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx
index 8e4910a..7f0b18c 100644
--- a/examples/integration-server/src/mcp-app.tsx
+++ b/examples/integration-server/src/mcp-app.tsx
@@ -137,8 +137,7 @@ function GetTimeAppInner({
log.info("Open link request", isError ? "rejected" : "accepted");
}, [app, linkUrl]);
- const canDownload =
- app.getHostCapabilities()?.downloadFile !== undefined;
+ const canDownload = app.getHostCapabilities()?.downloadFile !== undefined;
const handleDownloadFile = useCallback(async () => {
const sampleContent = JSON.stringify(
@@ -148,13 +147,35 @@ function GetTimeAppInner({
);
log.info("Requesting file download...");
const { isError } = await app.downloadFile({
- filename: "export.json",
- content: sampleContent,
- mimeType: "application/json",
+ contents: [
+ {
+ type: "resource",
+ resource: {
+ uri: "file:///export.json",
+ mimeType: "application/json",
+ text: sampleContent,
+ },
+ },
+ ],
});
log.info("Download", isError ? "rejected" : "accepted");
}, [app, serverTime]);
+ const handleDownloadLink = useCallback(async () => {
+ log.info("Requesting resource link download...");
+ const { isError } = await app.downloadFile({
+ contents: [
+ {
+ type: "resource_link",
+ uri: "resource:///sample-report.txt",
+ name: "sample-report.txt",
+ mimeType: "text/plain",
+ },
+ ],
+ });
+ log.info("Resource link download", isError ? "rejected" : "accepted");
+ }, [app]);
+
return (
-
Export current server time as JSON file
-
+
Download file via EmbeddedResource or ResourceLink
+
+
+
+
)}
diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts
index 513b2bf..858ab91 100644
--- a/scripts/generate-schemas.ts
+++ b/scripts/generate-schemas.ts
@@ -70,8 +70,10 @@ const JSON_SCHEMA_OUTPUT_FILE = join(GENERATED_DIR, "schema.json");
const EXTERNAL_TYPE_SCHEMAS = [
"ContentBlockSchema",
"CallToolResultSchema",
+ "EmbeddedResourceSchema",
"ImplementationSchema",
"RequestIdSchema",
+ "ResourceLinkSchema",
"ToolSchema",
];
diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx
index d12ba12..b5fbb62 100644
--- a/specification/draft/apps.mdx
+++ b/specification/draft/apps.mdx
@@ -1018,16 +1018,39 @@ Host SHOULD open the URL in the user's default browser or a new tab.
`ui/download-file` - Request host to download a file
```typescript
-// Request
+// Request (EmbeddedResource — inline content)
{
jsonrpc: "2.0",
id: 1,
method: "ui/download-file",
params: {
- filename: string, // Suggested filename
- content: string, // File content (text or base64-encoded)
- mimeType: string, // MIME type (e.g. "image/svg+xml", "application/json")
- encoding?: "utf-8" | "base64" // Content encoding, defaults to "utf-8"
+ contents: [
+ {
+ type: "resource",
+ resource: {
+ uri: "file:///export.json", // Used for suggested filename
+ mimeType: "application/json",
+ text: "{ ... }" // Text content (or `blob` for base64 binary)
+ }
+ }
+ ]
+ }
+}
+
+// Request (ResourceLink — host fetches)
+{
+ jsonrpc: "2.0",
+ id: 1,
+ method: "ui/download-file",
+ params: {
+ contents: [
+ {
+ type: "resource_link",
+ uri: "https://api.example.com/reports/q4.pdf",
+ name: "Q4 Report",
+ mimeType: "application/pdf"
+ }
+ ]
}
}
@@ -1051,12 +1074,15 @@ Host SHOULD open the URL in the user's default browser or a new tab.
MCP Apps run in sandboxed iframes where direct file downloads are blocked (`allow-downloads` is not set). `ui/download-file` provides a host-mediated mechanism for apps to offer file exports — useful for visualization tools (SVG/PNG export), document editors, data analysis tools, and any app that produces downloadable artifacts.
+The `contents` array uses standard MCP resource types (`EmbeddedResource` and `ResourceLink`), avoiding custom content formats. For `EmbeddedResource`, content is inline via `text` (UTF-8) or `blob` (base64). For `ResourceLink`, the host can retrieve the content directly from the URI.
+
Host behavior:
* Host SHOULD show a confirmation dialog before initiating the download.
-* Host SHOULD use the `filename` as the suggested filename for the download.
-* For `encoding: "base64"`, host MUST decode the content from base64 before creating the file.
+* For `EmbeddedResource`, host SHOULD derive the filename from the last segment of `resource.uri`.
+* For `EmbeddedResource` with `blob`, host MUST decode the content from base64 before creating the file.
+* For `ResourceLink`, host MAY fetch the resource on behalf of the app or open the URI directly.
* Host MAY reject the download based on security policy, file size limits, or user preferences.
-* Host SHOULD sanitize the filename to prevent path traversal.
+* Host SHOULD sanitize filenames to prevent path traversal.
`ui/message` - Send message content to the host's chat interface
diff --git a/src/app-bridge.ts b/src/app-bridge.ts
index de7d876..793782b 100644
--- a/src/app-bridge.ts
+++ b/src/app-bridge.ts
@@ -621,35 +621,37 @@ export class AppBridge extends Protocol<
* Register a handler for file download requests from the View.
*
* The View sends `ui/download-file` requests when the user wants to
- * download a file. The host should show a confirmation dialog and then
- * trigger the download.
+ * download a file. The params contain an array of MCP resource content
+ * items — either `EmbeddedResource` (inline data) or `ResourceLink`
+ * (URI the host can fetch). The host should show a confirmation dialog
+ * and then trigger the download.
*
* @param callback - Handler that receives download params and returns a result
- * - `params.filename` - Suggested filename for the download
- * - `params.content` - File content (text or base64-encoded)
- * - `params.mimeType` - MIME type of the file
- * - `params.encoding` - Content encoding ("utf-8" or "base64", defaults to "utf-8")
+ * - `params.contents` - Array of `EmbeddedResource` or `ResourceLink` items
* - `extra` - Request metadata (abort signal, session info)
* - Returns: `Promise` with optional `isError` flag
*
* @example
* ```ts
- * bridge.ondownloadfile = async ({ filename, content, mimeType, encoding }, extra) => {
- * const confirmed = await showDialog({
- * message: `Download "${filename}"?`,
- * buttons: ["Download", "Cancel"],
- * });
- * if (!confirmed) return { isError: true };
- *
- * const blob = encoding === "base64"
- * ? new Blob([Uint8Array.from(atob(content), c => c.charCodeAt(0))], { type: mimeType })
- * : new Blob([content], { type: mimeType });
- * const url = URL.createObjectURL(blob);
- * const link = document.createElement("a");
- * link.href = url;
- * link.download = filename;
- * link.click();
- * URL.revokeObjectURL(url);
+ * bridge.ondownloadfile = async ({ contents }, extra) => {
+ * for (const item of contents) {
+ * if (item.type === "resource") {
+ * // EmbeddedResource — inline content
+ * const res = item.resource;
+ * const blob = res.blob
+ * ? new Blob([Uint8Array.from(atob(res.blob), c => c.charCodeAt(0))], { type: res.mimeType })
+ * : new Blob([res.text ?? ""], { type: res.mimeType });
+ * const url = URL.createObjectURL(blob);
+ * const link = document.createElement("a");
+ * link.href = url;
+ * link.download = res.uri.split("/").pop() ?? "download";
+ * link.click();
+ * URL.revokeObjectURL(url);
+ * } else if (item.type === "resource_link") {
+ * // ResourceLink — host fetches or opens directly
+ * window.open(item.uri, "_blank");
+ * }
+ * }
* return {};
* };
* ```
diff --git a/src/app.ts b/src/app.ts
index ebf7e81..8e133d6 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -939,32 +939,56 @@ export class App extends Protocol {
* this provides a host-mediated mechanism for file exports. The host will
* typically show a confirmation dialog before initiating the download.
*
- * @param params - File content, filename, MIME type, and optional encoding
+ * Uses standard MCP resource types: `EmbeddedResource` for inline content
+ * and `ResourceLink` for content the host can fetch directly.
+ *
+ * @param params - Resource contents to download
* @param options - Request options (timeout, etc.)
* @returns Result with `isError: true` if the host denied the request (e.g., user cancelled)
*
* @throws {Error} If the request times out or the connection is lost
*
- * @example Download a JSON export
+ * @example Download a JSON file (embedded text resource)
* ```ts
* const data = JSON.stringify({ items: selectedItems }, null, 2);
* const { isError } = await app.downloadFile({
- * filename: "export.json",
- * content: data,
- * mimeType: "application/json",
+ * contents: [{
+ * type: "resource",
+ * resource: {
+ * uri: "file:///export.json",
+ * mimeType: "application/json",
+ * text: data,
+ * },
+ * }],
* });
* if (isError) {
* console.warn("Download denied or cancelled");
* }
* ```
*
- * @example Download binary content (base64)
+ * @example Download binary content (embedded blob resource)
* ```ts
* const { isError } = await app.downloadFile({
- * filename: "image.png",
- * content: base64EncodedPng,
- * mimeType: "image/png",
- * encoding: "base64",
+ * contents: [{
+ * type: "resource",
+ * resource: {
+ * uri: "file:///image.png",
+ * mimeType: "image/png",
+ * blob: base64EncodedPng,
+ * },
+ * }],
+ * });
+ * ```
+ *
+ * @example Download via resource link (host fetches)
+ * ```ts
+ * const { isError } = await app.downloadFile({
+ * contents: [{
+ * type: "resource_link",
+ * uri: "https://api.example.com/reports/q4.pdf",
+ * name: "Q4 Report",
+ * mimeType: "application/pdf",
+ * }],
* });
* ```
*
diff --git a/src/generated/schema.json b/src/generated/schema.json
index 9cc87ae..08e229a 100644
--- a/src/generated/schema.json
+++ b/src/generated/schema.json
@@ -92,33 +92,187 @@
"params": {
"type": "object",
"properties": {
- "filename": {
- "type": "string",
- "description": "Suggested filename for the download."
- },
- "content": {
- "type": "string",
- "description": "File content — text or base64-encoded binary."
- },
- "mimeType": {
- "type": "string",
- "description": "MIME type of the file (e.g. \"image/svg+xml\", \"application/json\")."
- },
- "encoding": {
- "description": "Content encoding. Defaults to \"utf-8\". Use \"base64\" for binary content.",
- "anyOf": [
- {
- "type": "string",
- "const": "utf-8"
- },
- {
- "type": "string",
- "const": "base64"
- }
- ]
+ "contents": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "const": "resource"
+ },
+ "resource": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "uri": {
+ "type": "string"
+ },
+ "mimeType": {
+ "type": "string"
+ },
+ "_meta": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "text": {
+ "type": "string"
+ }
+ },
+ "required": ["uri", "text"],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "uri": {
+ "type": "string"
+ },
+ "mimeType": {
+ "type": "string"
+ },
+ "_meta": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ },
+ "blob": {
+ "type": "string"
+ }
+ },
+ "required": ["uri", "blob"],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "annotations": {
+ "type": "object",
+ "properties": {
+ "audience": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["user", "assistant"]
+ }
+ },
+ "priority": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "lastModified": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
+ }
+ },
+ "additionalProperties": false
+ },
+ "_meta": {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {}
+ }
+ },
+ "required": ["type", "resource"],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "icons": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "src": {
+ "type": "string"
+ },
+ "mimeType": {
+ "type": "string"
+ },
+ "sizes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "theme": {
+ "type": "string",
+ "enum": ["light", "dark"]
+ }
+ },
+ "required": ["src"],
+ "additionalProperties": false
+ }
+ },
+ "uri": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "mimeType": {
+ "type": "string"
+ },
+ "annotations": {
+ "type": "object",
+ "properties": {
+ "audience": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["user", "assistant"]
+ }
+ },
+ "priority": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1
+ },
+ "lastModified": {
+ "type": "string",
+ "format": "date-time",
+ "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
+ }
+ },
+ "additionalProperties": false
+ },
+ "_meta": {
+ "type": "object",
+ "properties": {},
+ "additionalProperties": {}
+ },
+ "type": {
+ "type": "string",
+ "const": "resource_link"
+ }
+ },
+ "required": ["name", "uri", "type"],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "description": "Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types."
}
},
- "required": ["filename", "content", "mimeType"],
+ "required": ["contents"],
"additionalProperties": false
}
},
diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts
index 4e7c336..47e5978 100644
--- a/src/generated/schema.test.ts
+++ b/src/generated/schema.test.ts
@@ -35,10 +35,6 @@ export type McpUiOpenLinkResultSchemaInferredType = z.infer<
typeof generated.McpUiOpenLinkResultSchema
>;
-export type McpUiDownloadFileRequestSchemaInferredType = z.infer<
- typeof generated.McpUiDownloadFileRequestSchema
->;
-
export type McpUiDownloadFileResultSchemaInferredType = z.infer<
typeof generated.McpUiDownloadFileResultSchema
>;
@@ -131,6 +127,10 @@ export type McpUiClientCapabilitiesSchemaInferredType = z.infer<
typeof generated.McpUiClientCapabilitiesSchema
>;
+export type McpUiDownloadFileRequestSchemaInferredType = z.infer<
+ typeof generated.McpUiDownloadFileRequestSchema
+>;
+
export type McpUiMessageRequestSchemaInferredType = z.infer<
typeof generated.McpUiMessageRequestSchema
>;
@@ -187,12 +187,6 @@ expectType(
expectType(
{} as spec.McpUiOpenLinkResult,
);
-expectType(
- {} as McpUiDownloadFileRequestSchemaInferredType,
-);
-expectType(
- {} as spec.McpUiDownloadFileRequest,
-);
expectType(
{} as McpUiDownloadFileResultSchemaInferredType,
);
@@ -307,6 +301,12 @@ expectType(
expectType(
{} as spec.McpUiClientCapabilities,
);
+expectType(
+ {} as McpUiDownloadFileRequestSchemaInferredType,
+);
+expectType(
+ {} as spec.McpUiDownloadFileRequest,
+);
expectType(
{} as McpUiMessageRequestSchemaInferredType,
);
diff --git a/src/generated/schema.ts b/src/generated/schema.ts
index a0cd659..e6ff4ba 100644
--- a/src/generated/schema.ts
+++ b/src/generated/schema.ts
@@ -5,8 +5,10 @@ import { z } from "zod";
import {
ContentBlockSchema,
CallToolResultSchema,
+ EmbeddedResourceSchema,
ImplementationSchema,
RequestIdSchema,
+ ResourceLinkSchema,
ToolSchema,
} from "@modelcontextprotocol/sdk/types.js";
@@ -161,41 +163,6 @@ export const McpUiOpenLinkResultSchema = z
})
.passthrough();
-/**
- * @description Request to download a file through the host.
- *
- * Sent from the View to the Host when the app wants to trigger a file download.
- * Since MCP Apps run in sandboxed iframes where direct downloads are blocked,
- * this provides a host-mediated mechanism for file exports.
- * The host SHOULD show a confirmation dialog before initiating the download.
- *
- * @see {@link app!App.downloadFile `App.downloadFile`} for the method that sends this request
- */
-export const McpUiDownloadFileRequestSchema = z.object({
- method: z.literal("ui/download-file"),
- params: z.object({
- /** @description Suggested filename for the download. */
- filename: z.string().describe("Suggested filename for the download."),
- /** @description File content — text or base64-encoded binary. */
- content: z
- .string()
- .describe("File content \u2014 text or base64-encoded binary."),
- /** @description MIME type of the file (e.g. "image/svg+xml", "application/json"). */
- mimeType: z
- .string()
- .describe(
- 'MIME type of the file (e.g. "image/svg+xml", "application/json").',
- ),
- /** @description Content encoding. Defaults to "utf-8". Use "base64" for binary content. */
- encoding: z
- .union([z.literal("utf-8"), z.literal("base64")])
- .optional()
- .describe(
- 'Content encoding. Defaults to "utf-8". Use "base64" for binary content.',
- ),
- }),
-});
-
/**
* @description Result from a file download request.
* @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`}
@@ -755,6 +722,28 @@ export const McpUiClientCapabilitiesSchema = z.object({
),
});
+/**
+ * @description Request to download a file through the host.
+ *
+ * Sent from the View to the Host when the app wants to trigger a file download.
+ * Since MCP Apps run in sandboxed iframes where direct downloads are blocked,
+ * this provides a host-mediated mechanism for file exports.
+ * The host SHOULD show a confirmation dialog before initiating the download.
+ *
+ * @see {@link app!App.downloadFile `App.downloadFile`} for the method that sends this request
+ */
+export const McpUiDownloadFileRequestSchema = z.object({
+ method: z.literal("ui/download-file"),
+ params: z.object({
+ /** @description Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types. */
+ contents: z
+ .array(z.union([EmbeddedResourceSchema, ResourceLinkSchema]))
+ .describe(
+ "Resource contents to download \u2014 embedded (inline data) or linked (host fetches). Uses standard MCP resource types.",
+ ),
+ }),
+});
+
/**
* @description Request to send a message to the host's chat interface.
* @see {@link app!App.sendMessage `App.sendMessage`} for the method that sends this request
diff --git a/src/spec.types.ts b/src/spec.types.ts
index a1fa81c..20987d3 100644
--- a/src/spec.types.ts
+++ b/src/spec.types.ts
@@ -13,8 +13,10 @@
import type {
CallToolResult,
ContentBlock,
+ EmbeddedResource,
Implementation,
RequestId,
+ ResourceLink,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
@@ -181,14 +183,8 @@ export interface McpUiOpenLinkResult {
export interface McpUiDownloadFileRequest {
method: "ui/download-file";
params: {
- /** @description Suggested filename for the download. */
- filename: string;
- /** @description File content — text or base64-encoded binary. */
- content: string;
- /** @description MIME type of the file (e.g. "image/svg+xml", "application/json"). */
- mimeType: string;
- /** @description Content encoding. Defaults to "utf-8". Use "base64" for binary content. */
- encoding?: "utf-8" | "base64";
+ /** @description Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types. */
+ contents: (EmbeddedResource | ResourceLink)[];
};
}
From c49b4b5e5bcc21628fc670b9498a6307e2c9ab46 Mon Sep 17 00:00:00 2001
From: "Hryhorii (Greg) Avcharov"
Date: Sun, 22 Feb 2026 02:21:22 +0100
Subject: [PATCH 004/157] fix: add audio/video support for basic-host (#460)
---
examples/basic-host/serve.ts | 2 ++
1 file changed, 2 insertions(+)
diff --git a/examples/basic-host/serve.ts b/examples/basic-host/serve.ts
index 03c3a0b..bc3fbff 100644
--- a/examples/basic-host/serve.ts
+++ b/examples/basic-host/serve.ts
@@ -82,6 +82,8 @@ function buildCspHeader(csp?: McpUiResourceCsp): string {
`img-src 'self' data: blob: ${resourceDomains}`.trim(),
// Fonts: same-origin + data/blob URIs + specified domains
`font-src 'self' data: blob: ${resourceDomains}`.trim(),
+ // Media (audio/video): same-origin + data/blob URIs + specified domains
+ `media-src 'self' data: blob: ${resourceDomains}`.trim(),
// Network requests: same-origin + specified API/tile domains
`connect-src 'self' ${connectDomains}`.trim(),
// Workers: same-origin + blob (dynamic workers) + specified domains
From 6e40513dbf1b0a48fc2b201930b6e81a8d24b7a6 Mon Sep 17 00:00:00 2001
From: Conor Kelly <108370777+conorkel@users.noreply.github.com>
Date: Sat, 21 Feb 2026 18:09:18 -0800
Subject: [PATCH 005/157] Adding ChatGPT to the readme
Added ChatGPT as a supporting client in the readme. Also updated links to re-direct to the relevant MCP Apps docs on respective client pages
---
README.md | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index b7f872a..9427f4b 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
MCP Apps
- Build interactive UIs for MCP tools — charts, forms, dashboards — that render inline in any compliant chat client.
+ Build interactive UIs for MCP tools — charts, forms, dashboards — that render inline in Claude, ChatGPT and any other compliant chat client.
Why
·
@@ -80,12 +80,12 @@ it will guide you through the rest.
## Supported Clients
-
-
-
-
-
-
+
+
+
+
+
+
> [!NOTE]
@@ -97,7 +97,7 @@ it will guide you through the rest.
## Why MCP Apps?
MCP tools return text and structured data. That works for many cases, but not
-when you need an interactive UI, like a chart, form, or video player.
+when you need an interactive UI, like a chart, form, design canvas or video player.
MCP Apps provide a standardized way to deliver interactive UIs from MCP servers.
Your UI renders inline in the conversation, in context, in any compliant host.
From f07bdfbe274e5ef6e3d8f0d73cea7d8594ea88dd Mon Sep 17 00:00:00 2001
From: Anton Pidkuiko MacBook
Date: Mon, 23 Feb 2026 13:08:34 +0000
Subject: [PATCH 006/157] docs: remove redundant resource-type bullets from
download-file spec
Remove bullets that restate standard EmbeddedResource/ResourceLink behavior
already defined in the MCP resource spec. Keep download-specific host behavior
(confirmation dialog, filename derivation, security policy, path sanitization).
---
specification/draft/apps.mdx | 2 --
1 file changed, 2 deletions(-)
diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx
index b5fbb62..e7c343b 100644
--- a/specification/draft/apps.mdx
+++ b/specification/draft/apps.mdx
@@ -1079,8 +1079,6 @@ The `contents` array uses standard MCP resource types (`EmbeddedResource` and `R
Host behavior:
* Host SHOULD show a confirmation dialog before initiating the download.
* For `EmbeddedResource`, host SHOULD derive the filename from the last segment of `resource.uri`.
-* For `EmbeddedResource` with `blob`, host MUST decode the content from base64 before creating the file.
-* For `ResourceLink`, host MAY fetch the resource on behalf of the app or open the URI directly.
* Host MAY reject the download based on security policy, file size limits, or user preferences.
* Host SHOULD sanitize filenames to prevent path traversal.
From 219b4b16058b3648c2949af37b7ce783c5bdd19c Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Mon, 23 Feb 2026 13:25:25 -0800
Subject: [PATCH 007/157] fix(pdf-server): remove page-loading overlay (#493)
* fix(pdf-server): remove page-loading overlay
The overlay had multiple issues:
- Hardcoded white background broke dark mode
- Could get stuck visible, breaking sizing/layout
- Added little value since previous page stays visible during render
and the 150ms delay meant most renders completed before it showed
* prettier:fix
---
examples/pdf-server/src/mcp-app.css | 40 -----------------------------
examples/pdf-server/src/mcp-app.ts | 38 ---------------------------
package-lock.json | 3 +++
3 files changed, 3 insertions(+), 78 deletions(-)
diff --git a/examples/pdf-server/src/mcp-app.css b/examples/pdf-server/src/mcp-app.css
index c385365..008b22a 100644
--- a/examples/pdf-server/src/mcp-app.css
+++ b/examples/pdf-server/src/mcp-app.css
@@ -261,46 +261,6 @@ body {
--min-font-size-inv: calc(1 / var(--min-font-size, 1));
}
-/* Page Loading Overlay - shown while page data is being fetched */
-.page-loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- display: flex;
- align-items: center;
- justify-content: center;
- background: rgba(255, 255, 255, 0.9);
- border-radius: 4px;
- z-index: 10;
-}
-
-.page-loading-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.75rem;
- padding: 1.5rem 2rem;
- background: var(--bg000);
- border: 1px solid var(--bg200);
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
-}
-
-.page-loading-spinner {
- width: 32px;
- height: 32px;
- border: 3px solid var(--bg200);
- border-top-color: var(--text100);
- border-radius: 50%;
- animation: spin 0.8s linear infinite;
-}
-
-.page-loading-text {
- font-size: 0.85rem;
- color: var(--text100);
-}
.text-layer :is(span, br) {
color: transparent;
diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts
index 46fbafb..4278d92 100644
--- a/examples/pdf-server/src/mcp-app.ts
+++ b/examples/pdf-server/src/mcp-app.ts
@@ -624,9 +624,6 @@ async function renderPage() {
isRendering = true;
pendingPage = null;
- // Show loading overlay while page data is being fetched
- showPageLoadingOverlay();
-
try {
const pageToRender = currentPage;
const page = await pdfDocument.getPage(pageToRender);
@@ -719,7 +716,6 @@ async function renderPage() {
log.error("Error rendering page:", err);
showError(`Failed to render page ${currentPage}`);
} finally {
- hidePageLoadingOverlay();
preloadPaused = false;
isRendering = false;
@@ -1013,40 +1009,6 @@ type RangeResult = { bytes: Uint8Array; totalBytes: number };
const rangeCache = new Map();
const inflightRequests = new Map>();
-// Page loading overlay state — shown after a delay so cached pages never flash it
-let pageLoadingOverlay: HTMLElement | null = null;
-let pageLoadingTimer: ReturnType | null = null;
-const PAGE_LOADING_DELAY_MS = 150;
-
-function showPageLoadingOverlay() {
- if (pageLoadingOverlay || pageLoadingTimer) return; // Already showing or scheduled
-
- pageLoadingTimer = setTimeout(() => {
- pageLoadingTimer = null;
- const overlay = document.createElement("div");
- overlay.className = "page-loading-overlay";
- overlay.innerHTML = `
-
-
- Loading page...
-
- `;
- canvasContainerEl.appendChild(overlay);
- pageLoadingOverlay = overlay;
- }, PAGE_LOADING_DELAY_MS);
-}
-
-function hidePageLoadingOverlay() {
- if (pageLoadingTimer) {
- clearTimeout(pageLoadingTimer);
- pageLoadingTimer = null;
- }
- if (pageLoadingOverlay) {
- pageLoadingOverlay.remove();
- pageLoadingOverlay = null;
- }
-}
-
// Max bytes per server request (must match server's MAX_CHUNK_BYTES)
const MAX_CHUNK_BYTES = 512 * 1024;
diff --git a/package-lock.json b/package-lock.json
index fde38a3..f97924a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -527,6 +527,9 @@
"@modelcontextprotocol/sdk": "^1.24.0",
"zod": "^4.1.13"
},
+ "bin": {
+ "mcp-server-debug": "dist/index.js"
+ },
"devDependencies": {
"@types/cors": "^2.8.19",
"@types/express": "^5.0.0",
From 4d9ba1ea0e583ff573bb18166512640d02abefbd Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Mon, 23 Feb 2026 13:27:15 -0800
Subject: [PATCH 008/157] pdf-server: remove domain allowlist, require HTTPS
only (#487)
Replace the hardcoded list of allowed remote origins with a simple
HTTPS protocol check. Any HTTPS URL is now accepted for remote PDFs.
Local file allowlisting remains unchanged.
---
examples/pdf-server/main.ts | 4 ----
examples/pdf-server/server.ts | 43 ++++++-----------------------------
2 files changed, 7 insertions(+), 40 deletions(-)
diff --git a/examples/pdf-server/main.ts b/examples/pdf-server/main.ts
index 310a312..50dc43a 100644
--- a/examples/pdf-server/main.ts
+++ b/examples/pdf-server/main.ts
@@ -19,7 +19,6 @@ import {
pathToFileUrl,
fileUrlToPath,
allowedLocalFiles,
- allowedRemoteOrigins,
DEFAULT_PDF,
} from "./server.js";
@@ -132,9 +131,6 @@ async function main() {
}
console.error(`[pdf-server] Ready (${urls.length} URL(s) configured)`);
- console.error(
- `[pdf-server] Allowed origins: ${[...allowedRemoteOrigins].join(", ")}`,
- );
if (stdio) {
await startStdioServer(createServer);
diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts
index f2118e4..e68fdde 100644
--- a/examples/pdf-server/server.ts
+++ b/examples/pdf-server/server.ts
@@ -2,7 +2,7 @@
* PDF MCP Server
*
* An MCP server that displays PDFs in an interactive viewer.
- * Supports local files and remote URLs from academic sources (arxiv, biorxiv, etc).
+ * Supports local files and remote HTTPS URLs.
*
* Tools:
* - list_pdfs: List available PDFs
@@ -44,26 +44,6 @@ export const CACHE_MAX_LIFETIME_MS = 60_000; // 60 seconds
/** Max size for cached PDFs (defensive limit to prevent memory exhaustion) */
export const CACHE_MAX_PDF_SIZE_BYTES = 50 * 1024 * 1024; // 50MB
-/** Allowed remote origins (security allowlist) */
-export const allowedRemoteOrigins = new Set([
- "https://agrirxiv.org",
- "https://arxiv.org",
- "https://chemrxiv.org",
- "https://edarxiv.org",
- "https://engrxiv.org",
- "https://hal.science",
- "https://osf.io",
- "https://psyarxiv.com",
- "https://ssrn.com",
- "https://www.biorxiv.org",
- "https://www.eartharxiv.org",
- "https://www.medrxiv.org",
- "https://www.preprints.org",
- "https://www.researchsquare.com",
- "https://www.sportarxiv.org",
- "https://zenodo.org",
-]);
-
/** Allowed local file paths (populated from CLI args) */
export const allowedLocalFiles = new Set();
@@ -134,14 +114,11 @@ export function validateUrl(url: string): { valid: boolean; error?: string } {
return { valid: true };
}
- // Remote URL - check against allowed origins
+ // Remote URL - require HTTPS
try {
const parsed = new URL(url);
- const origin = `${parsed.protocol}//${parsed.hostname}`;
- if (
- ![...allowedRemoteOrigins].some((allowed) => origin.startsWith(allowed))
- ) {
- return { valid: false, error: `Origin not allowed: ${origin}` };
+ if (parsed.protocol !== "https:") {
+ return { valid: false, error: `Only HTTPS URLs are allowed: ${url}` };
}
return { valid: true };
} catch {
@@ -417,7 +394,7 @@ export function createServer(): McpServer {
// Create session-local cache (isolated per server instance)
const { readPdfRange } = createPdfCache();
- // Tool: list_pdfs - List available PDFs (local files + allowed origins)
+ // Tool: list_pdfs - List available PDFs
server.tool(
"list_pdfs",
"List available PDFs that can be displayed",
@@ -443,7 +420,7 @@ export function createServer(): McpServer {
);
}
parts.push(
- `Remote PDFs from ${[...allowedRemoteOrigins].join(", ")} can also be loaded dynamically.`,
+ `Any remote PDF accessible via HTTPS can also be loaded dynamically.`,
);
return {
@@ -451,7 +428,6 @@ export function createServer(): McpServer {
structuredContent: {
localFiles: pdfs.filter((p) => p.type === "local").map((p) => p.url),
allowedDirectories: [...allowedLocalDirs],
- allowedOrigins: [...allowedRemoteOrigins],
},
};
},
@@ -531,11 +507,6 @@ export function createServer(): McpServer {
},
);
- // Build allowed domains list for tool description (strip https:// and www.)
- const allowedDomains = [...allowedRemoteOrigins]
- .map((origin) => origin.replace(/^https?:\/\/(www\.)?/, ""))
- .join(", ");
-
// Tool: display_pdf - Show interactive viewer
registerAppTool(
server,
@@ -547,7 +518,7 @@ export function createServer(): McpServer {
Accepts:
- Local files explicitly added to the server (use list_pdfs to see available files)
- Local files under directories provided by the client as MCP roots
-- Remote PDFs from: ${allowedDomains}`,
+- Any remote PDF accessible via HTTPS`,
inputSchema: {
url: z.string().default(DEFAULT_PDF).describe("PDF URL"),
page: z.number().min(1).default(1).describe("Initial page"),
From 47e437b63190d2a140c4dcd4aa1a50d84006ac7f Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Tue, 24 Feb 2026 00:31:37 +0000
Subject: [PATCH 009/157] ci: add missing examples to deploy workflows (#494)
Add basic-server-preact, basic-server-solid, basic-server-svelte,
basic-server-vue, and debug-server to the publish workflows so they
are included in pkg-pr-new and npm deployments.
Co-authored-by: Claude Opus 4.6
---
.github/workflows/npm-publish.yml | 1 +
.github/workflows/publish.yml | 5 +++++
2 files changed, 6 insertions(+)
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index 280927c..5852443 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -107,6 +107,7 @@ jobs:
- budget-allocator-server
- cohort-heatmap-server
- customer-segmentation-server
+ - debug-server
- map-server
- pdf-server
- scenario-modeler-server
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 926710e..5f1d3ac 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -24,11 +24,16 @@ jobs:
- run: |
npx pkg-pr-new publish \
. \
+ ./examples/basic-server-preact \
./examples/basic-server-react \
+ ./examples/basic-server-solid \
+ ./examples/basic-server-svelte \
./examples/basic-server-vanillajs \
+ ./examples/basic-server-vue \
./examples/budget-allocator-server \
./examples/cohort-heatmap-server \
./examples/customer-segmentation-server \
+ ./examples/debug-server \
./examples/map-server \
./examples/pdf-server \
./examples/scenario-modeler-server \
From 3de035dc8d5f6e60f83daf68a70e87a06fa75715 Mon Sep 17 00:00:00 2001
From: Bryan Thompson
Date: Mon, 23 Feb 2026 18:51:43 -0600
Subject: [PATCH 010/157] Add MCPB packaging and Claude Code plugin for
pdf-server (#491)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Add MCPB packaging and Claude Code plugin for pdf-server
Two distribution paths for the pdf-server:
1. **MCPB bundle** (Claude Desktop MCP Directory):
- manifest.json: v0.3 manifest with tools, compatibility, privacy
- .mcpbignore: excludes source/tests from bundle
- icon.png: 64x64 PDF document icon
2. **Claude Code plugin** (plugin/):
- .mcp.json: declares local stdio server via npx
- commands: /pdf-research:read, /pdf-research:summarize
- skills: pdf-reading skill for document navigation
- Tested end-to-end: MCP server connects, all tools available
The plugin uses a local stdio MCP server pattern — the first known
instance in a knowledge-work plugin (all existing plugins use remote
HTTP connectors).
Co-Authored-By: Claude Opus 4.6 (1M context)
* Run prettier:fix to satisfy CI formatting checks
Co-Authored-By: Claude Opus 4.6 (1M context)
---------
Co-authored-by: Claude Opus 4.6 (1M context)
---
examples/pdf-server/.mcpbignore | 21 ++++++++
examples/pdf-server/icon.png | Bin 0 -> 549 bytes
examples/pdf-server/manifest.json | 48 ++++++++++++++++++
.../plugin/.claude-plugin/plugin.json | 8 +++
examples/pdf-server/plugin/.mcp.json | 8 +++
examples/pdf-server/plugin/CONNECTORS.md | 16 ++++++
examples/pdf-server/plugin/README.md | 40 +++++++++++++++
examples/pdf-server/plugin/commands/read.md | 21 ++++++++
.../pdf-server/plugin/commands/summarize.md | 23 +++++++++
.../plugin/skills/pdf-reading/SKILL.md | 35 +++++++++++++
10 files changed, 220 insertions(+)
create mode 100644 examples/pdf-server/.mcpbignore
create mode 100644 examples/pdf-server/icon.png
create mode 100644 examples/pdf-server/manifest.json
create mode 100644 examples/pdf-server/plugin/.claude-plugin/plugin.json
create mode 100644 examples/pdf-server/plugin/.mcp.json
create mode 100644 examples/pdf-server/plugin/CONNECTORS.md
create mode 100644 examples/pdf-server/plugin/README.md
create mode 100644 examples/pdf-server/plugin/commands/read.md
create mode 100644 examples/pdf-server/plugin/commands/summarize.md
create mode 100644 examples/pdf-server/plugin/skills/pdf-reading/SKILL.md
diff --git a/examples/pdf-server/.mcpbignore b/examples/pdf-server/.mcpbignore
new file mode 100644
index 0000000..fe11c64
--- /dev/null
+++ b/examples/pdf-server/.mcpbignore
@@ -0,0 +1,21 @@
+# Source files (built output in dist/ is what we need)
+*.ts
+tsconfig*.json
+vite.config.ts
+src/
+
+# Tests
+*.test.*
+
+# Development assets
+screenshot.png
+grid-cell.png
+pdf-icon.svg
+
+# Plugin directory (separate distribution path)
+plugin/
+
+# Git and CI
+.git/
+.github/
+.gitignore
diff --git a/examples/pdf-server/icon.png b/examples/pdf-server/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..fb7ea3ec7e35192490d43a91bb5e5107d847191e
GIT binary patch
literal 549
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|Tv8)E(|mmy
zw18|52FCVG1{RPKAeI7R1_tH@j10^`nh_+nfC(<^uz(rC1}St4blLz^*6-=!7*fIb
zcD7+&i-CyC|7DK^L^vNQbm%QVHBY(S@k6e-_ZKp?7*=q$N0@QwFeok75tzY{(s+Q4fv-`JL4--0L4qZXL4e~8
zgIwDOE(W=#f+&X5n<7F(!$e$lE__z`>$js~O8K`n*4Y=9F3Vi=+KT%-z{vPvkUxyvMQMb35@icFr_P_Q!_mtmfGKC+kOQ>JGd);NVg#CJdRxv!7S){4O
z81whg?og%=K08IGGOVk6cvp+DVp&X=7en~|4|!7=Y=ZS%RU1~vS8VrUn5Q{?k=18.0.0" }
+ },
+ "privacy_policies": ["https://www.anthropic.com/privacy"]
+}
diff --git a/examples/pdf-server/plugin/.claude-plugin/plugin.json b/examples/pdf-server/plugin/.claude-plugin/plugin.json
new file mode 100644
index 0000000..f7442e7
--- /dev/null
+++ b/examples/pdf-server/plugin/.claude-plugin/plugin.json
@@ -0,0 +1,8 @@
+{
+ "name": "pdf-research",
+ "version": "0.1.0",
+ "description": "Read, navigate, and analyze PDF documents — academic papers, reports, and local files. Uses a local PDF MCP server for interactive viewing and text extraction.",
+ "author": {
+ "name": "Anthropic"
+ }
+}
diff --git a/examples/pdf-server/plugin/.mcp.json b/examples/pdf-server/plugin/.mcp.json
new file mode 100644
index 0000000..f81cdb2
--- /dev/null
+++ b/examples/pdf-server/plugin/.mcp.json
@@ -0,0 +1,8 @@
+{
+ "mcpServers": {
+ "pdf": {
+ "command": "npx",
+ "args": ["@modelcontextprotocol/server-pdf", "--stdio"]
+ }
+ }
+}
diff --git a/examples/pdf-server/plugin/CONNECTORS.md b/examples/pdf-server/plugin/CONNECTORS.md
new file mode 100644
index 0000000..39def09
--- /dev/null
+++ b/examples/pdf-server/plugin/CONNECTORS.md
@@ -0,0 +1,16 @@
+# Connectors
+
+## Local MCP Server
+
+This plugin uses a **local MCP server** instead of a remote connector.
+The PDF server runs on your machine via `npx`.
+
+| Category | Server | How it runs |
+| ---------- | ---------------------------------- | -------------------------------------- |
+| PDF viewer | `@modelcontextprotocol/server-pdf` | Local stdio via `npx` (auto-installed) |
+
+### Requirements
+
+- Node.js >= 18
+- Internet access for remote PDFs (arXiv, bioRxiv, etc.)
+- No API keys or authentication needed
diff --git a/examples/pdf-server/plugin/README.md b/examples/pdf-server/plugin/README.md
new file mode 100644
index 0000000..32a80ab
--- /dev/null
+++ b/examples/pdf-server/plugin/README.md
@@ -0,0 +1,40 @@
+# PDF Research Plugin
+
+Read and analyze PDF documents using a local MCP server. Designed for
+academic paper review, document analysis, and research workflows.
+
+## What It Does
+
+- **Open PDFs** from local files, arXiv, bioRxiv, and other academic sources
+- **Interactive viewer** with search, navigation, and zoom
+- **AI-powered analysis** -- summarize, extract key points, compare papers
+
+## Commands
+
+| Command | What it does |
+| ------------------------- | ---------------------- |
+| `/pdf-research:read` | Open a PDF for reading |
+| `/pdf-research:summarize` | Summarize a document |
+
+## How It Works
+
+This plugin uses a **local MCP server** (`@modelcontextprotocol/server-pdf`)
+that runs on your machine via `npx`. No API keys or remote services needed --
+the PDF server starts automatically when the plugin loads.
+
+This is different from other knowledge-work plugins which use remote HTTP
+connectors. The local server pattern enables offline PDF access and avoids
+the need for a remote deployment.
+
+## Requirements
+
+- Node.js >= 18
+- Internet for remote PDFs (arXiv, etc.)
+
+## Supported PDF Sources
+
+- Local files (file paths in your working directory)
+- [arXiv](https://arxiv.org) papers
+- [bioRxiv](https://biorxiv.org) / [medRxiv](https://medrxiv.org) preprints
+- [chemRxiv](https://chemrxiv.org), [Zenodo](https://zenodo.org), [OSF](https://osf.io)
+- [HAL Science](https://hal.science), [SSRN](https://ssrn.com)
diff --git a/examples/pdf-server/plugin/commands/read.md b/examples/pdf-server/plugin/commands/read.md
new file mode 100644
index 0000000..39817bc
--- /dev/null
+++ b/examples/pdf-server/plugin/commands/read.md
@@ -0,0 +1,21 @@
+---
+description: Open a PDF for reading and analysis
+---
+
+# Read PDF
+
+Open a PDF document for interactive reading. Accepts local file paths,
+arXiv URLs, or other academic paper URLs.
+
+## Instructions
+
+1. If the user provides a URL or file path, call `display_pdf` with that URL
+2. If no URL given, call `list_pdfs` first to show available documents
+3. After displaying, offer to summarize, extract key points, or answer questions
+
+## Supported Sources
+
+- Local files (drag-and-drop or file path)
+- arXiv papers (arxiv.org/abs/... or arxiv.org/pdf/...)
+- bioRxiv, medRxiv, chemRxiv preprints
+- Zenodo, OSF, HAL, SSRN
diff --git a/examples/pdf-server/plugin/commands/summarize.md b/examples/pdf-server/plugin/commands/summarize.md
new file mode 100644
index 0000000..62ee4ba
--- /dev/null
+++ b/examples/pdf-server/plugin/commands/summarize.md
@@ -0,0 +1,23 @@
+---
+description: Summarize the current PDF or a new document
+---
+
+# Summarize PDF
+
+Generate a structured summary of a PDF document.
+
+## Instructions
+
+1. If a PDF is already displayed, work with it
+2. If user provides a new URL/path, call `display_pdf` first
+3. Read through the document and produce:
+ - **Title and authors** (if academic paper)
+ - **Key findings** (3-5 bullet points)
+ - **Methodology** (brief summary)
+ - **Conclusions**
+ - **Relevance** — what makes this paper notable
+
+## Notes
+
+- For long documents, focus on abstract, introduction, and conclusion sections
+- Ask the user which sections they care most about if the doc is very long
diff --git a/examples/pdf-server/plugin/skills/pdf-reading/SKILL.md b/examples/pdf-server/plugin/skills/pdf-reading/SKILL.md
new file mode 100644
index 0000000..2e79917
--- /dev/null
+++ b/examples/pdf-server/plugin/skills/pdf-reading/SKILL.md
@@ -0,0 +1,35 @@
+---
+name: pdf-reading
+description: Read and navigate PDF documents using the local PDF server. Reference this when the user wants to open, read, search, or interact with any PDF file or academic paper.
+---
+
+# PDF Reading
+
+You have access to a local PDF server that provides interactive document viewing.
+
+## Available Tools
+
+- **list_pdfs** -- Show available PDFs. Call with no arguments.
+- **display_pdf** -- Open a PDF in the interactive viewer.
+ - `url` (string): URL or local file path
+ - `page` (number, optional): Starting page number
+
+## How to Use
+
+**When the user mentions a PDF, paper, or document:**
+1. If they give a URL or path, call `display_pdf` directly
+2. If they say "open the paper" without specifying, call `list_pdfs` to show options
+3. After displaying, offer to summarize, extract data, answer questions
+
+**arXiv shortcuts:**
+- `arxiv.org/abs/2301.12345` is auto-converted to the PDF URL
+- Users can just say "open arxiv 2301.12345"
+
+**Supported remote sources:**
+arXiv, bioRxiv, medRxiv, chemRxiv, Zenodo, OSF, HAL Science, SSRN
+
+## Best Practices
+
+- Always display the PDF before trying to analyze it
+- For multi-page documents, ask which section the user cares about
+- When comparing papers, display them one at a time and note key differences
From 0be854b2448a3d3181f586e0846e10e83dde2fc3 Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Tue, 24 Feb 2026 01:48:17 +0000
Subject: [PATCH 011/157] ci: build and publish MCPB bundle for pdf-server
(#495)
- Add *.mcpb to .gitignore (built artifacts)
- Validate MCPB pack in CI (Linux x64 only)
- Build and upload .mcpb as GitHub release asset on publish
---
.github/workflows/ci.yml | 5 +++++
.github/workflows/npm-publish.yml | 32 +++++++++++++++++++++++++++++++
.gitignore | 5 ++++-
3 files changed, 41 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 671b7a3..d47e665 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -66,6 +66,11 @@ jobs:
- run: npm run prettier
+ - name: Build MCPB bundle (pdf-server)
+ if: runner.os == 'Linux' && matrix.name == 'Linux x64'
+ run: npx -y @anthropic-ai/mcpb pack
+ working-directory: examples/pdf-server
+
e2e:
runs-on: ubuntu-latest
steps:
diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml
index 5852443..d969585 100644
--- a/.github/workflows/npm-publish.yml
+++ b/.github/workflows/npm-publish.yml
@@ -138,3 +138,35 @@ jobs:
run: npm publish --workspace examples/${{ matrix.example }} --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_SECRET }}
+
+ publish-mcpb:
+ runs-on: ubuntu-latest
+ if: github.event_name == 'release'
+ environment: Release
+ needs: [publish-examples]
+
+ permissions:
+ contents: write
+
+ steps:
+ - uses: actions/checkout@v6
+ - uses: oven-sh/setup-bun@v2
+ with:
+ bun-version: latest
+ - uses: actions/setup-node@v6
+ with:
+ node-version: "22"
+ cache: npm
+ - run: npm ci
+
+ - name: Build pdf-server
+ run: npm run build --workspace examples/pdf-server
+
+ - name: Pack MCPB bundle
+ run: npx -y @anthropic-ai/mcpb pack
+ working-directory: examples/pdf-server
+
+ - name: Upload MCPB to release
+ run: gh release upload "${{ github.event.release.tag_name }}" examples/pdf-server/*.mcpb --clobber
+ env:
+ GH_TOKEN: ${{ github.token }}
diff --git a/.gitignore b/.gitignore
index 9c6a137..abb7660 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,7 @@ intermediate-findings/
playwright-report/
test-results/
__pycache__/
-*.pyc
\ No newline at end of file
+*.pyc
+
+# MCPB bundles (built artifacts)
+*.mcpb
\ No newline at end of file
From 82b1f29085e899b2a531f1dd9dd9f5c88c59e60f Mon Sep 17 00:00:00 2001
From: Mel Ludowise
Date: Mon, 23 Feb 2026 17:58:32 -0800
Subject: [PATCH 012/157] Set audioSession.type to playback in sheet music app
(#489)
Use the Audio Session API to declare audio as media playback so the
platform handles it correctly: background audio, lock screen controls,
Dynamic Island integration, and bypassing the iOS silent switch.
---
examples/sheet-music-server/src/mcp-app.ts | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/examples/sheet-music-server/src/mcp-app.ts b/examples/sheet-music-server/src/mcp-app.ts
index 48b44ca..68f32ee 100644
--- a/examples/sheet-music-server/src/mcp-app.ts
+++ b/examples/sheet-music-server/src/mcp-app.ts
@@ -30,6 +30,17 @@ const statusEl = document.getElementById("status")!;
const sheetMusicEl = document.getElementById("sheet-music")!;
const audioControlsEl = document.getElementById("audio-controls")!;
+// =============================================================================
+// Audio Session
+// =============================================================================
+
+// Declare this app's audio as media playback so the platform can handle it
+// correctly: background audio, lock screen controls, Dynamic Island, and
+// bypassing the iOS silent switch. See https://w3c.github.io/audio-session/
+if ("audioSession" in navigator) {
+ (navigator.audioSession as { type: string }).type = "playback";
+}
+
// =============================================================================
// ABC Rendering
// =============================================================================
From ec389f473b7d6107a003f820a1c39a7eddbbfc3d Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Tue, 24 Feb 2026 02:39:24 +0000
Subject: [PATCH 013/157] chore: bump ext-apps to 1.1.1 (#496)
Bug fixes / improvements since 1.1.0:
- fix: add audio/video support for basic-host (#460)
- fix(pdf-server): remove page-loading overlay (#493)
- pdf-server: remove domain allowlist, require HTTPS only (#487)
- Set audioSession.type to playback in sheet music app (#489)
- Add MCPB packaging and Claude Code plugin for pdf-server (#491)
---
examples/basic-host/package.json | 2 +-
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/budget-allocator-server/package.json | 2 +-
examples/cohort-heatmap-server/package.json | 2 +-
.../customer-segmentation-server/package.json | 2 +-
examples/debug-server/package.json | 2 +-
examples/integration-server/package.json | 2 +-
examples/map-server/package.json | 2 +-
examples/pdf-server/package.json | 2 +-
examples/qr-server/package.json | 2 +-
examples/quickstart/package.json | 2 +-
examples/say-server/package.json | 2 +-
examples/scenario-modeler-server/package.json | 2 +-
examples/shadertoy-server/package.json | 2 +-
examples/sheet-music-server/package.json | 2 +-
examples/system-monitor-server/package.json | 2 +-
examples/threejs-server/package.json | 2 +-
examples/transcript-server/package.json | 2 +-
examples/video-resource-server/package.json | 2 +-
examples/wiki-explorer-server/package.json | 2 +-
package-lock.json | 54 +++++++++----------
package.json | 2 +-
27 files changed, 53 insertions(+), 53 deletions(-)
diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json
index e943b00..bbbedde 100644
--- a/examples/basic-host/package.json
+++ b/examples/basic-host/package.json
@@ -1,7 +1,7 @@
{
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host",
"name": "@modelcontextprotocol/ext-apps-basic-host",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"scripts": {
"build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"",
diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json
index ad56940..e716fa5 100644
--- a/examples/basic-server-preact/package.json
+++ b/examples/basic-server-preact/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Basic MCP App Server example using Preact",
"repository": {
diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json
index f75b57d..3d1b2a4 100644
--- a/examples/basic-server-react/package.json
+++ b/examples/basic-server-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-react",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Basic MCP App Server example using React",
"repository": {
diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json
index 47a0afa..bfbe9fb 100644
--- a/examples/basic-server-solid/package.json
+++ b/examples/basic-server-solid/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Basic MCP App Server example using Solid",
"repository": {
diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json
index b7eccd8..4556f3b 100644
--- a/examples/basic-server-svelte/package.json
+++ b/examples/basic-server-svelte/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Basic MCP App Server example using Svelte",
"repository": {
diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json
index 69fa77f..ada760c 100644
--- a/examples/basic-server-vanillajs/package.json
+++ b/examples/basic-server-vanillajs/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-vanillajs",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Basic MCP App Server example using vanilla JavaScript",
"repository": {
diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json
index 0e9a92a..551aac4 100644
--- a/examples/basic-server-vue/package.json
+++ b/examples/basic-server-vue/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-vue",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Basic MCP App Server example using Vue",
"repository": {
diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json
index 3a69610..58d340a 100644
--- a/examples/budget-allocator-server/package.json
+++ b/examples/budget-allocator-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-budget-allocator",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Budget allocator MCP App Server with interactive visualization",
"repository": {
diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json
index 6a77ee6..05fb7c9 100644
--- a/examples/cohort-heatmap-server/package.json
+++ b/examples/cohort-heatmap-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-cohort-heatmap",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Cohort heatmap MCP App Server for retention analysis",
"repository": {
diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json
index fc6fa98..58e335d 100644
--- a/examples/customer-segmentation-server/package.json
+++ b/examples/customer-segmentation-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-customer-segmentation",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Customer segmentation MCP App Server with filtering",
"repository": {
diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json
index 9462872..624eb23 100644
--- a/examples/debug-server/package.json
+++ b/examples/debug-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-debug",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Debug MCP App Server for testing all SDK capabilities",
"repository": {
diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json
index fc25449..5ac0f1b 100644
--- a/examples/integration-server/package.json
+++ b/examples/integration-server/package.json
@@ -1,6 +1,6 @@
{
"name": "integration-server",
- "version": "1.0.0",
+ "version": "1.1.1",
"private": true,
"type": "module",
"scripts": {
diff --git a/examples/map-server/package.json b/examples/map-server/package.json
index 704a09d..1cbf631 100644
--- a/examples/map-server/package.json
+++ b/examples/map-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-map",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "MCP App Server example with CesiumJS 3D globe and geocoding",
"repository": {
diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json
index 49f2028..a99da7b 100644
--- a/examples/pdf-server/package.json
+++ b/examples/pdf-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-pdf",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer",
"repository": {
diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json
index c72289a..b36258e 100644
--- a/examples/qr-server/package.json
+++ b/examples/qr-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-qr",
- "version": "1.0.0",
+ "version": "1.1.1",
"private": true,
"scripts": {
"start": "uv run server.py",
diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json
index aa0adc6..ef0002b 100644
--- a/examples/quickstart/package.json
+++ b/examples/quickstart/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/quickstart",
- "version": "0.4.1",
+ "version": "1.1.1",
"type": "module",
"private": true,
"description": "Quickstart MCP App Server example",
diff --git a/examples/say-server/package.json b/examples/say-server/package.json
index bdf9093..bae9418 100644
--- a/examples/say-server/package.json
+++ b/examples/say-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-say",
- "version": "1.0.0",
+ "version": "1.1.1",
"private": true,
"description": "Streaming TTS MCP App Server with karaoke-style text highlighting",
"repository": {
diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json
index ff19550..adcb752 100644
--- a/examples/scenario-modeler-server/package.json
+++ b/examples/scenario-modeler-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-scenario-modeler",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Financial scenario modeling MCP App Server",
"repository": {
diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json
index f0ae879..f59881f 100644
--- a/examples/shadertoy-server/package.json
+++ b/examples/shadertoy-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-shadertoy",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders",
"repository": {
diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json
index 17ae21d..4d48ad9 100644
--- a/examples/sheet-music-server/package.json
+++ b/examples/sheet-music-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-sheet-music",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "MCP App Server for rendering and playing sheet music from ABC notation",
"repository": {
diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json
index 53182e3..5bc04bb 100644
--- a/examples/system-monitor-server/package.json
+++ b/examples/system-monitor-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-system-monitor",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "System monitor MCP App Server with real-time stats",
"repository": {
diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json
index 1140f6d..f4a5376 100644
--- a/examples/threejs-server/package.json
+++ b/examples/threejs-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-threejs",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Three.js 3D visualization MCP App Server",
"repository": {
diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json
index c65fc46..9f50d69 100644
--- a/examples/transcript-server/package.json
+++ b/examples/transcript-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-transcript",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "MCP App Server for live speech transcription",
"repository": {
diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json
index c0f0abd..a45e237 100644
--- a/examples/video-resource-server/package.json
+++ b/examples/video-resource-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-video-resource",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "MCP App Server demonstrating video resources served as base64 blobs",
"repository": {
diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json
index f655b0e..a5d3304 100644
--- a/examples/wiki-explorer-server/package.json
+++ b/examples/wiki-explorer-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-wiki-explorer",
- "version": "1.1.0",
+ "version": "1.1.1",
"type": "module",
"description": "Wikipedia link explorer MCP App Server with graph visualization",
"repository": {
diff --git a/package-lock.json b/package-lock.json
index f97924a..ad45b72 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/ext-apps",
- "version": "1.1.0",
+ "version": "1.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/ext-apps",
- "version": "1.1.0",
+ "version": "1.1.1",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -79,7 +79,7 @@
},
"examples/basic-host": {
"name": "@modelcontextprotocol/ext-apps-basic-host",
- "version": "1.1.0",
+ "version": "1.1.1",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.24.0",
@@ -122,7 +122,7 @@
},
"examples/basic-server-preact": {
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -166,7 +166,7 @@
},
"examples/basic-server-react": {
"name": "@modelcontextprotocol/server-basic-react",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -213,7 +213,7 @@
},
"examples/basic-server-solid": {
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -257,7 +257,7 @@
},
"examples/basic-server-svelte": {
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -301,7 +301,7 @@
},
"examples/basic-server-vanillajs": {
"name": "@modelcontextprotocol/server-basic-vanillajs",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -343,7 +343,7 @@
},
"examples/basic-server-vue": {
"name": "@modelcontextprotocol/server-basic-vue",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -387,7 +387,7 @@
},
"examples/budget-allocator-server": {
"name": "@modelcontextprotocol/server-budget-allocator",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -430,7 +430,7 @@
},
"examples/cohort-heatmap-server": {
"name": "@modelcontextprotocol/server-cohort-heatmap",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -477,7 +477,7 @@
},
"examples/customer-segmentation-server": {
"name": "@modelcontextprotocol/server-customer-segmentation",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -520,7 +520,7 @@
},
"examples/debug-server": {
"name": "@modelcontextprotocol/server-debug",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -561,7 +561,7 @@
"license": "MIT"
},
"examples/integration-server": {
- "version": "1.0.0",
+ "version": "1.1.1",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.24.0",
@@ -606,7 +606,7 @@
},
"examples/map-server": {
"name": "@modelcontextprotocol/server-map",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -648,7 +648,7 @@
},
"examples/pdf-server": {
"name": "@modelcontextprotocol/server-pdf",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -691,14 +691,14 @@
},
"examples/qr-server": {
"name": "@modelcontextprotocol/server-qr",
- "version": "1.0.0",
+ "version": "1.1.1",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0"
}
},
"examples/quickstart": {
"name": "@modelcontextprotocol/quickstart",
- "version": "0.4.1",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -730,7 +730,7 @@
},
"examples/say-server": {
"name": "@modelcontextprotocol/server-say",
- "version": "1.0.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0"
@@ -738,7 +738,7 @@
},
"examples/scenario-modeler-server": {
"name": "@modelcontextprotocol/server-scenario-modeler",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -786,7 +786,7 @@
},
"examples/shadertoy-server": {
"name": "@modelcontextprotocol/server-shadertoy",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -828,7 +828,7 @@
},
"examples/sheet-music-server": {
"name": "@modelcontextprotocol/server-sheet-music",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -871,7 +871,7 @@
},
"examples/system-monitor-server": {
"name": "@modelcontextprotocol/server-system-monitor",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -915,7 +915,7 @@
},
"examples/threejs-server": {
"name": "@modelcontextprotocol/server-threejs",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -964,7 +964,7 @@
},
"examples/transcript-server": {
"name": "@modelcontextprotocol/server-transcript",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -1007,7 +1007,7 @@
},
"examples/video-resource-server": {
"name": "@modelcontextprotocol/server-video-resource",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -1049,7 +1049,7 @@
},
"examples/wiki-explorer-server": {
"name": "@modelcontextprotocol/server-wiki-explorer",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
diff --git a/package.json b/package.json
index 71206ae..fe5bec0 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"url": "https://github.com/modelcontextprotocol/ext-apps"
},
"homepage": "https://github.com/modelcontextprotocol/ext-apps",
- "version": "1.1.0",
+ "version": "1.1.1",
"license": "MIT",
"description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.",
"type": "module",
From 9340bd800e53ab9334e7d1e50eb14a7091b91993 Mon Sep 17 00:00:00 2001
From: Anton Pidkuiko MacBook
Date: Tue, 24 Feb 2026 16:57:18 +0000
Subject: [PATCH 014/157] =?UTF-8?q?test:=20add=20App=20=E2=86=92=20AppBrid?=
=?UTF-8?q?ge=20integration=20test=20for=20downloadFile?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/app-bridge.test.ts | 30 ++++++++++++++++++++++++++++++
1 file changed, 30 insertions(+)
diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts
index 202b2ed..310e38e 100644
--- a/src/app-bridge.test.ts
+++ b/src/app-bridge.test.ts
@@ -712,6 +712,36 @@ describe("App <-> AppBridge integration", () => {
expect(result.content).toEqual(resultContent);
});
+ it("ondownloadfile setter registers handler for ui/download-file requests", async () => {
+ const downloadParams = {
+ contents: [
+ {
+ type: "resource" as const,
+ resource: {
+ uri: "file:///export.json",
+ mimeType: "application/json",
+ text: '{"key":"value"}',
+ },
+ },
+ ],
+ };
+ const receivedRequests: unknown[] = [];
+
+ bridge.ondownloadfile = async (params) => {
+ receivedRequests.push(params);
+ return {};
+ };
+
+ await bridge.connect(bridgeTransport);
+ await app.connect(appTransport);
+
+ const result = await app.downloadFile(downloadParams);
+
+ expect(receivedRequests).toHaveLength(1);
+ expect(receivedRequests[0]).toMatchObject(downloadParams);
+ expect(result).toEqual({});
+ });
+
it("callServerTool throws a helpful error when called with a string instead of params object", async () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
From b4642b433871eb7c5174e286b401ad068e5c3c1f Mon Sep 17 00:00:00 2001
From: Jonathan Hefner
Date: Tue, 24 Feb 2026 11:36:59 -0600
Subject: [PATCH 015/157] 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 @@
@@ -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
-
-
-
+
+
+
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
-
-
-
+
+
+
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
-
-
-
+
+
+
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.
-
+
## 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
-
-
-
+
+
+
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
-
-
-
+
+
+
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
-
-
+
+
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
-
-
-
+
+
+
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
-
-
-
+
+
+
From 281a26b021f1d04d9f2a7b439528f26bc28b2ed9 Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Tue, 24 Feb 2026 18:23:57 +0000
Subject: [PATCH 016/157] pdf-server: robust path validation + folder/file root
support (#497)
* pdf-server: support folders in cli + files in roots
* fix(pdf-server): use path.relative for robust ancestor dir matching
- Replace string prefix matching (startsWith) with path.relative-based
isAncestorDir() which correctly handles trailing slashes, path
normalization, and prevents both .. traversal and prefix attacks.
- Use path.resolve consistently when adding paths to allowedLocalFiles
and allowedLocalDirs (main.ts was missing this).
- Use resolved path for exact file match check in validateUrl.
- Improve error message formatting for easier debugging.
- Add tests for trailing slashes, grandparent dirs, and isAncestorDir.
* chore(pdf-server): remove JSONL interaction logging
Remove the withLogging transport wrapper, logInteraction helper,
InteractionLogger type, and all log parameter threading that were
added for debugging.
* fix(pdf-server): support computer:// URL scheme for local files
Some clients (e.g. Claude Code) use computer:// instead of file://
for local file references. Handle both schemes in isFileUrl and
fileUrlToPath.
* fix(pdf-server): add debug logging for rejected files, ignore .jsonl logs
* fix(pdf-server): accept bare file paths in addition to file:// URLs
Clients like Claude Desktop may pass raw absolute paths (e.g.
/Users/.../file.pdf) instead of file:// URLs. Handle these in
validateUrl and readPdfRange.
* debug(pdf-server): add hex char diagnostics for path mismatch
* fix(pdf-server): resolve symlinks to handle sandbox path remapping
Use fs.realpathSync to resolve symlinks/bind mounts when comparing
file paths against allowed directories. This fixes the path namespace
mismatch where Claude Desktop sends container paths (e.g. /sessions/...)
while MCP roots use host paths (e.g. /Users/...).
Both the file path and directory paths are resolved through symlinks
before comparison, so either side can be a symlink.
Also stores realpath of roots in allowedLocalDirs/Files so matching
works in both directions.
Removes verbose hex diagnostics (no longer needed).
* fix(pdf-server): decode percent-encoded bare paths (e.g. %20 for spaces)
Clients may send bare file paths with percent-encoded characters
(e.g., Application%20Support). Apply decodeURIComponent to bare paths
before resolving, matching the behavior already used for file:// URLs.
* fix(pdf-server): add full diagnostics to client-facing error message
Include url, resolved path, realpath, allowedFiles, and per-directory
path.relative results in the error returned to the client, so we can
diagnose why isAncestorDir fails when paths look identical.
* debug(pdf-server): add hex dump to diagnose invisible char differences
path.relative returns 10 levels of .. despite paths looking identical,
meaning they differ at the byte level. Add hex dump of first 30 chars
of both paths to catch invisible Unicode or encoding differences.
* fix(pdf-server): use inode comparison for bind-mount path matching
The cowork VM remaps paths via bind mounts: the tool input uses
VM-internal paths (/sessions/...) while MCP roots use host paths
(/Users/...). Since path.relative can't match across bind mounts,
compare directory inodes instead (dev+ino from fs.statSync).
Walks up the file's parent directories checking each against allowed
dirs by inode, which works regardless of path namespace differences.
* fix(pdf-server): resolve VM-internal paths via directory basename matching
When the MCP server runs on the host but receives unrewritten VM paths
(e.g., /sessions/name/mnt/uploads/file.pdf), the tool call url argument
isn't rewritten by the cowork VM proxy because it doesn't know which
arguments are file paths.
Fix: when path validation fails, try to match the file by finding the
directory basename (e.g., 'uploads') in the VM path and looking for
the relative path suffix under each allowed directory on the host.
Also returns the resolved host path so readPdfRange reads the correct
file on the host filesystem.
* revert(pdf-server): remove resolveVmPath hack
The VM path mismatch (VM-internal paths not rewritten in tool call
arguments) should be fixed in the cowork VM proxy layer, not worked
around in the MCP server.
Removes:
- resolveVmPath() function
- resolvedPath field from validateUrl return type
- effectiveUrl remapping in tool handlers
- VM path resolution test
* nit
---
examples/pdf-server/.gitignore | 1 +
examples/pdf-server/.mcpbignore | 3 +
examples/pdf-server/main.ts | 14 ++-
examples/pdf-server/server.test.ts | 149 +++++++++++++++++++++++++++++
examples/pdf-server/server.ts | 129 +++++++++++++++++++------
5 files changed, 265 insertions(+), 31 deletions(-)
diff --git a/examples/pdf-server/.gitignore b/examples/pdf-server/.gitignore
index 849ddff..a856bb2 100644
--- a/examples/pdf-server/.gitignore
+++ b/examples/pdf-server/.gitignore
@@ -1 +1,2 @@
dist/
+*.jsonl
diff --git a/examples/pdf-server/.mcpbignore b/examples/pdf-server/.mcpbignore
index fe11c64..3f7859c 100644
--- a/examples/pdf-server/.mcpbignore
+++ b/examples/pdf-server/.mcpbignore
@@ -7,6 +7,9 @@ src/
# Tests
*.test.*
+# Debug logs
+*.jsonl
+
# Development assets
screenshot.png
grid-cell.png
diff --git a/examples/pdf-server/main.ts b/examples/pdf-server/main.ts
index 50dc43a..ddf26c2 100644
--- a/examples/pdf-server/main.ts
+++ b/examples/pdf-server/main.ts
@@ -5,6 +5,7 @@
*/
import fs from "node:fs";
+import path from "node:path";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -20,6 +21,7 @@ import {
fileUrlToPath,
allowedLocalFiles,
DEFAULT_PDF,
+ allowedLocalDirs,
} from "./server.js";
/**
@@ -120,10 +122,16 @@ async function main() {
// Register local files in whitelist
for (const url of urls) {
if (isFileUrl(url)) {
- const filePath = fileUrlToPath(url);
+ const filePath = path.resolve(fileUrlToPath(url));
if (fs.existsSync(filePath)) {
- allowedLocalFiles.add(filePath);
- console.error(`[pdf-server] Registered local file: ${filePath}`);
+ const s = fs.statSync(filePath);
+ if (s.isFile()) {
+ allowedLocalFiles.add(filePath);
+ console.error(`[pdf-server] Registered local file: ${filePath}`);
+ } else if (s.isDirectory()) {
+ allowedLocalDirs.add(filePath);
+ console.error(`[pdf-server] Registered local directory: ${filePath}`);
+ }
} else {
console.error(`[pdf-server] Warning: File not found: ${filePath}`);
}
diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts
index 95efa7d..dd5ac44 100644
--- a/examples/pdf-server/server.test.ts
+++ b/examples/pdf-server/server.test.ts
@@ -3,6 +3,7 @@ import path from "node:path";
import {
createPdfCache,
validateUrl,
+ isAncestorDir,
allowedLocalFiles,
allowedLocalDirs,
pathToFileUrl,
@@ -271,4 +272,152 @@ describe("validateUrl with MCP roots (allowedLocalDirs)", () => {
expect(result.valid).toBe(false);
expect(result.error).toContain("File not found");
});
+
+ it("should allow a file under an allowed dir with trailing slash", () => {
+ const dir = path.resolve(import.meta.dirname);
+ // Simulate a dir stored with a trailing slash (e.g. from CLI path)
+ allowedLocalDirs.add(dir + "/");
+
+ const filePath = path.join(dir, "server.ts");
+ const result = validateUrl(pathToFileUrl(filePath));
+ expect(result.valid).toBe(true);
+ });
+
+ it("should allow a file under a grandparent allowed dir", () => {
+ // Allow a directory two levels up from the file
+ const grandparent = path.resolve(path.join(import.meta.dirname, ".."));
+ allowedLocalDirs.add(grandparent);
+
+ const filePath = path.join(import.meta.dirname, "server.ts");
+ const result = validateUrl(pathToFileUrl(filePath));
+ expect(result.valid).toBe(true);
+ });
+
+ it("should accept computer:// URLs as local files", () => {
+ const dir = path.resolve(import.meta.dirname);
+ allowedLocalDirs.add(dir);
+
+ const filePath = path.join(dir, "server.ts");
+ const encoded = encodeURIComponent(filePath).replace(/%2F/g, "/");
+ const result = validateUrl(`computer://${encoded}`);
+ expect(result.valid).toBe(true);
+ });
+
+ it("should accept bare absolute paths as local files", () => {
+ const dir = path.resolve(import.meta.dirname);
+ allowedLocalDirs.add(dir);
+
+ const filePath = path.join(dir, "server.ts");
+ const result = validateUrl(filePath);
+ expect(result.valid).toBe(true);
+ });
+
+ it("should decode percent-encoded bare paths (e.g. %20 for spaces)", () => {
+ const fs = require("node:fs");
+ const os = require("node:os");
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pdf test "));
+ const testFile = path.join(tmpDir, "file.txt");
+
+ try {
+ fs.writeFileSync(testFile, "hello");
+ allowedLocalDirs.add(tmpDir);
+
+ // Encode spaces as %20 in the path (as some clients do)
+ const encoded = testFile.replace(/ /g, "%20");
+ const result = validateUrl(encoded);
+ expect(result.valid).toBe(true);
+ } finally {
+ fs.rmSync(tmpDir, { recursive: true });
+ }
+ });
+
+ it("should allow file accessed via symlink when real dir is allowed", () => {
+ const fs = require("node:fs");
+ const os = require("node:os");
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pdf-test-"));
+ const realDir = path.join(tmpDir, "real");
+ const linkDir = path.join(tmpDir, "link");
+ const testFile = path.join(realDir, "test.txt");
+
+ try {
+ fs.mkdirSync(realDir);
+ fs.writeFileSync(testFile, "hello");
+ fs.symlinkSync(realDir, linkDir);
+
+ // Allow the REAL directory
+ allowedLocalDirs.add(realDir);
+
+ // Access via the SYMLINK path — should still be allowed
+ const symlinkPath = path.join(linkDir, "test.txt");
+ const result = validateUrl(symlinkPath);
+ expect(result.valid).toBe(true);
+ } finally {
+ fs.rmSync(tmpDir, { recursive: true });
+ }
+ });
+
+ it("should allow file when allowed dir is a symlink to real parent", () => {
+ const fs = require("node:fs");
+ const os = require("node:os");
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pdf-test-"));
+ const realDir = path.join(tmpDir, "real");
+ const linkDir = path.join(tmpDir, "link");
+ const testFile = path.join(realDir, "test.txt");
+
+ try {
+ fs.mkdirSync(realDir);
+ fs.writeFileSync(testFile, "hello");
+ fs.symlinkSync(realDir, linkDir);
+
+ // Allow the SYMLINK directory
+ allowedLocalDirs.add(linkDir);
+
+ // Access via the REAL path — should still be allowed
+ const result = validateUrl(testFile);
+ expect(result.valid).toBe(true);
+ } finally {
+ fs.rmSync(tmpDir, { recursive: true });
+ }
+ });
+});
+
+describe("isAncestorDir", () => {
+ it("should return true for a direct child", () => {
+ expect(isAncestorDir("/Users/test/dir", "/Users/test/dir/file.pdf")).toBe(
+ true,
+ );
+ });
+
+ it("should return true for a nested child", () => {
+ expect(isAncestorDir("/Users/test", "/Users/test/sub/dir/file.pdf")).toBe(
+ true,
+ );
+ });
+
+ it("should return false for a file outside the dir", () => {
+ expect(isAncestorDir("/Users/test/dir", "/Users/test/other/file.pdf")).toBe(
+ false,
+ );
+ });
+
+ it("should return false for the dir itself", () => {
+ expect(isAncestorDir("/Users/test/dir", "/Users/test/dir")).toBe(false);
+ });
+
+ it("should prevent .. traversal", () => {
+ expect(
+ isAncestorDir("/Users/test/dir", "/Users/test/dir/../other/file.pdf"),
+ ).toBe(false);
+ });
+
+ it("should prevent prefix-based traversal", () => {
+ // /tmp/safe should NOT match /tmp/safevil/file.pdf
+ expect(isAncestorDir("/tmp/safe", "/tmp/safevil/file.pdf")).toBe(false);
+ });
+
+ it("should handle dirs with trailing slash", () => {
+ expect(isAncestorDir("/Users/test/dir/", "/Users/test/dir/file.pdf")).toBe(
+ true,
+ );
+ });
});
diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts
index e68fdde..ccc400f 100644
--- a/examples/pdf-server/server.ts
+++ b/examples/pdf-server/server.ts
@@ -60,7 +60,7 @@ const DIST_DIR = import.meta.filename.endsWith(".ts")
// =============================================================================
export function isFileUrl(url: string): boolean {
- return url.startsWith("file://");
+ return url.startsWith("file://") || url.startsWith("computer://");
}
export function isArxivUrl(url: string): boolean {
@@ -81,7 +81,8 @@ export function normalizeArxivUrl(url: string): string {
}
export function fileUrlToPath(fileUrl: string): string {
- return decodeURIComponent(fileUrl.replace("file://", ""));
+ // Support both file:// and computer:// (used by some clients for local files)
+ return decodeURIComponent(fileUrl.replace(/^(?:file|computer):\/\//, ""));
}
export function pathToFileUrl(filePath: string): string {
@@ -89,29 +90,86 @@ export function pathToFileUrl(filePath: string): string {
return `file://${encodeURIComponent(absolutePath).replace(/%2F/g, "/")}`;
}
-export function validateUrl(url: string): { valid: boolean; error?: string } {
- if (isFileUrl(url)) {
- const filePath = fileUrlToPath(url);
+/**
+ * Check if `dir` is an ancestor of `filePath` using path.relative,
+ * which is more robust than string prefix matching (handles normalization).
+ */
+export function isAncestorDir(dir: string, filePath: string): boolean {
+ const rel = path.relative(dir, filePath);
+ // Must be non-empty (not the dir itself when checking files),
+ // must not start with ".." (escaping), and must not be absolute (different root).
+ return rel !== "" && !rel.startsWith("..") && !path.isAbsolute(rel);
+}
+
+/**
+ * Check if `url` looks like an absolute local file path (not a URL scheme).
+ * Handles Unix paths (/...), home-relative (~), and Windows drive letters (C:\...).
+ */
+function isLocalPath(url: string): boolean {
+ return (
+ url.startsWith("/") || url.startsWith("~") || /^[A-Za-z]:[/\\]/.test(url)
+ );
+}
+
+export function validateUrl(url: string): {
+ valid: boolean;
+ error?: string;
+} {
+ if (isFileUrl(url) || isLocalPath(url)) {
+ // fileUrlToPath already decodes percent-encoding; for bare paths,
+ // decode here in case the client sends %20 for spaces etc.
+ const filePath = isFileUrl(url)
+ ? fileUrlToPath(url)
+ : decodeURIComponent(url);
const resolved = path.resolve(filePath);
- // Check exact match (CLI args)
- const exactMatch = allowedLocalFiles.has(filePath);
+ // Check exact match (CLI args / roots)
+ if (allowedLocalFiles.has(resolved)) {
+ if (!fs.existsSync(resolved)) {
+ return { valid: false, error: `File not found: ${resolved}` };
+ }
+ return { valid: true };
+ }
- // Check directory match (MCP roots)
- const dirMatch = [...allowedLocalDirs].some(
- (dir) => resolved === dir || resolved.startsWith(dir + path.sep),
+ // Check directory match (MCP roots / CLI dirs).
+ // Try both the raw path and its realpath (resolves symlinks).
+ let realResolved: string | undefined;
+ try {
+ realResolved = fs.realpathSync(resolved);
+ } catch {
+ // File may not exist yet at this path
+ }
+ if (
+ [...allowedLocalDirs].some((dir) => {
+ let realDir: string | undefined;
+ try {
+ realDir = fs.realpathSync(dir);
+ } catch {
+ // Dir may not exist
+ }
+ return (
+ isAncestorDir(dir, resolved) ||
+ (realResolved != null && isAncestorDir(dir, realResolved)) ||
+ (realDir != null && isAncestorDir(realDir, resolved)) ||
+ (realDir != null &&
+ realResolved != null &&
+ isAncestorDir(realDir, realResolved))
+ );
+ })
+ ) {
+ if (!fs.existsSync(resolved)) {
+ return { valid: false, error: `File not found: ${resolved}` };
+ }
+ return { valid: true };
+ }
+
+ console.error(
+ `[pdf-server] Local file not in allowed list: ${resolved}\n Allowed dirs: ${[...allowedLocalDirs].join(", ")}`,
);
-
- if (!exactMatch && !dirMatch) {
- return {
- valid: false,
- error: `Local file not in allowed list: ${filePath}`,
- };
- }
- if (!fs.existsSync(filePath)) {
- return { valid: false, error: `File not found: ${filePath}` };
- }
- return { valid: true };
+ return {
+ valid: false,
+ error: `Local file not in allowed list: ${resolved}\nAllowed directories: ${[...allowedLocalDirs].join(", ")}`,
+ };
}
// Remote URL - require HTTPS
@@ -238,8 +296,10 @@ export function createPdfCache(): PdfCache {
const normalized = isArxivUrl(url) ? normalizeArxivUrl(url) : url;
const clampedByteCount = Math.min(byteCount, MAX_CHUNK_BYTES);
- if (isFileUrl(normalized)) {
- const filePath = fileUrlToPath(normalized);
+ if (isFileUrl(normalized) || isLocalPath(normalized)) {
+ const filePath = isFileUrl(normalized)
+ ? fileUrlToPath(normalized)
+ : decodeURIComponent(normalized);
const stats = await fs.promises.stat(filePath);
const totalBytes = stats.size;
@@ -353,11 +413,17 @@ async function refreshRoots(server: Server): Promise {
const { roots } = await server.listRoots();
allowedLocalDirs.clear();
for (const root of roots) {
- if (root.uri.startsWith("file://")) {
+ if (isFileUrl(root.uri)) {
const dir = fileUrlToPath(root.uri);
const resolved = path.resolve(dir);
try {
- if (fs.statSync(resolved).isDirectory()) {
+ const s = fs.statSync(resolved);
+ if (s.isFile()) {
+ console.error(
+ `[pdf-server] Root is a file, not a directory (skipped): ${resolved}`,
+ );
+ allowedLocalFiles.add(resolved);
+ } else if (s.isDirectory()) {
allowedLocalDirs.add(resolved);
console.error(`[pdf-server] Root directory allowed: ${resolved}`);
}
@@ -441,7 +507,7 @@ export function createServer(): McpServer {
title: "Read PDF Bytes",
description: "Read a range of bytes from a PDF (max 512KB per request)",
inputSchema: {
- url: z.string().describe("PDF URL"),
+ url: z.string().describe("PDF URL or local file path"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
.number()
@@ -471,7 +537,11 @@ export function createServer(): McpServer {
try {
const normalized = isArxivUrl(url) ? normalizeArxivUrl(url) : url;
- const { data, totalBytes } = await readPdfRange(url, offset, byteCount);
+ const { data, totalBytes } = await readPdfRange(
+ normalized,
+ offset,
+ byteCount,
+ );
// Base64 encode for JSON transport
const bytes = Buffer.from(data).toString("base64");
@@ -520,7 +590,10 @@ Accepts:
- Local files under directories provided by the client as MCP roots
- Any remote PDF accessible via HTTPS`,
inputSchema: {
- url: z.string().default(DEFAULT_PDF).describe("PDF URL"),
+ url: z
+ .string()
+ .default(DEFAULT_PDF)
+ .describe("PDF URL or local file path"),
page: z.number().min(1).default(1).describe("Initial page"),
},
outputSchema: z.object({
From 17286171946d04d0069463d36f4bc825138a3685 Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Tue, 24 Feb 2026 18:37:57 +0000
Subject: [PATCH 017/157] ci: auto-fix prettier formatting in pre-commit hook
and CI (#498)
Pre-commit hook:
- Stage all files modified by prettier:fix, not just src/generated/
CI (GitHub Actions):
- Add prettier-fix job that runs on PRs from the same repo
- Auto-commits formatting fixes back to the PR branch
- Skips fork PRs (no write access to fork branches)
Co-authored-by: Claude
---
.github/workflows/ci.yml | 29 +++++++++++++++++++++++++++++
.husky/pre-commit | 4 ++--
2 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d47e665..6e0c78f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,6 +10,35 @@ permissions:
contents: read
jobs:
+ prettier-fix:
+ name: Auto-fix formatting
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ github.head_ref }}
+ token: ${{ github.token }}
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: "20"
+
+ - run: npm ci
+
+ - run: npm run prettier:fix
+
+ - name: Commit formatting changes
+ run: |
+ git diff --quiet && exit 0
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add -A
+ git commit -m "style: auto-fix prettier formatting"
+ git push
+
build:
strategy:
fail-fast: false
diff --git a/.husky/pre-commit b/.husky/pre-commit
index 6fd6718..942f085 100644
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -17,5 +17,5 @@ fi
npm run build:all
npm run prettier:fix
-# Stage any changes to generated files (they may have been reformatted by prettier)
-git add src/generated/
+# Stage any files reformatted by prettier (generated files, source, docs, etc.)
+git diff --name-only --diff-filter=M | xargs -r git add
From 02737db779318c83d6c4eff77bd0a0164f33a6ff Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Tue, 24 Feb 2026 21:20:59 +0000
Subject: [PATCH 018/157] fix: sync local SDK build into node_modules for
examples (#504)
npm workspaces hoists example dependencies to the root node_modules,
but installs the published registry copy of @modelcontextprotocol/ext-apps
instead of linking to the local source. This causes type-check failures
when examples use features not yet published to npm.
Add scripts/link-self.mjs which copies the freshly-built dist/ and
package.json into the hoisted node_modules copy after each SDK build,
ensuring examples always type-check against the latest local types.
See: https://github.com/npm/feedback/discussions/774
---
package.json | 2 +-
scripts/link-self.mjs | 14 ++++++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
create mode 100644 scripts/link-self.mjs
diff --git a/package.json b/package.json
index fe5bec0..1358a43 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
"start": "npm run examples:dev",
"generate:schemas": "tsx scripts/generate-schemas.ts && prettier --write \"src/generated/**/*\"",
"sync:snippets": "bun scripts/sync-snippets.ts",
- "build": "npm run generate:schemas && npm run sync:snippets && node scripts/run-bun.mjs build.bun.ts",
+ "build": "npm run generate:schemas && npm run sync:snippets && node scripts/run-bun.mjs build.bun.ts && node scripts/link-self.mjs",
"prepack": "npm run build",
"build:all": "npm run examples:build",
"test": "bun test src",
diff --git a/scripts/link-self.mjs b/scripts/link-self.mjs
new file mode 100644
index 0000000..41051bf
--- /dev/null
+++ b/scripts/link-self.mjs
@@ -0,0 +1,14 @@
+/**
+ * Workaround: npm workspaces don't symlink the root package into node_modules
+ * when child workspaces depend on it — it installs a stale registry copy instead.
+ * This script syncs the freshly-built dist/ and package.json into that copy
+ * so examples always type-check against the latest local types.
+ * See: https://github.com/npm/feedback/discussions/774
+ */
+import { cpSync, existsSync } from "fs";
+
+const target = "node_modules/@modelcontextprotocol/ext-apps";
+if (!existsSync(target)) process.exit(0);
+
+cpSync("dist", `${target}/dist`, { recursive: true });
+cpSync("package.json", `${target}/package.json`);
From 5373f01d6f55114ec0a3eb9c864fea6f233589bc Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Wed, 25 Feb 2026 01:43:51 +0000
Subject: [PATCH 019/157] chore: bump ext-apps to 1.1.2 (#503)
* chore: bump ext-apps to 1.1.2
Changes since 1.1.1:
- feat: add ui/download-file method for host-mediated file downloads (#475)
- pdf-server: robust path validation + folder/file root support (#497)
- ci: auto-fix prettier formatting in pre-commit hook and CI (#498)
* fix: sync local SDK build into node_modules for examples
npm workspaces hoists example dependencies to the root node_modules,
but installs the published registry copy of @modelcontextprotocol/ext-apps
instead of linking to the local source. This causes type-check failures
when examples use features not yet published to npm.
Add scripts/link-self.mjs which copies the freshly-built dist/ and
package.json into the hoisted node_modules copy after each SDK build,
ensuring examples always type-check against the latest local types.
See: https://github.com/npm/feedback/discussions/774
* Revert "fix: sync local SDK build into node_modules for examples"
This reverts commit e3387e8434f3def030f5a223a70632092c929119.
---
examples/basic-host/package.json | 2 +-
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/budget-allocator-server/package.json | 2 +-
examples/cohort-heatmap-server/package.json | 2 +-
.../customer-segmentation-server/package.json | 2 +-
examples/debug-server/package.json | 2 +-
examples/integration-server/package.json | 2 +-
examples/map-server/package.json | 2 +-
examples/pdf-server/package.json | 2 +-
examples/qr-server/package.json | 2 +-
examples/quickstart/package.json | 2 +-
examples/say-server/package.json | 2 +-
examples/scenario-modeler-server/package.json | 2 +-
examples/shadertoy-server/package.json | 2 +-
examples/sheet-music-server/package.json | 2 +-
examples/system-monitor-server/package.json | 2 +-
examples/threejs-server/package.json | 2 +-
examples/transcript-server/package.json | 2 +-
examples/video-resource-server/package.json | 2 +-
examples/wiki-explorer-server/package.json | 2 +-
package-lock.json | 54 +++++++++----------
package.json | 2 +-
27 files changed, 53 insertions(+), 53 deletions(-)
diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json
index bbbedde..cf8ff4d 100644
--- a/examples/basic-host/package.json
+++ b/examples/basic-host/package.json
@@ -1,7 +1,7 @@
{
"homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host",
"name": "@modelcontextprotocol/ext-apps-basic-host",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"scripts": {
"build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"",
diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json
index e716fa5..166b75f 100644
--- a/examples/basic-server-preact/package.json
+++ b/examples/basic-server-preact/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Basic MCP App Server example using Preact",
"repository": {
diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json
index 3d1b2a4..0df2201 100644
--- a/examples/basic-server-react/package.json
+++ b/examples/basic-server-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-react",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Basic MCP App Server example using React",
"repository": {
diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json
index bfbe9fb..b8115cb 100644
--- a/examples/basic-server-solid/package.json
+++ b/examples/basic-server-solid/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Basic MCP App Server example using Solid",
"repository": {
diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json
index 4556f3b..2f6a7b6 100644
--- a/examples/basic-server-svelte/package.json
+++ b/examples/basic-server-svelte/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Basic MCP App Server example using Svelte",
"repository": {
diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json
index ada760c..44d3fdb 100644
--- a/examples/basic-server-vanillajs/package.json
+++ b/examples/basic-server-vanillajs/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-vanillajs",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Basic MCP App Server example using vanilla JavaScript",
"repository": {
diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json
index 551aac4..e77102b 100644
--- a/examples/basic-server-vue/package.json
+++ b/examples/basic-server-vue/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-basic-vue",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Basic MCP App Server example using Vue",
"repository": {
diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json
index 58d340a..ed111c4 100644
--- a/examples/budget-allocator-server/package.json
+++ b/examples/budget-allocator-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-budget-allocator",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Budget allocator MCP App Server with interactive visualization",
"repository": {
diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json
index 05fb7c9..21b560b 100644
--- a/examples/cohort-heatmap-server/package.json
+++ b/examples/cohort-heatmap-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-cohort-heatmap",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Cohort heatmap MCP App Server for retention analysis",
"repository": {
diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json
index 58e335d..fb3c6d4 100644
--- a/examples/customer-segmentation-server/package.json
+++ b/examples/customer-segmentation-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-customer-segmentation",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Customer segmentation MCP App Server with filtering",
"repository": {
diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json
index 624eb23..ec2a644 100644
--- a/examples/debug-server/package.json
+++ b/examples/debug-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-debug",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Debug MCP App Server for testing all SDK capabilities",
"repository": {
diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json
index 5ac0f1b..3ef3b3c 100644
--- a/examples/integration-server/package.json
+++ b/examples/integration-server/package.json
@@ -1,6 +1,6 @@
{
"name": "integration-server",
- "version": "1.1.1",
+ "version": "1.1.2",
"private": true,
"type": "module",
"scripts": {
diff --git a/examples/map-server/package.json b/examples/map-server/package.json
index 1cbf631..54037f8 100644
--- a/examples/map-server/package.json
+++ b/examples/map-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-map",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "MCP App Server example with CesiumJS 3D globe and geocoding",
"repository": {
diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json
index a99da7b..beac872 100644
--- a/examples/pdf-server/package.json
+++ b/examples/pdf-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-pdf",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer",
"repository": {
diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json
index b36258e..b72db3e 100644
--- a/examples/qr-server/package.json
+++ b/examples/qr-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-qr",
- "version": "1.1.1",
+ "version": "1.1.2",
"private": true,
"scripts": {
"start": "uv run server.py",
diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json
index ef0002b..13353e1 100644
--- a/examples/quickstart/package.json
+++ b/examples/quickstart/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/quickstart",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"private": true,
"description": "Quickstart MCP App Server example",
diff --git a/examples/say-server/package.json b/examples/say-server/package.json
index bae9418..edc4a55 100644
--- a/examples/say-server/package.json
+++ b/examples/say-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-say",
- "version": "1.1.1",
+ "version": "1.1.2",
"private": true,
"description": "Streaming TTS MCP App Server with karaoke-style text highlighting",
"repository": {
diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json
index adcb752..af91923 100644
--- a/examples/scenario-modeler-server/package.json
+++ b/examples/scenario-modeler-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-scenario-modeler",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Financial scenario modeling MCP App Server",
"repository": {
diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json
index f59881f..7829dba 100644
--- a/examples/shadertoy-server/package.json
+++ b/examples/shadertoy-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-shadertoy",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders",
"repository": {
diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json
index 4d48ad9..54ea185 100644
--- a/examples/sheet-music-server/package.json
+++ b/examples/sheet-music-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-sheet-music",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "MCP App Server for rendering and playing sheet music from ABC notation",
"repository": {
diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json
index 5bc04bb..0806b0b 100644
--- a/examples/system-monitor-server/package.json
+++ b/examples/system-monitor-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-system-monitor",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "System monitor MCP App Server with real-time stats",
"repository": {
diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json
index f4a5376..d984170 100644
--- a/examples/threejs-server/package.json
+++ b/examples/threejs-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-threejs",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Three.js 3D visualization MCP App Server",
"repository": {
diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json
index 9f50d69..e72c109 100644
--- a/examples/transcript-server/package.json
+++ b/examples/transcript-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-transcript",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "MCP App Server for live speech transcription",
"repository": {
diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json
index a45e237..cf64132 100644
--- a/examples/video-resource-server/package.json
+++ b/examples/video-resource-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-video-resource",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "MCP App Server demonstrating video resources served as base64 blobs",
"repository": {
diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json
index a5d3304..4eedb14 100644
--- a/examples/wiki-explorer-server/package.json
+++ b/examples/wiki-explorer-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@modelcontextprotocol/server-wiki-explorer",
- "version": "1.1.1",
+ "version": "1.1.2",
"type": "module",
"description": "Wikipedia link explorer MCP App Server with graph visualization",
"repository": {
diff --git a/package-lock.json b/package-lock.json
index ad45b72..0dcf024 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/ext-apps",
- "version": "1.1.1",
+ "version": "1.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@modelcontextprotocol/ext-apps",
- "version": "1.1.1",
+ "version": "1.1.2",
"hasInstallScript": true,
"license": "MIT",
"workspaces": [
@@ -79,7 +79,7 @@
},
"examples/basic-host": {
"name": "@modelcontextprotocol/ext-apps-basic-host",
- "version": "1.1.1",
+ "version": "1.1.2",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.24.0",
@@ -122,7 +122,7 @@
},
"examples/basic-server-preact": {
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -166,7 +166,7 @@
},
"examples/basic-server-react": {
"name": "@modelcontextprotocol/server-basic-react",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -213,7 +213,7 @@
},
"examples/basic-server-solid": {
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -257,7 +257,7 @@
},
"examples/basic-server-svelte": {
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -301,7 +301,7 @@
},
"examples/basic-server-vanillajs": {
"name": "@modelcontextprotocol/server-basic-vanillajs",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -343,7 +343,7 @@
},
"examples/basic-server-vue": {
"name": "@modelcontextprotocol/server-basic-vue",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -387,7 +387,7 @@
},
"examples/budget-allocator-server": {
"name": "@modelcontextprotocol/server-budget-allocator",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -430,7 +430,7 @@
},
"examples/cohort-heatmap-server": {
"name": "@modelcontextprotocol/server-cohort-heatmap",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -477,7 +477,7 @@
},
"examples/customer-segmentation-server": {
"name": "@modelcontextprotocol/server-customer-segmentation",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -520,7 +520,7 @@
},
"examples/debug-server": {
"name": "@modelcontextprotocol/server-debug",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -561,7 +561,7 @@
"license": "MIT"
},
"examples/integration-server": {
- "version": "1.1.1",
+ "version": "1.1.2",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
"@modelcontextprotocol/sdk": "^1.24.0",
@@ -606,7 +606,7 @@
},
"examples/map-server": {
"name": "@modelcontextprotocol/server-map",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -648,7 +648,7 @@
},
"examples/pdf-server": {
"name": "@modelcontextprotocol/server-pdf",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -691,14 +691,14 @@
},
"examples/qr-server": {
"name": "@modelcontextprotocol/server-qr",
- "version": "1.1.1",
+ "version": "1.1.2",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0"
}
},
"examples/quickstart": {
"name": "@modelcontextprotocol/quickstart",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -730,7 +730,7 @@
},
"examples/say-server": {
"name": "@modelcontextprotocol/server-say",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0"
@@ -738,7 +738,7 @@
},
"examples/scenario-modeler-server": {
"name": "@modelcontextprotocol/server-scenario-modeler",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -786,7 +786,7 @@
},
"examples/shadertoy-server": {
"name": "@modelcontextprotocol/server-shadertoy",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -828,7 +828,7 @@
},
"examples/sheet-music-server": {
"name": "@modelcontextprotocol/server-sheet-music",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -871,7 +871,7 @@
},
"examples/system-monitor-server": {
"name": "@modelcontextprotocol/server-system-monitor",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -915,7 +915,7 @@
},
"examples/threejs-server": {
"name": "@modelcontextprotocol/server-threejs",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -964,7 +964,7 @@
},
"examples/transcript-server": {
"name": "@modelcontextprotocol/server-transcript",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -1007,7 +1007,7 @@
},
"examples/video-resource-server": {
"name": "@modelcontextprotocol/server-video-resource",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
@@ -1049,7 +1049,7 @@
},
"examples/wiki-explorer-server": {
"name": "@modelcontextprotocol/server-wiki-explorer",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/ext-apps": "^1.0.0",
diff --git a/package.json b/package.json
index 1358a43..6a2d0fa 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"url": "https://github.com/modelcontextprotocol/ext-apps"
},
"homepage": "https://github.com/modelcontextprotocol/ext-apps",
- "version": "1.1.1",
+ "version": "1.1.2",
"license": "MIT",
"description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.",
"type": "module",
From 627ead3805a1a880a6026c8a292e541c703679f2 Mon Sep 17 00:00:00 2001
From: Olivier Chafik
Date: Thu, 26 Feb 2026 04:00:23 +0000
Subject: [PATCH 020/157] 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 021/157] 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 022/157] 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 023/157] 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 024/157] 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 025/157] 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
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
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(
+ /
]*>.*?<\/ul>/,
+ `
${group}
`,
+ );
+ }
+
+ // Inject script to mark the current sidebar nav link with a "current" class.
+ // TypeDoc does not natively add this class for document pages.
+ // The sidebar is populated asynchronously from compressed navigation data,
+ // so we use a MutationObserver to detect when links appear.
+ // Pathname comparison strips trailing slashes and .html extensions to handle
+ // servers with clean-URL mode (e.g. `serve` drops .html).
+ const currentNavScript = ``;
+ page.contents = page.contents.replace(
+ "