跳到主内容
创建时间:最后更新:作者:白川

工具、Skill、MCP 与插件:怎样扩展能力

按动作、方法、接入协议与宿主扩展点区分能力,理解它们如何组合。

工具、Skill、MCP 与插件:怎样扩展能力

想让智能体更会检查代码,可能需要更准确的扫描工具,也可能需要一套检查流程,或者接入现有分析服务。这几种需求不在同一层,用同一个"安装插件"来描述会隐藏关键区别。

上一篇讲会话与记忆,解决的是"跨轮次留下什么"。这一篇接着讲能力扩展,解决的是"这一轮能做更多什么"。

先问缺少的是动作、方法,还是接入方式,再选择工具、Skill、MCP 或具体宿主的扩展机制。

四者各自补什么

概念主要解决的问题教学例子
工具
程序能执行什么动作
读取文件并返回声明名称
Skill
某类任务怎样做
先确认范围、检查,再报告限制
MCP
宿主怎样与外部能力服务交换信息
接入一个代码分析服务
扩展或插件
怎样向具体宿主接入代码能力
注册工具、命令、事件或界面

这是职责上的比较,具体产品的名字与支持范围会不同。插件没有一个跨所有宿主通用的运行模型,也不应默认互相兼容。

四者的关系可以理解成一句话:工具提供动作,Skill 规定动作怎么用,MCP 规定动作怎么从外部来,扩展规定动作怎么嵌进这个宿主。

缺什么,该补哪一层

上面那张表是静态比较。真正用起来需要一个判断顺序。按下面的顺序问,通常在第二步或第三步就能停。

你缺的是补哪一层典型信号
一个动作本身
工具
想让它读某个目录、查某个 API,但宿主没有这个动作
一套方法
Skill
动作都有,但不知道先做什么、做完怎么报告
一个外部服务的接入方式
MCP
能力在另一个程序里,而且想跨宿主复用
宿主自己的扩展点
扩展
需要加命令、改界面、拦工具调用、做审批

四步的顺序有讲究。先确认动作有没有,缺动作时不要用 Skill 绕过去。 Skill 里的"运行检查脚本"只是一条任务说明,要真正运行,仍需要可用工具、正确路径和执行权限。把 Skill 复制进目录,不会自动创建操作系统能力。

第二步和第三步的分界是"能力在哪儿"。如果这件事在你自己的机器上就能做完,一个本地工具就够;如果它背后是一个已经在跑的独立服务,而且别的宿主也需要用同一套,那才值得走 MCP。

第四步是"宿主内部的事"。加一个斜杠命令、给工具调用加一道确认、往上下文里注入内容,这些都不属于前三层——它们要写代码进宿主。

四层各长什么样

抽象的分层容易停在口头。把这四层各自最小的一个实例放在一起,差别会具体很多。

工具是宿主里注册的一个函数,带名称、描述和参数 schema:

ts
import { Type } from "typebox";

pi.registerTool({
  name: "greet",
  label: "Greet",
  description: "Greet someone by name",
  parameters: Type.Object({ name: Type.String({ description: "Name to greet" }) }),
  async execute(toolCallId, params, signal, onUpdate, ctx) {
    return { content: [{ type: "text", text: `Hello ${params.name}` }], details: {} };
  },
});

Skill 是一个目录,里面放一份 SKILL.md:

markdown
---
name: pdf-tools
description: Extract text and tables from PDF files. Use when reading, converting, or inspecting PDFs.
---

# PDF tools

Read `references/formats.md` before converting a document. Run scripts relative to this skill directory.

MCP 是一份服务器配置,声明用哪个可执行文件连上去:

json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
    }
  }
}

扩展是一个 TypeScript 模块,默认导出工厂函数:

ts
import { type ExtensionAPI, isToolCallEventType } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  pi.on("tool_call", async (event, ctx) => {
    if (isToolCallEventType("bash", event) && event.input.command.includes("rm -rf")) {
      const ok = await ctx.ui.confirm("Dangerous!", "Allow rm -rf?");
      if (!ok) return { block: true, reason: "Blocked by user" };
    }
  });
}

四份代码的差别,正是四层的差别:工具是一个函数,Skill 是一份文档,MCP 是一个外部进程的连接方式,扩展是一段在宿主里运行的代码。

只有第四种会执行你自己的代码,这决定了它的风险等级和排查方式——前三种出问题,多半是配置或依赖;第四种出问题,先看它自己。

工具提供动作,不负责保证所有决策

工具通常有名称、描述、参数约束与结果。模型可以选择并构造调用,但宿主仍要验证并执行。说明"只读文件"不能替代实际只读实现,参数 Schema 合法也不能保证路径有权限。

一个工具存在于注册表,不代表本轮已向模型开放;模型能看到,也不代表它真的选择过。最终判断应回到调用与结果证据,而不是安装成功提示。

这三个"不代表"对应三种不同环节:

环节成立的标志不成立时说明什么
注册成功
工具出现在可用列表里
配置层有问题,模型压根看不见它
对模型开放
工具声明出现在这一轮请求的系统提示里
宿主没把它声明给模型,模型无法选择它
真的被调用
事件流里出现工具执行记录,且有对应的结果条目
模型选择了别的路径,或调用被拒绝了

第 2 条最容易踩坑。工具装好了、界面里也显示了,但宿主这一轮没有把它写进请求——这时候模型的"不用这个工具"是正确行为,排查方向应该是请求组装,不是工具实现。

Skill 提供方法与支持材料

Skill 可以把检查步骤、参考资料和模板放在一起,让任务方法被复用。宿主可能先提供名称与描述,需要时再展开正文;具体发现与调用规则应按相应产品核对。

Pi 的行为是可以逐字确认的。启动时,Pi 扫描配置的 Skill 位置,只把每个 Skill 的 name、description 和路径写进系统提示,不写正文。系统提示里对应的那一段长这样:

text
The following skills provide specialized instructions for specific tasks.
Use the read tool to load a skill's file when the task matches its description.
Resolve relative paths against the skill's directory (parent of SKILL.md). Use absolute paths in tool calls.

<available_skills>
  <skill>
    <name>pdf-tools</name>
    <description>Extract text and tables from PDF files. Use when reading, converting, or inspecting PDFs.</description>
    <location>/Users/jeffery/.pi/agent/skills/pdf-tools/SKILL.md</location>
  </skill>
</available_skills>

任务匹配时,模型才去读 SKILL.md 并按里面的说明执行。这条设计的目的很明确:把详细指引挡在上下文之外,直到真的需要。

有两个细节会直接影响使用体验。一是 disable-model-invocation: true:设了这个字段的 Skill 完全不进系统提示,只能通过显式命令 /skill:name 调用。它的用途是把长流程的命令行操作留在人工触发这一侧。二是模型可能匹配不到相关 Skill——文档里直说了"A model might fail to load a relevant skill",所以需要强制时用 /skill:name,参数会作为一条用户请求追加到加载后的指令后面:

text
/skill:pdf-tools extract report.pdf

MCP 连接宿主与服务

MCP 可以把工具、资源等能力通过协议提供给 AI 应用。规范的架构页把三方角色定义得很清楚,可以直接引用:

MCP Host: The AI application that coordinates and manages one or multiple MCP clients

MCP Client: A component that maintains a connection to an MCP server and obtains context from an MCP server for the MCP host to use

MCP Server: A program that provides context to MCP clients

关系是:Host 为每个 Server 建一个 Client,每个 Client 各自维持一条连接。Server 可以在本机,也可以在远端——本地 Server 通常走 stdio 传输,远端 Server 通常走 Streamable HTTP 传输。

本版规范只有两种 transport。 传输层页把标准绑定列得很清楚:stdio 用标准输入输出流传换行分隔的消息;Streamable HTTP 用 HTTP POST 发送每条消息,回复要么是一个 JSON 对象,要么是一个请求级 SSE 流。旧版的 HTTP+SSE 传输已经不是其中之一:

The HTTP+SSE transport from protocol version 2024-11-05 has been deprecated since protocol version 2025-03-26 and is classified as Deprecated under the feature lifecycle policy (SEP-2596).

所以"SSE 是第三种 transport"这个说法要改。SSE 在本版里是 Streamable HTTP 的可选流式能力,不是独立的一层。同理,文档站路径里的 2026-07-28 是规范修订版的日期式版本标识——它标记的是这一版规范的内容,不是发布日期。

这里有一个容易踩的坑:规范当前修订版,和某个客户端实际在说的版本,是两件事。 Pi 自带的 MCP 客户端在 packages/mcp/src/protocol/types.ts 里把最新版本写死为:

ts
export const LATEST_PROTOCOL_VERSION = "2025-11-25";
export const SUPPORTED_PROTOCOL_VERSIONS = [
  LATEST_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05"
] as const;

连接时它走的还是带 initialize 握手的老流程:先发 initialize 请求带上自己的协议版本和 capabilities,收到服务端回传的版本后校验,不在支持列表里就直接报错,然后补发 notifications/initialized。

所以读 MCP 文档时要分清三件事:规范现在写了什么、你的客户端实现到了哪一版、对端服务端支持哪一版。把某个版本的消息字段当成永远不变的通用格式,是最常见的误用。

本版规范定义三种核心原语:

MCP defines three core primitives that servers can expose

原语提供什么例子
Tools
模型可以调用的可执行函数
读文件、调 API、查数据库
Resources
提供上下文信息的数据源
文件内容、数据库记录、API 响应
Prompts
帮助组织交互的可复用模板
系统提示、few-shot 示例

每种原语都有对应的发现方法(*/list)、获取方法(*/get),部分还有执行方法(tools/call)。客户端先 tools/list 拿到工具列表,再调用其中某一个。

规范不定权限模型。 这一点容易被误解。规范的授权页把范围写得很窄:

The Model Context Protocol provides authorization capabilities at the transport level

也就是说,认证是传输层的事——HTTP 传输用 bearer token、API key 和自定义请求头,规范推荐用 OAuth 取令牌。capabilities 字段描述的是"支持哪些原语、要不要接收变更通知",也就是双方能处理哪些操作,不是"允许对系统做什么"。

同一版规范里还有几项已废弃。Sampling(让 Server 反过来请求模型生成内容)、Logging(Server 向 Client 发日志)以及 Roots(Client 告知 Server 关注哪些目录)都已标记为 deprecated,官方给的迁移方向是:Sampling 改为直接对接模型 Provider API,Logging 改为写 stderr 或用 OpenTelemetry,Roots 改为通过工具参数、资源 URI 或服务端配置传递目录。

Roots 尤其值得留意。规范自己的说明写得很直白:"while roots communicate intended boundaries, they do not enforce security restrictions."它是一个范围约定,不是隔离机制。

MCP 也不决定模型一定调用哪个工具,不替应用管理全部上下文,更不意味着服务因此拥有桌面上的任意权限。Server 能做到什么,取决于它被允许以什么身份运行——这是宿主的权限问题,权限与提示注入那一篇会展开。

在 Pi 里接一个 MCP 服务器

Pi 内置了对 MCP 的支持。配置一个本地 stdio 服务器,检查连接,然后启动 Pi:

bash
pi mcp add filesystem -- npx -y @modelcontextprotocol/server-filesystem .
pi mcp list

远端服务器用 --url:

bash
pi mcp add docs --url https://example.com/mcp --bearer-token-env-var DOCS_TOKEN

pi mcp list 会连接每一个启用的服务器,打印它的状态、工具和错误。如果某个配置项无效或有服务器没连上,命令以状态码 1 退出——这让它可以直接用在脚本里做健康检查。想看当前接了哪些服务器,这是最直接的一条命令。

配置放在两个位置:用户级的 ~/.pi/agent/mcp.json 对所有项目生效,项目级的 .pi/mcp.json 只在项目被信任之后才读取。同名的项目配置会覆盖用户级配置。文件格式和别的 MCP 客户端一致:

json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "."]
    },
    "docs": {
      "url": "https://example.com/mcp",
      "headers": { "Authorization": "Bearer ${DOCS_TOKEN}" },
      "description": "Search and read the product documentation"
    }
  }
}

pi mcp remove <server> 移除一个服务器,已存的 OAuth 凭据会保留。

工具暴露的名字是 mcp__<服务器名>__<工具名>。 这个命名有实际用途:它让 --tools 可以按前缀过滤。--tools 默认不会移除 MCP 工具,但只要有一条以 mcp__ 开头,就会开始过滤:

bash
pi --tools read,codemode,'mcp__radius__*'

这条命令只保留 radius 这个服务器的工具。想一次禁用全部 MCP,用 --no-mcp——它不连接任何服务器,也就没有 MCP 工具和 /mcp 命令。

暴露方式本身有四种,默认值也值得知道:

exposure行为适合什么
codemode
默认值。可被 codemode 脚本调用,但不直接声明给模型
一般 MCP 服务器
deferred
直到 tool_search 为下一次调用加载匹配项时才声明
工具很多、希望先搜索再调用
direct
像内置工具一样声明给模型,同时也能被 codemode 调用
少量、高频使用的工具
hidden
已注册但不可达
应当保持不可用的服务器或工具

toolExposure 可以按单个工具覆盖服务器的暴露方式,键是精确的工具名或带 * 的模式,精确名优先于模式、模式之间第一个匹配优先。

在会话内部,/mcp 可以查看连接状态、登录登出、重连、改暴露方式、启停服务器。pi mcp add、pi mcp remove、pi mcp list、pi mcp login、pi mcp logout 这些命令不需要开会话就能跑,Agent 自己也可以通过 bash 调用它们——但要注意,shell 层的 pi mcp 命令不加载扩展,只能看到文件里配置的服务器。

扩展负责接入宿主自己的扩展点

在支持扩展的宿主中,可以通过代码注册自定义工具,增加命令、监听事件或展示结果。它运行在哪里、能访问什么、如何重载与释放资源,都取决于具体宿主。

扩展和宿主同进程运行,可能具有相同操作系统权限;"这是个插件"并不意味着它被沙箱隔离。Pi 官方扩展文档在讲扩展位置时给了明确的安全声明:

Security: Extensions run with your full system permissions and can execute arbitrary code. Only install from sources you trust.

这句话不是套话,是可以被验证的机制。扩展是宿主进程内加载的 TypeScript 模块,能看到提示词、工具调用、文件、凭据和会话历史。Pi 的安全文档把边界说得更具体:Pi 以启动它的账户权限读写文件、改变和执行文件,并且不会在每次工具调用前都请求批准;扩展、包管理器、语言服务器和其他子进程都以同样的权限运行,除非有操作系统或虚拟化边界把限制住。

安装前需要考虑代码来源和实际权限,不能只看展示名称。Pi 对应的做法是项目信任机制:.pi/extensions、.pi/settings.json、.pi/mcp.json、.pi/skills 这些项目级资源,都要先给出信任决定才会加载。但信任机制管的是"启动时加载什么",管不了"运行起来能访问什么"——文档自己写明了这一点。

名字叫"插件"的,未必是插件

生态里叫 plugin 的东西可能实为扩展、Skill 包、MCP 服务器集合,或者只是一个安装脚本。看包里的清单比看名字可靠。

判断一个包到底提供了什么,看它自己的资源声明。Pi 的包用 package.json 里的 pi 字段显式列出内容:

json
{
  "name": "my-pi-package",
  "keywords": ["pi-package"],
  "pi": {
    "extensions": ["./src/extension.ts"],
    "skills": ["./resources/skills"],
    "prompts": ["./resources/prompts/*.md"],
    "themes": ["./resources/themes/*.json"]
  }
}

有 extensions 意味着它会执行代码;有 skills 意味着它提供的是方法说明和材料;两者都有的包,风险等级完全不同。这一层展开是另一个专栏的事,这里只留一条可用的判断依据。

组合比替代更常见

同一个检查功能可以由扩展注册工具,工具调用 MCP 服务,Skill 再指导模型按合适步骤使用。也可以没有 MCP,由宿主直接提供一个本地工具。

两种结构都可能合理。若只是读取一个教学文件,直接工具已经足够;已有稳定外部服务需要复用时,标准接入才可能更有价值。不要为了凑齐名词增加每一层,也不要把它们误当成四种互相排斥的 Agent 类型。

组合起来之后,判断仍然要落在证据上。工具被调用了吗,结果回填了吗,模型是基于哪一层的信息给出的结论——这三件事各自可以在事件流和会话文件里查到。第 6 篇讲的排查顺序在这里同样适用。

小结

四层能力解决四类缺口:缺动作用工具,缺方法用 Skill,缺外部服务接入用 MCP,缺宿主集成用扩展。

MCP 这一层有几件事值得记住:本版只有 stdio 和 Streamable HTTP 两种 transport,SSE 是后者的可选流式能力;三种核心原语是 Tools、Resources、Prompts;规范只管传输层认证,不定义"允许对系统做什么"的权限模型;Sampling、Logging、Roots 已废弃。

装上扩展之前,先看它是不是要在你的宿主进程里执行代码。扩展和宿主同进程运行,权限相同,也没有默认沙箱。

下一篇会接着这个思路,介绍工作流与协作:什么时候用固定流程,什么时候让模型自己决定下一步。

资料依据