skill-router-mcp
README.md
# skill-router-mcp
[](https://www.npmjs.com/package/skill-router-mcp)
[](LICENSE)
**Skill routing for SKILL.md libraries, served over MCP to any agent.**
Point it at a directory of [Agent Skills](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/overview)
(`SKILL.md` files with YAML frontmatter) and any MCP client — Claude Code,
Cursor, Windsurf, Codex, or your own agent — can discover skills, match a task
to the best one, and load instructions on demand without blowing its context
window.

```
match_skill("create a word document")
→ [{ name: "docx", score: 0.85, requires: ["filesystem", "python"] }, ...]
get_skill("docx")
→ full SKILL.md instructions (token-capped, section-aware)
```
## Why this exists (honest version)
Claude Code and Codex now load Agent Skills natively, and several MCP skill
servers already exist ([skillz](https://github.com/intellectronica/skillz),
[mcp-skill-hub](https://github.com/undermybelt/mcp-skill-hub),
[skillserver](https://github.com/mudler/skillserver),
[Skills Over MCP](https://www.skillsovermcp.com/)). What none of them do is
**rank skills against a task query** — they rely on the host LLM picking from
descriptions, which degrades as skill libraries grow. This project's focus is
the routing layer: a scored `match_skill` backed by local embeddings (Ollama +
`nomic-embed-text`, with automatic keyword fallback), plus strict path
security and context-window discipline.
Measured on a [48-query labeled benchmark](benchmark/RESULTS.md) over 16
skills (queries written before measuring, not tuned):
| Engine | Top-1 | Top-3 | MRR | ms/query |
|---|---|---|---|---|
| keyword | 77.1% | 85.4% | 0.813 | ~1 |
| **semantic** (nomic-embed-text) | **93.8%** | **97.9%** | **0.958** | ~69 |
Both engines score 100% on queries that share words with the skill
description — the gap is entirely on paraphrased (62.5% → 93.8%) and
indirect (68.8% → 87.5%) queries like *"combine several invoices into a
single file for printing"* → `pdf`. Reproduce with `npm run benchmark`.
Every `match_skill` response reports which `engine` produced the ranking
(`semantic` or `keyword`), so fallback is never silent.
## Quick start
Run it straight from npm — no clone, no build:
```bash
SKILLS_ROOT=/path/to/skills npx skill-router-mcp
```
Wire it into Claude Code (`.mcp.json` in your project):
```json
{
"mcpServers": {
"skill-router": {
"command": "npx",
"args": ["skill-router-mcp"],
"env": { "SKILLS_ROOT": "/absolute/path/to/your/skills" }
}
}
}
```
Semantic matching is optional — install [Ollama](https://ollama.com) and
`ollama pull nomic-embed-text` to enable it; otherwise it falls back to
keyword matching automatically.
### From source
```bash
git clone https://github.com/anujkumar8076/skill-router-mcp
cd skill-router-mcp
npm install && npm run build
SKILLS_ROOT=/path/to/skills node dist/index.js
```
## Tools
| Tool | Purpose |
|---|---|
| `list_skills()` | All skills — name + description only (cheap, call first) |
| `match_skill(query, top_k?)` | Top-k skills for a task, with 0–1 scores and `requires` |
| `get_skill(name)` | Full SKILL.md, capped at `MAX_TOKENS` (default 8000); returns `truncated` + `sections` |
| `get_skill_section(name, section)` | One H2 section — for skills too big for one fetch |
| `rescan_skills()` | Force re-index (a chokidar watcher also auto-reindexes) |
## SEP-2640 resources
Skills are also served through the MCP Resources primitive per the
[Skills Over MCP](https://modelcontextprotocol.io/community/skills-over-mcp/charter)
Working Group draft (SEP-2640), so any spec-aware host can consume them
without knowing this server's tools:
- `skill://index.json` — enumerable discovery index (Agent Skills discovery
schema 0.2.0)
- `skill://<name>/SKILL.md` — each skill as a `text/markdown` resource, with
`requires`/`works_in` frontmatter exposed under the
`io.modelcontextprotocol.skills/` `_meta` prefix
- The `io.modelcontextprotocol/skills` extension capability is declared at
initialization
Resource reads pass through the same allowlist validation as the tools.
## Skill format
Standard Agent Skills format, with two optional routing fields:
```markdown
---
name: docx
description: "Use when the user wants to create or edit Word documents."
requires: [filesystem, python] # tools the agent needs to execute this skill
works_in: [claude_code] # environments the skill is known to work in
---
# Instructions...
```
Agents should check `requires` against their own toolset before loading a
skill — **this server delivers instructions; your agent supplies execution.**
## Configuration
| Env var | Default | Meaning |
|---|---|---|
| `SKILLS_ROOT` | `./skills` | Directory scanned (recursively) for `SKILL.md` files |
| `MAX_TOKENS` | `8000` | Token cap on `get_skill` responses |
| `WATCH_SKILLS` | `true` | Set `false` to disable the file watcher (network drives, Docker volumes — use `rescan_skills` instead) |
| `EMBEDDINGS` | `auto` | `auto` = semantic if Ollama is reachable, else keyword; `on` / `off` to force |
| `OLLAMA_URL` | `http://127.0.0.1:11434` | Ollama endpoint for embeddings |
| `EMBEDDING_MODEL` | `nomic-embed-text` | Embedding model (`ollama pull nomic-embed-text` first) |
Skill vectors are cached by content hash in
`~/.cache/skill-router-mcp/embeddings.json`, so restarts and re-indexes only
embed skills whose text changed. If Ollama goes down mid-session, matching
degrades to keyword automatically — routing never hard-fails.
## Security model
- **Allowlist, not paths.** Skill names are sanitized to `[a-z0-9_-]`, looked
up in an index built at startup, and the resolved file is canonicalized and
verified to live inside `SKILLS_ROOT`. User input never constructs a path.
- **Rejected lookups are logged** to stderr.
- **Trust boundary:** skill *content* is injected into your agent's context.
Only point `SKILLS_ROOT` at skills you trust — a 2026 study of 31k
marketplace skills found ~26% contained prompt-injection or exfiltration
patterns.
- **Index-time content lint.** Every skill is scanned for suspicious
patterns: prompt injection ("ignore previous instructions"), concealment
("don't tell the user"), exfiltration (send-to-URL, known exfil endpoints),
credential access (`~/.ssh`, `.env`, API-key harvesting), pipe-to-shell,
decode-and-execute, destructive commands, and opaque base64 blobs.
Findings are logged at index time, shown as `lint_warnings` in
`list_skills`, and attached to `get_skill` responses *before* the content
so a reviewing agent sees the warning first. The lint flags — it never
blocks — and rules favor precision over recall.
## Roadmap
- [x] Embedding-based `match_skill` (local, Ollama `nomic-embed-text`) behind
the `Matcher` interface, with content-hash caching + keyword fallback
- [x] Routing benchmark: keyword vs. embeddings on a 48-query labeled set —
see [benchmark/RESULTS.md](benchmark/RESULTS.md)
- [x] Index-time content lint for suspicious skill patterns (10 rules,
surfaced in `list_skills` and `get_skill`)
- [x] SEP-2640 alignment: `skill://` resources, `skill://index.json`
discovery index, and the skills extension capability
## Development
```bash
npm test # vitest: security, indexer, matchers, content
npm run build # tsc → dist/
node scripts/smoke.mjs # drive the built server over stdio with real queries
npm run benchmark # keyword vs semantic routing accuracy (needs Ollama)
```
MIT
TDQS
A4.4/5.0
Scored across 5 tools
Disambiguation5/5
Each tool has a clearly distinct purpose: listing skills, fetching full skill content, fetching sections, matching skills to tasks, and re-indexing. No overlap or ambiguity.
Naming Consistency5/5
All tool names follow a consistent verb_noun pattern with snake_case (get_skill, get_skill_section, list_skills, match_skill, rescan_skills), making them predictable.
Tool Count5/5
5 tools is well-scoped for a skill router: listing, fetching, section retrieval, matching, and re-indexing. Each tool earns its place without redundancy.
Completeness5/5
Covers all essential operations for skill management: discovery (list_skills), retrieval (get_skill, get_skill_section), intelligent selection (match_skill), and maintenance (rescan_skills). No obvious gaps.
Maintenance
ActivityInactive
ResponsivenessNo issues