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

MCP Server Python 3.12+ License MIT CI

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:

mcpservers.org

mcpmarket.com

mcp.so

Como funciona

  1. Busca arquivos de código-fonte nos repositórios GitHub configurados
  2. Analisa símbolos de código (funções, classes, métodos, componentes) usando Tree-sitter
  3. 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)
  4. 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
  5. Opcionalmente, indexa o histórico de commits em uma coleção separada do Qdrant (somente denso)
  6. 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/:

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_PROVIDER padrão é jina. Se você iniciar sem --profile jina mas deixar o provedor no padrão, o semcode inicializará (Jina é required: false no 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çoPerfilPortaVolumePropósito
Qdrantsempre6333 (HTTP), 6334 (gRPC)qdrant_dataBanco de vetores
Jina Embeddings (TEI)jina8087embeddings_cacheServidor de modelo de embedding
semcode MCPsempre8090monta ./config.yaml somente leitura com -with-configServidor 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 suportam streamable-http — novas configurações devem usar a configuração streamable-http acima.

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 filtros root e exclude
  • 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 CodeSymbol por 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 getUserById indexa como get, user, by, id alé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 em tests/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 Qdrant
  • tests/test_reindex_route.py — testes de rotas HTTP

Ferramentas MCP

FerramentaDescrição
search_codeBusca híbrida (densa + BM25) por consulta, com filtros opcionais para idioma, serviço e tipo de símbolo
find_symbolConsulta um símbolo pelo nome — correspondência exata ou correspondência de token sem diferenciar maiúsculas/minúsculas quando exact=false
find_usagesEncontra código que referencia um nome de símbolo específico (busca semântica, depois exclui a própria definição)
get_code_contextBusca o código-fonte completo de um arquivo — ou um símbolo específico dentro dele — diretamente do GitHub
reindexDispara a indexação de código de um ou de todos os serviços (incremental por padrão; force para re-incorporar)
index_historyIndexa o histórico de commits do git; busca automaticamente os diffs de commits que não os possuem
search_commitsBusca no histórico de commits do git com linguagem natural
get_commitObtém detalhes completos de um commit específico, incluindo arquivos alterados e diffs
list_indexed_servicesLista serviços indexados com contagens de chunks e arquivos, idiomas e horário da última indexação
index_statsMostra 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

PromptArgumentosDescrição
service_overviewserviceOrienta 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:

EndpointCorpoDescriçã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ávelPadrãoDescrição
GITHUB_TOKEN(obrigatório)Token do GitHub com acesso de leitura ao repositório
QDRANT_URLhttp://localhost:6333URL de conexão do Qdrant
QDRANT_COLLECTIONcode_symbolsNome da coleção para vetores de símbolos de código
QDRANT_COMMITS_COLLECTIONgit_commitsNome da coleção para vetores de mensagens de commit
EMBEDDINGS_PROVIDERjinaUm de jina, jina-api, voyage, openai, ollama — consulte Provedores de incorporação abaixo
GIT_HISTORY_MAX_COMMITS500Máximo de commits indexados por serviço
CODE_CONTEXT_CACHE_SIZE128Arquivos armazenados em cache na memória para get_code_context (chaveados pelo SHA do blob); 0 desativa
CODE_CONTEXT_CACHE_TTL900Segundos durante os quais o conteúdo de um arquivo em cache permanece válido
MCP_TRANSPORTstreamable-httpUm de streamable-http, sse, stdio
MCP_HOST / MCP_PORT127.0.0.1 / 8090Endereço de vinculação do servidor
CONFIG_PATH./config.yamlCaminho 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ávelPadrãoAplica-se aDescrição
JINA_URLhttp://localhost:8087jinaURL base do TEI
JINA_MODELjinaai/jina-embeddings-v2-base-codejinaApenas informativo — a flag --model-id do contêiner TEI é o que realmente carrega. Edite docker-compose.yaml para alterar modelos.
JINA_DIMENSIONS768jinaDimensões de vetor do modelo TEI
JINA_API_KEY(obrigatório se provider=jina-api)jina-apiChave da API Jina AI (endpoint hospedado em api.jina.ai)
JINA_API_MODELjina-embeddings-v2-base-codejina-apiModelo Jina hospedado — também suporta jina-code-embeddings-0.5b, jina-code-embeddings-1.5b
JINA_API_DIMENSIONS(nativo)jina-apiSubstituiçã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)voyageChave da API Voyage AI
VOYAGE_MODELvoyage-code-3voyageModelo de incorporação Voyage
VOYAGE_DIMENSIONS(nativo)voyageSubstituição opcional — Voyage code-3 suporta 256 / 512 / 1024 / 2048
OPENAI_API_KEY(obrigatório se provider=openai)openaiChave da API OpenAI
OPENAI_EMBEDDING_MODELtext-embedding-3-largeopenaiModelo de incorporação OpenAI
OPENAI_DIMENSIONS(nativo)openaiSubstituição opcional (modelos text-embedding-3-* suportam redução)
OLLAMA_URLhttp://localhost:11434ollamaURL base do Ollama
OLLAMA_MODELnomic-embed-textollamaModelo de incorporação Ollama
OLLAMA_DIMENSIONS(nativo)ollamaObrigató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 embeddings
  • text-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)