refactor: finish MCP SDK v2 migration

This commit is contained in:
Andrew Khadder 2026-07-14 17:07:30 -07:00
commit f94cb38b0b
No known key found for this signature in database
3 changed files with 684 additions and 540 deletions

View file

@ -77,13 +77,15 @@ Before writing any code, analyze the server's existing tools and determine which
## Step 2: Add Dependencies
```bash
npm install @modelcontextprotocol/ext-apps
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 zod@^4.2.0
npm install -D vite vite-plugin-singlefile
```
Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React).
Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
Use the exact base MCP SDK prerelease required by ext-apps. Existing servers
that still import `@modelcontextprotocol/sdk` v1 must migrate those imports and
handler schemas before adding the App integration.
## Step 3: Set Up the Build Pipeline
@ -146,10 +148,14 @@ Transform plain MCP tools into App tools with UI.
**Before** (plain MCP tool):
```typescript
server.tool("my-tool", { param: z.string() }, async (args) => {
const data = await fetchData(args.param);
return { content: [{ type: "text", text: JSON.stringify(data) }] };
});
server.registerTool(
"my-tool",
{ inputSchema: z.object({ param: z.string() }) },
async ({ param }) => {
const data = await fetchData(param);
return { content: [{ type: "text", text: JSON.stringify(data) }] };
},
);
```
**After** (App tool with UI):
@ -158,17 +164,22 @@ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@model
const resourceUri = "ui://my-tool/mcp-app.html";
registerAppTool(server, "my-tool", {
description: "Shows data with an interactive UI",
inputSchema: { param: z.string() },
_meta: { ui: { resourceUri } },
}, async (args) => {
const data = await fetchData(args.param);
return {
content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts
structuredContent: { data }, // structured data for the UI
};
});
registerAppTool(
server,
"my-tool",
{
description: "Shows data with an interactive UI",
inputSchema: z.object({ param: z.string() }),
_meta: { ui: { resourceUri } },
},
async ({ param }) => {
const data = await fetchData(param);
return {
content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts
structuredContent: { data }, // structured data for the UI
};
},
);
```
Key guidance:
@ -187,17 +198,21 @@ import path from "node:path";
const resourceUri = "ui://my-tool/mcp-app.html";
registerAppResource(server, {
uri: resourceUri,
name: "My Tool UI",
mimeType: RESOURCE_MIME_TYPE,
}, async () => {
const html = await fs.readFile(
path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
"utf-8",
);
return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
});
registerAppResource(
server,
"My Tool UI",
resourceUri,
{},
async () => {
const html = await fs.readFile(
path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
"utf-8",
);
return {
contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
};
},
);
```
If multiple tools share the same UI, they can reference the same `resourceUri` and the same resource registration.
@ -271,25 +286,32 @@ registerAppTool(server, "poll-data", {
});
```
The UI calls these via `app.callServerTool("poll-data", {})`.
The UI calls these via
`app.callServerTool({ name: "poll-data", arguments: {} })`.
### CSP Configuration
If the UI needs to load external resources (fonts, APIs, CDNs), declare the domains:
```typescript
registerAppResource(server, {
uri: resourceUri,
name: "My Tool UI",
mimeType: RESOURCE_MIME_TYPE,
_meta: {
ui: {
connectDomains: ["api.example.com"], // fetch/XHR targets
resourceDomains: ["cdn.example.com"], // scripts, styles, images
frameDomains: ["embed.example.com"], // nested iframes
registerAppResource(server, "My Tool UI", resourceUri, {}, async () => ({
contents: [
{
uri: resourceUri,
mimeType: RESOURCE_MIME_TYPE,
text: html,
_meta: {
ui: {
csp: {
connectDomains: ["https://api.example.com"], // fetch/XHR targets
resourceDomains: ["https://cdn.example.com"], // scripts, styles, images
frameDomains: ["https://embed.example.com"], // nested iframes
},
},
},
},
},
}, async () => { /* ... */ });
],
}));
```
### Streaming Partial Input
@ -326,7 +348,14 @@ server.server.oninitialized = () => {
}, appToolHandler);
} else {
// Text-only client — register plain tool
server.tool("my-tool", "Shows data", { param: z.string() }, plainToolHandler);
server.registerTool(
"my-tool",
{
description: "Shows data",
inputSchema: z.object({ param: z.string() }),
},
plainToolHandler,
);
}
};
```

View file

@ -115,19 +115,20 @@ Create a new MCP server with tool and resource registration. This wraps the exis
### Dependencies
```bash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 zod@^4.2.0
npm install -D tsx vite vite-plugin-singlefile
```
Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
Use `npm install` with the exact base MCP SDK prerelease required by ext-apps.
Do not substitute unpublished local packages or guess a different prerelease.
### Server Code
Create `server.ts`:
```typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { McpServer } from "@modelcontextprotocol/server";
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
import fs from "node:fs/promises";
import path from "node:path";
@ -140,7 +141,9 @@ const resourceUri = "ui://my-app/mcp-app.html";
// Register the tool — inputSchema maps to the app's data sources
registerAppTool(server, "show-app", {
description: "Displays the app with the given parameters",
inputSchema: { query: z.string().describe("The search query") },
inputSchema: z.object({
query: z.string().describe("The search query"),
}),
_meta: { ui: { resourceUri } },
}, async (args) => {
// Process args server-side if needed
@ -151,19 +154,29 @@ registerAppTool(server, "show-app", {
});
// Register the HTML resource
registerAppResource(server, {
uri: resourceUri,
name: "My App UI",
mimeType: RESOURCE_MIME_TYPE,
// Add CSP domains from Step 2 if needed:
// _meta: { ui: { connectDomains: ["api.example.com"], resourceDomains: ["cdn.example.com"] } },
}, async () => {
const html = await fs.readFile(
path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
"utf-8",
);
return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
});
registerAppResource(
server,
"My App UI",
resourceUri,
{},
async () => {
const html = await fs.readFile(
path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
"utf-8",
);
return {
contents: [
{
uri: resourceUri,
mimeType: RESOURCE_MIME_TYPE,
text: html,
// Add CSP domains from Step 2 if needed:
// _meta: { ui: { csp: { connectDomains: ["https://api.example.com"], resourceDomains: ["https://cdn.example.com"] } } },
},
],
};
},
);
// Start the server
const transport = new StdioServerTransport();
@ -310,7 +323,10 @@ const data = await fetch("/api/data").then(r => r.json());
// After (hybrid):
async function fetchData(): Promise<any> {
if (isMcpApp) {
const result = await app.callServerTool("fetch-data", {});
const result = await app.callServerTool({
name: "fetch-data",
arguments: {},
});
return result.structuredContent;
}
return fetch("/api/data").then(r => r.json());
@ -437,7 +453,8 @@ registerAppTool(server, "refresh-data", {
});
```
The UI calls these via `app.callServerTool("refresh-data", {})`.
The UI calls these via
`app.callServerTool({ name: "refresh-data", arguments: {} })`.
### Streaming Partial Input

View file

@ -110,11 +110,12 @@ See `/tmp/mcp-ext-apps/docs/patterns.md` for detailed recipes:
**Always** use `npm install` to add dependencies rather than manually writing version numbers:
```bash
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod express cors
npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 @modelcontextprotocol/node@2.0.0-beta.4 @modelcontextprotocol/express@2.0.0-beta.4 zod@^4.2.0 express cors
npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors
```
This lets npm resolve the latest compatible versions. **Never** specify version numbers from memory.
Use the exact base MCP SDK prerelease required by ext-apps. Do not substitute
unpublished local packages or guess a different prerelease.
### TypeScript Server Execution