Skip to content

Commit 76f2de7

Browse files
authored
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.
1 parent b8a1349 commit 76f2de7

19 files changed

+860
-57
lines changed

docs/agent-skills.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
---
22
title: Agent Skills
3+
group: Getting Started
4+
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.
35
---
46

57
# Agent Skills

docs/authorization.md

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
---
2+
title: Authorization
3+
group: Security
4+
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.
5+
---
6+
7+
# Authorization
8+
9+
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:
10+
11+
- **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.
12+
- **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.
13+
14+
## Shared setup
15+
16+
Regardless of which approach you choose, you need OAuth discovery metadata and token verification. These are the same for both.
17+
18+
### OAuth discovery metadata
19+
20+
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:
21+
22+
**[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.
23+
24+
**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:
25+
26+
```ts
27+
app.get("/.well-known/oauth-authorization-server", (_req, res) => {
28+
res.json({
29+
...oauthMetadata,
30+
client_id_metadata_document_supported: true,
31+
});
32+
});
33+
```
34+
35+
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.
36+
37+
### Token verification
38+
39+
Verify access tokens as JWTs against the identity provider's JWKS endpoint. The `jose` library handles key fetching and caching:
40+
41+
```ts
42+
import { createRemoteJWKSet, jwtVerify } from "jose";
43+
44+
const JWKS = createRemoteJWKSet(new URL(`${IDP_DOMAIN}/.well-known/jwks.json`));
45+
46+
const { payload } = await jwtVerify(token, JWKS, {
47+
issuer: IDP_DOMAIN,
48+
});
49+
```
50+
51+
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.
52+
53+
## Per-server authorization
54+
55+
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.
56+
57+
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.
58+
59+
## Per-tool authorization
60+
61+
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.
62+
63+
### How it works
64+
65+
1. The server maintains a set of tool names that require authorization
66+
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
67+
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)
68+
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
69+
5. On retry, the server verifies the token, extracts the user identity, and creates a per-request MCP server instance with that auth context
70+
6. Unprotected tools pass through without any token check — they work for everyone
71+
72+
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.
73+
74+
### Enforcing HTTP 401
75+
76+
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.
77+
78+
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:
79+
80+
```ts
81+
/** Tools that require a valid Bearer token — checked at the HTTP level for proper 401. */
82+
const PROTECTED_TOOLS = new Set(["get_account_balance", "manage_branch_admin"]);
83+
84+
app.all("/mcp", async (req, res) => {
85+
// Parse the JSON-RPC body — it may be a single message or a batch
86+
const messages = Array.isArray(req.body) ? req.body : [req.body];
87+
88+
// Check if any message is a tools/call for a protected tool
89+
const needsAuth = messages.some(
90+
(msg: any) =>
91+
msg?.method === "tools/call" && PROTECTED_TOOLS.has(msg.params?.name),
92+
);
93+
94+
// Extract and verify the Bearer token
95+
let authInfo: AuthInfo | undefined;
96+
const authHeader = req.headers.authorization;
97+
98+
if (authHeader?.startsWith("Bearer ")) {
99+
try {
100+
const token = authHeader.slice(7);
101+
const { payload } = await jwtVerify(token, JWKS, {
102+
issuer: IDP_DOMAIN,
103+
});
104+
authInfo = { token, sub: payload.sub as string };
105+
} catch {
106+
if (needsAuth) {
107+
return res
108+
.status(401)
109+
.set(
110+
"WWW-Authenticate",
111+
`Bearer resource_metadata="${resourceMetadataUrl}"`,
112+
)
113+
.json({
114+
error: "invalid_token",
115+
error_description: "The access token is invalid",
116+
});
117+
}
118+
}
119+
} else if (needsAuth) {
120+
return res
121+
.status(401)
122+
.set(
123+
"WWW-Authenticate",
124+
`Bearer resource_metadata="${resourceMetadataUrl}"`,
125+
)
126+
.json({
127+
error: "invalid_token",
128+
error_description: "Authorization required",
129+
});
130+
}
131+
132+
// Create a per-request MCP server with the auth context.
133+
// authInfo is undefined for public tool calls, populated for
134+
// authenticated requests — tool handlers use it to scope data
135+
// to the authenticated user.
136+
const server = createServer(authInfo);
137+
// ... handle the request with transport
138+
});
139+
```
140+
141+
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.
142+
143+
### Defence-in-depth in tool handlers
144+
145+
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:
146+
147+
```ts
148+
registerAppTool(
149+
server,
150+
"get_account_balance",
151+
{
152+
description: "Get account balance",
153+
inputSchema: { accountId: z.string() },
154+
},
155+
async ({ accountId }) => {
156+
if (!authInfo) {
157+
return {
158+
isError: true,
159+
content: [
160+
{
161+
type: "text",
162+
text: "Authorization required to access account data.",
163+
},
164+
],
165+
};
166+
}
167+
168+
const balance = await getBalance(authInfo.sub, accountId);
169+
return {
170+
content: [{ type: "text", text: `Balance: ${balance}` }],
171+
};
172+
},
173+
);
174+
```
175+
176+
### UI-initiated auth escalation
177+
178+
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:
179+
180+
1. A public tool (e.g., `manage_branch`) loads the UI without requiring authorization
181+
2. The user clicks a button that calls a protected tool via `app.callServerTool()`
182+
3. The MCP host receives HTTP `401` and automatically runs the OAuth flow
183+
4. After the user completes the OAuth flow, the host retries the tool call with the acquired token
184+
5. The protected data appears in the UI
185+
186+
```tsx
187+
function BranchItem({ branch }: { branch: Branch }) {
188+
const [adminData, setAdminData] = useState(null);
189+
190+
async function handleManage() {
191+
// This call may trigger the OAuth flow if the user
192+
// hasn't been authorized yet — the host handles it
193+
// transparently.
194+
const result = await app.callServerTool({
195+
name: "manage_branch_admin",
196+
arguments: { branch_id: branch.id },
197+
});
198+
setAdminData(result.structuredContent);
199+
}
200+
201+
return (
202+
<div>
203+
<span>{branch.name}</span>
204+
<button onClick={handleManage}>Manage</button>
205+
{adminData && <AdminPanel data={adminData} />}
206+
</div>
207+
);
208+
}
209+
```
210+
211+
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.

docs/csp-cors.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
title: CSP and CORS
3+
group: Security
4+
description: Configure Content Security Policy and CORS for MCP Apps that make network requests from sandboxed iframes, including connectDomains, resourceDomains, and stable origin setup.
5+
---
6+
7+
# CSP & CORS
8+
9+
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.
10+
11+
**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.
12+
13+
**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.
14+
15+
<!-- prettier-ignore -->
16+
```ts source="../src/server/index.examples.ts#registerAppResource_withDomain"
17+
// Computes a stable origin from an MCP server URL for hosting in Claude.
18+
function computeAppDomainForClaude(mcpServerUrl: string): string {
19+
const hash = crypto
20+
.createHash("sha256")
21+
.update(mcpServerUrl)
22+
.digest("hex")
23+
.slice(0, 32);
24+
return `${hash}.claudemcpcontent.com`;
25+
}
26+
27+
const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp");
28+
29+
registerAppResource(
30+
server,
31+
"Company Dashboard",
32+
"ui://dashboard/view.html",
33+
{
34+
description: "Internal dashboard with company data",
35+
},
36+
async () => ({
37+
contents: [
38+
{
39+
uri: "ui://dashboard/view.html",
40+
mimeType: RESOURCE_MIME_TYPE,
41+
text: dashboardHtml,
42+
_meta: {
43+
ui: {
44+
// CSP: tell browser the app is allowed to make requests
45+
csp: {
46+
connectDomains: ["https://api.example.com"],
47+
},
48+
// CORS: give app a stable origin for the API server to allowlist
49+
//
50+
// (Public APIs that use `Access-Control-Allow-Origin: *` or API
51+
// key auth don't need this.)
52+
domain: APP_DOMAIN,
53+
},
54+
},
55+
},
56+
],
57+
}),
58+
);
59+
```
60+
61+
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.
62+
63+
> [!NOTE]
64+
> 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`).

docs/favicons/apple-touch-icon.png

7.76 KB
Loading

docs/favicons/favicon-16x16.png

298 Bytes
Loading

docs/favicons/favicon-32x32.png

3.58 KB
Loading
298 Bytes
Loading
519 Bytes
Loading

docs/favicons/favicon-dark.ico

5.04 KB
Binary file not shown.

docs/favicons/favicon.ico

1.25 KB
Binary file not shown.

0 commit comments

Comments
 (0)