
Finch 调用 MCP 实战:从零开始制作一个 MCP 小程序
Finch 接入 MCP 实战:连接 stdio/HTTP/OAuth 三类服务,用 ToolSearch 按需调用,开发封装 MCP 小程序,附踩坑清单。

Finch 接入 MCP 实战:连接 stdio/HTTP/OAuth 三类服务,用 ToolSearch 按需调用,开发封装 MCP 小程序,附踩坑清单。
本文是一篇完整的实战教程。我们会从 MCP 协议的基本概念讲起,一步步演示 Finch 如何连接、发现、调用 MCP 服务,最后动手开发一个把 MCP 能力封装起来的 Finch 小程序(mini tool),并完成调试、安装与发布的全流程。全文约一万字,建议收藏后跟着操作。
大语言模型再聪明,如果只能聊天、不能动手,它的价值就要打对折。写代码的助手碰不到你的文件系统,做数据分析的助手连不上数据库,搞运营的助手摸不到浏览器——这中间缺的,是一条标准化的「手和眼」的通道。
MCP(Model Context Protocol,模型上下文协议)就是为了解决这个问题而生的。它由 Anthropic 在 2024 年底提出并开源,定义了 AI 应用(客户端)与外部能力提供方(服务器)之间的统一通信协议。你可以把它类比为「AI 世界的 USB-C 接口」:任何服务只要实现了 MCP 协议,任何支持 MCP 的 AI 应用就都能即插即用地调用它。
Finch 是一个桌面端 AI Agent,它对 MCP 的支持是「原生且完整」的:
Authorization: Bearer 头)、非标准鉴权头(如 X-Api-Key)、以及符合 MCP 规范的 OAuth 流程(Discovery + 动态客户端注册 DCR + PKCE);换句话说,Finch 既是 MCP 的消费者(连接并调用各种 MCP 服务),也可以成为 MCP 能力的再包装者(通过小程序把 MCP 调用变成普通用户点一下就能用的功能)。本文的主线,就是走完这两步。
我们会完成三件事:
mcp-dashboard 的 Finch 小程序,把「查询某个 MCP 服务状态 + 一键调用常用工具」封装成 Composer 按钮和 Agent 工具,并完成本地调试与安装。在动手之前,用最短的篇幅把 MCP 的核心概念过一遍。已经熟悉的读者可以直接跳到第三章。
┌────────────┐ MCP 协议 ┌────────────┐ 真实 API ┌────────────┐
│ MCP Host │ ◄──────────► │ MCP Server │ ◄─────────► │ 外部系统 │
│ (Finch) │ │ (能力提供方)│ │ (DB/浏览器等)│
└────────────┘ └────────────┘ └────────────┘
一个 MCP Server 可以向客户端声明三种能力:
read_file、navigate_page、execute_sql;Finch 对 Tools 的支持最完整,这也是本文实战的重点。
stdio:Server 是本地子进程,客户端把它拉起来,通过标准输入输出交换 JSON-RPC 消息。适合本地工具(文件系统、本地数据库、Git)。配置要素是「命令 + 参数 + 环境变量」:
{
"command": "npx",
"args": "-y @modelcontextprotocol/server-filesystem /path/to/dir",
"env": { "SOME_KEY": "..." }
}
HTTP(Streamable HTTP):Server 是远程 HTTP 服务,客户端 POST JSON-RPC 消息到一个 endpoint。适合云端服务、团队共享的服务、有 OAuth 需求的 SaaS。配置要素是「URL + 鉴权头」。
⚠️ 一个极易踩的坑:MCP 的协议头(
MCP-Protocol-Version、Mcp-Method、Mcp-Name、Mcp-Param-*)是由客户端在每次请求时自动生成的,绝对不要在服务端配置里把它们写成固定的自定义头。Mcp-Name必须与请求体里的params.name/params.uri一致;如果服务端报HeaderMismatch(错误码 -32020)或抱怨缺少Mcp-Name头,问题几乎总是出在客户端/传输层的协议协商(版本、SDK 版本、transport 包装),而不是「少配了一个头」。
Finch 用一个内置的 MCP 管理工具 来管理所有 MCP 服务连接,支持六个动作:list、add、edit、remove、connect、disconnect。下面我们逐个实战。
任何时候想搞清楚 Finch 当前连了哪些 MCP 服务,第一步都是 list。在对话里直接说:
「帮我看看现在配置了哪些 MCP 服务」
Finch 会调用管理工具并返回类似这样的清单:
┌──────────────────┬──────────┬─────────────────────────────┬────────┐
│ 名称 │ 类型 │ 目标 │ 状态 │
├──────────────────┼──────────┼─────────────────────────────┼────────┤
│ chrome-devtools │ stdio │ npx chrome-devtools-mcp@... │ 已连接 │
│ filesystem │ stdio │ npx @modelcontextprotocol/..│ 未连接 │
└──────────────────┴──────────┴─────────────────────────────┴────────┘
注意清单里会区分「用户在 servers.json 里配置的」和「小程序(扩展)注入的」两类服务——后者不能被 edit/remove,只能随小程序一起启停。
我们以官方的 filesystem server 为例。对 Finch 说:
「帮我添加一个 MCP 服务,名字叫 filesystem,用 npx 启动 @modelcontextprotocol/server-filesystem,允许访问 ~/Documents」
Finch 会发起 action=add,此时有几个关键行为值得说明:
secretEnvKeys 声明哪些环境变量是敏感的,由表单安全收集,模型全程看不到值。非敏感变量(比如 BASE_URL)则用 plainEnvKeys 声明。command 参数,传输方式自动判定为 stdio。提交表单后,配置落入本地的 servers.json:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/you/Documents"]
}
}
}
假设你有一个部署在 https://mcp.example.com/mcp 的团队内部服务,用 Bearer Token 鉴权。对 Finch 说:
「添加一个 HTTP MCP 服务,名字 example,地址 https://mcp.example.com/mcp」
这里有个非常贴心的设计:对标准的 Token 鉴权 HTTP 服务,你只需要提供 name 和 url。表单会自动显示一个「API token」输入框,客户端自动生成标准的 Authorization: Bearer <token> 请求头。你(和 Finch)永远不需要手动传入、查看或编辑 token 本身。
只有当服务端要求非标准头名时(比如 X-Api-Key),才需要在 add 时声明 authHeader="X-Api-Key",token 会以该头的原始值发送。
如果目标服务支持 MCP 规范的 OAuth(如某些 SaaS 的官方 MCP endpoint),添加时声明 oauth=true:
「添加 MCP 服务 notion,地址 https://mcp.notion.com/mcp,用 OAuth 授权」
保存后,客户端会走标准流程:
action=connect 后打开浏览器完成授权,回调后 token 安全落盘,过期自动刷新。想断开授权时调用 action=disconnect 即可。
action=edit 会以当前配置预填表单让你修改。一个实用技巧:重命名服务用 edit + newName,确认表单会预填新名字,点保存就完成迁移,所有引用旧名字的地方需要相应更新。
服务连上了,工具在哪?这是 Finch 与普通 MCP 客户端最大的差异点:MCP 工具默认不加载。
一个功能齐全的 MCP server 可能暴露三五十个工具,每个工具的 schema 都要占上下文。如果连五个服务,光工具定义就能吃掉几万 token,模型还容易被无关工具干扰。Finch 的策略是:
mcp__<server>__<tool> 函数注入当前 run;已知目标服务名时,永远优先精确搜索。比如要用 chrome-devtools 的浏览器能力:
ToolSearch({
source: "mcp:chrome-devtools",
query: "all tools provided by the chrome-devtools MCP server",
limit: 50
})
这里有两个实战要点:
source:"mcp" 泛搜所有服务。注入后的工具名遵循固定格式:mcp__<服务名>__<工具名>,例如:
mcp__filesystem__read_filemcp__chrome-devtools__navigate_pagemcp__tavily__tavily_searchFinch 的权限系统可以精确到单个工具授权(mcp__filesystem__read_file),也支持通配符(mcp__filesystem__*)。在默认权限模式下,第一次调用会弹权限卡片;勾选「当前会话行动」可切到 acceptCalls 模式,本会话内自动放行安全操作。
下面是一段真实对话驱动的完整流程。用户说:
「用 filesystem MCP 看看我 Documents 目录下有哪些 markdown 文件,把最大的那个读出来总结一下」
Finch 内部的执行序列:
Step 1 — 激活工具
ToolSearch(source="mcp:filesystem", query="list directory and read file", limit=20)
→ 注入 mcp__filesystem__list_directory, mcp__filesystem__read_file, ...
Step 2 — 列目录
mcp__filesystem__list_directory({ path: "/Users/you/Documents" })
→ ["notes.md", "draft.md", "report-2026.md", ...]
Step 3 — 逐个读取元信息、找出最大文件、读取内容、输出总结
整个过程用户只需要在权限卡片上点一两次确认,其余全自动。
chrome-devtools 是另一个高频场景。它的工具集覆盖:导航、截图、a11y/DOM 快照、点击填表、执行 JS、Console/Network 检查、设备模拟、Lighthouse 审计、性能追踪、堆快照、文件上传、弹窗处理、等待页面状态。
一条重要的实战经验:元素定位用 UID,不用 CSS 选择器。正确工作流是:
1. mcp__chrome-devtools__navigate_page → 打开目标 URL
2. mcp__chrome-devtools__take_snapshot → 拿到页面元素的 UID 树
3. mcp__chrome-devtools__click / fill → 把 UID 传给交互工具
4. mcp__chrome-devtools__take_screenshot → 可视化验证结果
比如「打开 example.com 的登录页,填入测试账号并截图」这样的任务,Finch 会严格按 snapshot → 定位 UID → 交互 → 截图验证的顺序执行,而不是盲猜选择器。
在进入小程序开发之前,先盘一下 Finch + MCP 最有价值的几类场景,帮你建立「什么需求该找什么服务」的直觉。
一个组合玩法:让 Finch 用 chrome-devtools 打开你正在开发的页面,跑一次 Lighthouse,把 Performance/SEO/Accessibility 分数整理成表格,再针对扣分项直接修改源码——全程一句话触发。
什么时候该用 MCP,什么时候用 Finch 内置能力或小程序原生代码?经验法则:
现在进入本文的核心:把 MCP 能力做成 Finch 小程序(mini tool)。
Finch 小程序是 Finch 的官方扩展机制,比 Skill(纯提示词注入)更强,因为它是真实运行的代码。一个小程序可以提供:
finch_shell_open、create_git_branch、markdown_editor_document,它们都来自小程序);开发小程序的正确姿势是先加载官方的 finch-mini-tool-creator skill——对 Finch 说「我要创建一个 Finch 小程序」,它会自动 invoke 该 skill,拿到完整的 finch.d.ts API 说明和脚手架流程。
我们的小程序叫 mcp-dashboard,目标:
mcp_quick_call:让模型在对话中以受控方式快速调用某个服务的指定工具(带参数白名单校验);用官方 CLI 初始化:
npx @finchtoys/minitools create mcp-dashboard
生成的结构:
mcp-dashboard/
├── finch.mini.json # 清单:id、名称、版本、权限声明
├── src/
│ ├── index.ts # 入口:注册工具与按钮
│ ├── tools.ts # Agent 工具实现
│ ├── button.tsx # Composer 按钮的表单 UI
│ └── finch.d.ts # 官方类型定义(只读参考)
├── package.json
└── README.md
清单文件示例:
{
"id": "mcp-dashboard",
"name": "MCP 面板",
"version": "0.1.0",
"description": "查看并测试 Finch 已连接的 MCP 服务,受控快调工具",
"permissions": ["mcp", "network"],
"guidance": "当用户询问 MCP 服务状态、连接情况,或需要快速测试某个 MCP 工具时,使用 mcp_quick_call / mcp_list_status 工具。"
}
guidance 字段就是注入到模型上下文里的那行指引——写得越具体(触发场景 + 工具名),模型越知道什么时候该用它。
src/tools.ts:
import type { FinchMiniTool } from "./finch.d.ts";
export function registerTools(ctx: FinchMiniTool.Context) {
// 工具一:列出所有 MCP 服务状态
ctx.registerTool({
name: "mcp_list_status",
description: "列出 Finch 当前配置的所有 MCP 服务及其连接状态",
parameters: {
type: "object",
properties: {},
},
async handler() {
const servers = await ctx.mcp.list();
return {
content: [{
type: "text",
text: servers
.map(s => `- ${s.name} [${s.transport}] ${s.status}`)
.join("\n"),
}],
};
},
});
// 工具二:受控快调
ctx.registerTool({
name: "mcp_quick_call",
description:
"在参数白名单内快速调用指定 MCP 服务的工具。" +
"调用前必须先列出服务确认目标存在。",
parameters: {
type: "object",
properties: {
server: { type: "string", description: "MCP 服务名" },
tool: { type: "string", description: "工具名" },
args: { type: "object", description: "工具参数" },
},
required: ["server", "tool"],
},
async handler({ server, tool, args }) {
// 白名单校验:只放行只读类工具,杜绝意外副作用
const ALLOW: Record<string, string[]> = {
filesystem: ["list_directory", "read_file", "search_files"],
"chrome-devtools": ["take_snapshot", "take_screenshot", "list_pages"],
};
if (!ALLOW[server]?.includes(tool)) {
return {
isError: true,
content: [{ type: "text",
text: `工具 ${server}/${tool} 不在白名单内,拒绝调用。` }],
};
}
const result = await ctx.mcp.callTool(server, tool, args ?? {});
return result;
},
});
}
几个设计要点:
ctx.mcp 是小程序运行时暴露的 MCP 桥:list / callTool 等方法由 Finch 宿主代理执行,复用宿主已保存的连接与凭据——小程序代码里永远不出现 token;src/button.tsx(简化示意):
export default function McpPanelButton({ finch }) {
const [servers, setServers] = React.useState([]);
React.useEffect(() => {
finch.mcp.list().then(setServers);
}, []);
return (
<div style={{ padding: 16, minWidth: 360 }}>
<h3>MCP 服务面板</h3>
{servers.map(s => (
<div key={s.name} style={{ display: "flex",
justifyContent: "space-between", padding: "8px 0" }}>
<span>{s.name} <small>({s.transport})</small></span>
<span style={{ color: s.status === "connected"
? "#22c55e" : "#94a3b8" }}>
{s.status === "connected" ? "● 已连接" : "○ 未连接"}
</span>
<button onClick={async () => {
const r = await finch.mcp.callTool(s.name, "ping", {});
finch.toast(r.isError ? "连通性异常" : "连通正常");
}}>测试</button>
</div>
))}
<button onClick={() => finch.sendPrompt(
"请检查所有 MCP 服务的状态并汇总成表格")}>
让 Finch 全面体检
</button>
</div>
);
}
finch.sendPrompt 是按钮与对话联动的桥梁:点击后把一段提示词发给当前会话,让 Agent 接手做深度检查——UI 负责轻量展示,Agent 负责复杂推理,各司其职。
src/index.ts:
import { registerTools } from "./tools.ts";
import McpPanelButton from "./button.tsx";
export default function activate(ctx) {
registerTools(ctx);
ctx.registerButton({
id: "mcp-panel",
label: "MCP 面板",
icon: "plug",
component: McpPanelButton,
});
}
本地开发用 watch 模式:
npx @finchtoys/minitools dev
它会把小程序以开发模式装入 Finch,代码改动热重载。调试技巧:
console.log 输出会进入小程序日志面板,开发模式下也可在终端看到;reload 重启小程序生效;list 确认服务本身连通,排除是宿主连接问题还是小程序代码问题;调试满意后:
# 安装到个人目录(仅自己可用)
npx @finchtoys/minitools add ./mcp-dashboard --scope personal
# 或发布到 npm
npm publish
# 其他人即可一键安装
npx @finchtoys/minitools add mcp-dashboard
如果想让小程序进入 Finch 官方社区目录(其他用户能在应用内的「搜索小程序」里直接搜到并安装),需要向官方提交上架申请——对 Finch 说「帮我把 mcp-dashboard 提交到 Finch 官方社区」,它会走官方反馈流程(分类为 minitool)发起上架请求,由官方审核收录。
把本文涉及的经验教训集中成一份 checklist,建议对照自查。
MCP-Protocol-Version / Mcp-Method / Mcp-Name 等协议头 → ✅ 这些由客户端逐请求自动生成;HeaderMismatch(-32020) 就去加自定义头 → ✅ 排查客户端协议协商:MCP 版本、SDK 版本、transport 包装;authHeader → ✅ 留空,表单会自动生成 Authorization: Bearer <token>;只有 X-Api-Key 这类非标准头名才需要声明。secretEnvKeys / token 输入框),绝不在聊天里粘贴,也绝不写进小程序源码;source:"mcp:<server>" 精确搜,泛搜只做兜底;take_snapshot 拿 UID,不要传 CSS 选择器;close_page 清理,切换服务前先停掉旧的后台任务。finch-mini-tool-creator skill 再动手,以 finch.d.ts 为唯一 API 事实来源,不要凭记忆写接口;update / reload,小程序不会自动热更新已安装的正式版。回顾全文,我们走完了 Finch 使用 MCP 的完整闭环:
mcp__<server>__<tool>,节省上下文、精确授权;MCP 生态还在快速扩张,每周都有新的官方与社区 server 出现。建议的进阶路径:
工具的意义不在于连接了多少服务,而在于把多少重复劳动变成了「一句话」甚至「一次点击」。这正是 Finch + MCP + 小程序这套组合想交付的东西。
祝你造得开心。🐦
参与讨论
评论