工具是模型可以调用的函数。用 server.tool(name, config) 注册一个工具:
server.tool("search_issues", {
title: "Search issues",
description: "Search open GitHub issues in a repository. Use it before filing a new issue.",
input: z.object({
repo: z.string().describe("owner/name"),
query: z.string(),
limit: z.number().int().max(50).default(10),
}),
annotations: { readOnlyHint: true, openWorldHint: true },
run: async ({ repo, query, limit }, { signal }) => {
const res = await fetch(`https://api.github.com/search/issues?q=${encodeURIComponent(`${query} repo:${repo}`)}&per_page=${limit}`, { signal });
const { items } = await res.json();
return items.map((i: { title: string; html_url: string }) => `${i.title} — ${i.html_url}`).join("\n");
},
});配置项#
| 选项 | 类型 | 说明 |
|---|---|---|
description | string | 工具做什么、什么时候用。模型会读取这段文字。 |
title | string | 客户端显示的名称。 |
input | Standard Schema | 参数的 schema。没有参数的工具可以省略。 |
output | Standard Schema | 结构化结果的 schema。见结构化输出。 |
annotations | ToolAnnotations | 给客户端的提示:readOnlyHint、destructiveHint、idempotentHint、openWorldHint。 |
run | (input, ctx) => value | 处理函数,可以是异步的。 |
工具名长度为 1 到 128 个字符,只能包含字母、数字、_、. 和 -。注册无效或重复的名称会立即抛出错误,因此问题在服务器启动时就会暴露,而不是等到模型调用工具时。
输入校验#
picomcp 在调用 run 之前,会用你的 schema 校验参数。默认值和转换都会生效,所以上面例子里的 limit 在 run 中始终是数字。
校验失败时,工具不会被调用。客户端会收到一个指明出错字段的工具错误,模型可以据此修正调用并重试:
Invalid arguments for tool "search_issues":
limit: Too big: expected number to be <=50返回值#
按最自然的方式返回即可。picomcp 会把它转换成 MCP 结果:
run 返回 | 客户端收到 |
|---|---|
string | 一个文本块 |
| 对象、数组或数字 | 以文本形式发送的 JSON。对象还会作为 structuredContent 发送。 |
| 一个内容块,或内容块数组 | 原样发送这些内容块 |
{ content: [...] } | 原样发送结果,完全由你掌控 |
undefined | 空结果 |
用辅助函数构建内容块:
import { audio, image, link, text } from "picomcp";
run: async () => [
text("Here is the chart:"),
image(await renderPng(), "image/png"), // Uint8Array or base64 string
link("file:///reports/q3.csv", "q3.csv", { mimeType: "text/csv" }),
];结构化输出#
当结果要交给客户端或代码处理,而不只是给模型看时,请声明 output schema:
server.tool("get_price", {
input: z.object({ sku: z.string() }),
output: z.object({ sku: z.string(), cents: z.number().int(), currency: z.literal("EUR") }),
run: async ({ sku }) => ({ sku, cents: await priceOf(sku), currency: "EUR" }),
});这样 picomcp 会:
- 在
tools/list中把该 schema 声明为outputSchema; - 在类型层面检查
run的返回值; - 在运行时校验返回值,并作为
structuredContent发送,同时附带一份 JSON 文本,供旧版客户端使用。
如果返回值不符合 schema,客户端会收到工具错误,而不是格式错误的数据。
错误#
在 run 中抛出错误。错误信息会变成模型可以读取并作出反应的工具错误(isError: true):
run: async ({ id }) => {
const user = await db.users.find(id);
if (!user) throw new Error(`No user with id ${id}. Call list_users to see valid ids.`);
return user;
};错误信息要写给模型看:说明哪里出了问题,以及下一步该怎么做。
工具错误是正常对话的一部分,协议错误则不是。只有当请求本身有问题时才使用 McpError,例如用户没有访问权限:
import { McpError } from "picomcp";
throw new McpError(-32001, "Not authorised for this workspace");取消#
run 的第二个参数带有一个 AbortSignal。当客户端发送 notifications/cancelled 时它会被触发,通常是因为用户点了停止。把它传给 fetch 以及其他支持它的 API:
run: async ({ url }, { signal }) => {
const res = await fetch(url, { signal });
return res.text();
};请求被取消后,picomcp 不会再为它发送响应,这是协议的要求。
元数据#
如果客户端随请求发送了 _meta 对象,ctx.meta 中就会包含它。客户端用它来做链路追踪和进度令牌。
写出好工具#
- 一个工具只做一件事。
create_invoice和send_invoice比invoice(action)更好。 - 描述每个参数。 每个字段上的
.describe()都会进入模型看到的 JSON Schema。 - 少返回一些。 模型的每个 token 都有成本。只返回有用的字段,而不是整个 API 响应。
- 如实标注。
readOnlyHint: true会让客户端跳过确认提示。不要把它用在会写入数据的工具上。