
Skill Mcp
io.github.chrischall由 GitHub 账号 chrischall 发布v1.2.3更新于 Oct 30, 2026
Serves a directory of Agent Skills over MCP: list, load, read files, run declared scripts
概览
本地 MCP 服务器,用于提供 Agent Skills 目录:列出技能、返回其说明与文件,并只运行技能声明的脚本。
- 可以做什么
- 列出配置根目录下的技能,含描述、文件数、已声明脚本及问题报告
- 原样返回某个技能的 SKILL.md 正文及其捆绑文件清单
- 按路径读取一个或多个捆绑文件,以文本或带媒体类型的 base64 返回,受大小上限约束
- 以参数数组方式运行已声明的脚本,返回退出码、标准输出、标准错误与耗时
- 在客户端支持时,把每个技能注册为 MCP 提示、每个捆绑文件注册为资源
- 适用场景
- 适合在本机维护一批 Agent Skills 目录,并希望助手能发现、阅读并遵循这些技能,包括运行其中声明的辅助脚本。也适合希望用一个通用适配器对接任意技能目录的场景。
- 运行要求
- 通过 stdio 在本机运行,通常用 npx 启动 npm 包 @chrischall/skill-mcp,因此需要 Node.js。用 SKILLS_DIR 指定技能目录(多个用冒号分隔),未设置时提供包内示例技能。另有可选变量控制授权、确认与令牌签名。脚本仅以 node 运行。
安装
在 SourceWeft 中
- 打开 控制台中的 Skill Mcp,将其添加到工作区。
- 为需要使用其工具的对话启用该服务。
Desktop only,通过 STDIO。 STDIO 服务会启动本地进程,因此需要 SourceWeft 桌面宿主。
其他 MCP 客户端
参照 仓库 中的启动说明。
README
skill-mcp
An MCP server that serves a directory of Agent Skills. Point it at skills; it lists them, hands out their instructions and their bundled files, and runs only the scripts a skill declares.
It is a generic adapter, not a curated set: the skills are content it reads, and the same build serves whatever it is pointed at.
The npm package is @chrischall/skill-mcp (unscoped skill-mcp is taken by
someone else on npm). Everything else — the repo, the binary, the registry
identity io.github.chrischall/skill-mcp — is unscoped.
What a skill is
A directory holding SKILL.md: YAML frontmatter (name, description, and
optionally the mcp-host: block below) followed by instructions, plus whatever
files those instructions refer to. Three layouts are found under each root:
A skill is served under its DIRECTORY name, never under the name in its
own frontmatter (a frontmatter name that disagrees is reported and otherwise
ignored; where the root itself is the skill, the root directory names it). That
is a security rule, not a tidiness one: the owner's grant names a skill, so a
bundle that could choose its own name could claim its neighbour's and be handed
the neighbour's granted script and granted variables. For the same reason, two
directories that really do contribute one name — only possible across two roots
— have both refused and reported, rather than one of them winning by scan
order.
The tools
skill_file takes a paths[] batch, so the files a SKILL.md points at come
back in one round trip: at most 8 paths per call, 1 MiB per entry and 4 MiB per
call (summed from file sizes before a byte is read, so the answer never depends
on the order the paths were listed in). One bad path fails only its own entry —
the rest of the batch is still served. Read the caps as bounds on this server's
own heap: they cap the allocation, not only the answer, because a hosted
child has a hard 256 MiB data limit and a bundle may be larger than that.
Each skill is also registered as an MCP prompt (its body is the message) and
each bundled file as a resource (skill://<name>/<path>), because a client that
supports those surfaces presents a skill better than a tool call does. It is a
second door, never the only one: when this server runs on
mcp-host and a registration narrows
enabledTools, prompts/list and resources/list come back empty and the
handshake stops advertising those capabilities — so the tools carry the whole
experience.
Discovery reports, it never goes quiet
Anything that keeps a directory from being served comes back in skill_list's
problems, with the path and the reason: no SKILL.md, frontmatter that will
not parse, a name two directories both claim (both refused), a declared script that is not in
the bundle, a symlink leading out of the root or out of a skill, a filename the
read tools could not address. One bad skill costs itself and never the listing,
and there is no third outcome where something is dropped in silence — a
symlinked skill directory is served when it stays inside the root (so
skills/foo -> ../shared/foo works) and reported when it does not. It is
served under its target directory's name — skills/foo -> ../bar is the skill
bar, reported as a name-mismatch so a grant is written against the right
name — and a directory reached twice (a link to a skill that is also listed
directly) is one skill, not a duplicate.
Problem reasons
Each entry in problems is {path, reason, detail}. detail is prose for a
person; reason is the stable value to switch on.
The set is additive. New reasons arrive without a major version bump
(environ-unscrubbed is the most recent), so a client that switches on reason
must treat a value it does not recognise as a generic problem and show its
detail, never fail on it.
The execution fence
skill_run executes third-party code. Every rule below narrows which code
runs and what it is handed; each has its own test.
- Only a script the skill DECLARES. Not "any file under
scripts/", not "anything executable". An undeclared path is refused, saying it must be declared and listing the ones that are. - Only inside that skill's own directory. The path is checked as a string
first (plain segments; no leading
/, no.or.., no backslash, no percent escape, no NUL) and then again after resolution: the real path, with symlinks followed, must still be inside the skill's real directory, and it must be a regular file. Both checks, because a string check alone misses a symlink planted inside the bundle and a resolved check alone accepts shapes that should never have been joined. The same discipline governsskill_file: a read out of a skill directory is another skill's bundle at best. - An argv array, never a shell string.
spawnwithshell: false, no interpolation, nosh -c. Arguments are passed through verbatim. - An interpreter from a closed set, named by the declaration — never
inferred from the extension and never taken from the file's own shebang, since
a file that can choose its own interpreter has already chosen its own program.
v1 runs
nodeand nothing else (see What v1 cannot run). - Bounded, and the call always returns. A wall-clock timeout (60 s default,
per-script override, hard 300 s ceiling), 1 MiB captured per stream with
truncated: truerather than a silent cut, and oneskill_runat a time. On timeout the process group is signalled, which reaches the script and any child that stayed in its group. It does not reach a grandchild that detached into a group of its own, and such a grandchild also holds the stdio pipes open — so the run settles on the process exiting plus a short drain, under a hard deadline, rather than on the pipes closing. That is what guarantees the tool call returns within its budget and frees the one-at-a-time lock; it is not a guarantee that a deliberately detached grandchild is dead. Bounding that is the tier's job (an unprivileged uid,prlimitNPROC, and a machine that stops), not this adapter's. - An env allowlist. A script gets
PATH,HOME,LANG,TZ,TMPDIR,MCP_DATA_DIRwhen the host set one, the egress proxy variables (HTTP_PROXY,HTTPS_PROXY,http_proxy,https_proxy,NO_PROXY,no_proxy,NODE_USE_ENV_PROXY) when the host set them — on mcp-host's isolated tier the network is fenced and a script without them cannot reach even an allowlisted host — and exactly the variables that script asked for and the owner granted — never this server's own environment. The fixed half mirrors mcp-host'sINSTALL_ALLOWLIST(packages/runner-node/src/spawn-env.ts), for the reason that file gives: a host constant a hosted declaration cannot widen by one name. This decides what a script is handed, not what a hostile script can reach. A script runs as the same uid as this server. On Linux a same-uid process can read/proc/<parent pid>/environ, so at boot the server wipes that exec-time environment block (src/scrub-environ.ts;process.envkeeps every value) and that one-line read comes back empty. If a kernel or sandbox refuses that wipe, a hosted server runs no script at all —skill_runrefuses andskill_listreportsenviron-unscrubbed— unless the owner setsMCP_SKILL_ALLOW_UNSCRUBBED=1; standalone it only warns on stderr. The values still live in the server's memory, reachable throughptraceor/proc/<pid>/memon a kernel at Yamaptrace_scope = 0. Real isolation between skills that do not trust each other needs the tier (a distinct uid for scripts,/procmountedhidepid=2, or a non-dumpable server); until then, only put skills in one registration if you would give each of them every credential it holds. - A non-zero exit is a normal, reported outcome — exit code, stdout and stderr all come back. It is never an exception that loses the output.
skill_runasks before it runs anything. On a client that can show a confirmation prompt (Claude Code) the user is asked there, unless the server setsMCP_CONFIRM_ELICITATION=off. Otherwise the first call starts no process and returnsstatus: "confirmation-required"with a preview of exactly what would run — the interpreter, the argv, the working directory, the timeout, and the names of the variables the script would be handed (field by field in the preview) — plus aconfirmToken. Only a repeat call with the same arguments and that token runs the script, once. A token is bound to that exact run: changing the skill, script or argv is refused asDRAFT_CHANGED, and a used token asTOKEN_REUSED. See Confirmations.
The skill_run preview
The first call's preview.willRun is what the user approves, and it is the
confirmToken's payload: the token is bound to it. Names only — never a value.
A client should render the fields it knows and pass the rest through: like
problems above, the optional fields are additive.
Why the confirmation gate is blanket
The fleet convention gates mutating tools. Whether a given script mutates anything is something this server cannot know: it never reads a script, and it deliberately does not analyse one — a machine-generated verdict about somebody else's code gets trusted in a way an author's declaration does not. Unknown effects are therefore treated as mutating.
The obvious softening — let a skill mark a script read-only and skip the gate for it — is refused because it is circular: the same author wrote the script and the sentence describing it, so a self-declared "read-only" authorizes nothing. That leaves a blanket gate. Its cost is one extra round-trip on a read-only helper; its benefit is that the preview is the one place a caller sees the exact call before any of it happens.
What the fence does NOT buy
A declared script is still arbitrary code. These rules narrow which code
runs and with what; none of them makes the code safe. A script you allow can
read the whole skills tree, spend the machine's CPU, and send whatever it holds
anywhere its network permits. skill_run's output caps are truncation, not
confidentiality: nothing redacts a script's stdout, and nothing could.
This is not a sandbox. Run it against skills you have read, or run it
somewhere that fences it — under mcp-host that means the isolated tier
(fly-machine): a microVM per registration, an unprivileged uid, prlimit
bounds, and nftables default-deny with a declared egress allowlist. This server
is a narrowing on top of such a fence, not a replacement for one.
What v1 cannot run
The set of interpreters is node, one entry, and that is a measured decision
rather than an oversight: mcp-host's runner image is Node + git + tar +
util-linux + nftables, with no python3, curl or jq, while real skills are
overwhelmingly Python (70 .py against 1 .js in anthropics/skills at
3b3fad96).
So a skill declaring a Python script is reported by skill_list under
unavailableScripts, with the interpreter and this deployment's set named, and
skill_run refuses it in the same words. Its instructions still serve —
an instructions-only skill is a useful skill, and most published skills are
exactly that. A pinned interpreter is a follow-up that arrives as a dependency,
never as an image change.
The mcp-host: declaration block
Optional, inside SKILL.md's frontmatter:
A declaration narrows; it never grants. The author of the scripts also wrote the block naming them, so nothing in it is an authorization — it says which files are entry points and what each wants. What makes a script runnable, and a variable reach it, is somebody else accepting it.
Read strictly: YAML 1.2 core schema, anchors and aliases refused, a 64 KiB cap, an unknown MAJOR version refused wholesale, unknown keys ignored and reported by name, and a block that does not parse reported with the parser's position rather than treated as absent. A broken block costs a skill its scripts, never its instructions, and never the rest of the listing.
Configuration
MCP_SKILL_RUN deserves the emphasis. When it is present, what may run is the
declaration intersected with it — a row naming a script the skill did not
declare grants nothing (and is reported), and a row naming a variable the script
did not ask for grants nothing. There is no spelling of it that makes something
runnable which a skill did not declare, which is what makes it safe to read from
an environment that also carries a registration's own variables.
When it is absent, the default depends on whether a host started this child, and the hosted half is fail-closed.
- Hosted — any variable mcp-host's runner injects is present
(
MCP_SKILLS_PATH,MCP_HOST_METER_FILE,MCP_DATA_DIR,MCP_BLOB_BASE_URL): nothing is granted and nothing runs. Every skill's instructions and files are still served — that is a working, useful connector. The reason is that one child holds one environment holding every credential the owner set, so a skill whose frontmatter named its neighbour's variable would otherwise be handed the neighbour's credential with nobody having decided to give it. (That is about what a script is handed; it is not a sandbox — see the env allowlist note above on/proc/<pid>/environ.) It deliberately does not key onMCP_SKILLS_PATHalone: mcp-host does not inject that variable yet, so today's only hosted channel isSKILLS_DIRin a registration's plainenv, and that must not land on the open default. The marker check can only ever move the default in the fail-closed direction. - Standalone — no injected marker at all: the skill's own declaration
stands. Nothing is injecting anything, and the person who pointed the server at
a directory is the owner. That includes the variables a script declares:
a skill whose
env:namesGITHUB_TOKENis handed yourGITHUB_TOKENfrom this server's environment (which an MCP client usually fills from your shell), gated only by theskill_runconfirmation. The preview says so by name (envWarning); only point a standalone server at skills you would hand those secrets to, or setMCP_SKILL_RUNto grant variables explicitly.
skill_list reports which case it is (grantFrom, plus a grantNote in the
hosted one) and lists a skill's declared-but-ungranted scripts, so "nothing
runs" is never indistinguishable from "nothing was declared".
Confirmations
Trust posture
- This server's code is the operator's; the skills are yours. It reads a fixed set of directories handed to it, fetches nothing, installs nothing, and has no tool that takes a path outside a skill's own directory.
- It vets nothing. There is no badge, no publisher allowlist, no scan. A skill's instructions and its scripts are exactly as trustworthy as whoever wrote them.
- A read is treated as dangerously as an execution, because the directory it reads from sits beside everything else on the machine.
- It caches nothing and stores nothing. The catalog is scanned once at boot and held in memory; no file is written anywhere.
Hosting on mcp-host
mint.yaml at the repo root says how this MCP wants to be registered. Four
things it deliberately does not propose, because only a registration can
decide them:
-
The runtime — and it could not, by rule. A manifest may never name one (docs/MINT-MANIFEST.md §5): which tier a registration lands on is decided by who is asking, not by the package, since a file that could ask for
fly-sharedwould be a stranger's package requesting a seat on the operator's own machine. A hosted skill server belongs on the isolated tier (fly-machine) with a declared egress policy, and that is the registration's choice to make. -
The skills themselves. They arrive as a pinned dependency (a
github-archivenaming a repository and an exact commit) and land in a read-only slot the runner names throughMCP_SKILLS_PATH. This package cannot know which ones a given registration carries. -
state.dataDir. The adapter needs no persistence. Turn it on when a registration's skills have scripts that need somewhere to write — the slot is read-only, soMCP_DATA_DIR(with it on) orTMPDIR(without) is where a script's output goes — and give the reason there. -
The real egress allowlist.
mint.yamlproposesallow: [], which is what the adapter itself needs: it reaches nothing. The hosts a registration needs are the ones its SKILLS declare, shown at the preview with who declared them and accepted by the owner. On the isolated tier a host that is not on the list appears either as an HTTP 403 from the loopback proxy or as a plain timeout — the two are indistinguishable from inside a script, soskill_runattaches a note saying so whenever a call fails on a machine that looks fenced.
Narrowing enabledTools to [skill_list, skill_load, skill_file] is the
non-executable switch, enforced at the gateway rather than here — a stronger
statement than this server refusing skill_run, and it costs the prompt and
resource surfaces entirely.
Development
This project was developed and is maintained by AI. Use at your own discretion.
来源:README.md,提交 740ad68
工具
0版本历史
13- v1.2.3最新Oct 29, 2026
- v1.2.2Oct 16, 2026
- v1.2.1Oct 10, 2026
- v1.2.0Oct 7, 2026
- v1.1.2Oct 5, 2026
- v1.1.1Oct 3, 2026
- v1.1.0Sep 24, 2026
- v1.0.4Sep 23, 2026
- v1.0.3Sep 23, 2026
- v1.0.2Sep 21, 2026
- v1.0.0Sep 20, 2026
- v0.3.0Sep 18, 2026
- v0.2.1Sep 16, 2026
评分
已满足适用检查项的 50%
该评分衡量此收录条目的完整程度,依据注册中心条目、端点与公开来源计算得出。它不是对服务器本身的评价,也不表示服务器安全或当前可用:可连通性是带日期的检查结果,评分也不包含安装量或用户评价。
- 近 30 天内可连通25 分
请保持端点能够响应 MCP 握手;SourceWeft 每 30 天探测一次。
- 已声明认证方式10 分
请在注册中心条目中声明凭据请求头,或保持端点能够响应探测,以便判定其认证方式。
- 已列出工具15 分
请让服务器在无凭据时响应 tools/list,或发布可在沙箱中无凭据启动的软件包。
- 描述必需15 分
- 图标10 分
- 文档15 分
- 许可证10 分

