阅读本指南后,您将掌握如何将后端 MCP 服务器连接至 ChatGPT,定义工具,注册 UI 模板,并使用小组件运行时将它们串联起来。您将为 ChatGPT 应用打下坚实基础,实现返回结构化数据、渲染交互式小组件,并确保模型、服务器与 UI 保持同步。如果您想直接查看实现过程,可以直接跳转到文末的示例。
使用编辑器中的 OpenAI Docs MCP 服务器,加速开发进程。
概述
MCP 服务器为您的应用所做的工作
ChatGPT 应用由三个组件构成
- 您的 MCP 服务器:负责定义工具、执行身份验证、返回数据,并将每个工具指向相应的 UI 包。
- 小组件/UI 包:在 ChatGPT 的 iframe 中渲染,并通过 MCP 应用 UI 桥(基于
postMessage的 JSON-RPC)与宿主进行通信。 - 模型:决定何时调用工具,并使用您返回的结构化数据来叙述操作体验。
稳健的服务器实现能保持组件间的清晰界限,让您能够独立迭代 UI 和数据。请记住:您负责构建 MCP 服务器并定义工具,而 ChatGPT 模型则会根据您提供的元数据决定何时调用它们。
开始之前
先决条件
- 熟悉 TypeScript 或 Python 以及 Web 打包工具(Vite、esbuild 等)。
- MCP 服务器可通过 HTTP 访问(本地环境即可开始)。
- 已构建导出根脚本(React 或原生 JS)的 UI 包。
示例项目布局
your-chatgpt-app/
├─ server/
│ └─ src/index.ts # MCP server + tool handlers
├─ web/
│ ├─ src/component.tsx # React widget
│ └─ dist/app.{js,css} # Bundled assets referenced by the server
└─ package.json
架构流程
- 用户的 Prompt 促使 ChatGPT 调用您的一种 MCP 工具。
- 您的服务器运行处理程序,获取权威数据,并返回
structuredContent、_meta以及 UI 元数据。 - ChatGPT 加载工具描述符中链接的 HTML 模板(服务类型为
text/html;profile=mcp-app),并通过 MCP 应用桥将工具输入/结果传送到 iframe(例如ui/notifications/tool-result)。 - 小组件基于工具结果进行渲染,可以再次调用工具(使用
tools/call),并根据需要选择性地使用仅限 ChatGPT 的扩展功能。 - 模型读取
structuredContent以叙述所发生的情况,因此请确保数据简洁且幂等——ChatGPT 可能会重试工具调用。
User prompt
↓
ChatGPT model ──► MCP tool call ──► Your server ──► Tool response (`structuredContent`, `_meta`, `content`)
│ │
└───── renders narration ◄──── widget iframe ◄──────┘
(HTML template + MCP Apps bridge)
使用 MCP 应用 UI 桥
ChatGPT 支持用于 UI 通信的开放式 MCP 应用标准
- 基于
postMessage的 JSON-RPC 2.0 消息。 - 用于宿主↔iframe UI 通信的
ui/*方法和通知。 - 通过
tools/call进行的 MCP 工具调用。
从 MCP 应用桥开始,以保持您的 UI 在不同宿主间的可移植性,然后在需要 ChatGPT 特定功能时添加相关扩展。如需深入了解和映射指南,请参阅 ChatGPT 中的 MCP 应用兼容性。
了解 window.openai 小组件运行时
window.openai 是一个 Apps SDK 兼容层,也是可选 ChatGPT 扩展功能的宿主。对于新应用,默认使用 MCP 应用桥,并将 window.openai 视为提供 ChatGPT 特有功能的 API。
独特功能包括
- 文件处理(ChatGPT 扩展):
uploadFile、selectFiles和getFileDownloadUrl涵盖了文件上传、选择和下载。 - 宿主界面(ChatGPT 扩展):
requestModal可打开由宿主控制的模态框。 - 商业(ChatGPT 扩展):
requestCheckout可打开即时结账(如果已启用)。
有关 window.openai 的完整参考,请参阅 ChatGPT UI 指南。
当您需要宿主控制的覆盖层时使用 requestModal——例如,打开锚定在“添加到购物车”按钮上的结账或详情视图,以便购物者无需调整内嵌小组件的大小即可查看选项。要在模态框中显示不同的 UI 模板,请传递您注册的模板 URI(例如通过 registerAppResource)。
当这些 API 能显著改善您的 ChatGPT 体验时使用它们,但请保持您的核心 UI 桥基于 MCP 应用标准构建。有关实现模式,请参阅 构建您的 ChatGPT UI。
选择一个 SDK
Apps SDK 适用于任何 MCP 实现,但官方 SDK 是最快的入门方式。它们内置了工具/模式辅助程序、HTTP 服务器脚手架、资源注册工具以及端到端类型安全,让您可以专注于业务逻辑。
- Python SDK – 使用 FastMCP 或 FastAPI 快速迭代。仓库:
modelcontextprotocol/python-sdk。 - TypeScript SDK – 当您的技术栈已是 Node/React 时是理想之选。仓库:
modelcontextprotocol/typescript-sdk,发布为@modelcontextprotocol/sdk。文档位于 modelcontextprotocol.io。
安装与您后端语言匹配的 SDK,然后按照以下步骤操作。
# TypeScript / Node
npm install @modelcontextprotocol/sdk @modelcontextprotocol/ext-apps zod
# Python
pip install mcp
构建您的 MCP 服务器
步骤 1 – 注册组件模板
每个 UI 包都作为带有 MCP 应用 UI MIME 类型 (text/html;profile=mcp-app) 的 MCP 资源公开。如果您使用 @modelcontextprotocol/ext-apps/server,建议使用 RESOURCE_MIME_TYPE 而不是硬编码该字符串。
注册模板并包含有关边框、域和 CSP 规则的元数据。
// Registers the Kanban widget HTML entry point served to ChatGPT.
import {
registerAppResource,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { readFileSync } from "node:fs";
const server = new McpServer({ name: "kanban-server", version: "1.0.0" });
const HTML = readFileSync("web/dist/kanban.js", "utf8");
const CSS = readFileSync("web/dist/kanban.css", "utf8");
registerAppResource(
server,
"kanban-widget",
"ui://widget/kanban-board.html",
{},
async () => ({
contents: [
{
uri: "ui://widget/kanban-board.html",
mimeType: RESOURCE_MIME_TYPE,
text: `
<div id="kanban-root"></div>
<style>${CSS}</style>
<script type="module">${HTML}</script>
`.trim(),
_meta: {
ui: {
prefersBorder: true,
domain: "https://myapp.example.com",
csp: {
connectDomains: ["https://api.myapp.example.com"], // example API domain
resourceDomains: ["https://*.oaistatic.com"], // example CDN allowlist
// Optional: allow embedding specific iframe origins.
frameDomains: ["https://*.example-embed.com"],
},
},
},
},
],
})
);
如果您需要在小组件内嵌入 iframe,请使用 _meta.ui.csp.frameDomains 来声明源许可名单。如果不设置 frameDomains,子框架默认被拦截。由于 iframe 内容难以检查,启用子框架的小组件会受到额外审查,可能无法获得目录分发资格。
最佳实践: 当您以破坏性的方式更改小组件的 HTML/JS/CSS 时,请给模板赋予一个新的 URI(或使用新的文件名),以便 ChatGPT 始终加载更新后的包,而不是缓存的版本。
将 URI 视为您的缓存键。当您更新标记或包时,请对 URI 进行版本控制并更新对它的所有引用(例如 registerAppResource URI、工具描述符中的 _meta.ui.resourceUri 以及模板列表中的 contents[].uri)。ChatGPT 承认 _meta["openai/outputTemplate"] 作为 OpenAI 特定的兼容性别名。
// Old
contents: [{ uri: "ui://widget/kanban-board.html" /* ... */ }];
// New
contents: [{ uri: "ui://widget/kanban-board-v2.html" /* ... */ }];
如果您频繁发布更新,请保持简短、一致的版本控制方案,以便您可以向前(或向后)滚动,而不必重复使用相同的 URI。
步骤 2 – 描述工具
工具是模型进行推理的契约。为每个用户意图定义一个工具(例如 list_tasks, update_task)。每个描述符都应包含:
- 机器可读的名称和人类可读的标题。
- 参数模式。使用下方的 Node 辅助程序时,请使用 Zod 原始形状或标准模式;其他 SDK 可能公开 JSON Schema 或数据类。
- 返回的
structuredContent的模式 (outputSchema),以便客户端和模型了解工具结果的形状。 - 指向模板 URI 的
_meta.ui.resourceUri。 - 可选的
_meta.ui.visibility,用于控制工具是可由模型、UI 还是两者调用。 - 可选的 ChatGPT 扩展(例如工具运行时的短状态文本)。
模型会检查这些描述符以决定工具何时适合用户的请求,因此请将名称、描述和模式视为您用户体验 (UX) 的一部分。
设计处理程序以实现幂等性——模型可能会重试调用。
// Example app that exposes a kanban-board tool with schema, metadata, and handler.
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";
registerAppTool(
server,
"kanban-board",
{
title: "Show Kanban Board",
inputSchema: { workspace: z.string() },
outputSchema: {
columns: z.array(
z.object({
id: z.string(),
title: z.string(),
tasks: z.array(
z.object({
id: z.string(),
title: z.string(),
status: z.string(),
})
),
})
),
},
_meta: {
ui: { resourceUri: "ui://widget/kanban-board.html" },
// ChatGPT extension (optional):
// "openai/toolInvocation/invoking": "Preparing the board…",
// "openai/toolInvocation/invoked": "Board ready.",
},
},
async ({ workspace }) => {
const board = await loadBoard(workspace);
return {
structuredContent: board.summary,
content: [{ type: "text", text: `Showing board ${workspace}` }],
_meta: board.details,
};
}
);
记忆与工具调用
记忆由用户控制并由模型协调:模型在选择或参数化工具调用时决定是否以及如何使用它。默认情况下,记忆在应用中是关闭的。用户可以为应用启用或禁用记忆。应用不会收到单独的记忆流;它们只能看到模型在工具输入中包含的内容。当记忆关闭时,请求会在没有记忆的模型上下文中重新评估。
最佳实践
- 保持工具输入明确且对正确性至关重要;不要依赖记忆来处理关键字段。
- 将记忆视为提示而非权威;当对用户流程很重要且可能产生副作用时,请确认用户偏好。
- 在缺少上下文时提供安全默认值或询问后续问题。
- 使工具能够抵御重试、重新评估或缺少记忆的情况。
- 对于写入或破坏性操作,请在当前轮次中重新确认意图和关键参数。
步骤 3 – 返回结构化数据和元数据
每个工具响应都可以包含三个同级有效负载:
structuredContent– 小组件使用且模型读取的简洁 JSON。仅包含模型应看到的内容。content– 模型响应的可选叙述(Markdown 或纯文本)。_meta– 专供小组件使用的大型或敏感数据。_meta永远不会到达模型。
// Returns concise structuredContent for the model plus rich _meta for the widget.
async function loadKanbanBoard(workspace: string) {
const tasks = await db.fetchTasks(workspace);
return {
structuredContent: {
columns: ["todo", "in-progress", "done"].map((status) => ({
id: status,
title: status.replace("-", " "),
tasks: tasks.filter((task) => task.status === status).slice(0, 5),
})),
},
content: [
{
type: "text",
text: "Here's the latest snapshot. Drag cards in the widget to update status.",
},
],
_meta: {
tasksById: Object.fromEntries(tasks.map((task) => [task.id, task])),
lastSyncedAt: new Date().toISOString(),
},
};
}
小组件通过 MCP 应用桥(例如 ui/notifications/tool-result)接收这些有效负载,而模型仅能看到 structuredContent 和 content。
步骤 4 – 本地运行
- 构建您的 UI 包(在
web/中运行npm run build)。 - 启动 MCP 服务器(Node, Python 等)。
- 尽早并经常使用 MCP Inspector 来调用
https://:<port>/mcp、列出根目录并验证您的小组件是否正确渲染。Inspector 映射了 ChatGPT 的小组件运行时,并在部署前发现问题。
对于 TypeScript 项目,通常看起来像这样
npm run build # compile server + widget
node dist/index.js # start the compiled MCP server
步骤 5 – 公开 HTTPS 端点
ChatGPT 要求使用 HTTPS。在开发过程中,使用 ngrok(或类似工具)隧道连接本地主机。
ngrok http <port>
# Forwarding: https://<subdomain>.ngrok.app -> http://127.0.0.1:<port>
在 ChatGPT 开发者模式中创建连接器时使用 ngrok URL。对于生产环境,部署到低延迟的 HTTPS 宿主(Cloudflare Workers, Fly.io, Vercel, AWS 等)。
示例
这是一个精简的 TypeScript 服务器加上原生小组件。有关完整项目,请参考公开的 Apps SDK 示例。
// server/src/index.ts
import {
registerAppResource,
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "hello-world", version: "1.0.0" });
registerAppResource(
server,
"hello",
"ui://widget/hello.html",
{},
async () => ({
contents: [
{
uri: "ui://widget/hello.html",
mimeType: RESOURCE_MIME_TYPE,
text: `
<div id="root"></div>
<script type="module" src="https://example.com/hello-widget.js"></script>
`.trim(),
},
],
})
);
registerAppTool(
server,
"hello_widget",
{
title: "Show hello widget",
inputSchema: { name: z.string() },
outputSchema: { message: z.string() },
_meta: { ui: { resourceUri: "ui://widget/hello.html" } },
},
async ({ name }) => ({
structuredContent: { message: `Hello ${name}!` },
content: [{ type: "text", text: `Greeting ${name}` }],
_meta: {},
})
);
// hello-widget.js
const root = document.getElementById("root");
root.textContent = "Loading…";
const update = (toolResult) => {
const message = toolResult?.structuredContent?.message ?? "Hi!";
root.textContent = message;
};
window.addEventListener(
"message",
(event) => {
if (event.source !== window.parent) return;
const message = event.data;
if (!message || message.jsonrpc !== "2.0") return;
if (message.method !== "ui/notifications/tool-result") return;
update(message.params);
},
{ passive: true }
);
故障排除
- 小组件未渲染 – 确保模板资源返回
mimeType: "text/html;profile=mcp-app",并且打包的 JS/CSS URL 在沙箱内可以解析。 - 没有
ui/*消息到达 – 宿主仅为text/html;profile=mcp-app资源启用 MCP 应用桥;仔细检查 MIME 类型以及小组件是否在没有 CSP 违规的情况下加载。 - CSP 或 CORS 失败 – 使用
_meta.ui.csp来允许您从中获取数据的确切域;沙箱会拦截其他一切。 - 始终加载旧包 – 每当部署破坏性更改时,对模板 URI 或文件名进行缓存清除。
- 结构化有效负载过大 – 将
structuredContent削减到模型真正需要的内容;超大的有效负载会降低模型性能并减慢渲染速度。
高级功能
组件发起的工具调用
使用 tools/call 直接从您的 UI 调用工具。默认情况下,工具对模型和 UI 均可见。使用 _meta.ui.visibility 来限制工具的可见范围。
"_meta": {
"ui": {
"resourceUri": "ui://widget/kanban-board.html",
"visibility": ["model", "app"]
}
}
工具可见性
要使工具可从您的 UI 调用但对模型隐藏,请将 _meta.ui.visibility 设置为 ["app"]。这使得工具可通过 tools/call 对小组件可用,而不会影响模型对工具的选择。
"_meta": {
"ui": {
"resourceUri": "ui://widget/kanban-board.html",
"visibility": ["app"]
}
}
工具注释与启发
MCP 工具必须包含描述工具潜在影响的 工具注释 (tool annotations)。这些提示对于工具定义是必需的。
我们关注的三个提示是:
readOnlyHint: 对于仅检索或计算信息且不在 ChatGPT 之外创建、更新、删除或发送数据的工具(搜索、查找、预览),请设置为true。openWorldHint: 对于仅影响有限目标的工具(例如在您自己的产品中“按 ID 更新任务”),请设置为false。对于可以写入任意 URL/文件/资源的工具,请保留为true。destructiveHint: 对于能够删除、覆盖或产生不可逆副作用的工具,请设置为true。
openWorldHint 和 destructiveHint 仅在写入操作时相关(即 readOnlyHint=false 时)。
准确设置这些提示,以便正确描述工具的影响。
如果您省略这些提示(或将它们保留为 null),请将其视为验证错误,并更新工具定义以包含它们。
示例工具描述符
{
"name": "update_task",
"title": "Update task",
"annotations": {
"readOnlyHint": false,
"openWorldHint": false,
"destructiveHint": false
}
}
文件处理
ChatGPT 扩展(可选): 如果您的工具接受用户提供的文件,请使用 _meta["openai/fileParams"] 声明文件参数。该值应为应被视为文件的顶级输入模式字段列表。不支持嵌套的文件字段。
文件参数描述了文件处理的输入端:它们告诉 ChatGPT 哪些工具参数包含运行时应授权并作为文件引用传递的文件。
每个声明的文件参数接收一个具有以下形状的对象:
{
"download_url": "https://...",
"file_id": "file_...",
"mime_type": "image/png",
"file_name": "input.png"
}
download_url 和 file_id 是必需的。mime_type 和 file_name 是可选的。download_url 是临时的,仅应在处理当前工具调用时使用。如果文件引用来自小组件上传、选定的文件或其他工具结果,则在需要从 ChatGPT 请求新的下载 URL 时使用 file_id。
示例
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";
const imageFileSchema = z.object({
download_url: z.string(),
file_id: z.string(),
mime_type: z.string().optional(),
file_name: z.string().optional(),
});
registerAppTool(
server,
"process_image",
{
title: "process_image",
description: "Processes an image",
inputSchema: {
imageToProcess: imageFileSchema,
},
outputSchema: {
download_url: z.string(),
file_id: z.string(),
mime_type: z.string().optional(),
file_name: z.string().optional(),
},
_meta: {
ui: { resourceUri: "ui://widget/widget.html" },
"openai/fileParams": ["imageToProcess"],
},
},
async ({ imageToProcess }) => {
return {
content: [],
structuredContent: {
download_url: imageToProcess.download_url,
file_id: imageToProcess.file_id,
mime_type: imageToProcess.mime_type,
file_name: imageToProcess.file_name,
},
};
}
);
要从工具返回可下载文件,请在 structuredContent 中包含文件引用,通常在如 file_uri 之类的字段下。
{
"structuredContent": {
"file_uri": {
"download_url": "https://...",
"file_id": "file_...",
"mime_type": "application/pdf",
"file_name": "report.pdf"
}
}
}
这是文件处理的输出端。当结果是可下载文件时,您的工具应返回文件引用,而不是内联二进制数据或 base64 内容。ChatGPT 可以使用返回的 file_id 为小组件提供新的临时下载 URL。
ChatGPT 还支持将可下载文件作为 MCP resource_link 内容返回。请将其用于应作为下载内容公开给用户的文件输出。对于直接 Web 下载,请返回 MCP 定义的 https:// 资源 URI。ChatGPT 会在下载文件前征求用户许可。
内容安全策略 (CSP)
在小组件资源上设置 _meta.ui.csp,以便沙箱知道允许哪些域用于 connect-src、img-src、frame-src 等。这是广泛分发前的必要条件。
"_meta": {
"ui": {
"csp": {
"connectDomains": ["https://api.example.com"],
"resourceDomains": ["https://persistent.oaistatic.com"],
"frameDomains": ["https://*.example-embed.com"]
}
}
}
connectDomains– 您的小组件可以从中获取数据的宿主。resourceDomains– 图像、字体和脚本等静态资源的宿主。frameDomains– 可选;您的小组件可能嵌入为 iframe 的宿主。没有frameDomains的小组件无法渲染子框架。
如果您想使用 window.openai.openExternal(...) 而不看到安全链接警告,请在 openai/widgetCSP 下使用 redirect_domains 字段。
警告:不鼓励使用 frameDomains,仅应在嵌入 iframe 对您的体验至关重要时使用(例如代码编辑器或笔记本环境)。声明 frameDomains 的应用在审查时会受到更高程度的审查,并且很可能被拒绝或禁止广泛分发。
小组件域
在小组件资源模板(registerAppResource 模板)上设置 _meta.ui.domain。这是应用提交的必需项,且每个应用必须唯一。ChatGPT 在 <domain>.web-sandbox.oaiusercontent.com 下渲染小组件,这也启用了全屏弹出按钮。
"_meta": {
"ui": {
"csp": {
"connectDomains": ["https://api.example.com"],
"resourceDomains": ["https://persistent.oaistatic.com"]
},
"domain": "https://myapp.example.com"
}
}
组件描述
ChatGPT 扩展(可选): 在小组件资源上设置 _meta["openai/widgetDescription"],让小组件描述自己,减少小组件下方多余的文本。
"_meta": {
"ui": {
"csp": {
"connectDomains": ["https://api.example.com"],
"resourceDomains": ["https://persistent.oaistatic.com"]
},
"domain": "https://myapp.example.com"
},
"openai/widgetDescription": "Shows an interactive zoo directory rendered by get_zoo_animals."
}
本地化内容
ChatGPT 在客户端请求中发送 _meta["openai/locale"](带有 _meta["webplus/i18n"] 作为遗留键)中的请求区域设置。使用 RFC 4647 匹配来选择最接近的支持的区域设置,并在您的响应中回传它,并据此格式化数字/日期。
客户端上下文提示
ChatGPT 还可能在客户端请求元数据中发送提示,如 _meta["openai/userAgent"] 和 _meta["openai/userLocation"]。这些有助于定制分析或格式化,但切勿依赖它们进行授权。
一旦您的模板、工具和小组件运行时连接完毕,优化应用最快的方法是使用 ChatGPT 本身:在真实的对话中调用您的工具,查看日志,并使用浏览器开发工具调试小组件。当一切看起来正常时,将您的 MCP 服务器置于 HTTPS 之后,您的应用就准备好供用户使用了。
公司知识兼容性
ChatGPT 中的公司知识(商业、企业和教育版)可以调用应用中的任何只读工具。它倾向于 search/fetch,且只有实现了 search 和 fetch 工具输入签名的应用才会被包含为公司知识源。这些是连接器和深度研究所需的相同工具形状(请参阅 MCP 文档)。
在实践中,您应该:
- 将 search 和 fetch 输入模式完全实现为 MCP 模式。公司知识兼容性检查仅针对输入参数。
- 用
readOnlyHint: true标记其他只读工具,以便 ChatGPT 可以安全地调用它们。
要加入,请使用 MCP 模式实现 search 和 fetch,并为引用返回规范的 url 值。有关资格、管理员启用和可用性的详细信息,请参阅 ChatGPT 中的公司知识 以及 构建 MCP 服务器 中的 MCP 工具模式。
虽然兼容性检查侧重于输入模式,但您仍应为 search 和 fetch 返回推荐的结果形状,以便 ChatGPT 可以可靠地引用来源。text 字段在您的工具响应中是 JSON 编码的字符串。
搜索结果形状(MCP 包装前的工具有效负载)
{
"results": [
{
"id": "doc-1",
"title": "Human-readable title",
"url": "https://example.com"
}
]
}
字段
results- 搜索结果数组。results[].id- 文档或项目的唯一 ID。results[].title- 人类可读的标题。results[].url- 引用的规范 URL。
在 MCP 中,将此 JSON 作为 structuredContent 返回,并将相同的值作为 JSON 字符串包含在 content 中以实现兼容性。
搜索工具响应包装器
{
"structuredContent": {
"results": [
{
"id": "doc-1",
"title": "Human-readable title",
"url": "https://example.com"
}
]
},
"content": [
{
"type": "text",
"text": "{\"results\":[{\"id\":\"doc-1\",\"title\":\"Human-readable title\",\"url\":\"https://example.com\"}]}"
}
]
}
获取结果形状(MCP 包装前的工具有效负载)
{
"id": "doc-1",
"title": "Human-readable title",
"text": "Full text of the document",
"url": "https://example.com",
"metadata": { "source": "optional key/value pairs" }
}
字段
id- 文档或项目的唯一 ID。title- 人类可读的标题。text- 文档或项目的全文。url- 引用的规范 URL。metadata- 关于结果的可选键/值对。
对于 fetch,以相同方式返回文档 JSON。
获取工具响应包装器
{
"structuredContent": {
"id": "doc-1",
"title": "Human-readable title",
"text": "Full text of the document",
"url": "https://example.com",
"metadata": { "source": "optional key/value pairs" }
},
"content": [
{
"type": "text",
"text": "{\"id\":\"doc-1\",\"title\":\"Human-readable title\",\"text\":\"Full text of the document\",\"url\":\"https://example.com\",\"metadata\":{\"source\":\"optional key/value pairs\"}}"
}
]
}
这是一个显示 search 和 fetch 工具的最小 TypeScript 示例。
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
const server = new McpServer({ name: "acme-knowledge", version: "1.0.0" });
const searchOutputSchema = {
results: z.array(
z.object({
id: z.string(),
title: z.string(),
url: z.string().url(),
})
),
};
const fetchOutputSchema = {
id: z.string(),
title: z.string(),
text: z.string(),
url: z.string().url(),
metadata: z.record(z.string(), z.string()).optional(),
};
server.registerTool(
"search",
{
title: "Search knowledge",
inputSchema: { query: z.string() },
outputSchema: searchOutputSchema,
annotations: { readOnlyHint: true },
},
async ({ query }) => {
const structuredContent = {
results: [{ id: "doc-1", title: "Overview", url: "https://example.com" }],
};
return {
structuredContent,
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
};
}
);
server.registerTool(
"fetch",
{
title: "Fetch document",
inputSchema: { id: z.string() },
outputSchema: fetchOutputSchema,
annotations: { readOnlyHint: true },
},
async ({ id }) => {
const structuredContent = {
id,
title: "Overview",
text: "Full text...",
url: "https://example.com",
metadata: { source: "acme" },
};
return {
structuredContent,
content: [{ type: "text", text: JSON.stringify(structuredContent) }],
};
}
);
安全提醒
- 将
structuredContent、content、_meta和小组件状态视为用户可见——切勿嵌入 API 密钥、令牌或机密。 - 不要依赖
_meta["openai/userAgent"]、_meta["openai/locale"]或其他提示进行授权;在您的 MCP 服务器和后端 API 中实施授权。 - 除非服务器验证了调用者的身份和意图,否则避免公开仅限管理员使用的或破坏性的工具。