跳至正文
浏览章节

技能与 MCP 服务器

给 bingo 增添本事的两条路:写成文件的流程,以及来自另一个进程的工具。

两条路

技能是写成散文的流程。磁盘上的一份 SKILL.md 会变成一条人可以打出来的 /name 命令、一个模型可以调用的工具,以及系统提示词里说明它存在的一行。

MCP 服务器是来自另一个进程的工具。配置好的服务器会在后台拨号连上,它们的工具像 别的工具一样交给模型——不受信任,所以权限闸门对每一次调用都要问。

两者进来的方式是一样的。注册是同步的,且不做 I/O,所以这两个插件谁都不注册一组固定的 东西:各自贡献一个来源,用它此刻手上有的东西作答,而答以空手从来都不算错 (ADR-0009)。会话进行中保存的技能会出现在下一次补全里;还在拨号的服务器还没有工具, 这不是一个错误。

技能住在哪里

三层,最要紧的在前:

  1. ~/.bingo/skills/<name>/SKILL.md —— 你的,在每个项目里都算数。
  2. 从工作目录一直往上到文件系统根、每一级上的 .bingo/skills/<name>/SKILL.md,近的先说话——所以一个包比包着它的仓库先开口。
  3. 打包在二进制里的 guide 技能,磁盘上任何同名技能都会盖过它。

实际发生的事是从工作目录往上走,而不是去找 git 根:不会 fork 任何 git 进程,而且 仓库之外的目录也有技能。在两层里互为祖先的同一个目录只读一次。

除非 frontmatter 另有说法,目录名就是技能的名字。只要任何被监视的路径的大小或修改时间 变了,这个库就会重读那一层,所以保存一个技能不必重启就能生效。

一份 SKILL.md 是什么

frontmatter 可以没有,而且只有当 --- 是文件第一行时才算数。会读六个字段:

字段含义
name这个技能应什么名字;不写就用目录名
description它是干什么的;不写就用正文第一行
argument-hint补全时显示在名字旁边的东西,例如 [issue-number]
arguments位置参数的名字,按顺序
allowed-tools会读、会记录,但从不强制执行
model会读、会记录,但从不强制执行

arguments 两种写法都行——arguments: issue branch 和 arguments: [issue, branch] 说的是同一件事。其他任何键都是忽略而不是拒绝,所以一份 带着 bingo 不读的字段的文件照样能用。

frontmatter 之后的一切都是正文,而正文就是一句提示词。

---
name: deploy
description: Ship a branch to an environment, with the checks that must pass first.
argument-hint: <environment>
arguments: environment
---

Deploy to $environment.

Before you start, run the test suite and stop if anything fails.
The deploy script is at ${BINGO_SKILL_DIR}/deploy.sh.

一个技能露面的三种方式

作为命令。 每个技能都是它自己的 /name,属于 skill 家族,在 / 下拉框里名字 旁边跟着参数提示。它不是即时的:技能是一句提示词,所以它开出一个回合,并等待正在跑的 那个。运行它产生一个 Prompt 结果——展开后的正文成为这个回合被问的东西。

/deploy staging

作为工具。 模型带着一个名字和可选的 arguments 调用 Skill,拿回那个技能的说明 作为结果——所以一段正文只在被需要时才花上下文。Skill 是只读且受信任的,它的权限匹配 对象就是技能的名字,也就是说规则可以按名字指到它:

{ "permissions": { "deny": ["Skill(deploy)"] } }

指了一个不存在的技能,得到的回答是那些存在的技能,而不是一个失败。

作为系统提示词里的一行。 一个贡献者把每个可用的技能列成 - name — description, 上面有一小段前言,说明调用 Skill 是够到一个技能的方式,而人打 /<name> 是同一件事。 只有名字和描述;超过 250 个字符的描述会被截掉,因为再长它就不是描述了。一个技能都没有 时,这一块整个不出现。

参数

替换是从左到右的一遍,所以一个自身包含 $1 的值会作为文本插入,绝不会被再展开一次。

占位符代表
$ARGUMENTS名字后面打的所有东西
$1 … $9它按空白切开的那些词,从 1 开始
$namearguments: 为那个名字声明的位置上的那个词
${BINGO_SKILL_DIR}这个技能自己的目录

带下标的占位符在它那个位置没有词时,原样保留;带名字的则变成空。其他任何以 $ 开头 的东西都不是占位符,一概不动,而 $ARGUMENTS[0] 会整个保留,不会展开一半。没有任何 占位符要用的参数不会被丢掉——它们会作为最后一行 ARGUMENTS: <text> 追加上去。

和它借鉴的那套方言有两处差别值得知道:这里的 $N 从 1 开始,而 \$1 不能转义一个 占位符。

配置一个 MCP 服务器

两个设置键,都由 mcp 插件认领:mcpServers,按名字合并;以及 disabledMcpServers, 它跨层累加。被禁用的服务器绝不会去拨号。

{
  "mcpServers": {
    "files": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/srv/data"]
    },
    "issues": {
      "type": "http",
      "url": "https://mcp.example.com/v1",
      "headers": { "Authorization": "Bearer …" }
    }
  },
  "disabledMcpServers": ["issues"]
}

两种传输,靠 type 区分,默认是 stdio:

  • stdio —— 一个在自己的 stdin 和 stdout 上说这套协议的子进程:command、 args、env、cwd。
  • http —— 一个 streamable-HTTP 端点:url、headers。

一条记录在被读到时检查一次,绝不半懂不懂地带着走。属于另一种传输的字段会导致启动失败 ——url and headers belong to an http server、command, args and env belong to a stdio server——缺了 command 或 url 也一样,schema 不认识的任何键也一样。一个静悄悄 永远拨不通的服务器,比一个会说出原因的更糟。

env 和 headers 底下的值是人放令牌的地方,所以它们绝不会被打印出来:只打印它们被 给的名字。

--mcp-config

bingo --mcp-config ./bundle.json "read the open issues"

一个 JSON 文件,它的 mcpServers 会为这次运行加进来——宿主的一个 bundle。只有那个键 会从里面取走;文件里别的东西一概不动。它成为一个设置层,压在那些文件之上、命令行 之下,所以一个 bundle 是往你的设置已经点名的那些服务器上加,而不是把它们换掉。

不存在的路径会被拒绝,理由是 --mcp-config: <path> does not exist;没有 mcpServers 键的文件则是 --mcp-config: <path> has no mcpServers。

一个服务器的工具怎么到达模型

这个插件贡献一个工具来源,而 start 为每个启用的服务器在各自的任务上放一次拨号,然后 立刻返回。每次拨号有五秒的连接超时。一个回合开始时已经落地的东西,就是那个回合的工具 集合。

这意味着一次会话的第一个回合可能在某个慢服务器答话之前就跑了。这是故意的:另一种做法 是一个会话在名单上最慢的那个服务器回复之前根本起不来。

一个服务器的工具以 mcp__<server>__<tool> 的形式到达模型。两个名字都原样照抄,所以 名字里本身带 __ 的服务器或工具,照样能被为它写的规则够到。目录条目里带着它来自哪个 服务器。

它们按不受信任来过闸

服务器说自己什么都不算数。一个 MCP 工具穿着失败即拒绝的默认特性——不是只读、不是并发 安全、不受信任——所以不管 readOnlyHint 声称了什么,闸门对每一次调用都要问。这和二进制 之外来的每一个工具所受的待遇是同一套,包括 跨进程插件。

权限语法在两种粒度上都能指到它们:

{
  "permissions": {
    "allow": ["mcp__files__read_file"],
    "deny":  ["mcp__issues"]
  }
}

mcp__server 覆盖那个服务器的每一个工具;mcp__server__tool 覆盖一个。 见设置与权限。

/mcp

一条即时命令——它读那张表并发起拨号,且不碰任何正在跑的回合正在用的东西,所以一个回合 已经收集好的工具集合保持原样。

不带参数时,它回答一张 server、status 和 tools 的表,其中状态是 connecting、 connected、disabled,或者带着理由的 failed:。三个动词会改变东西:

/mcp reconnect <server>
/mcp enable <server>
/mcp disable <server>

它们各自在启动了某件事的那一刻就作答,而不是在做完的时候——一次握手要好几秒,而一条 为它等着的命令就是一条会挂住的命令。它做了什么,会在下一次 /mcp 里显现。点了一个 没配置过的服务器,它会告诉你配置过的是哪些。

一个服务器的噪音去哪里

子进程的 stderr 进 ~/.bingo/data/logs/mcp-<server>.log,绝不进终端。放着不管的话, 它会继承终端并糊在一个全屏界面上,而那个界面从不重画自己的滚动历史。

该伸手拿哪一个

当知识是一套流程——做什么、按什么顺序做、怎么检查它成了——而且 bingo 已经有把它执行 下去所需的工具时,技能是对的形状。在被调用之前,它只花掉提示词里的一行。

当 bingo 需要一项它没有的能力时——另一个系统的数据、另一个团队的 API——MCP 服务器 是对的形状。如果你想要的是一个 bingo 原生的工具、命令或钩子而不是一个 MCP 服务器,那 跨进程插件桥是另一条路,而且它承载 MCP 没有 对应形状的视图和补全。