关于 skill-mcp
用于将 Agent Skills 目录提供给 MCP 客户端的通用服务,适合管理技能指令及其配套文件。它还可运行技能显式声明的脚本,并通过限制和确认控制执行。
主要功能
- 01发现并列出技能
- 02读取技能指令与文件清单
- 03批量读取技能文件
- 04运行技能声明的脚本
- 05报告目录问题并限制脚本执行
适用场景
- 01让 Agent 按需发现可用技能
- 02加载技能指令及引用文件
- 03在确认后运行获准的技能脚本
- 04排查未能提供的技能目录
MCP 相关
通过 MCP 工具列出、加载技能、读取文件及运行声明的脚本;每项技能也可注册为 MCP 提示词,文件可作为资源提供。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
4收录的工具列表 · 更新于 1 天前
此工具无需参数。
详细介绍
skill-mcp
一个用于提供 Agent Skills 目录的 MCP 服务。将它指向 skills 目录; 它会列出其中的技能、提供技能说明和打包文件,并且只运行技能声明的脚本。
它是通用适配器,而非精选技能集:技能是它读取的内容,同一个构建版本可以服务于任何指定目录。
npx @chrischall/skill-mcp # serves the example skill bundled here
SKILLS_DIR=~/my-skills npx @chrischall/skill-mcp
npm 包名为 @chrischall/skill-mcp(未限定范围的 skill-mcp 已被 npm 上的其他人占用)。其他所有内容——仓库、二进制文件和注册表标识 io.github.chrischall/skill-mcp——都不限定范围。
什么是技能
一个包含 SKILL.md 的目录:文件由 YAML frontmatter(name、description,以及可选的下方 mcp-host: 块)和后续说明组成,另外还可以包含说明所引用的任意文件。每个根目录下有以下三种布局:
<root>/SKILL.md # the root IS one skill
<root>/<name>/SKILL.md # a directory of skills
<root>/skills/<name>/SKILL.md
技能以其所在目录的名称提供,绝不使用其 frontmatter 中的 name(若 frontmatter 中的 name 不一致,会报告该情况,但除此之外会将其忽略;若根目录本身就是技能,则以根目录命名)。这是一条安全规则,而非整洁性要求:所有者的授权针对的是技能,因此,如果一个技能包可以自行命名,就可能冒用相邻技能的名称,从而取得该相邻技能获准使用的脚本和变量。出于同样的原因,若两个目录确实都提供了同一个名称——这种情况只可能出现在两个根目录之间——两个目录都会被拒绝并报告,而不是按扫描顺序择一使用。
工具
| 工具 | 参数 | 返回值 |
|---|---|---|
skill_list | — | 找到的每个技能:名称、描述、适用场景、文件数、是否声明了可运行脚本以及具体是哪些脚本;另附 problems,因此列表为空时不会让人摸不着头脑 |
skill_load | name | 原样返回 SKILL.md 正文,以及技能包文件清单。引用的文件不会内联返回——这正是 skill_file 的用途 |
skill_file | name、paths[] | 每个路径对应一条结果,顺序与请求一致:文本、附带媒体类型的 base64,或该路径自身的错误。每条最多 1 MiB;超出时返回 truncated: true,而不是悄悄截断 |
skill_run | name、script、args[]、confirmToken | {exitCode, stdout, stderr, truncated, durationMs} |
skill_file 接受 paths[] 批量请求,因此 SKILL.md 引用的文件可以通过一次往返全部获取:每次调用最多 8 个路径,每条最多 1 MiB,每次调用总计最多 4 MiB(在读取任何字节前,根据文件大小求和,因此结果不会受路径列出顺序影响)。某个路径出错只会影响对应条目,其余批量请求仍会正常提供。请将这些上限理解为对本服务自身堆内存的限制:它们限制的是分配量,而不只是返回内容,因为托管子进程的数据限制为 256 MiB,而技能包可能比这个限制更大。
每个技能也会注册为 MCP 提示词(正文即消息),每个打包文件也会注册为资源(skill://<name>/<path>),因为支持这些接口的客户端能比通过工具调用更好地呈现技能。这是第二个入口,绝不是唯一入口:当此服务运行在 mcp-host 上,且某项注册缩小了 enabledTools 的范围时,prompts/list 和 resources/list 会返回空列表,握手也会停止通告这些能力——因此,工具本身就能提供完整体验。
发现报告,永不静默
任何导致某个目录无法被提供的情况,都会出现在 skill_list 的
problems 中,并附上路径和原因:没有 SKILL.md、frontmatter
无法解析、两个目录声明了同一个名称(两者都会被拒绝)、声明的脚本不在
软件包中、符号链接指向根目录或某个技能之外、读取工具无法处理的文件名。一个有问题的技能只会影响自身,不会影响列表;也不会出现第三种情况,即某项内容被悄无声息地丢弃——如果符号链接指向根目录内,符号链接技能目录就会被提供(因此 skills/foo -> ../shared/foo 可用);如果指向根目录外,则会报告。提供时使用其目标目录的名称——skills/foo -> ../bar 就是技能
bar,并报告为 name-mismatch,以便按正确的名称授予权限——而同一个目录被访问两次(一个链接指向同时被直接列出的技能)时,只算一个技能,不算重复项。
问题原因
problems 中的每个条目都是 {path, reason, detail}。detail 是面向
用户的说明;reason 是用于分支判断的稳定值。
| reason | 含义 |
|---|---|
root-unreadable | 无法读取配置的根目录 |
no-skill-md | 候选目录中没有 SKILL.md |
unreadable-skill-md | SKILL.md 不是常规文件,或无法读取 |
malformed-frontmatter | frontmatter 无法解析 |
declaration | mcp-host: 块被拒绝,或包含未知键(在 detail 中注明);受影响的是该技能的脚本,不包括其说明 |
duplicate-name | 两个目录声明了同一个技能名称;两者都会被拒绝 |
name-mismatch | frontmatter 中的 name 与目录名称不一致;以目录名称为准 |
unusable-name | 目录自身的名称无法处理,因此跳过该技能 |
declared-script-missing | 声明的脚本不在软件包中 |
symlink-escape | 符号链接指向根目录或某个技能之外 |
unusable-path | 读取工具无法处理的文件名,因此未纳入清单 |
file-limit | 某个技能包含的文件或目录超过每个技能的上限(每项均为 2000 个);清单会列出已遍历的内容 |
skill-limit | 找到的技能超过 MAX_SKILLS(32);其余技能将被忽略 |
grant | MCP_SKILL_RUN 指定了该技能未声明的脚本,因此该行不会授予任何权限 |
environ-unscrubbed | 运行环境为托管环境,执行时的环境变量块无法清除,因此不会运行任何脚本(见下方环境变量允许列表说明) |
原因集合会不断扩充。 新原因会在不提升主版本号的情况下加入
(最近加入的是 environ-unscrubbed),因此,按 reason 分支处理的
客户端必须将无法识别的值视为通用问题并显示其 detail,绝不能因此失败。
执行边界
skill_run 会执行第三方代码。以下每条规则都会缩小运行的代码范围,并限制传递给代码的内容;每条规则都有各自的测试。
- 仅允许运行技能声明的脚本。 不是“
scripts/下的任意文件”,也不是“任何可执行文件”。未声明的路径会被拒绝,并提示必须先声明,同时列出已声明的路径。 - 仅限该技能自己的目录。 首先将路径作为字符串检查(仅普通路径段;不得以
/开头,不得包含.或..、反斜杠、百分号转义或 NUL),解析后再检查一次:跟随符号链接后的真实路径仍必须位于该技能的真实目录内,并且必须是普通文件。两项检查缺一不可:仅检查字符串会漏掉放在技能包内的符号链接,而仅检查解析后的路径则会接受本不应拼接的路径形式。skill_file也遵循同样的规则:读取技能目录之外的文件,至少会读到另一个技能包。 - 使用 argv 数组,绝不使用 shell 字符串。 使用
shell: false的spawn,不进行插值,不使用sh -c。参数按原样传递。 - 解释器必须来自固定集合,并由声明指定——绝不根据扩展名推断,也绝不读取文件自身的 shebang,因为能自行选择解释器的文件,实际上已经自行选择了要运行的程序。v1 只运行
node(参见 What v1 cannot run)。 - 设有上限,且调用始终会返回。 墙钟超时(默认 60 s,可按脚本覆盖,上限为 300 s),每个输出流捕获上限为 1 MiB,超出时返回
truncated: true,而非静默截断;同一时间只能运行一个skill_run。超时时会向进程组发送信号,作用于脚本及任何仍留在该组中的子进程。但信号不会作用于已脱离并进入自己进程组的孙进程,而这样的孙进程也会使标准 I/O 管道保持打开状态——因此,运行会在进程退出后加上短暂的排空时间结束,并受严格截止时间约束,而不是等到管道关闭。这样可以确保工具调用在预算时间内返回,并释放一次只能运行一个脚本的锁;但这并不保证刻意脱离的孙进程会被终止。限制此类进程属于该层级的职责(非特权 uid、prlimitNPROC,以及会停止运行的机器),而非此适配器的职责。 - 环境变量采用允许列表。 脚本可获得
PATH、HOME、LANG、TZ、TMPDIR,以及宿主设置过的MCP_DATA_DIR;还可获得宿主设置过的出站代理变量(HTTP_PROXY、HTTPS_PROXY、http_proxy、https_proxy、NO_PROXY、no_proxy、NODE_USE_ENV_PROXY)——在 mcp-host 的隔离层级中,网络受到限制,没有这些变量的脚本甚至无法访问允许列表中的主机;此外,脚本只能获得它请求且所有者批准的变量——绝不继承此服务自身的环境。固定部分与 mcp-host 的INSTALL_ALLOWLIST(packages/runner-node/src/spawn-env.ts)保持一致,原因正如该文件所述:宿主常量不能被托管声明通过增加变量名而扩大范围。 这决定了脚本会获得哪些变量,并不决定恶意脚本能够访问什么。 脚本与此服务以相同的 uid 运行。在 Linux 上,相同 uid 的进程可以读取/proc/<parent pid>/environ,因此服务启动时会清除该执行时环境块(src/scrub-environ.ts;process.env中仍保留所有值),这样读取便会返回空内容。如果内核或沙箱拒绝此清除操作,托管服务将完全不运行脚本——skill_run会拒绝运行,skill_list会报告environ-unscrubbed——除非所有者设置MCP_SKILL_ALLOW_UNSCRUBBED=1;独立运行时只会向 stderr 发出警告。这些值仍保存在服务内存中;如果内核的 Yamaptrace_scope = 0,可通过ptrace或/proc/<pid>/mem访问它们。若要在互不信任的技能之间实现真正的隔离,需要依靠该层级(为脚本使用独立 uid、将/proc以hidepid=2挂载,或让服务不可转储);在此之前,只有当你愿意向同一注册中的每个技能提供该注册持有的所有凭据时,才将这些技能放在一起。 - 非零退出是正常结果,会照常报告——退出代码、stdout 和 stderr 都会返回。它绝不会作为异常导致输出丢失。
skill_run运行任何内容前都会先征求确认。 对于能够显示确认提示词的客户端(Claude Code),会在客户端中询问用户,除非服务设置了MCP_CONFIRM_ELICITATION=off。否则,首次调用不会启动进程,而是返回status: "confirmation-required",并预览确切的运行内容——解释器、argv、工作目录、超时时间,以及脚本将获得的变量名称(在预览中逐项列出)——同时返回一个confirmToken。只有使用相同参数和该令牌再次调用时,才会运行一次脚本。令牌仅适用于完全相同的运行:更改技能、脚本或 argv 会被拒绝,并返回DRAFT_CHANGED;重复使用令牌则返回TOKEN_REUSED。请参阅确认。
skill_run 预览
首次调用中的 preview.willRun 就是用户批准的内容,也是 confirmToken 的载荷:令牌与其绑定。仅显示名称,绝不显示值。
| 字段 | |
|---|---|
skill | 技能,以其目录名标识 |
interpreter | 声明的解释器(node) |
argv | 脚本路径后接参数,完全按照实际传递的形式显示 |
cwd | 技能自己的目录,脚本将在此运行 |
timeoutMs | 墙钟时间预算,已应用 300 s 上限 |
envNames | 脚本将获得的所有变量,按名称排序,不仅限于已授权的变量。包括固定允许列表(PATH、HOME、LANG、TZ、TMPDIR)中已设置的变量、主机注入的 MCP_DATA_DIR 和出口代理变量(HTTP_PROXY、HTTPS_PROXY、它们的小写形式、NO_PROXY/no_proxy、NODE_USE_ENV_PROXY)中已设置的变量,以及每个已声明并获授权且已设置的变量。它由运行时使用的同一函数计算,因此与脚本实际收到的变量名称完全一致 |
envNotSet | 仅在非空时显示:已声明并获授权、但在此服务器环境中未设置的变量,因此脚本不会获得它们 |
envWarning | 仅限独立模式,且仅在适用时显示:说明此第三方脚本将获得哪些变量,仅仅因为其自身的 SKILL.md 要求提供这些变量,而没有 MCP_SKILL_RUN 授权。只有当你愿意把这些机密交给该技能时,才应批准 |
客户端应呈现其已知字段,并将其余字段原样传递:与上文的 problems 一样,可选字段可以随时增加。
为什么确认门槛一律适用
整个服务集群的惯例是对会修改内容的工具设置门槛。此服务器无法知道某个脚本是否会修改内容:它从不读取脚本,也刻意不对脚本进行分析——机器生成的对他人代码的判断,会以作者的声明不会获得的方式受到信任。因此,未知影响一律视为会修改内容。
显而易见的放宽方式是允许技能将脚本标记为只读,从而跳过确认门槛;但这不可取,因为这会形成循环论证:编写脚本的作者也编写了描述脚本的语句,因此作者自称“只读”并不能证明任何事。所以只能一律设置门槛。代价是只读辅助脚本也要多一次往返;好处是预览是调用方在任何操作发生前看到精确调用内容的唯一机会。
这道防护不能提供什么
已声明的脚本仍是任意代码。 这些规则只缩小了可运行的代码范围,并限制其运行方式;它们都不能让代码变安全。你允许运行的脚本可以读取整个技能树、耗尽机器的 CPU,也可以将其持有的任何内容发送到网络权限允许的任何地方。skill_run 的输出上限只是截断,并非保密措施:没有任何机制会过滤脚本的标准输出,也不可能做到。
这不是沙箱。 请仅对你已阅读过的技能运行脚本,或者在有防护措施的环境中运行——在 mcp-host 中,这意味着隔离层级(fly-machine):每次注册使用一个微型虚拟机、非特权 uid、prlimit 限制,以及采用默认拒绝策略并配置了声明式出口允许列表的 nftables。此服务器是在这类防护之上进一步收窄权限,而非取而代之。
v1 无法运行什么
解释器集合只有一个条目:node。这是经过权衡后的决定,并非疏忽:mcp-host 的运行镜像包含 Node、git、tar、util-linux 和 nftables,但不包含 python3、curl 或 jq;然而,实际技能绝大多数使用 Python(在 anthropics/skills 的 3b3fad96 版本中,.py 文件有 70 个,.js 文件只有 1 个)。
因此,声明使用 Python 脚本的技能会由 skill_list 列在 unavailableScripts 下,并注明解释器和此部署所提供的解释器集合;skill_run 也会以相同措辞拒绝运行。它的说明仍然可用——仅包含说明的技能也很有用,而且大多数已发布的技能正是如此。指定固定解释器的功能将作为后续依赖项引入,而不是通过更改镜像来实现。
mcp-host: 声明块
可选,位于 SKILL.md 的 frontmatter 中:
---
name: weather
description: Forecasts and geocoding.
mcp-host:
version: 1
run:
- script: scripts/forecast.js
interpreter: node
env: [WEATHER_API_KEY] # variables this SCRIPT asks for
timeout: 30 # seconds; clamped to 300
env: # fields proposed for the SERVER's environment
- name: WEATHER_API_KEY
secret: true
egress: [api.weather.example] # hosts this skill reaches; a proposal
---
声明只会缩小范围,绝不会授予权限。 脚本的作者也编写了命名这些脚本的声明块,因此其中没有任何内容构成授权——它只说明哪些文件是入口点,以及每个文件需要什么。让脚本可以运行、让变量能够传递给脚本,取决于其他人是否接受它。
严格解析:YAML 1.2 核心模式;拒绝锚点和别名;上限为 64 KiB;遇到未知的 MAJOR 版本时整体拒绝;忽略未知键,并按名称报告;无法解析的声明块会报告解析器给出的出错位置,而不会视为不存在。损坏的声明块会让技能失去其脚本,但不会影响其指令,也不会影响列表的其余内容。
配置
| 变量 | 含义 |
|---|---|
MCP_SKILLS_PATH | 以 : 分隔的槽位根目录,由 mcp-host 的运行器注入。优先级高于所有其他设置 |
SKILLS_DIR | 本地使用时与之相同。仅当 MCP_SKILLS_PATH 未设置时读取 |
MCP_SKILL_RUN | 可选的 JSON [{skill, script, env?}]——所有者的授权。只缩小范围 |
MCP_SKILL_ALLOW_UNSCRUBBED | 设为 1 时,即使托管服务无法清除其执行时的环境块,也允许其运行脚本(参见环境变量允许列表说明)。默认关闭 |
| (均未设置) | 此软件包自带的 skills/ 目录 |
需要特别强调 MCP_SKILL_RUN。设置该变量后,可运行的内容是声明与它的交集——如果其中某行命名了技能未声明的脚本,则不会授予任何权限(并会报告);如果某行命名了脚本未请求的变量,也不会授予任何权限。无论如何设置,都不能让技能未声明的内容变得可运行,因此从同时包含注册项自身变量的环境中读取它才是安全的。
如果未设置,默认行为取决于是否由宿主启动了此子进程;托管模式会默认拒绝所有权限。
- 托管模式——只要存在任何由 mcp-host 的运行器注入的变量(
MCP_SKILLS_PATH、MCP_HOST_METER_FILE、MCP_DATA_DIR、MCP_BLOB_BASE_URL):就不会授予任何权限,也不会运行任何内容。 仍会提供每个技能的指令和文件——这仍然是一个正常且实用的连接器。原因在于,一个子进程持有一个包含所有者设置的每项凭据的环境变量;因此,如果某个技能在 frontmatter 中命名了相邻技能的变量,除非有人决定将该凭据授予它,否则它本不该拿到该凭据。(这里说的是脚本收到什么;这并非沙箱——关于/proc/<pid>/environ,请参见上面的环境变量允许列表说明。)默认情况下会刻意不单独检查MCP_SKILLS_PATH:mcp-host 目前尚未注入该变量,因此当下唯一的托管渠道是在注册项普通env中设置SKILLS_DIR,这不得导致默认开启。标记检查只可能让默认行为进一步趋向拒绝授权。 - 独立模式——完全没有注入标记:采用技能自身的声明。没有注入任何内容,而将服务器指向某个目录的人就是所有者。**这也包括脚本声明的变量:**如果某个技能的
env:中列出了GITHUB_TOKEN,那么该技能会从此服务器的环境变量中收到你的GITHUB_TOKEN(MCP 客户端通常会从你的 shell 中填入该变量),唯一的门槛是skill_run确认。预览会按名称说明这一点(envWarning);独立服务器只应指向你愿意向其提供这些机密信息的技能,或者设置MCP_SKILL_RUN来明确授予变量。
skill_list 会报告当前属于哪种情况(grantFrom,托管模式下还会提供 grantNote),并列出技能中已声明但未获授权的脚本,因此“没有内容运行”绝不会与“没有声明任何内容”混为一谈。
确认
| variable | default | |
|---|---|---|
MCP_CONFIRM_MODE | ask-user | 对于无法显示确认提示词的客户端(claude.ai、Claude Desktop),写入操作会如何处理。ask-user:分两步进行——首次调用不执行任何操作,只返回预览和令牌;模型必须先在聊天中获得你的批准,才能再次调用并提供该令牌。auto:同样分两步进行,但模型自行查看预览后即可使用令牌。refuse:此类客户端的写入操作会被拒绝。能够显示提示词的客户端(Claude Code)始终会收到实际提示词,除非 MCP_CONFIRM_ELICITATION=off。无法识别的值会按 refuse 处理。 |
MCP_CONFIRM_ELICITATION | on | off 表示永不显示确认提示词,因此所有客户端都会采用 MCP_CONFIRM_MODE 的行为。对于声称能够显示提示词、但实际上从不显示的客户端(写入操作会一直挂起——opencode 2.0.x),请设置此项。任何其他值都会按 on 处理,并在 stderr 上显示警告。 |
MCP_CONFIRM_TTL_SECONDS | 600 | 令牌的有效时长。 |
MCP_CONFIRM_SECRET | random per process | 签名密钥;仅当令牌必须在服务重启后仍然有效时才设置。 |
信任立场
- 此服务的代码由操作者提供;技能由你提供。它只读取交给它的固定目录,不会获取任何内容,也不会安装任何内容,并且没有任何工具会访问技能自身目录以外的路径。
- 它不会审核任何内容。 没有徽章、没有发布者允许列表,也没有扫描。技能的指令和脚本与其作者一样值得信任。
- 读取和执行一样危险,因为读取的目录与机器上的其他所有内容都在同一位置。
- 它不缓存也不存储任何内容。 启动时会扫描一次目录,并将目录清单保存在内存中;不会在任何位置写入文件。
在 mcp-host 上托管
仓库根目录中的 mint.yaml 说明了此 MCP 应如何注册。它有意不提议以下四项,因为只有注册过程才能决定:
-
运行时——并且按规则,它不能指定运行时。 清单不得指定运行时(docs/MINT-MANIFEST.md §5):注册归属哪个层级,由请求方决定,而非由软件包决定,因为允许请求
fly-shared的文件,就等于让陌生人的软件包请求在操作者自己的机器上占用资源。托管的技能服务应放在隔离层级(fly-machine),并声明出站策略;这应由注册过程决定。 -
技能本身。 技能作为固定版本的依赖项提供(
github-archive指定一个仓库和一个精确提交),并放入运行器通过MCP_SKILLS_PATH指定的只读位置。此软件包无法知道特定注册包含哪些技能。 -
state.dataDir。 此适配器无需持久化存储。当注册所包含的技能带有需要写入位置的脚本时,才启用此项——该位置为只读,因此脚本输出会写入MCP_DATA_DIR(启用时)或TMPDIR(未启用时)——并在此说明原因。 -
实际的出站允许列表。
mint.yaml提议使用allow: [],这正是适配器本身所需的配置:它不会访问任何内容。注册需要访问的主机由其技能声明,并会在预览中显示声明者,供所有者接受。在隔离层级上,不在列表中的主机可能会返回来自环回代理的 HTTP 403,也可能只表现为超时——在脚本内部两者无法区分,因此当skill_run在看似受限的机器上调用失败时,会附上一条说明。
将 enabledTools 缩减为 [skill_list, skill_load, skill_file],即可关闭可执行功能;这一开关由网关而非此处强制执行,比此服务拒绝 skill_run 更有力,但会完全禁用提示词和资源接口。
开发
npm install
npm run build # tsc → dist/, esbuild → dist/bundle.js
npm test # tsc typecheck + vitest
本项目由 AI 开发并维护。请自行斟酌使用。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。