semcode

Acerca de Semantic code-search (semcode) MCP. Indexa símbolos de código e historial de commits. Combina embeddings densos con vectores dispersos BM25 para una búsqueda híbrida que equilibra la comprensión semántica con la precisión de palabras clave.

Documentación

MCP Server Python 3.12+ License MIT Release CI

semcode

Un servidor MCP (Model Context Protocol) que proporciona búsqueda semántica híbrida sobre código en un conjunto de repositorios de GitHub que tú listas en config.yaml. Analiza símbolos con Tree-sitter e indexa tanto el código como el historial de commits de git, para que los clientes de IA puedan consultarlos por lenguaje natural o por nombre de símbolo.

La recuperación híbrida combina embeddings densos con BM25, de modo que tanto las consultas en lenguaje natural ("¿dónde publicamos eventos de pedidos?") como las búsquedas por nombre de símbolo (PlaceOrderRequest) funcionan bien.

Enviado en:

mcpservers.org

mcpmarket.com

mcp.so

Cómo funciona

  1. Obtiene archivos fuente de los repositorios de GitHub configurados
  2. Analiza símbolos de código (funciones, clases, métodos, componentes) usando Tree-sitter
  3. Genera dos embeddings por símbolo: un vector semántico denso (proveedor conectable: Jina Code V2 por defecto, o Voyage / OpenAI / Ollama) y un vector disperso BM25 basado en tokens de identificadores de código (camelCase / snake_case divididos en subpalabras)
  4. Almacena ambos en Qdrant y los recupera con búsqueda híbrida — Reciprocal Rank Fusion (RRF) sobre los resultados densos y dispersos — para que las consultas en lenguaje natural y las búsquedas por nombre de símbolo funcionen bien
  5. Opcionalmente indexa el historial de commits en una colección separada de Qdrant (solo denso)
  6. Expone herramientas de búsqueda e indexación a través del protocolo MCP (y una pequeña API HTTP)

La indexación es incremental — los archivos se omiten cuando su SHA de blob de Git coincide con la última versión indexada. Los archivos que ya no existen (o que se analizan a cero símbolos) se limpian automáticamente. Pasa force: true para re-embedding todo.

¿Quieres más detalles? ¡Mira el blog!

Documentación

La documentación detallada de los internals del sistema RAG está en goodbyeplanet.github.io/semcode:

Lenguajes soportados

El lenguaje se detecta automáticamente por extensión de archivo o nombre de archivo — no se necesita configuración.

Go, Java, Python, TypeScript / JavaScript (React), Rust, C#, C, C++, Ruby, PHP, Kotlin, Scala, Swift, Dart, Bash, SQL, Lua, R, Dockerfile, Docker Compose, Markdown, JSON, HTML, CSS, XML.

La mayoría de los analizadores son conscientes del framework donde importa — estereotipos Spring y rutas HTTP para Java/Kotlin, FastAPI/Pydantic para Python, ASP.NET para C#, Rails para Ruby, Laravel/Symfony para PHP, widgets React/SwiftUI/Flutter, etc. Ver server/parser/ para los detalles de extracción por lenguaje.

Configuración

Requisitos previos: Python 3.12+, Docker, token de GitHub

# Install dependencies
uv sync

# Copy environment file, then edit .env to set GITHUB_TOKEN
# (a fine-grained PAT with Contents: read on the target repos is sufficient)
cp .env.example .env

# Optional — only if you want curated/static services (see below for the alternative):
# copy the services config, then list the repositories you want indexed
cp config.example.yaml config.yaml

Configura qué repositorios indexar en config.yaml:

services:
  - name: my-service
    github_repo: owner/repo
    github_ref: main              # optional, defaults to "main" — branch, tag, or commit SHA
    root: src/main/java           # optional — limit indexing to this subdirectory (useful for monorepos)
    exclude:                      # optional — skip matching paths
      - "**/vendor/**"
      - "**/node_modules/**"

El indexador descubre e indexa automáticamente todos los archivos con extensiones reconocidas. Usa root para limitar un servicio a un subdirectorio dentro de un repositorio compartido, y exclude para omitir rutas que no quieras indexar (pruebas, artefactos de compilación, código generado, etc.).

Escalando más allá de un puñado de repos: config.yaml es una lista estática y curada — excelente para un pequeño número de servicios, pero indexar cientos de repos de esta manera significa cientos de entradas mantenidas a mano. Como alternativa (o complemento), POST /reindex acepta una definición de repositorio en línea y lo registra sobre la marcha, sin necesidad de entrada en config.yaml — ver examples/github-actions/reindex-on-merge.yml para un flujo de trabajo listo para usar que auto-registra un repositorio y lo reindexa en cada merge. Detalles en la sección API HTTP a continuación. Si un nombre colisiona entre los dos, la entrada de config.yaml siempre gana.

No necesitas un config.yaml en absoluto para ejecutarlo de esta manera — un archivo faltante se trata como cero servicios configurados, no como un error. docker-compose.yaml refleja esto: por defecto no monta config.yaml, por lo que make docker-up / make docker-up-jina funcionan de inmediato para configuraciones solo de registro ad-hoc. Si también quieres servicios curados, copia config.example.yaml a config.yaml (arriba) y usa los objetivos -with-config en su lugar, que superponen docker-compose.config-yaml.yml encima para agregar el montaje: make docker-up-with-config / make docker-up-jina-with-config (o docker compose -f docker-compose.yaml -f docker-compose.config-yaml.yml up -d directamente). No edites a mano la línea de volumen en docker-compose.yaml en sí — montar por bind un config.yaml que no existe en el host silenciosamente crea un directorio vacío allí en lugar de dejar la ruta ausente, lo que rompe el servidor (se muestra como un error claro si ocurre: CONFIG_PATH (...) is a directory, not a file).

Un solo GITHUB_TOKEN lee cada repositorio que indexas de esta manera. Para un puñado de entradas config.yaml, un PAT de grano fino con alcance a esos repos es suficiente, pero para auto-registro a nivel de organización — donde cualquier repositorio puede incorporarse solo agregando el flujo de trabajo — un PAT necesitaría que su lista de acceso a repos se actualizara fuera de banda cada vez que un nuevo repositorio comience a usarlo. Una GitHub App instalada a nivel de organización (todos los repos, Contents: read) evita eso: los nuevos repos se cubren automáticamente, sin mantenimiento de token por incorporación.

Ejecución

Hay dos formas de ejecutarlo, dependiendo de si quieres que los embeddings provengan de un contenedor local o de un proveedor alojado. Elige una:

Ruta A — Jina local vía TEI (predeterminado, sin clave API requerida):

make docker-up-jina
# or: docker-compose --profile jina up

Ruta B — proveedor alojado (Voyage / OpenAI) o Ollama local:

# 1. In .env, set EMBEDDINGS_PROVIDER=voyage|openai|ollama and the relevant API key.
# 2. Then start without the jina profile:
make docker-up
# or: docker-compose up

¿Usando config.yaml para servicios curados? Usa la variante -with-config del objetivo que corresponda arriba (make docker-up-with-config / make docker-up-jina-with-config) — ver la sección Configuración.

⚠ El EMBEDDINGS_PROVIDER predeterminado es jina. Si inicias sin --profile jina pero dejas el proveedor en el predeterminado, semcode arrancará (Jina es required: false en compose) pero la primera llamada de embedding fallará con un error de conexión — no hay respaldo automático.

Servicios iniciados con health checks y volúmenes persistentes:

ServicioPerfilPuertoVolumenPropósito
Qdrantsiempre6333 (HTTP), 6334 (gRPC)qdrant_dataBase de datos vectorial
Jina Embeddings (TEI)jina8087embeddings_cacheServidor de modelo de embeddings
semcode MCPsiempre8090monta ./config.yaml de solo lectura con -with-configServidor MCP + HTTP

El servidor MCP inicia con colecciones vacías — dispara un índice inicial llamando a la herramienta MCP reindex o POST /reindex (ver abajo).

Conectando clientes de IA

Una vez que el servidor esté en ejecución, apunta tu cliente de IA a http://localhost:8090/mcp.

Claude Code (CLI)

claude mcp add --transport http semcode http://localhost:8090/mcp

Otros clientes MCP (Claude Desktop, Cursor, etc.) — agrega una entrada a la configuración MCP del cliente:

{
  "mcpServers": {
    "semcode": {
      "transport": "http",
      "url": "http://localhost:8090/mcp"
    }
  }
}

Conexión a través de stdio

En lugar de apuntar a un servidor HTTP en ejecución, el cliente puede iniciar el proceso del servidor en sí y hablar con él a través de stdin/stdout. Esto aún necesita Qdrant accesible (por ejemplo, docker-compose up qdrant) y un entorno Python local con dependencias instaladas (uv sync).

Claude Code (CLI)

claude mcp add semcode --transport stdio --env MCP_TRANSPORT=stdio -- uv run --directory /path/to/semcode python -m server.main

Otros clientes MCP — agrega una entrada a la configuración MCP del cliente:

{
  "mcpServers": {
    "semcode": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/semcode", "python", "-m", "server.main"],
      "env": {
        "MCP_TRANSPORT": "stdio"
      }
    }
  }
}

GITHUB_TOKEN, QDRANT_URL, y las variables del proveedor de embeddings aún se leen de .env en el directorio del proyecto — uv run lo recoge automáticamente.

Conexión a través de SSE

SSE es el transporte HTTP MCP heredado, reemplazado por streamable-http. Úsalo solo para clientes que aún no soportan streamable-http — las nuevas configuraciones deberían usar la configuración streamable-http arriba.

Establece MCP_TRANSPORT=sse en .env (o el entorno) e inicia el servidor de la misma manera que streamable-http (make docker-up / make docker-up-jina, o uv run python -m server.main localmente). El servidor expone un endpoint SSE en http://localhost:8090/sse.

Claude Code (CLI)

claude mcp add --transport sse semcode http://localhost:8090/sse

Otros clientes MCP — agrega una entrada a la configuración MCP del cliente:

{
  "mcpServers": {
    "semcode": {
      "transport": "sse",
      "url": "http://localhost:8090/sse"
    }
  }
}

Indexación

El pipeline de indexación está orientado a símbolos: cada función, clase, método o componente se convierte en su propio fragmento con un embedding vectorial y un payload rico.

  • Descubrimiento — lista todos los archivos en el repositorio en github_ref, aplicando filtros root y exclude
  • Detección de cambios — compara el SHA de blob de Git del archivo con el último valor indexado; los archivos sin cambios se omiten
  • Análisis — Tree-sitter recorre el AST y emite objetos CodeSymbol por lenguaje
  • Texto de embedding denso — etiqueta de lenguaje, tipo de símbolo, clase padre, paquete, extras de framework (estereotipo Spring, ruta HTTP, Lombok, React memo), docstring, firma y fuente (fuente truncada en EMBEDDING_MAX_CHARS, un predeterminado consciente del proveedor — ver docs/configuration.md)
  • Texto de embedding disperso (BM25) — firma, docstring y fuente. Los identificadores de código se dividen en subpalabras ( camelCase, snake_case) antes de la tokenización, por lo que getUserById se indexa como get, user, by, id así como el token completo
  • Procesamiento por lotes — el proveedor denso procesa en lotes de 32 (Jina/TEI, Ollama) o 128 (Voyage, OpenAI); BM25 se ejecuta en proceso
  • Upsert — ambos vectores se almacenan bajo un solo punto en Qdrant, claveado por un UUID determinista (por servicio / archivo / símbolo / línea)
  • Limpieza — las entradas para archivos que ya no están en el repositorio (o que ahora se analizan a cero símbolos) se eliminan

La indexación del historial de Git es un pipeline separado y opcional que embedding los mensajes de commit y las rutas de archivos cambiados en la colección git_commits. Los diffs unificados completos se almacenan en el payload y son recuperables a través de la herramienta get_commit. El número de commits por servicio está limitado por GIT_HISTORY_MAX_COMMITS (predeterminado 500).

Pruebas

uv sync --group dev
uv run pytest

Las pruebas viven bajo tests/:

  • tests/parser/test_*.py — un archivo por lenguaje; captura el comportamiento del analizador contra fixtures canónicos en tests/fixtures/<language>/
  • tests/test_pipeline.py, tests/test_store.py, tests/test_git_history.py — pruebas de integración para el pipeline de indexación y el almacén Qdrant
  • tests/test_reindex_route.py — pruebas de rutas HTTP

Herramientas MCP

HerramientaDescripción
search_codeBúsqueda híbrida (densa + BM25) por consulta, con filtros opcionales por idioma, servicio y tipo de símbolo
find_symbolBusca un símbolo por nombre: coincidencia exacta o coincidencia de token sin distinguir mayúsculas cuando exact=false
find_usagesEncuentra código que referencia un nombre de símbolo dado (búsqueda semántica, luego excluye la definición en sí)
get_code_contextObtiene el código fuente completo de un archivo, o un símbolo específico dentro de él, directamente desde GitHub
reindexActiva la indexación de código de uno o todos los servicios (incremental por defecto; force para re-embedding)
index_historyIndexa el historial de commits de git; obtiene automáticamente los diffs de los commits que les falten
search_commitsBusca en el historial de commits de git con lenguaje natural
get_commitObtiene detalles completos de un commit específico, incluidos archivos modificados y diffs
list_indexed_servicesLista los servicios indexados con recuentos de fragmentos y archivos, idiomas y hora de última indexación
index_statsMuestra estadísticas de la colección Qdrant y los servicios configurados

find_symbol(exact=false) coincide con un índice de texto completo sobre los tokens camelCase/snake_case del nombre del símbolo, por lo que order o ord encuentra placeOrderRequest en ~2 ms independientemente del tamaño de la colección. Los fragmentos a mitad de token (rder) también coinciden, pero recurren a un escaneo del lado del cliente que es lineal en el tamaño de la colección. Las colecciones indexadas antes de que existiera este campo usan ese mismo recurso hasta que se reindexen, y como la detección de cambios omite archivos sin cambios, poblar el campo requiere una reindexación forzada (POST /reindex {"force": true}), que re-embediza cada símbolo. Consulta docs/retrieval-rrf.md.

Prompts de MCP

PromptArgumentosDescripción
service_overviewserviceGuía al cliente para producir una visión general arquitectónica de un servicio: puntos de entrada HTTP, tipos de dominio y convenciones notables del framework
system_design_overview(ninguno)Guía al cliente para producir una visión general completa del diseño del sistema: inventario de servicios, topología de comunicación, almacenes de datos compartidos y preocupaciones transversales: incluye diagramas Mermaid

API HTTP

Además de las herramientas MCP, el servidor expone dos endpoints HTTP para activar la indexación desde CI/CD o programadores externos:

EndpointCuerpoDescripción
POST /reindex{"service": "<name>"?, "force": <bool>?, "github_repo": "<owner/repo>"?, "github_ref": "<ref>"?, "root": "<path>"?, "exclude": [<glob>, ...]?}Reindexa uno o todos los servicios: devuelve NDJSON
POST /reindex-history{"service": "<name>"?, "force": <bool>?}Indexa el historial de commits de git: devuelve NDJSON

Todos los cuerpos son opcionales: omite service para actuar sobre todos los servicios, omite force para indexación incremental. Ambos endpoints transmiten JSON delimitado por nuevas líneas (un marco por línea) para que puedas consumir el progreso en tiempo real desde pipelines de CI/CD o cualquier otro cliente.

Registrar un repositorio sin config.yaml: si el cuerpo de POST /reindex incluye github_repo, el nombre service se registra con esa definición de repositorio (persistido, por lo que sobrevive a reinicios y se comporta como un servicio config.yaml a partir de entonces) antes de que se ejecute la indexación: service es obligatorio en este caso. github_ref tiene como valor predeterminado main; root/exclude reflejan los mismos campos en config.yaml. Un nombre service ya definido en config.yaml siempre tiene prioridad sobre uno registrado de esta manera. No hay autenticación en este endpoint, igual que en el resto de /reindex, así que colócalo detrás de tu propio límite de red antes de exponerlo. Consulta examples/github-actions/reindex-on-merge.yml para un flujo de trabajo listo para usar.

Formas de los marcos:

// in-flight progress
{"type": "progress", "phase": "discovery|upserting|cleanup", "current": 12, "total": 200, "percentage": 6.0, "service": "my-service"}
// final summary (one per request)
{"type": "done", "result": {"files": 42, "chunks": 318, "skipped": 5}}
// emitted instead of "done" on failure
{"type": "error", "message": "..."}

Para /reindex, los marcos upserting llevan current como un recuento monótonamente creciente de archivos resueltos (indexados, omitidos como sin cambios o descartados por un error de obtención/análisis), por lo que siempre termina en total. Los archivos se indexan de forma concurrente, por lo que los marcos se emiten por lote en lugar de por archivo.

Para /reindex-history, el valor phase es discovery|embedding|upserting y el resultado done es {"new": int, "skipped": int, "diff_updated": int}.

Variables de entorno

VariablePredeterminadoDescripción
GITHUB_TOKEN(obligatorio)Token de GitHub con acceso de lectura al repositorio
QDRANT_URLhttp://localhost:6333URL de conexión de Qdrant
QDRANT_COLLECTIONcode_symbolsNombre de la colección para vectores de símbolos de código
QDRANT_COMMITS_COLLECTIONgit_commitsNombre de la colección para vectores de mensajes de commit
EMBEDDINGS_PROVIDERjinaUno de jina, jina-api, voyage, openai, ollama: consulta Proveedores de embedding a continuación
GIT_HISTORY_MAX_COMMITS500Máximo de commits indexados por servicio
CODE_CONTEXT_CACHE_SIZE128Archivos almacenados en caché en memoria para get_code_context (clave por SHA de blob); 0 desactiva
CODE_CONTEXT_CACHE_TTL900Segundos durante los cuales el contenido de un archivo en caché sigue siendo válido
MCP_TRANSPORTstreamable-httpUno de streamable-http, sse, stdio
MCP_HOST / MCP_PORT127.0.0.1 / 8090Dirección de enlace del servidor
MCP_STATELESS_HTTPtrueSirve streamable-http sin seguimiento de sesión (no se necesitan sesiones fijas); false restaura sesiones MCP por cliente. Solo streamable-http
CONFIG_PATH./config.yamlRuta al archivo de configuración de servicios

Proveedores de embedding

El backend de embedding se puede seleccionar mediante EMBEDDINGS_PROVIDER. El predeterminado es jina para que las implementaciones existentes sigan funcionando sin cambios. Cada proveedor deriva sus propias dimensiones de vector del modelo configurado: no es necesario establecer dimensiones manualmente a menos que quieras anularlas.

VariablePredeterminadoSe aplica aDescripción
JINA_URLhttp://localhost:8087jinaURL base de TEI
JINA_MODELjinaai/jina-embeddings-v2-base-codejinaSolo informativo: la bandera --model-id del contenedor TEI es lo que realmente carga. Edita docker-compose.yaml para cambiar modelos.
JINA_DIMENSIONS768jinaDimensiones del vector del modelo TEI
JINA_API_KEY(obligatorio si provider=jina-api)jina-apiClave de API de Jina AI (endpoint alojado en api.jina.ai)
JINA_API_MODELjina-embeddings-v2-base-codejina-apiModelo Jina alojado: también admite jina-code-embeddings-0.5b, jina-code-embeddings-1.5b
JINA_API_DIMENSIONS(nativo)jina-apiAnulación opcional de Matryoshka (los modelos de embeddings de código admiten reducción); obligatorio para modelos sin un valor nativo predeterminado
VOYAGE_API_KEY(obligatorio si provider=voyage)voyageClave de API de Voyage AI
VOYAGE_MODELvoyage-code-3voyageModelo de embedding de Voyage
VOYAGE_DIMENSIONS(nativo)voyageAnulación opcional: Voyage code-3 admite 256 / 512 / 1024 / 2048
OPENAI_API_KEY(obligatorio si provider=openai)openaiClave de API de OpenAI
OPENAI_EMBEDDING_MODELtext-embedding-3-largeopenaiModelo de embedding de OpenAI
OPENAI_DIMENSIONS(nativo)openaiAnulación opcional (los modelos text-embedding-3-* admiten reducción)
OLLAMA_URLhttp://localhost:11434ollamaURL base de Ollama
OLLAMA_MODELnomic-embed-textollamaModelo de embedding de Ollama
OLLAMA_DIMENSIONS(nativo)ollamaObligatorio si se usa un modelo que no está en la tabla de dimensiones integrada

voyage-code-3 supera a jinaai/jina-embeddings-v2-base-code en la mayoría de los puntos de referencia de recuperación de código, por lo que cambiar a Voyage también es una palanca de calidad, no solo de flexibilidad. Cambio de proveedores sobre un índice existente: si el tamaño de los vectores del nuevo proveedor difiere de la colección Qdrant existente, el servidor falla rápidamente al inicio con un error claro que señala la colección problemática. Para cambiar, elimina ambas colecciones (code_symbols y git_commits) mediante la interfaz o API de Qdrant, y luego reindexa. No hay migración automática.

Configuración solo alojada (sin contenedor TEI local): establece EMBEDDINGS_PROVIDER y la clave de API correspondiente en .env, luego inicia sin el perfil jina (docker-compose up / make docker-up). El contenedor jina-embeddings no se iniciará.

Colecciones Qdrant

code_symbols — un punto por símbolo analizado, que lleva dos vectores nombrados:

  • text-dense — distancia coseno, HNSW (m=16, ef_construct=128), tamaño determinado por el proveedor de incrustaciones
  • text-sparse — BM25 sobre tokens de subpalabras de identificadores de código, índice disperso en memoria

search_code consulta ambos mediante una llamada Qdrant query_points con FusionQuery(fusion=RRF). Los campos de payload indexados (language, service, symbol_type, chunk_tier, parent_name, file_path) son utilizables como filtros. search_code y find_symbol exponen chunk_tier ("method" o "class") directamente, por lo que una consulta puede limitarse solo a clases o solo a métodos. El payload completo también incluye signature, docstring, annotations, package, start_line, end_line, file_hash, indexed_at, y extras específicos del lenguaje (http_method, http_route, spring_stereotype, lombok_annotations, is_async, uses_memo, …).

git_commits — un vector solo denso por commit (coseno, HNSW m=16 / ef_construct=128). El payload incluye sha, service, message, author_name, author_email, committed_at, indexed_at, has_diff, diff_truncated, y files (matriz de archivos modificados con filename, status, additions, deletions, patch). sha, service, author_name, y has_diff son campos de payload indexados.

Versionado y lanzamientos

La versión vive en un solo lugar: version en pyproject.toml. server/main.py la lee a través de importlib.metadata.version("semcode") y la reporta como el serverInfo.version de MCP, por lo que un cliente siempre ve la versión del paquete que realmente está ejecutando.

Nunca edites esa versión manualmente. Los lanzamientos están automatizados con release-please (.github/workflows/release.yml):

  1. Los commits llegan a main usando Conventional Commits.
  2. release-please abre (o actualiza) un PR de chore(main): release X.Y.Z que incrementa pyproject.toml y escribe CHANGELOG.md a partir de esos mensajes de commit.
  3. Fusionar ese PR etiqueta el commit como vX.Y.Z y crea un Release de GitHub. Nada se lanza hasta que un humano lo fusiona.

Un lanzamiento actualmente produce un Release de GitHub con el código fuente y el changelog generado. Nada se publica en PyPI, y no se envía ninguna imagen de contenedor — semcode todavía se construye localmente desde el Dockerfile.

Tipos de commit e incrementos de versión

Prefijo de commitIncrementoSección del changelog
feat!: / BREAKING CHANGE:major⚠ Cambios de ruptura
feat:minorCaracterísticas
fix:patchCorrecciones de errores
perf:patchMejoras de rendimiento
deps:patchDependencias
refactor:patchRefactorización de código
docs: test: style: chore: ci: build:patchoculto

Dependabot está configurado (.github/dependabot.yml) para usar el prefijo deps, por lo que una semana de actualizaciones de dependencias aún produce un lanzamiento de parche que marca qué conjunto de dependencias se probó junto.

Major está reservado para cambios de ruptura en las firmas de herramientas MCP o el esquema de configuración — las superficies de las que dependen los clientes y las implementaciones.

Notas

  • Debido a que los mensajes de commit en main impulsan todo, los títulos de squash-merge deben ser Conventional Commits bien formados. El repositorio debería tener "Default to PR title for squash merges" habilitado.
  • El PR de lanzamiento se crea con GITHUB_TOKEN, que por diseño no activa otros flujos de trabajo — CI no se vuelve a ejecutar en el propio PR de lanzamiento. Los commits que lanza ya fueron probados en main.

Estructura del proyecto

server/
├── main.py          # MCP server entry point + lifespan
├── config.py        # Settings and service configuration
├── state.py         # Shared store singletons
├── parser/          # Tree-sitter parsers (Go, Java, Python, TypeScript, Rust, C#, C, C++, Ruby, PHP, Kotlin, Scala, Swift, Dart, Bash, SQL, Lua, R, Dockerfile, Compose, Markdown, JSON, HTML, CSS, XML)
├── embeddings/      # Pluggable dense providers (Jina/Voyage/OpenAI/Ollama) + BM25 sparse + code identifier tokenizer
├── indexer/         # GitHub fetcher, code indexing pipeline, git history pipeline
├── store/           # Qdrant vector stores (code_symbols hybrid + git_commits dense)
├── tools/           # MCP tool implementations (search, index, history, admin)
├── prompts/         # MCP prompt templates (service_overview, system_design_overview)
└── routes/          # HTTP routes (reindex, reindex-history)