refactor: finish MCP SDK v2 migration
This commit is contained in:
parent
a76e3a7691
commit
f94cb38b0b
3 changed files with 684 additions and 540 deletions
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue