semcode
Sobre o MCP de busca semântica de código (semcode). Indexa símbolos de código e histórico de commits. Combina embeddings densos com vetores esparsos BM25 para busca híbrida que equilibra compreensão semântica com precisão de palavras-chave.
Documentação
semcode
Um servidor MCP (Model Context Protocol) que fornece busca semântica híbrida sobre código em um conjunto de
repositórios GitHub que você lista em config.yaml. Ele analisa símbolos
com Tree-sitter e indexa tanto o código quanto o histórico de commits do git, para que clientes de IA possam consultá-los por
linguagem natural ou por nome de símbolo.
A recuperação híbrida combina embeddings densos com BM25, de modo que tanto consultas em linguagem natural
("onde publicamos eventos de pedido?") quanto buscas por nome de símbolo (PlaceOrderRequest) funcionam bem.
Submetido em:
Como funciona
- Busca arquivos de código-fonte nos repositórios GitHub configurados
- Analisa símbolos de código (funções, classes, métodos, componentes) usando Tree-sitter
- Gera dois embeddings por símbolo — um vetor semântico denso (provedor plugável: Jina Code V2 por padrão, ou Voyage / OpenAI / Ollama) e um vetor esparso BM25 baseado em tokens de identificadores de código (camelCase / snake_case divididos em subpalavras)
- Armazena ambos no Qdrant e os recupera com busca híbrida — Fusão de Classificação Recíproca (RRF) sobre os resultados densos e esparsos — para que consultas em linguagem natural e buscas por nome de símbolo funcionem bem
- Opcionalmente, indexa o histórico de commits em uma coleção separada do Qdrant (somente denso)
- Expõe ferramentas de busca e indexação através do protocolo MCP (e uma pequena API HTTP)
A indexação é incremental — arquivos são ignorados quando o SHA do blob Git corresponde à última versão indexada.
Arquivos que não existem mais (ou que são analisados para zero símbolos) são limpos automaticamente. Passe force: true
para re-embedar tudo.
Quer entrar em mais detalhes? Confira o blog!
Documentação
Documentação aprofundada dos internals do sistema RAG vive em docs/:
- Pipeline de ingestão — como o código é descoberto, analisado, embebido e armazenado
- Vetores densos — provedores de embedding e estratégia de texto
- Vetores esparsos — BM25 e o tokenizador de código
- Recuperação com RRF — busca híbrida e ferramentas MCP
- Configuração — todas as variáveis de ambiente e config.yaml
Linguagens suportadas
A linguagem é detectada automaticamente pela extensão do arquivo ou nome — nenhuma configuração necessária.
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.
A maioria dos parsers é ciente de frameworks onde importa — estereótipos Spring e rotas HTTP para Java/Kotlin, FastAPI/Pydantic
para Python, ASP.NET para C#, Rails para Ruby, Laravel/Symfony para PHP, widgets React/SwiftUI/Flutter, etc. Veja
server/parser/ para os detalhes de extração por linguagem.
Configuração
Pré-requisitos: Python 3.12+, Docker, token do 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
Configure quais repositórios indexar em 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/**"
O indexador descobre e indexa automaticamente todos os arquivos com extensões reconhecidas. Use root para escopar um serviço a um
subdiretório dentro de um repositório compartilhado, e exclude para pular caminhos que você não quer indexar (testes, artefatos de build, código
gerado, etc.).
Escalando além de um punhado de repositórios: config.yaml é uma lista estática e curada — ótima para um pequeno número de serviços,
mas indexar centenas de repositórios dessa forma significa centenas de entradas mantidas manualmente. Como alternativa (ou complemento),
POST /reindex aceita uma definição de repositório inline e o registra em tempo real, sem necessidade de entrada em config.yaml — veja
examples/github-actions/reindex-on-merge.yml para um fluxo de trabalho plug-and-play
que auto-registra um repositório e o reindexa a cada merge. Detalhes na seção API HTTP abaixo. Se um
nome colidir entre os dois, a entrada em config.yaml sempre vence.
Você não precisa de um config.yaml para rodar dessa forma — um arquivo ausente é tratado como zero serviços configurados, não um
erro. docker-compose.yaml reflete isso: por padrão ele não monta config.yaml, então make docker-up /
make docker-up-jina funcionam de imediato para configurações somente com registro ad-hoc. Se você também quiser serviços curados,
copie config.example.yaml para config.yaml (acima) e use os alvos -with-config em vez disso, que adicionam
docker-compose.config-yaml.yml por cima para incluir a montagem: make docker-up-with-config / make docker-up-jina-with-config
(ou docker compose -f docker-compose.yaml -f docker-compose.config-yaml.yml up -d diretamente). Não edite manualmente a
linha de volume em docker-compose.yaml — montar por bind um config.yaml que não existe no host silenciosamente
cria um diretório vazio lá em vez de deixar o caminho ausente, o que quebra o servidor (exibido como um erro
claro se acontecer: CONFIG_PATH (...) is a directory, not a file).
Um único GITHUB_TOKEN lê cada repositório que você indexa dessa forma. Para um punhado de entradas config.yaml, um
PAT de escopo fino limitado a esses repositórios é suficiente, mas para auto-registro em toda a organização — onde qualquer repositório pode se registrar
sozinho apenas adicionando o workflow — um PAT precisaria ter sua lista de acesso a repositórios atualizada fora de banda toda vez que um novo repositório
começasse a usá-lo. Um GitHub App instalado em toda a organização (todos os repositórios, Contents: read) evita isso: novos repositórios são cobertos
automaticamente, sem manutenção de token por onboarding.
Executando
Há duas maneiras de executar, dependendo se você quer que os embeddings venham de um contêiner local ou de um provedor hospedado. Escolha uma:
Caminho A — Jina local via TEI (padrão, sem necessidade de chave de API):
make docker-up-jina
# or: docker-compose --profile jina up
Caminho B — provedor hospedado (Voyage / OpenAI) ou 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 serviços curados? Use a variante -with-config do alvo acima que se aplica
(make docker-up-with-config / make docker-up-jina-with-config) — veja a seção Configuração.
⚠ O
EMBEDDINGS_PROVIDERpadrão éjina. Se você iniciar sem--profile jinamas deixar o provedor no padrão, o semcode inicializará (Jina érequired: falseno compose) mas a primeira chamada de embedding falhará com um erro de conexão — não há fallback automático.
Serviços iniciados com health checks e volumes persistentes:
| Serviço | Perfil | Porta | Volume | Propósito |
|---|---|---|---|---|
| Qdrant | sempre | 6333 (HTTP), 6334 (gRPC) | qdrant_data | Banco de vetores |
| Jina Embeddings (TEI) | jina | 8087 | embeddings_cache | Servidor de modelo de embedding |
| semcode MCP | sempre | 8090 | monta ./config.yaml somente leitura com -with-config | Servidor MCP + HTTP |
O servidor MCP inicia com coleções vazias — dispare uma indexação inicial chamando a ferramenta MCP reindex
ou POST /reindex (veja abaixo).
Conectando clientes de IA
Uma vez que o servidor esteja rodando, aponte seu cliente de IA para http://localhost:8090/mcp.
Claude Code (CLI)
claude mcp add --transport http semcode http://localhost:8090/mcp
Outros clientes MCP (Claude Desktop, Cursor, etc.) — adicione uma entrada ao config MCP do cliente:
{
"mcpServers": {
"semcode": {
"transport": "http",
"url": "http://localhost:8090/mcp"
}
}
}
Conectando via stdio
Em vez de apontar para um servidor HTTP em execução, o cliente pode iniciar o processo do servidor ele mesmo e
falar com ele via stdin/stdout. Isso ainda precisa que o Qdrant esteja acessível (ex.: docker-compose up qdrant)
e um ambiente Python local com dependências 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
Outros clientes MCP — adicione uma entrada ao config MCP do cliente:
{
"mcpServers": {
"semcode": {
"command": "uv",
"args": ["run", "--directory", "/path/to/semcode", "python", "-m", "server.main"],
"env": {
"MCP_TRANSPORT": "stdio"
}
}
}
}
GITHUB_TOKEN, QDRANT_URL, e variáveis do provedor de embedding ainda são lidas de .env no
diretório do projeto — uv run as pega automaticamente.
Conectando via SSE
SSE é o transporte HTTP MCP legado, substituído por
streamable-http. Use apenas para clientes que ainda não suportamstreamable-http— novas configurações devem usar a configuraçãostreamable-httpacima.
Defina MCP_TRANSPORT=sse em .env (ou no ambiente) e inicie o servidor da mesma forma que
streamable-http (make docker-up / make docker-up-jina, ou uv run python -m server.main
localmente). O servidor expõe um endpoint SSE em http://localhost:8090/sse.
Claude Code (CLI)
claude mcp add --transport sse semcode http://localhost:8090/sse
Outros clientes MCP — adicione uma entrada ao config MCP do cliente:
{
"mcpServers": {
"semcode": {
"transport": "sse",
"url": "http://localhost:8090/sse"
}
}
}
Indexação
O pipeline de indexação é orientado a símbolos: cada função, classe, método ou componente se torna seu próprio chunk com um embedding de vetor e um payload rico.
- Descoberta — lista todos os arquivos no repositório em
github_ref, aplicando filtrosrooteexclude - Detecção de mudanças — compara o SHA do blob Git do arquivo com o último valor indexado; arquivos inalterados são ignorados
- Análise — Tree-sitter percorre a AST e emite objetos
CodeSymbolpor linguagem - Texto de embedding denso — rótulo de linguagem, tipo de símbolo, classe pai, pacote, extras de framework (estereótipo Spring,
rota HTTP, Lombok, React memo), docstring, assinatura e fonte (fonte truncada em
EMBEDDING_MAX_CHARS, um padrão ciente do provedor — veja docs/configuration.md) - Texto de embedding esparso (BM25) — assinatura, docstring e fonte. Identificadores de código são divididos em subpalavras (
camelCase, snake_case) antes da tokenização, então
getUserByIdindexa comoget,user,by,idalém do token completo - Loteamento — provedor denso em lotes de 32 (Jina/TEI, Ollama) ou 128 (Voyage, OpenAI); BM25 roda em processo
- Upsert — ambos os vetores armazenados sob um único ponto no Qdrant, chaveado por um UUID determinístico (por serviço / arquivo / símbolo / linha)
- Limpeza — entradas para arquivos que não estão mais no repositório (ou que agora analisam para zero símbolos) são deletadas
A indexação do histórico do Git é um pipeline separado e opcional que embebe mensagens de commit e caminhos de arquivos
alterados na coleção git_commits. Diffs unificados completos são armazenados no payload e recuperáveis
via a ferramenta get_commit. O número de commits por serviço é limitado por GIT_HISTORY_MAX_COMMITS
(padrão 500).
Testes
uv sync --group dev
uv run pytest
Testes vivem sob tests/:
tests/parser/test_*.py— um arquivo por linguagem; captura o comportamento do parser contra fixtures canônicas emtests/fixtures/<language>/tests/test_pipeline.py,tests/test_store.py,tests/test_git_history.py— testes de integração para o pipeline de indexação e o armazenamento Qdranttests/test_reindex_route.py— testes de rotas HTTP
Ferramentas MCP
| Ferramenta | Descrição |
|---|---|
search_code | Busca híbrida (densa + BM25) por consulta, com filtros opcionais para idioma, serviço e tipo de símbolo |
find_symbol | Consulta um símbolo pelo nome — correspondência exata ou correspondência de token sem diferenciar maiúsculas/minúsculas quando exact=false |
find_usages | Encontra código que referencia um nome de símbolo específico (busca semântica, depois exclui a própria definição) |
get_code_context | Busca o código-fonte completo de um arquivo — ou um símbolo específico dentro dele — diretamente do GitHub |
reindex | Dispara a indexação de código de um ou de todos os serviços (incremental por padrão; force para re-incorporar) |
index_history | Indexa o histórico de commits do git; busca automaticamente os diffs de commits que não os possuem |
search_commits | Busca no histórico de commits do git com linguagem natural |
get_commit | Obtém detalhes completos de um commit específico, incluindo arquivos alterados e diffs |
list_indexed_services | Lista serviços indexados com contagens de chunks e arquivos, idiomas e horário da última indexação |
index_stats | Mostra estatísticas da coleção Qdrant e serviços configurados |
find_symbol(exact=false) corresponde a um índice de texto completo sobre os tokens camelCase/snake_case do nome do símbolo, portanto
order ou ord encontra placeOrderRequest em ~2 ms, independentemente do tamanho da coleção. Fragmentos no meio do token (rder) ainda
correspondem, mas recorrem a uma varredura no lado do cliente que é linear em relação ao tamanho da coleção. Coleções indexadas antes deste campo
existir usam esse mesmo fallback até serem reindexadas — e como a detecção de alterações ignora arquivos inalterados, popular o
campo requer uma reindexação forçada (POST /reindex {"force": true}), que re-incorpora cada símbolo. Consulte
docs/retrieval-rrf.md.
Prompts MCP
| Prompt | Argumentos | Descrição |
|---|---|---|
service_overview | service | Orienta o cliente na produção de uma visão geral arquitetural de um serviço: pontos de entrada HTTP, tipos de domínio e convenções notáveis de framework |
system_design_overview | (nenhum) | Orienta o cliente na produção de uma visão geral completa do design do sistema: inventário de serviços, topologia de comunicação, armazenamentos de dados compartilhados e preocupações transversais — inclui diagramas Mermaid |
API HTTP
Além das ferramentas MCP, o servidor expõe dois endpoints HTTP para disparar a indexação a partir de CI/CD ou agendadores externos:
| Endpoint | Corpo | Descrição |
|---|---|---|
POST /reindex | {"service": "<name>"?, "force": <bool>?, "github_repo": "<owner/repo>"?, "github_ref": "<ref>"?, "root": "<path>"?, "exclude": [<glob>, ...]?} | Reindexa um ou todos os serviços — retorna NDJSON |
POST /reindex-history | {"service": "<name>"?, "force": <bool>?} | Indexa o histórico de commits do git — retorna NDJSON |
Todos os corpos são opcionais — omita service para agir em todos os serviços, omita force para indexação incremental.
Ambos os endpoints transmitem JSON delimitado por nova linha (um quadro por linha) para que você possa consumir o progresso
em tempo real a partir de pipelines de CI/CD ou qualquer outro cliente.
Registrando um repositório sem config.yaml: se o corpo de POST /reindex incluir github_repo, o
nome service é registrado com essa definição de repositório (persistido, portanto sobrevive a reinicializações e se comporta
como um serviço config.yaml a partir de então) antes de a indexação ser executada — service é obrigatório neste caso.
github_ref tem como padrão main; root/exclude espelham os mesmos campos em config.yaml. Um nome service
já definido em config.yaml sempre vence um registrado dessa forma. Não há
autenticação neste endpoint — igual ao restante de /reindex — portanto coloque-o atrás do seu próprio limite de rede
antes de expô-lo. Consulte
examples/github-actions/reindex-on-merge.yml para um
fluxo de trabalho pronto para uso.
Formatos de quadro:
// 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-history, o valor de phase é discovery|embedding|upserting e o resultado de done é
{"new": int, "skipped": int, "diff_updated": int}.
Variáveis de ambiente
| Variável | Padrão | Descrição |
|---|---|---|
GITHUB_TOKEN | (obrigatório) | Token do GitHub com acesso de leitura ao repositório |
QDRANT_URL | http://localhost:6333 | URL de conexão do Qdrant |
QDRANT_COLLECTION | code_symbols | Nome da coleção para vetores de símbolos de código |
QDRANT_COMMITS_COLLECTION | git_commits | Nome da coleção para vetores de mensagens de commit |
EMBEDDINGS_PROVIDER | jina | Um de jina, jina-api, voyage, openai, ollama — consulte Provedores de incorporação abaixo |
GIT_HISTORY_MAX_COMMITS | 500 | Máximo de commits indexados por serviço |
CODE_CONTEXT_CACHE_SIZE | 128 | Arquivos armazenados em cache na memória para get_code_context (chaveados pelo SHA do blob); 0 desativa |
CODE_CONTEXT_CACHE_TTL | 900 | Segundos durante os quais o conteúdo de um arquivo em cache permanece válido |
MCP_TRANSPORT | streamable-http | Um de streamable-http, sse, stdio |
MCP_HOST / MCP_PORT | 127.0.0.1 / 8090 | Endereço de vinculação do servidor |
CONFIG_PATH | ./config.yaml | Caminho para o arquivo de configuração dos serviços |
Provedores de incorporação
O backend de incorporação é selecionável via EMBEDDINGS_PROVIDER. O padrão é jina para que as implantações
existentes continuem funcionando sem alterações. Cada provedor deriva suas próprias dimensões de vetor do
modelo configurado — não é necessário definir dimensões manualmente, a menos que você queira substituí-las.
| Variável | Padrão | Aplica-se a | Descrição |
|---|---|---|---|
JINA_URL | http://localhost:8087 | jina | URL base do TEI |
JINA_MODEL | jinaai/jina-embeddings-v2-base-code | jina | Apenas informativo — a flag --model-id do contêiner TEI é o que realmente carrega. Edite docker-compose.yaml para alterar modelos. |
JINA_DIMENSIONS | 768 | jina | Dimensões de vetor do modelo TEI |
JINA_API_KEY | (obrigatório se provider=jina-api) | jina-api | Chave da API Jina AI (endpoint hospedado em api.jina.ai) |
JINA_API_MODEL | jina-embeddings-v2-base-code | jina-api | Modelo Jina hospedado — também suporta jina-code-embeddings-0.5b, jina-code-embeddings-1.5b |
JINA_API_DIMENSIONS | (nativo) | jina-api | Substituição opcional de Matryoshka (modelos de incorporação de código suportam redução); obrigatório para modelos sem padrão nativo |
VOYAGE_API_KEY | (obrigatório se provider=voyage) | voyage | Chave da API Voyage AI |
VOYAGE_MODEL | voyage-code-3 | voyage | Modelo de incorporação Voyage |
VOYAGE_DIMENSIONS | (nativo) | voyage | Substituição opcional — Voyage code-3 suporta 256 / 512 / 1024 / 2048 |
OPENAI_API_KEY | (obrigatório se provider=openai) | openai | Chave da API OpenAI |
OPENAI_EMBEDDING_MODEL | text-embedding-3-large | openai | Modelo de incorporação OpenAI |
OPENAI_DIMENSIONS | (nativo) | openai | Substituição opcional (modelos text-embedding-3-* suportam redução) |
OLLAMA_URL | http://localhost:11434 | ollama | URL base do Ollama |
OLLAMA_MODEL | nomic-embed-text | ollama | Modelo de incorporação Ollama |
OLLAMA_DIMENSIONS | (nativo) | ollama | Obrigatório se usar um modelo que não está na tabela de dimensões integrada |
voyage-code-3 supera jinaai/jina-embeddings-v2-base-code na maioria dos benchmarks de recuperação de código,
portanto, mudar para Voyage também é uma alavanca de qualidade, não apenas de flexibilidade.
Mudando de provedor com um índice existente: se o tamanho do vetor do novo provedor diferir da
coleção Qdrant existente, o servidor falha rapidamente na inicialização com um erro claro apontando para a
coleção problemática. Para mudar, descarte ambas as coleções (code_symbols e git_commits) via
interface ou API do Qdrant e depois reindexe. Não há migração automática.
Configuração somente hospedada (sem contêiner TEI local): defina EMBEDDINGS_PROVIDER e a chave de API relevante
em .env, depois inicie sem o perfil jina (docker-compose up / make docker-up). O
contêiner jina-embeddings não será iniciado.
Coleções Qdrant
code_symbols — um ponto por símbolo analisado, carregando dois vetores nomeados:
text-dense— distância de cosseno, HNSW (m=16,ef_construct=128), tamanho determinado pelo provedor de embeddingstext-sparse— BM25 sobre tokens de subpalavras de identificadores de código, índice esparso em memória
search_code consulta ambos via uma chamada query_points do Qdrant com FusionQuery(fusion=RRF). Campos de
payload indexados (language, service, symbol_type, chunk_tier, parent_name, file_path) podem
ser usados como filtros. search_code e find_symbol expõem chunk_tier ("method" ou "class")
diretamente, então uma consulta pode ser limitada apenas a classes ou apenas a métodos. O payload completo também inclui
signature, docstring, annotations, package,
start_line, end_line, file_hash, indexed_at, e extras específicos da linguagem (http_method,
http_route, spring_stereotype, lombok_annotations, is_async, uses_memo, …).
git_commits — um vetor somente denso por commit (cosseno, HNSW m=16 / ef_construct=128).
O payload inclui sha, service, message, author_name, author_email, committed_at,
indexed_at, has_diff, diff_truncated, e files (array de arquivos alterados com filename,
status, additions, deletions, patch). sha, service, author_name, e has_diff são
campos de payload indexados.
Estrutura do projeto
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)