A zero-infra MCP server that turns an Open Knowledge Format (OKF) bundle — a directory of markdown "concept" files with YAML frontmatter — into fast, lexical-first concept discovery for LLM agents. No vector DB, no embeddings, no GPU: discovery runs entirely in Python memory via BM25F + Token-Set Jaccard + Levenshtein, fused with Reciprocal Rank Fusion (RRF).
ConceptRouter is a sibling project to SpecRouter
(which does the same thing for OpenAPI specs -> callable tools). It reuses SpecRouter's retrieval core,
but most OKF concepts are knowledge documents, not callable actions — there's no generic
execute_tool that pretends every discovered concept can be "called" (see CLAUDE.md for
why that would be a category error). The one deliberate exception is execute_concept, described below.
ConceptRouter exposes three MCP tools and one MCP resource:
discover_concepts(query)— natural-language search over the bundle. Returns up totop_kconcept summaries (id,type,title,description,tags,resource,score,read_uri,executable).concept://{path}(MCP resource) — the full markdown (frontmatter + body) for one concept, addressed by theid/read_uria discovery hit gave you.execute_concept(concept_id, arguments)— for concepts whoseexecutablefield istrue(they declare their ownmethod/path/paramsfrontmatter — producer-defined extra fields OKF explicitly allows), makes the live HTTP call and returns{status, contentType, content}. Concepts without those fields (the vast majority) return a structured 400 — read them viaconcept://instead. RequiresCONCEPTROUTER_BASE_URL(concepts carry relative paths, not a base URL).refresh_index()— force a re-fetch (git pull, for a git-hosted bundle) + rebuild + persist.
The intended agent flow: call discover_concepts, then either fetch read_uri (knowledge) or call
execute_concept with the hit's id (when executable is true) — never both blended into one tool.
pip install -e ".[dev,fast]" # dev = pytest, fast = rapidfuzz accelerator (optional)
export CONCEPTROUTER_BUNDLE_SOURCE=sample/okf-sample
conceptrouter index # prebuild + persist the index
conceptrouter serve # start the MCP server over stdioPoint an MCP client at it (e.g. in mcp.json):
{
"mcpServers": {
"conceptrouter": {
"command": "conceptrouter",
"args": ["serve"],
"env": { "CONCEPTROUTER_BUNDLE_SOURCE": "sample/okf-sample" }
}
}
}CONCEPTROUTER_BUNDLE_SOURCE is either:
-
a local directory path — any directory of OKF concept
.mdfiles, walked recursively; or -
a git repo —
git+<url>[#ref][:subdir], cloned (shallow) into the cache dir and re-pulled onrefresh_index. For example, to point at the okf-viewer example bundle:export CONCEPTROUTER_BUNDLE_SOURCE=git+https://github.com/saschb2b/okf-viewer.git#main:docs
-
sample/okf-sample/— a small hand-written bundle (tables/metrics/runbooks/one API concept) used by the test suite. -
sample/petstore-api/— a larger, generated bundle: 29 endpoints across 7 categories (accounts/,finance/,pets/,monitoring/,orders/,notifications/,auth/), each concept carryingmethod/path/paramsfrontmatter and directly callable viaexecute_concept. Generated fromsample/petstore_openapi.json(an OpenAPI spec, in the same spirit as SpecRouter'ssample/petstore.json) via:python scripts/openapi_to_okf.py sample/petstore_openapi.json sample/petstore-api
scripts/openapi_to_okf.pyworks on any OpenAPI spec, not just this sample — it groups operations into category folders by their first tag and emits one concept per operation.
Same fielded BM25F + Jaccard + Levenshtein + RRF stack as SpecRouter, retargeted to concept fields instead of endpoint fields:
| Field | Role |
|---|---|
title, type |
High-weight, low length-normalization — short, high-signal identity fields. Also the two fields Levenshtein fuzzy-matches against. |
tags, headings |
Mid-weight structural signal. |
description, body |
Lower weight, high length-normalization — long free text. |
Typo tolerance comes from BM25F's fuzzy term expansion (CONCEPTROUTER_FUZZY_*) plus a down-weighted
standalone Levenshtein vote in RRF (CONCEPTROUTER_RRF_WEIGHT_LEVENSHTEIN=0.5 by default) — the same
tuning SpecRouter arrived at, since it's the same fusion math over a different field set.
Required: CONCEPTROUTER_BUNDLE_SOURCE. Common: CONCEPTROUTER_CACHE_DIR, CONCEPTROUTER_TOP_K,
CONCEPTROUTER_RRF_K, CONCEPTROUTER_RRF_WEIGHT_{BM25F,JACCARD,LEVENSHTEIN}. Fuzzy expansion:
CONCEPTROUTER_FUZZY_EXPAND|MAX_RATIO|MIN_TOKEN_LEN|WEIGHT_POWER. Logging (rotating file, never
stdout): CONCEPTROUTER_LOG_DIR|LEVEL|MAX_BYTES|BACKUP_COUNT|CONSOLE|ARGS. execute_concept bridge:
CONCEPTROUTER_BASE_URL (required for it to work — concepts carry relative paths) and
CONCEPTROUTER_AUTH_MODE + matching credential vars — same shape as SpecRouter's.
config.load_settings auto-loads a .env file (./.env or $CONCEPTROUTER_ENV_FILE); real env vars
override file values.
pytest # no network — conftest.py builds an in-memory bundle from sample/okf-sampleAll three tools are live: discover_concepts, execute_concept, refresh_index, plus the
concept://{path} resource. execute_concept only works for concepts that declare themselves
callable (producer-defined method/path/params frontmatter — see ConceptRecord.is_executable);
everything else — the vast majority of OKF concepts — stays read-only via concept://, by design.