codeindex MCP server
2026년 11월 14일 커밋심볼과 import를 추출하고 코드 인텔리전스를 위한 타입 기반 파일 간 링크 그래프를 구축하는 의존성 없는 리포지토리 인덱싱 엔진입니다.
다른 이름 maxgfr/codeindex
- 스타
- 1
- Tools
- —
- 다운로드, 30일
- —
- 최신 버전
- —
- 마지막 커밋
- 2026년 11월 14일
- 라이선스
- MIT
codeindex MCP란?
codeindex은(는) MCP server입니다. 심볼과 import를 추출하고 코드 인텔리전스를 위한 타입 기반 파일 간 링크 그래프를 구축하는 의존성 없는 리포지토리 인덱싱 엔진입니다.
프로젝트 README에서 생성된 요약
codeindex MCP server 정보
Agent Skills, CLI 또는 CI 게이트를 구축하는 개발자는 이 엔진을 사용하여 심층적인 리포지토리 분석을 수행할 수 있습니다. 이 엔진은 전통적인 패키지 의존성으로 설치하는 대신, 사용자가 단일 engine.mjs 파일로 프로젝트에 직접 포함하여 사용할 수 있는 독립형 의존성 없는 인덱싱 도구로 작동합니다. 엔진은 다양한 프로그래밍 언어에 대해 tree-sitter AST 또는 regex 폴백을 사용하여 결정론적 파일 워킹, 언어 감지 및 심볼 추출을 수행합니다.
코드베이스를 분석하여 import, 호출 및 상속을 추적하는 타입 기반 파일 간 링크 그래프를 구축합니다. TypeScript, Python, Go, Rust, PHP와 같은 다양한 생태계에 걸쳐 복잡한 import 경로를 해결합니다. 시스템은 byte-stable한 graph.json 및 symbols.json 파일과 SCIP 코드 인텔리전스 인덱스를 생성할 수 있습니다. 병렬 스캔을 위한 worker thread를 지원하고 성능 향상을 위한 증분 캐시 패스트패스를 제공하여 대규모 리포지토리를 처리할 수 있도록 설계되었습니다.
활용 사례
- 1커스텀 AI agent skills 구축AI agent에 코드베이스의 심볼과 그 관계에 대한 완전한 지도를 제공합니다.
- 2복잡한 타입 계층 구조 매핑대규모 프로젝트에서 서로 다른 타입이 인터페이스를 어떻게 확장하거나 구현하는지 식별합니다.
- 3코드 연결성 및 중요도 분석그래프 분석을 사용하여 리포지토리 내의 핵심 모듈이나 예상치 못한 연결을 찾습니다.
프로젝트의 README를 바탕으로 작성되었습니다. 모든 활용 사례는 해당 내용의 인용문과 대조하여 확인합니다.
구성
CODEINDEX_EMBED_ENDPOINT
| 이름 | 설명 | 유형 |
|---|---|---|
| CODEINDEX_EMBED_ENDPOINT | — |
자동 검사
2026년 11월 15일에 실행됨전체 보고서 →아직 인증되지 않았습니다. 서버는 80점 이상의 점수, 심각한 결함 없음, 그리고 검사에 충분한 데이터를 갖추어야 합니다. 검사 방식 · 점수를 높이기 위해 수정할 항목
- 소스 저장소 연결 및 접근 가능maxgfr/codeindex is public and reachablegithub.com/maxgfr/codeindex ↗통과
- 패키지가 이 저장소를 소스로 명시검사되지 않음
- 빌드 출처(Sigstore 증명)검사되지 않음
- 오픈 소스 라이선스 (OSI)MITspdx.org/licenses/MIT.html ↗통과
- 최근 1년 내 커밋 또는 릴리스Last commit or release 2026-11-14github.com/maxgfr/codeindex ↗통과
- OpenSSF Scorecard검사되지 않음
- 패키지 이름이 인기 패키지와 유사하지 않음검사되지 않음
- 패키지 및 해당 종속성에 중요한 보안 권고 없음검사되지 않음
- 모델에 대한 숨겨진 지침 없음0 tool descriptions and the README read; no override phrases, credential paths or hidden characters통과
- 파괴적인 도구에 라벨 표시됨검사되지 않음
- 원격 엔드포인트가 MCP 핸드셰이크에 응답검사되지 않음
README
GitHub에서 보기codeindex
Self-contained, deterministic repo-indexing engine: file walking, language
detection, symbol/import extraction (tree-sitter AST with a regex fallback),
import resolution, a typed cross-file link-graph, and graph analytics — shipped
as a single zero-dependency engine.mjs that consumer tools vendor (copy
into their repo) instead of installing.
Designed for downstream tools — agent skills, CLIs, CI gates — that vendor the engine as a single file instead of taking an npm dependency. How it stacks up against universal-ctags, Serena and Graphify: How it compares.
What it does
- Walk a repo deterministically: ignore lists,
.gitignoreand.git/info/exclude, binary/lockfile skips, a size cap, symlink-cycle guard. A symlink that stays inside the repo, file or directory, is an alias: its target is indexed once, under its own path.build,out,targetandtmpare skipped as build output unless git tracks files in them — they are ordinary package names too (a Gobuildpackage,com.acme.build). Nested repositories (a subdirectory with its own.git— linked worktrees, vendored clones, submodules) are skipped like git does, and.gititself — like the engine's own.codeindex— is never walked even when--ignore-dirreplaces the default ignore list. No file-count cap unless you ask for one (--max-files), and asking sets thecappedflag — never a silent truncation. - Scan every file into a
FileRecord: classification, language, symbols, imports, headings, hashes — with an incremental cache fastpath. Extraction runs across worker threads by default (--workers,CODEINDEX_WORKERS); artifacts are byte-identical either way, and anything that would make a worker's result differ falls back to the single-threaded path. JS/TS and Python imports are read from code only: comments, docstrings and string/template-literal text are masked first, so example code quoted in a JSDoc block, a docstring or a code generator's template never becomes an edge (JSDocimport("./x")types and@importtags, which are real type dependencies, are kept). The same scan runs with or without a grammar, soextractAstand the index report the same imports. Pythonfrom pkg import namealso linkspkg/name.pywhennameis a submodule (and nothing when it is a function or class); PHP group (use A\{B, C}) and commauselists and__DIR__-anchored includes are followed, and a traituseinside a class is not an import. Build output committed under an ordinary name is recognised by its content: minified JavaScript (not only a.min.jsname) and bundles (esbuild's// src/x.tsmodule banners, webpack's and ncc's module loader). It stays in the index with its summary and imports, flaggedgenerated: "minified"or"bundle"on itsFileRecordand graph node, but its symbols and call sites are not extracted: one-letter noise for the first, copies of the sources' definitions for the second. - Extract symbols via tree-sitter (15 committed grammars, plus 6 more via
grammars pull) or per-language regex rules (16 languages, always available). Each symbol carries its complete signature (parameters and return type, not the first physical line; one line, with no comment and no body — not an arrow's expression body, a Go interface's method list or a macro's expansion), its own doc comment, its qualifiedparent, and its line span — including the members a declaration-only walk misses: interface members, class fields, enum members, everydeclare/.d.tsdeclaration, Rust trait method signatures, Go interface method sets and type aliases, record components, constructorvalparameters and their TypeScript (private readonly dep: Dep) and PHP 8 (promoted) twins, every name of a multi-name declaration (var a, b int,int x, y;,a, b = 1, 2), C#definemacros and the members of atypedef struct, Python declarations underif TYPE_CHECKING:/try:/withblocks, Rubyclass << selfmethods,private def xdefinitions and the block ofPoint = Struct.new(…) do, Elixir clauses with awhenguard anddefguard, and the members of a class bound bymodule.exports =or an anonymousexport default class(a default export with no name of its own is named after the file stem). A doc comment is found across Rust attributes and TypeScript decorators. Visibility is read from a declaration's modifiers, never from its parameter names or default values; anexport { … }list marks only the bindings of its own scope; and a Python module's__all__(when written as literals) decides which of its top-level names are public, the names it imports and lists becomingreexportsymbols. An out-of-line C++ definition (void Widget::draw()) belongs to its class, and a Luafunction M.go()to its table; a.hheader is parsed as C++ when its content is (a namespace, class or template), as C otherwise. Vue, Svelte and Astro single-file components are extracted from their<script>blocks (and Astro's frontmatter) as the JS/TS theirlangnames, at their real lines: symbols, imports, and calls from both the script and the template, bound in the JS/TS call family. A Svelte prop (export let) and an Astro frontmatter export are not module exports, so they are never reported as dead code. The regex tier (the only one for Swift and Dart, and for the extended languages until a pull) reads code only: comments and strings are masked, so an example in a doc comment or a code generator's template is not a declaration. It takes each declaration's doc comment from the lines above it and, in brace languages, its line span, but only where the body's braces close as a formatter puts them; otherwise the span is left out rather than guessed. Its signature is the declaration's first line, and it reports noparent. Each file's summary is the first leading comment that describes something: license and copyright text (MIT, BSD, Apache, GPL, MPL, the Go "governed by" line), linter and editor magic comments (frozen_string_literal,-*- coding -*-,go:build), Xcode's file stamp and bundler region markers are skipped, and#reads as a comment only in languages where it is one — never a C#includeor a Rust#[attribute]. Docs get a title, section headings, a summary anddoc-linkrefs: markdown (ATX and setext headings, inline and reference links) and reStructuredText (Sphinx section titles;toctreeentries,:doc:roles andinclude/literalincludetargets as links). Other prose (.txt,.adoc) is indexed under its file name. - Resolve imports across languages: tsconfig
paths(tsc's precedence: exact alias, then longest prefix) andbaseUrl,extendschains into workspace packages and${configDir}, packageexportsandimports(#subpath), bundler?querysuffixes, Python import roots found the way mypy finds them (the dir holding each top-level package, so src layouts resolve and a package's owntyping.pydoes not shadow the stdlib), go.mod (a package's representative file is never a_test.gowhen it has other files), Cargo ([lib]names, renamed dependencies,#[path]modules, andusepaths read the way the crate's edition reads them), PSR-4, C# namespaces, and one JVM index for Java, Kotlin and Scala, so a Kotlin file importing a Java class (or the reverse, through the<File>Ktfacade) links; Scala selector groups and package-relative imports resolve too. Dart (relative andpackage:URIs of an in-repopubspec.yaml), Luarequire(a/b.lua,a/b/init.lua), shellsource/.of a literal path, and Elixiralias/import/useof a module the repo defines resolve as well. A markdown link starting with/is repo-root-relative, as GitHub renders it. - Build a typed link-graph:
import/call/extends/implements/use/doc-link/mentionedges at file and module level, plus Louvain communities, PageRank/betweenness centrality, a tests→code map (a test covers what it imports, uses or calls, the file it is named after, and in Go its whole package), and surprise-edge detection. Inheritance also yields a type hierarchy (what a type extends and implements, and what extends and implements IT) and a symbol-level graph for bounded "what does this reach" neighborhoods. Go states no implementations, so the hierarchy adds them: avar _ I = (*T)(nil)assertion, or a type whose methods (its own, plus the ones embedding promotes) match the interface's by name and parameter count. The second kind is markedstructural: true. These are answers tohierarchyandimplementationsonly, never graph.json edges. - Render byte-stable
graph.json/symbols.json(two builds of an unchanged repo are byte-identical), plus a SCIP code-intelligence index (index.scip: nested symbols, package identity, implementation relationships) via a hand-rolled zero-dependency protobuf encoder — validated by the officialscipCLI (stats/lint).
Measured against other indexers
"Finds better" is a claim, so the checks that count are the ones this project did not author. Four oracles score extraction against outside authorities — a real compiler, a mature indexer, and the grammars' own published queries and vocabulary:
| oracle | what makes it independent | result |
|---|---|---|
TypeScript compiler index (scip-typescript 0.4.0) |
an index built by the real TypeScript compiler — authoritative where every other check here is syntactic | 100% of its 93 named declarations, against ctags' 94.6% on the same files |
| universal-ctags differential (Universal Ctags 6.2.1) | an independent, mature indexer covering ~40 languages | reports 2,014 declarations ctags does not over 6 real repositories, and reproduces 61.7%–98.8% of ctags' names — what is left bucketed by kind, per repo below |
Official tags.scm queries |
the code-navigation patterns each grammar's own authors publish, and GitHub uses | 1 adjudicated difference, over the 14 of 17 languages that publish one |
| Grammar vocabulary | each tree-sitter grammar's own declared node types, read at runtime from the parser | 21 grammars audited, 209 declaration-ish node types still unhandled |
The one head-to-head
Exactly one figure on this page is a score: the one where both tools are
measured against the same third-party authority, rather than against each other.
On the 53 files of create-t3-turbo that an index built by the real TypeScript
compiler covers:
| against the compiler's 93 named declarations | found |
|---|---|
| codeindex | 100% |
| universal-ctags | 94.6% |
All 5 ctags missed are one construct (string-literal declaration names — module augmentations, quoted interface keys). That head-to-head is also the calibration for everything below: it is how far a syntactic oracle can be trusted on the ~40 languages no compiler here can check.
Every other percentage in this section is an overlap ratio between two tools that disagree about what counts as a declaration, which is a different thing and is not scored as one.
Per repository, against universal-ctags
Declaration names compared per file over real code, not fixtures. The
percentage here is not a score, which is why it is not in the last column. It
is |ours ∩ ctags| / |ctags| — the share of ctags' names this index also reports
— so by construction it can only ever show where we lose: nothing in it measures
what ctags omits. The two count columns are the directions that actually compare
the tools; read those.
| repo | files | both report | of ctags reproduced | ctags only | codeindex only |
|---|---|---|---|---|---|
| BurntSushi/ripgrep | 107 | 3,189 | 98.8% | 40 | 47 |
| gin-gonic/gin | 100 | 2,010 | 98.6% | 29 | 15 |
| pallets/flask | 86 | 1,516 | 93.9% | 98 | 6 |
| t3-oss/create-t3-turbo | 54 | 97 | 76.4% | 30 | 15 |
| nrwl/nx-examples | 87 | 87 | 69.6% | 38 | 27 |
| socialgouv/code-du-travail-numerique | 1,429 | 3,659 | 61.7% | 2,271 | 1,904 |
So no, the low rows are not "ctags finds more" — and that is measured, not
asserted. The differential records what the ctags only column is, bucketed
by the kind ctags itself assigned (ctagsOnlyByKind in the same record). On
code-du-travail, its 2,271 names are:
| ctags kind | count | what they are |
|---|---|---|
constant |
2,020 | all but a handful sit inside a function body, an object literal or a test block — read off the source, not assumed |
variable |
116 | same story |
property |
107 | object-literal keys (Conditions: ConditionsIcon) |
alias |
13 | import aliases — import type Engine from "publicodes" |
method / class / function / enumerator |
15 | object-literal methods and test-scope declarations |
That is a definition gap, not a hole: a declaration index omits locals and config
keys on purpose, which is the whole reason its output fits in a model's context.
The same holds on the other repos — ripgrep's 40 are mostly variable and Rust
implementation blocks, flask's include 21 that ctags itself labels
unknown (its kind for an import alias), gin's are its synthetic
anonMember/packageName.
And where a third tool can settle it, it does — create-t3-turbo is the 76.4%
row above, and it is also the head-to-head at the top of this section, the one
the real TypeScript compiler adjudicates 100% to 94.6% in our favour. A row
that looks like a loss against ctags is a row the authority scores as a win over
ctags. That is the whole reason the percentage is not in the score column.
And the residue is what the differential is genuinely for. Where it named real
misses they were fixed, not explained away: Go package clauses, Python PEP 484
re-exports, Rust in-function const/static, and — in EXTRACTOR_VERSION 12 —
declarations inside an IIFE, which is why code-du-travail moved to 3,659 here.
The honest limit stands: on the languages no compiler-backed oracle covers,
nothing proves the rest of that column is entirely surplus.
Refreshed by CODEINDEX_ORACLE=1 pnpm vitest run tests/oracles-external-diff.test.ts and by the weekly CI job; tool version,
corpus and date sit next to the figures in
tests/quality/external-oracles.json.
Against hand-labelled ground truth
The external indexers report declarations and nothing else, so doc comments,
complete signatures and call edges cannot be checked against them at all.
Those are covered by labels written here: tests/fixtures/quality/ holds every
declaration a correct indexer should report for 17 languages, with its kind,
visibility, doc and signature, plus a relevance-judged search corpus whose query
terms live only in prose.
| what is scored | score | measured on |
|---|---|---|
| symbol precision / recall | 100% / 100% | 346 labelled declarations in 23 files |
| kind accuracy | 100% | the same 346 declarations |
| visibility accuracy | 100% on 16 of 17 languages, 95.8% on Go | the same 346 declarations |
| doc comment attached | 100% | the 188 declarations labelled with a doc |
| complete signature | 100% | the 32 declarations labelled with a signature |
| call edges / inheritance (F1) | 100% / 100% | 54 labelled call sites, 24 relations |
| search MRR / nDCG@10 / recall@5 | 93.8% / 86.0% / 84.4% | 16 relevance-judged queries |
pnpm quality:report reproduces every number; tests/quality.test.ts enforces
them as a ratchet in both directions — losing quality fails CI, and gaining
it fails too until the baseline is refreshed in the same commit. Two builds of
an unchanged repo stay byte-identical.
One judged query still returns nothing relevant, and the reason is honest: it asks for "authentication" against a file that never writes "auth" in any form. No lexical index can answer that; the semantic tier is what it is for.
Use as a library (the vendoring model)
Consumers commit scripts/engine.mjs + scripts/engine.d.mts (fetched at a
pinned release tag) into src/vendor/ and import from it; their bundler inlines
the engine so they still ship a single file:
import { buildIndexArtifacts, renderGraphJson } from "./vendor/engine.mjs";
const { scan, graph, symbols } = buildIndexArtifacts("/path/to/repo");
The AST tier is optional: without a grammars/ directory next to the bundle
the engine silently uses its regex tier. Only tools that want AST precision
also vendor scripts/grammars/ (~17 MiB of wasm).
Inventory consumers
walk(root, options) also serves exhaustive inventories. The default source-indexing
policy is unchanged. Opt into includeBinary, includeLockfiles, includeOversize,
includeMinified and includeFileSymlinks (in-repo file links, skipped by default
as aliases), or replace binaryExtensions to keep textual SVG. These
controls are independent: a .lock extension still follows the binary policy.
filter({ rel, abs, directory }) prunes consumer output and out-of-scope trees
before descending. onSkip({ rel, reason, directory, size? }) records observed
exclusions, including nested repositories and broken links, without allocating a
second inventory for ordinary indexing. It does not enumerate pruned descendants;
capped still means that an explicitly requested file budget cut the walk short.
readTextEx(path) distinguishes empty, binary and unreadable files, reports the
encoding and original bytes, and marks whether decoded offsets address those bytes.
OffsetMap converts JavaScript string positions to UTF-8 byte positions. Consumers
keep their own classification and editing policy; readText remains the compatible
string-only reader.
Two grammar tiers
| tier | languages | how you get it |
|---|---|---|
| core (committed) | TypeScript, TSX, JavaScript, Python, Go, Rust, Java, C, C++, C#, Ruby, PHP, Scala, Bash, Lua | ships in the bundle — no network, no install |
| extended (pull-only) | Kotlin, Elixir, Zig, Solidity, HCL, Terraform | codeindex grammars pull |
The extended set is not in git: it adds ~6 MiB of wasm for languages most
repos do not contain, so committing it would grow every vendoring consumer's
checkout for a benefit only some can use. It ships inside the per-release
grammars-<version>.tar.gz asset instead. Without a pull those grammars are
simply absent and the engine falls back to the regex tier, exactly as it does for
a language it has no grammar for at all — codeindex grammars status reports
resolved-vs-missing per tier (and extendedPullNeeded while any extended
grammar is missing) so a Kotlin repo quietly indexed by regex is visible rather
than guesswork.
Not included, and why: Swift publishes no prebuilt wasm at all, and Dart's does not load under web-tree-sitter 0.26 — shipping it would be dead bytes advertising precision that silently degrades. Both have regex extractors.
Slim grammars (pull instead of vendor)
Consumers that want AST precision but not the ~17 MiB of vendored wasm can
codeindex grammars pull the grammars once into a shared, per-machine cache
(<XDG_CACHE_HOME|~/.cache>/codeindex/grammars/<ENGINE_VERSION>) instead:
codeindex grammars status # active tier (adjacent/env/cache/none) + whether a pull is needed
codeindex grammars pull # fetch the per-release grammars asset, sha256-verified, into the cache
Resolution is adjacent > env > cache > regex, per grammar: a
bundle-adjacent grammars/ still wins if present (offline setups are
untouched), then CODEINDEX_GRAMMARS_DIR, then the pulled cache — and a
grammar the winner lacks is looked up in the tiers below it. That is what lets
the npm package (which ships only the core wasms) pick up the extended ones a
pull put in the cache. The legacy CODEINDEX_GRAMMAR_DIR still pins one dir
with nothing behind it. pull fetches the official
grammars-<version>.tar.gz release asset (its .sha256 sidecar is verified
before anything is written, and a pull that cannot fetch it fails instead of
installing the archive unverified; a CODEINDEX_GRAMMARS_URL mirror carries no
checksum) and extracts it atomically; the same wasm bytes
produce byte-identical AST extraction from the cache as from a vendored dir.
It is fully offline-safe: with no grammars resolvable anywhere — and after a
failed or absent pull — the engine silently falls back to the regex tier exactly
as it does today; a pull never throws into indexing.
Use from npm
For consumers who don't want to vendor the bundle, @maxgfr/codeindex also
resolves as a regular package:
npm i @maxgfr/codeindex
import { scanRepo, ENGINE_VERSION } from "@maxgfr/codeindex";
const scan = scanRepo("/path/to/repo");
The CLI ships in the same package — see Use as a CLI below for the global install command. Consumer tools should still prefer vendoring: it keeps their own bundle single-file and pinned to an exact commit without an npm dependency.
codeindex MCP 질문
npm 의존성으로 설치하지 않고 사용할 수 있나요?
네, 단일 의존성 없는 engine.mjs 파일로 벤더링하여 사용할 수 있도록 설계되었습니다.
어떤 종류의 그래프 분석을 제공하나요?
Louvain 커뮤니티, PageRank/betweenness centrality, 그리고 테스트-코드 매핑을 제공합니다.