The term "external" is misleading for MCP Apps because developers typically interpret it as "third-party." However, MCP App HTML runs in a sandboxed iframe with no same-origin server, so *all* origins must be declared—including where your own bundled JS/CSS is served from. Using "network" instead avoids this confusion and aligns with the detailed explanations in `patterns.md` and `spec.types.ts`. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
291 lines
7.5 KiB
TypeScript
291 lines
7.5 KiB
TypeScript
/**
|
|
* Type-checked examples for {@link registerAppTool `registerAppTool`} and {@link registerAppResource `registerAppResource`}.
|
|
*
|
|
* These examples are included in the API documentation via `@includeCode` tags.
|
|
* Each function's region markers define the code snippet that appears in the docs.
|
|
*
|
|
* @module
|
|
*/
|
|
|
|
import * as crypto from "node:crypto";
|
|
import * as fs from "node:fs/promises";
|
|
import type {
|
|
McpServer,
|
|
ToolCallback,
|
|
ReadResourceCallback,
|
|
} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
import { z } from "zod";
|
|
import {
|
|
registerAppTool,
|
|
registerAppResource,
|
|
getUiCapability,
|
|
RESOURCE_MIME_TYPE,
|
|
} from "./index.js";
|
|
|
|
// Stubs for external functions used in examples
|
|
declare function fetchWeather(
|
|
location: string,
|
|
): Promise<{ temp: number; conditions: string }>;
|
|
declare function getCart(): Promise<{ items: unknown[]; total: number }>;
|
|
declare function updateCartItem(
|
|
itemId: string,
|
|
quantity: number,
|
|
): Promise<{ items: unknown[]; total: number }>;
|
|
|
|
/**
|
|
* Example: Module overview showing basic registration of tools and resources.
|
|
*/
|
|
function index_overview(
|
|
server: McpServer,
|
|
toolCallback: ToolCallback,
|
|
readCallback: ReadResourceCallback,
|
|
) {
|
|
//#region index_overview
|
|
// Register a tool that displays a view
|
|
registerAppTool(
|
|
server,
|
|
"weather",
|
|
{
|
|
description: "Get weather forecast",
|
|
_meta: { ui: { resourceUri: "ui://weather/view.html" } },
|
|
},
|
|
toolCallback,
|
|
);
|
|
|
|
// Register the HTML resource the tool references
|
|
registerAppResource(
|
|
server,
|
|
"Weather View",
|
|
"ui://weather/view.html",
|
|
{},
|
|
readCallback,
|
|
);
|
|
//#endregion index_overview
|
|
}
|
|
|
|
/**
|
|
* Example: Basic usage of registerAppTool with input schema and handler.
|
|
*/
|
|
function registerAppTool_basicUsage(server: McpServer) {
|
|
//#region registerAppTool_basicUsage
|
|
registerAppTool(
|
|
server,
|
|
"get-weather",
|
|
{
|
|
title: "Get Weather",
|
|
description: "Get current weather for a location",
|
|
inputSchema: { location: z.string() },
|
|
_meta: {
|
|
ui: { resourceUri: "ui://weather/view.html" },
|
|
},
|
|
},
|
|
async (args) => {
|
|
const weather = await fetchWeather(args.location);
|
|
return { content: [{ type: "text", text: JSON.stringify(weather) }] };
|
|
},
|
|
);
|
|
//#endregion registerAppTool_basicUsage
|
|
}
|
|
|
|
/**
|
|
* Example: Model-only visibility - tools visible to model but not callable by UI.
|
|
*/
|
|
function registerAppTool_modelOnlyVisibility(server: McpServer) {
|
|
//#region registerAppTool_modelOnlyVisibility
|
|
registerAppTool(
|
|
server,
|
|
"show-cart",
|
|
{
|
|
description: "Display the user's shopping cart",
|
|
_meta: {
|
|
ui: {
|
|
resourceUri: "ui://shop/cart.html",
|
|
visibility: ["model"],
|
|
},
|
|
},
|
|
},
|
|
async () => {
|
|
const cart = await getCart();
|
|
return { content: [{ type: "text", text: JSON.stringify(cart) }] };
|
|
},
|
|
);
|
|
//#endregion registerAppTool_modelOnlyVisibility
|
|
}
|
|
|
|
/**
|
|
* Example: App-only visibility - tools hidden from model, only callable by UI.
|
|
*/
|
|
function registerAppTool_appOnlyVisibility(server: McpServer) {
|
|
//#region registerAppTool_appOnlyVisibility
|
|
registerAppTool(
|
|
server,
|
|
"update-quantity",
|
|
{
|
|
description: "Update item quantity in cart",
|
|
inputSchema: { itemId: z.string(), quantity: z.number() },
|
|
_meta: {
|
|
ui: {
|
|
resourceUri: "ui://shop/cart.html",
|
|
visibility: ["app"],
|
|
},
|
|
},
|
|
},
|
|
async ({ itemId, quantity }) => {
|
|
const cart = await updateCartItem(itemId, quantity);
|
|
return { content: [{ type: "text", text: JSON.stringify(cart) }] };
|
|
},
|
|
);
|
|
//#endregion registerAppTool_appOnlyVisibility
|
|
}
|
|
|
|
/**
|
|
* Example: Basic usage of registerAppResource.
|
|
*/
|
|
function registerAppResource_basicUsage(server: McpServer) {
|
|
//#region registerAppResource_basicUsage
|
|
registerAppResource(
|
|
server,
|
|
"Weather View",
|
|
"ui://weather/view.html",
|
|
{
|
|
description: "Interactive weather display",
|
|
},
|
|
async () => ({
|
|
contents: [
|
|
{
|
|
uri: "ui://weather/view.html",
|
|
mimeType: RESOURCE_MIME_TYPE,
|
|
text: await fs.readFile("dist/view.html", "utf-8"),
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
//#endregion registerAppResource_basicUsage
|
|
}
|
|
|
|
/**
|
|
* Example: registerAppResource with CSP configuration for network access.
|
|
*/
|
|
function registerAppResource_withCsp(
|
|
server: McpServer,
|
|
musicPlayerHtml: string,
|
|
) {
|
|
//#region registerAppResource_withCsp
|
|
registerAppResource(
|
|
server,
|
|
"Music Player",
|
|
"ui://music/player.html",
|
|
{
|
|
description: "Audio player with external soundfonts",
|
|
},
|
|
async () => ({
|
|
contents: [
|
|
{
|
|
uri: "ui://music/player.html",
|
|
mimeType: RESOURCE_MIME_TYPE,
|
|
text: musicPlayerHtml,
|
|
_meta: {
|
|
ui: {
|
|
csp: {
|
|
resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images
|
|
connectDomains: ["https://api.example.com"], // For fetch/WebSocket
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
//#endregion registerAppResource_withCsp
|
|
}
|
|
|
|
/**
|
|
* Example: registerAppResource with stable origin for external API CORS allowlists.
|
|
*/
|
|
function registerAppResource_withDomain(
|
|
server: McpServer,
|
|
dashboardHtml: string,
|
|
) {
|
|
//#region 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,
|
|
},
|
|
},
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
//#endregion registerAppResource_withDomain
|
|
}
|
|
|
|
/**
|
|
* Example: Check for MCP Apps support in server initialization.
|
|
*/
|
|
function getUiCapability_checkSupport(
|
|
server: McpServer,
|
|
weatherHandler: ToolCallback,
|
|
textWeatherHandler: ToolCallback,
|
|
) {
|
|
//#region getUiCapability_checkSupport
|
|
server.server.oninitialized = () => {
|
|
const clientCapabilities = server.server.getClientCapabilities();
|
|
const uiCap = getUiCapability(clientCapabilities);
|
|
|
|
if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) {
|
|
// App-enhanced tool
|
|
registerAppTool(
|
|
server,
|
|
"weather",
|
|
{
|
|
description: "Get weather information with interactive dashboard",
|
|
_meta: { ui: { resourceUri: "ui://weather/dashboard" } },
|
|
},
|
|
weatherHandler,
|
|
);
|
|
} else {
|
|
// Text-only fallback
|
|
server.registerTool(
|
|
"weather",
|
|
{
|
|
description: "Get weather information",
|
|
},
|
|
textWeatherHandler,
|
|
);
|
|
}
|
|
};
|
|
//#endregion getUiCapability_checkSupport
|
|
}
|