Hook
W piątek rano odpaliłem /doctor w Claude Code. Zanim wpisałem jeden znak, prompt miał już 6000 tokenów. To nie model myślał. To nie był mój kontekst projektu. To były same opisy skili - wszystkie zainstalowane skille z personal, project i plugin scope, preloaded w system prompt na starcie sesji.
Miałem ~80 skili. Nie dlatego, że je zbieram - marketplace Claude Code sprawia, że instalowanie skili jest bezbolesne, a rozbudowana biblioteka realnie się przydaje. Koszt jest niewidzialny dopóki go nie zmierzysz.
To historia o tym, jak zbudowałem skills-radar - open-source serwer MCP, który rozwiązuje ten problem - w jeden dzień, dlaczego oczywiste podejście nie działa, i czego naprawdę wymaga produkcyjnie sensowne rozwiązanie.
Czego nikt nie naprawił
Pod koniec 2025 Anthropic wypuścił Tool Search Tool dla API. Toole oznaczone defer_loading: true są niewidzialne dopóki Claude nie zawoła wbudowanego tool_search_tool. Ich wewnętrzne liczby: 85% redukcji tokenów, accuracy Opusa 4.5 z 79.5% do 88.1% przy dużych bibliotekach toolów. Krótko potem to samo zostało wypuszczone dla MCP serverów w Claude Code.
Tylko że Tool Search jest dla MCP toolów. Skille to inny mechanizm - pliki w ~/.claude/skills/, ładowane przez Skill tool, nie przez MCP. Anthropic nie wypuścił jeszcze ekwiwalentu dla skili. Issue’y na GitHubie #16160 i #19105 wiszą otwarte.
Więc zbudowałem to. Nie dlatego, że nikt nie próbował - kilka projektów typu mcp-skill-server jest w obiegu - ale dlatego, że żaden z nich nie rozwiązuje problemu u korzenia.
Dylemat discovery
Naiwny RAG po skilach pada na pierwszej przeszkodzie:
Jeśli agent nie widzi, że skille istnieją, nigdy nie zapyta indeksu. Jeśli nigdy nie zapyta indeksu, lazy loading jest bezsensowny.
Większość community-owych projektów dostarcza jeden tool MCP - find_relevant_skill - i zakłada, że Claude zapyta go w każdej turze. Nie zapyta. Bez Tier-1 surface signala mówiącego agentowi “te skille istnieją i mniej więcej robią X, Y, Z”, retrieval na Tier-2 jest niewidzialny. MCP server zostaje nieużywany.
Tę lekcję wyniosłem z czytania prior artu (bobmatnyc/mcp-skillset, back1ply/agent-skill-loader, gotalab/skillport). Każdy łapał kawałki, ale żaden nie łączył: (a) wzorca search-then-load Anthropica, (b) hot-reloadu, (c) trust-tiered threat modelu, (d) air-gapped install path, (e) wsparcia dla wielu klientów.
Two-Tier Discovery - architektura
skills-radar rozdziela discovery na dwa komplementarne sygnały:
Tier 1 - mini-index, ~1k tokenów, zawsze preloaded. Płaska lista name + jednolinijkowy summary per skill, zapisywana do ~/.claude/SKILLS-INDEX.md i importowana w globalnym CLAUDE.md. Mówi agentowi co istnieje.
Tier 2 - load on-demand przez MCP. Dwa toole:
search_skills(query, top_k=5, tags=None)- hybrid retrieval (BM25 + dense embeddingi, waga 70/30) podescription + when_to_use. Zwraca rankingowane matche z name / description / trust / score / scope.load_skill(name)- pełny zsanityzowany SKILL.md, gdy agent zdecyduje się działać.
Body SKILL.md nigdy nie jest indeksowane do retrievalu - ładowane wyłącznie przy load_skill. Indeks zostaje mały, focused i accurate.
Wynik na moim 60-skilowym korpusie: 6000 tokenów → 1900 tokenów preloaded. ~68% redukcji. Koszt zostaje płaski przy skali do 500 skili.
Czemu dwa toole, a nie jeden? Bo skille są bardziej dyskretne niż toole. Gdy user mówi “use wcag-toolkit-lead”, nazwa jest oczywista - zawołaj load_skill bezpośrednio. Gdy mówi “audit my a11y”, intencja jest fuzzy - najpierw search_skills. Tool Search Anthropica daje jeden tool, bo toole zwykle wołane są po dokładnej nazwie; skille zarabiają drugi tool, bo ich użycie jest bardziej deklaratywne.
Threat model - bez kompromisów, od pierwszego dnia
Plik SKILL.md jest ładowany bezpośrednio do context window agenta jako instrukcje. Z perspektywy modelu różnica między system promptem a body skila jest w zasadzie nominalna - i jedno, i drugie kształtuje zachowanie. Złośliwy skill to wektor system-prompt injection.
Typowe surface’y ataku:
- Open-source kolekcje skili (np. 1000+ w
awesome-agent-skills) - każdy może coś wrzucić, kontrola jakości różna - Marketplace pluginów - przejęte konto maintainera shippuje złośliwy skill
- Skille z klonowanego repo - klonujesz repo i nagle jego
.claude/skills/są w twoich scan paths - Twoje własne przyszłe pomyłki - wkleiłeś coś bez weryfikacji
Naiwny RAG ładuje którekolwiek z tych jako autorytatywne instrukcje. Tego nie chcemy.
skills-radar dostarcza cztery warstwy obrony, nakładane przy ingest:
Trust tier assignment. Każdy skill jest tagowany przy ingest jako TRUSTED (config-explicit) > VERIFIED (Anthropic-official plugin cache) > USER (~/.claude/skills) > UNTRUSTED (cokolwiek innego). Tier widoczny w response z load_skill, więc downstream agent może odmówić UNTRUSTED.
Walidacja frontmattera. Reserved-word rejection (anthropic, claude), format nazwy (≤64 znaków, lowercase + hyphens), wymagane pola, max size 64KB.
Sanityzacja body. Strip XML injection tagów (<system>, <override>, <jailbreak>, …), katalog regexów dla prompt-injection (configurable), opcjonalne stripowanie składni live-execution dla klientów spoza Claude Code.
Size cap. Cap UTF-8 byte-length per SKILL.md. Skille przekraczające cap są odrzucane całkowicie.
To nie sprawia, że community skille są bezpieczne do uruchamiania na ślepo - sprawia, że są mierzalne, z surface area widoczną dla agenta. W połączeniu z explicit trust tiers downstream agenty mogą wdrożyć policy typu “domyślnie odrzucaj UNTRUSTED, wymagaj explicit user opt-in”.
Stack - trzy ścieżki, jedno repo
Default install działa cross-platform na jednej maszynie. Dwie opcjonalne ścieżki dodają mocy: 100% lokalny stack na Apple Silicon (zero sieci, zero chmury, zero Ollamy) i cross-platform LLM-augmented stack przez Ollamę.
| Layer | Default (cross-platform) | Mac 100% local (MLX) | Cross-platform LLM (Ollama) |
|---|---|---|---|
| Runtime | Python 3.11+ | - | - |
| MCP SDK | mcp (FastMCP) | - | - |
| Transport | stdio (Claude Code) / Streamable HTTP (production) | - | - |
| Embedder | sentence-transformers/all-MiniLM-L6-v2 (90 MB, 384-dim, CPU-fast) | MLX Qwen3-Embedding-8B-4bit-DWQ (4096-dim, Apple Silicon) | - |
| Lexical | BM25 via rank_bm25 | - | - |
| Vector store | ChromaDB (embedded, zero deps) | Qdrant (production, reusable across projects - share an instance with sdet-brain) | Qdrant |
| File watcher | watchdog 250 ms debounce | - | - |
| Query rewriter | NoOp | MLX Qwen3-Coder-30B-A3B-Instruct-4bit - lazy load + LRU cache | Ollama (gemma4:e4b), HTTP fallback |
| Reranker | NoOp | MLX Qwen3-Coder-30B-A3B - single-pass batch scoring | Ollama, per-pair scoring |
| Telemetry | Off (strict opt-in) | Same | Same |
Dlaczego dokładnie taki podział: defaulty są lekkie (90 MB model, zero infrastruktury), żeby open-source community mogło zainstalować przez pip install skills-radar i odpalić od razu. Dwie ścieżki dla power-userów są opt-in - nie puchną base install, ale są podpięte czysto, gdy przełączysz flagi w configu.
Design highlight to 100% lokalny stack na Apple Silicon. Ustaw:
embedder:
backend: mlx
model: mlx-community/Qwen3-Embedding-8B-4bit-DWQ
retrieval:
rewriter:
enabled: true
backend: mlx
model: mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit
reranker:
enabled: true
backend: mlx
model: mlx-community/Qwen3-Coder-30B-A3B-Instruct-4bit
…i cały pipeline - embedding, query rewriting, reranking - leci na twoim M-series GPU + Neural Engine. Bez Ollamy. Bez HTTP. Bez sieci. Identyczne zapytania trafiają w per-instance LRU cache; warm queries kosztują ~3 sekundy w MCP serverze (model w pamięci) zamiast 6+ przy zimnych wywołaniach z CLI.
Jakość retrievalu - liczby, które mają znaczenie
Polish fuzzy query to najczystsza demonstracja tradeoffu. Ten sam 60-skilowy korpus, to samo zapytanie, trzy konfiguracje:
Query: napisz mi post na LinkedIn o WCAG (mixed-language, ambiguous intent - może być “napisz posta o WCAG” albo “znajdź mi skill związany z WCAG”).
| Config | Top 5 (name, score) | Verdict |
|---|---|---|
| Default sentence-transformers, NoOp rewriter | content-writing-lead (0.54), ffcss-migrate (0.49 false positive), wcag-toolkit-lead (0.42), wcag-dynamic-test (0.32), wcag-report (0.29) | top-1 trafia, ale margines jest brzytwą; #2 to przypadkowy skill do migracji Tailwind→FFCSS |
| Default + Ollama rewriter (gemma4:e4b) | content-writing-lead (czystszy top), inne a11y skille się pojawiają | margines się rozszerza, +100-300 ms latency |
| MLX rewriter (Qwen3-Coder-30B-A3B-Instruct-4bit) | a11y-audit (0.71), a11y-orchestrator (0.65), a11y-fix (0.61), wcag-static-analyze (0.56), wcag-fix (0.44) | wszystkie 5 hitów to a11y/WCAG, top-1 powyżej 0.7, ~9 s na cold CLI / ~3 s warm w MCP serverze |
Dwie rzeczy do zauważenia. Po pierwsze - rewriter nie zachowuje pierwotnej intencji surface’owej. Z włączonym MLX rewriterem content-writing-lead w ogóle się nie pojawia. Rewriter znormalizował “post o WCAG” do keywordów typu web accessibility wcag accessibility standards accessibility standards - co jest OK dla intencji “znajdź mi skill związany z WCAG”, ale jest miss, jeśli rzeczywiście chciałeś pomocy z pisaniem. Tradeoff jest realny i udokumentowany; rewriter jest off by default.
Po drugie - dla angielskich technicznych zapytań default backend już jest solidny (top-1 powyżej 0.6 z czystą separacją). MLX stack zwraca się głównie przy fuzzy / multilingual / casual phrasing, gdzie mały embedder nie wyrabia. Jeśli twój zespół pisze zapytania w angielskim engineering-speak, możesz nigdy nie potrzebować MLX path. Jeśli jesteś mną i połowa promptów jest po polsku, MLX stack to różnica między “good enough most of the time” a “right every time”.
Lokalna telemetria opt-in
Dorzuciłem SQLite event log w ~/.local/share/skills-radar/stats.db. Trzy event kindy - search, load, index - każdy z relevantnymi polami (latency_ms, top1_score, trust tier itd.). Strict opt-in: domyślnie disabled, zero remote telemetry kiedykolwiek.
CLI skills-radar stats pokazuje:
- Top loaded skills (najczęściej rzeczywiście fetchowane, nie tylko wyszukiwane - mocny sygnał użyteczności)
- Top queries z częstotliwością
- Miss rate - searche z top-1 score < 0.4 (skalibrowane z obserwacji: poniżej tego progu ranking jest niewiarygodny)
- Recent events z detalem per-event
Miss rate powyżej 30% to sygnał, żeby włączyć Ollama rewriter albo upgrade’ować do MLX. Poniżej 15% default stack jest wystarczający.
TUI dashboard
skills-radar tui startuje real-time read-only dashboard na rich.Live z czterema panelami:
┌─ skills-radar v0.3.0a0 · 60 skills · 5/5 paths · embedder=mlx · store=qdrant ─┐
├─ Trust tier breakdown ────────────────────────────────────────────────────────┤
│ TRUSTED ████████░░░░░░░░ 31 │
│ VERIFIED ███████████░░░░░ 38 │
│ USER ░░░░░░░░░░░░░░░░ 0 │
├─ Top queries · miss 12% of 17 ─┬─ Recent events · live ─────────────────────┤
│ wcag accessibility audit 3 │ 18:42 search 0.79 wcag audit (132ms) │
│ napisz post na linkedin 2 │ 18:41 load perf-vue-runtime (48ms) │
│ vue memory leak 2 │ 18:40 search 0.67 vue memory leak ... │
├─ Top loaded skills ────────────┤ │
│ a11y-orchestrator 4 │ │
│ perf-vue-runtime 3 │ │
│ content-writing-lead 2 │ │
└────────────────────────────────┴──────────────────────────────────────────────┘
Stream recent events jest color-coded: zielony przy top-1 ≥ 0.6, żółty ≥ 0.4, czerwony < 0.4. Badge miss-rate w panelu top queries używa tego samego schematu. Możesz mieć to otwarte na drugim monitorze podczas pracy i obserwować jakość searcha w czasie rzeczywistym - bezcenne przy tuningu hub-tagów albo decyzji, kiedy włączyć rewriter.
Hot reload
watchdog obserwuje wszystkie skonfigurowane paths. Każdy created / modified / deleted / moved SKILL.md triggeruje single-record update w indeksie, z debouncingiem 250 ms, żeby zlepić bursty zapisów z edytora. Dodaj nowy SKILL.md, zapisz, zapytaj przez Claude Code natychmiast - bez restartu, bez komendy reindex.
To jest differentiator vs każdy prior art, który ewaluowałem. Nikt inny tego nie ma dobrze. back1ply/agent-skill-loader zbliża się, ale używa substring searcha, który nie skaluje. bobmatnyc/mcp-skillset w ogóle tego nie ma.
Deployment produkcyjny
Dla shared / multi-client / Docker deploymentów skills-radar serve --transport http chodzi na Streamable HTTP zgodnie z guidance MCP Python SDK: stateless_http=True, json_response=True dla horizontal scalability za load balancerem.
Wbudowany Dockerfile jest multi-stage: builder stage instaluje deps i pre-bake’uje model embeddingowy, więc runtime stage startuje w ~2 sekundy zamiast pobierać model 30-60 sekund przy first-run. Runtime jako non-root (uid 1000), z offline HF Hub flagami, więc działa w air-gapped środowiskach. Defaulty wewnątrz kontenera są strict: tier UNTRUSTED + strip_live_exec=true - community skille zamontowane przez Dockera nie powinny móc odpalać komend host-level.
docker-compose.yml montuje twoje ~/.claude/skills i plugin cache read-only i persystuje store ChromaDB jako named volume. Healthcheck POST-uje prawdziwy MCP initialize handshake (nie tylko GET - Streamable HTTP wymaga JSON-RPC body) i raportuje healthy w ~1 sekundę.
Co bym zrobił inaczej
Cztery rzeczy z perspektywy czasu:
-
Zacząć od threat modelu, nie od retrievalu. Pisałem sanityzację od pierwszego dnia, ale 60% efortu poszło najpierw na retrieval. Problem retrievalu jest ciekawy; threat model jest tym, co czyni narzędzie deployowalnym. Gdybym zaczynał od nowa, najpierw napisałbym
sanitize.pyitrust tiers, testy do obu, dopiero potem retrieval. -
Przetestować BM25 przed założeniem, że hybrid jest konieczny. Moja intuicja mówiła, że czysty BM25 będzie omijał za dużo semantic matchy. Przy krótkich technicznych opisach (50-300 znaków) sam BM25 daje 70-80% rezultatu. Hybrid retrieval zwraca się dopiero przy fuzzy / multilingual queries. Dla hyper-minimalnej wersji BM25-only zashippowałby się tydzień wcześniej.
-
Flaga
disable-model-invocation: truejest bardziej użyteczna, niż początkowo myślałem. Skille oznaczone manual-only są automatycznie filtrowane zsearch_skills- okazuje się, że niemała frakcja skili to template’y / reference docs, które nie powinny się auto-triggerować. Honorowanie tej flagi od pierwszego dnia jest tanie; retrofitting jest upierdliwy. -
Latencja MLX to złe miejsce na premature optimization. Pierwsza wersja MLX rewritera poszła prosto we wzorzec “score every candidate one at a time” (mirroring Ollama). Dla rerank’a 20 kandydatów to 20 osobnych inferencji - minuty per query. Single-pass batch (jeden prompt enumerujący wszystkich kandydatów, model zwraca linie
N=score, parsowane regexem) zwija to do jednej inferencji, ~5-15 s dla całej puli. Koszt: parser regex. Reward: usable latency. Ta sama lekcja stosuje się szerzej - przy lokalnych LLM batchuj tyle, ile pozwala context window, zanim zaczniesz tunować rozmiar modelu.
Ekonomia skali
Dla usera z 80 skilami:
| Strategy | Per-session cost | Worst case (use 1 skill) |
|---|---|---|
| Native Claude Code skill listing | ~6,000 tokens | ~6,000 tokens |
| skills-radar Two-Tier Discovery | ~1,000 tokens (mini-index) | ~1,000 + ~2,000 = ~3,000 tokens |
| skills-radar - multiple loads (5 skills) | ~1,000 + 5 × 2,000 = ~11,000 tokens | (rare - most sessions load 0-2) |
Net: w realnym przypadku (1-2 skille loaded per sesja) skills-radar oszczędza ~3-5k tokenów na sesję. Przy skali (500 skili) natywne podejście staje się nieużywalne; koszt skills-radar zostaje płaski.
To nie tylko cost story - to quality story. Badania Anthropica o transformer attention pokazują, że długi kontekst degraduje jakość odpowiedzi. Szczuplejszy prompt daje modelowi szansę zostać focused.
Co zostaje otwarte
Kilka kawałków świadomie zostawionych na następny milestone:
- FAISS store backend. Lżejszy niż ChromaDB (jeden plik, zero schemy), użyteczny jako fallback w restrictive środowiskach, gdzie nawet SQLite-backed vector DB jest za dużo.
- Voyage / OpenAI embedder backends. Cloud BYOK jako opcja dla power-userów, którzy chcą best-in-class embedding quality bez lokalnego modelu 4 GB.
- Auto-discovery z GitHub repo (np.
awesome-agent-skills). Jedna komenda CLI ściąga publiczną kolekcję skili do tieru UNTRUSTED z explicit confirm per-skill. - Crypto signing dla tieru VERIFIED. Dziś VERIFIED jest path-based (Anthropic-official plugin cache). Cryptographically signed skille z trust manifestem to naturalny następny krok dla community skill ecosystems.
- LLM-based prompt-injection scanner. Rozszerza katalog regexów. Mały lokalny model (np.
gemma4:e4bprzez Ollamę) klasyfikuje podejrzane body, których regexy nie złapały. Opt-in. - Sandbox
bundled_files. Dziś bundled files są enumerowane w response zload_skill. Przyszła wersja mogłaby opcjonalnie sandbox’ować read-only pliki referencowane jeden poziom w głąb, żeby agenty mogły je bezpiecznie pobierać. - Multi-language taksonomia hub-tagów. Rekomendowany słownik
hub-tags(a11y,perf,qaitd.) musi zostać opublikowany i przyjęty, żeby filtered search był użyteczny przy skali korpusu.
Repo + instalacja
pip install skills-radar
skills-radar config-init
skills-radar index
claude mcp add skills-radar -- skills-radar serve --transport stdio --watch
Zrestartuj Claude Code. /mcp pokazuje skills-radar connected. Odpal skills-radar mini-index i zaimportuj ~/.claude/SKILLS-INDEX.md w globalnym CLAUDE.md. To cały setup.
Source: github.com/darco81/skills-radar. Licencja MIT. Zbudowane w jeden majowy piątek 2026 między innymi rzeczami.