Skip to main content
Glama
README.md
# skill-router-mcp

[![npm version](https://img.shields.io/npm/v/skill-router-mcp.svg)](https://www.npmjs.com/package/skill-router-mcp)
[![license](https://img.shields.io/npm/l/skill-router-mcp.svg)](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.

![skill-router-mcp demo](assets/demo.svg)

```
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