Local code index · MCP server · zero config

A local code indexfor AI coding agents.

Symbols and references from your repo, fresh on every save, served to your agent over MCP as path:line answers of about 700 tokens. No API keys, no cloud.

pip install cidx

Python 3.11 to 3.13 · Windows, macOS, Linux · Apache-2.0This site documents 0.1.0a2. PyPI currently serves 0.1.0a1.

pallets/click78 files · 2,909 symbols
  • echofunctionsrc/click/utils.py:252
  • EchoingStdin.readmethodsrc/click/testing.py:51
  • echo_via_pagerfunctionsrc/click/termui.py:366

Type a name, or click a node.

classfunctionmethodconstexactimportname-onlyThe real index of pallets/click, built by cidx 0.1.0a2. Drag to rotate.

Setup

Add it to your agent in one block of JSON.

A command and its arguments. No API key, no account.

mcp config
{
  "mcpServers": {
    "cidx": {
      "command": "uvx",
      "args": [
        "cidx",
        "serve",
        "--repo",
        "/absolute/path/to/repo"
      ]
    }
  }
}

Nothing to install first.

  1. 1

    Paste the block

    Use your repo's absolute path. On Windows, forward slashes: C:/code/myrepo.

  2. 2

    Restart the agent

    It starts cidx, which builds the index and keeps it fresh.

  3. 3

    Ask about your code

    • search_symbols
    • find_definition
    • find_references
    • outline_file
    • repo_map
your agentMCP client
cidx servestdio, read-only
index.dbSQLite, outside your repo

See it work

Index, ask, edit, ask again.

Real cidx output on a real repository, replayed.

cidx index --repo .
indexed 78 files (0 skipped as too large, 0 unreadable, 0 removed as stale) -> 2909 symbols, 6675 refs in 0.95s
index: C:\Users\gativ\AppData\Local\cidx\c3d04c84175c922f\index.db
cidx stats --repo .
files: 78  symbols: 2909  refs: 6675
index: C:\Users\gativ\AppData\Local\cidx\c3d04c84175c922f\index.db

Stored in your user cache, never inside the repo.

Real output: cidx 0.1.0a2 on pallets/click@00e592c, 2026-10-01.

Through MCP

What your agent reads, before and after.

Locations and signatures instead of whole files.

The task“Who calls echo, and what does core.py define?”

Searching blind

grep, then read the file

shellgit grep -n echo

.github/workflows/publish.yaml:24:      - run: echo "SOURCE_DATE_EPOCH=$(git log -1 --pretty=%ct)" >> $GITHUB_ENV
.readthedocs.yaml:11:      - echo "Installing dependencies"
CHANGES.md:14:  prompt when the output stream does not support them, matching `echo()`.
CHANGES.md:25:- `style()` and `secho()` no longer silently drop the 256-color index `0`
CHANGES.md:80:- `echo_via_pager` flushes after each write, so passing a generator streams
CHANGES.md:83:- `echo_via_pager` and `get_pager_file` no longer close a borrowed stdout

… 718 more lines of docs, changelog, config and strings

724 lines · ~12,689 tokens

shellcat src/click/core.py

3,793 lines · ~36,897 tokens

Tool output in context~49,586 tokens

Asking cidx

two MCP tool calls

mcpfind_references("echo")

src/click/_termui_impl.py:264  echo  [import]  -> echo (src/click/utils.py)
src/click/_termui_impl.py:299  echo  [import]  -> echo (src/click/utils.py)
src/click/core.py:1405  echo  [import]  -> echo (src/click/utils.py)
src/click/core.py:1557  echo  [import]  -> echo (src/click/utils.py)
src/click/core.py:1587  echo  [import]  -> echo (src/click/utils.py)

… 38 more rows

truncated: true, total_matches: 200
index_age_ms: 9624

43 rows · ~730 tokens · 8 ms

mcpoutline_file("src/click/core.py")

truncated: true, total_matches: 308
index_age_ms: 9631

56 rows · ~711 tokens · 7 ms

Tool output in context~1,441 tokens

About 34x less tool output. Both sides are real output on pallets/click; tokens are estimated as characters / 4. This compares output size, not a full agent run.

The MCP surface

Five tools. All read-only.

cidx cannot write to or run anything in your repository.

  • You only roughly know a name.

    search_symbols(query)

    search_symbols("pass_cont")

    pass_context  function  src/click/decorators.py:28  def pass_context(f: t.Callable[te.Concatenate[Context, P], R]) -> t.Callable[P, R]
  • Where is this defined?

    find_definition(name)

    find_definition("Context.invoke")

    Context.invoke  method  src/click/core.py:850  def invoke( self, callback: t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any ) -> V
  • Who uses this?

    find_references(name)

    find_references("echo")

    src/click/_termui_impl.py:264  echo  [import]  -> echo (src/click/utils.py)
  • What is in this file?

    outline_file(path)

    outline_file("src/click/globals.py")

    13  get_current_context  function  def get_current_context(silent: t.Literal[False] = False) -> Context
  • What does this repository look like?

    repo_map()

    repo_map()

    src/click/decorators.py  (python, 55 symbols, 453 inbound)  P, R, T, _AnyCallable, FC, pass_context, pass_obj, make_pass_decorator, pass_meta_key, CmdType
  • Every response

    The same contract

    • path:line and a signature, never file bodies
    • max_tokens budget, default 700
    • truncated: true, total_matches: N when cut
    • index_age_ms freshness stamp
    • on a miss: says so, recommends grep

Try it

Drag the token budget.

cidx keeps whole rows until the next one would not fit, then marks the answer truncated. A real find_references("echo") answer, trimmed by the same rule.

rows returned
of 200
tokens used
~

Verified against the real server at max_tokens 150 and 700.

find_references("echo", max_tokens=700)
  1. src/click/_termui_impl.py:264 echo [import] -> echo (src/click/utils.py)
  2. src/click/_termui_impl.py:299 echo [import] -> echo (src/click/utils.py)
  3. src/click/core.py:1405 echo [import] -> echo (src/click/utils.py)
  4. src/click/core.py:1557 echo [import] -> echo (src/click/utils.py)
  5. src/click/core.py:1587 echo [import] -> echo (src/click/utils.py)
  6. src/click/core.py:2767 echo [import] -> echo (src/click/utils.py)
  7. src/click/decorators.py:544 echo [import] -> echo (src/click/utils.py)
  8. src/click/decorators.py:589 echo [import] -> echo (src/click/utils.py)
  9. src/click/decorators.py:615 echo [import] -> echo (src/click/utils.py)
  10. src/click/exceptions.py:61 echo [import] -> echo (src/click/utils.py)
  11. src/click/exceptions.py:106 echo [import] -> echo (src/click/utils.py)
  12. src/click/exceptions.py:107 echo [import] -> echo (src/click/utils.py)
  13. src/click/exceptions.py:339 echo [import] -> echo (src/click/utils.py)
  14. src/click/shell_completion.py:48 echo [import] -> echo (src/click/utils.py)
  15. src/click/shell_completion.py:52 echo [import] -> echo (src/click/utils.py)
  16. src/click/shell_completion.py:415 echo [import] -> echo (src/click/utils.py)
  17. src/click/shell_completion.py:423 echo [import] -> echo (src/click/utils.py)
  18. src/click/termui.py:243 echo [import] -> echo (src/click/utils.py)
  19. src/click/termui.py:274 echo [import] -> echo (src/click/utils.py)
  20. src/click/termui.py:285 echo [import] -> echo (src/click/utils.py)
  21. src/click/termui.py:339 echo [import] -> echo (src/click/utils.py)
  22. src/click/termui.py:612 echo [import] -> echo (src/click/utils.py)
  23. src/click/termui.py:810 echo [import] -> echo (src/click/utils.py)
  24. src/click/termui.py:996 echo [import] -> echo (src/click/utils.py)
  25. src/click/termui.py:1003 echo [import] -> echo (src/click/utils.py)
  26. tests/test_utils/test_prompt.py:53 echo [exact] -> test_prompts_eof.echo (tests/test_utils/test_prompt.py)
  27. tests/test_utils/test_prompt.py:96 echo [exact] -> test_prompts_eof.echo (tests/test_utils/test_prompt.py)
  28. examples/aliases/aliases.py:104 echo [name-only]
  29. examples/aliases/aliases.py:110 echo [name-only]
  30. examples/aliases/aliases.py:116 echo [name-only]
  31. examples/aliases/aliases.py:122 echo [name-only]
  32. examples/aliases/aliases.py:129 echo [name-only]
  33. examples/aliases/aliases.py:143 echo [name-only]
  34. examples/colors/colors.py:32 echo [name-only]
  35. examples/colors/colors.py:34 echo [name-only]
  36. examples/colors/colors.py:36 echo [name-only]
  37. examples/colors/colors.py:38 echo [name-only]
  38. examples/colors/colors.py:39 echo [name-only]
  39. examples/completion/completion.py:15 echo [name-only]
  40. examples/completion/completion.py:27 echo [name-only]
  41. examples/completion/completion.py:28 echo [name-only]
  42. examples/completion/completion.py:53 echo [name-only]
  43. examples/complex/complex/cli.py:19 echo [name-only]
  44. examples/imagepipe/imagepipe.py:90 echo [name-only]
  45. examples/imagepipe/imagepipe.py:98 echo [name-only]
  46. examples/imagepipe/imagepipe.py:115 echo [name-only]
  47. examples/imagepipe/imagepipe.py:118 echo [name-only]
  48. examples/imagepipe/imagepipe.py:126 echo [name-only]
  49. examples/imagepipe/imagepipe.py:141 echo [name-only]
  50. examples/imagepipe/imagepipe.py:159 echo [name-only]
  51. examples/imagepipe/imagepipe.py:200 echo [name-only]
  52. examples/imagepipe/imagepipe.py:204 echo [name-only]
  53. examples/imagepipe/imagepipe.py:216 echo [name-only]
  54. examples/imagepipe/imagepipe.py:232 echo [name-only]
  55. examples/imagepipe/imagepipe.py:246 echo [name-only]
  56. examples/imagepipe/imagepipe.py:258 echo [name-only]
  57. examples/imagepipe/imagepipe.py:280 echo [name-only]
  58. examples/naval/naval.py:24 echo [name-only]
  59. examples/naval/naval.py:34 echo [name-only]
  60. examples/naval/naval.py:43 echo [name-only]
  61. examples/naval/naval.py:64 echo [name-only]
  62. examples/naval/naval.py:72 echo [name-only]
  63. examples/repo/repo.py:17 echo [name-only]
  64. examples/repo/repo.py:81 echo [name-only]
  65. examples/repo/repo.py:84 echo [name-only]
  66. examples/repo/repo.py:85 echo [name-only]
  67. examples/repo/repo.py:96 echo [name-only]
  68. examples/repo/repo.py:97 echo [name-only]
  69. examples/repo/repo.py:113 echo [name-only]
  70. examples/repo/repo.py:142 echo [name-only]
  71. examples/repo/repo.py:146 echo [name-only]
  72. examples/repo/repo.py:150 echo [name-only]
  73. examples/repo/repo.py:151 echo [name-only]
  74. examples/repo/repo.py:166 echo [name-only]
  75. examples/termui/termui.py:18 echo [name-only]
  76. examples/termui/termui.py:19 echo [name-only]
  77. examples/termui/termui.py:125 echo [name-only]
  78. examples/termui/termui.py:127 echo [name-only]
  79. examples/termui/termui.py:129 echo [name-only]
  80. examples/termui/termui.py:150 echo [name-only]
  81. examples/termui/termui.py:151 echo [name-only]
  82. examples/termui/termui.py:152 echo [name-only]
  83. examples/termui/termui.py:159 echo [name-only]
  84. examples/termui/termui.py:161 echo [name-only]
  85. examples/termui/termui.py:162 echo [name-only]
  86. examples/termui/termui.py:167 echo [name-only]
  87. examples/validation/validation.py:46 echo [name-only]
  88. examples/validation/validation.py:47 echo [name-only]
  89. examples/validation/validation.py:48 echo [name-only]
  90. tests/test_arguments.py:15 echo [name-only]
  91. tests/test_arguments.py:16 echo [name-only]
  92. tests/test_arguments.py:28 echo [name-only]
  93. tests/test_arguments.py:30 echo [name-only]
  94. tests/test_arguments.py:51 echo [name-only]
  95. tests/test_arguments.py:71 echo [name-only]
  96. tests/test_arguments.py:131 echo [name-only]
  97. tests/test_arguments.py:218 echo [name-only]
  98. tests/test_arguments.py:227 echo [name-only]
  99. tests/test_arguments.py:238 echo [name-only]
  100. tests/test_arguments.py:288 echo [name-only]
  101. tests/test_arguments.py:343 echo [name-only]
  102. tests/test_arguments.py:384 echo [name-only]
  103. tests/test_arguments.py:397 echo [name-only]
  104. tests/test_arguments.py:427 echo [name-only]
  105. tests/test_arguments.py:453 echo [name-only]
  106. tests/test_arguments.py:454 echo [name-only]
  107. tests/test_arguments.py:470 echo [name-only]
  108. tests/test_arguments.py:483 echo [name-only]
  109. tests/test_arguments.py:527 echo [name-only]
  110. tests/test_arguments.py:605 echo [name-only]
  111. tests/test_arguments.py:617 echo [name-only]
  112. tests/test_arguments.py:637 echo [name-only]
  113. tests/test_arguments.py:659 echo [name-only]
  114. tests/test_arguments.py:736 echo [name-only]
  115. tests/test_basic.py:16 echo [name-only]
  116. tests/test_basic.py:56 echo [name-only]
  117. tests/test_basic.py:81 echo [name-only]
  118. tests/test_basic.py:100 echo [name-only]
  119. tests/test_basic.py:145 echo [name-only]
  120. tests/test_basic.py:150 echo [name-only]
  121. tests/test_basic.py:172 echo [name-only]
  122. tests/test_basic.py:184 echo [name-only]
  123. tests/test_basic.py:205 echo [name-only]
  124. tests/test_basic.py:228 echo [name-only]
  125. tests/test_basic.py:256 echo [name-only]
  126. tests/test_basic.py:279 echo [name-only]
  127. tests/test_basic.py:351 echo [name-only]
  128. tests/test_basic.py:412 echo [name-only]
  129. tests/test_basic.py:427 echo [name-only]
  130. tests/test_basic.py:501 echo [name-only]
  131. tests/test_basic.py:502 echo [name-only]
  132. tests/test_basic.py:514 echo [name-only]
  133. tests/test_basic.py:527 echo [name-only]
  134. tests/test_basic.py:548 echo [name-only]
  135. tests/test_basic.py:575 echo [name-only]
  136. tests/test_basic.py:606 echo [name-only]
  137. tests/test_basic.py:630 echo [name-only]
  138. tests/test_basic.py:695 echo [name-only]
  139. tests/test_basic.py:722 echo [name-only]
  140. tests/test_basic.py:733 echo [name-only]
  141. tests/test_basic.py:787 echo [name-only]
  142. tests/test_chain.py:9 echo [name-only]
  143. tests/test_chain.py:22 echo [name-only]
  144. tests/test_chain.py:26 echo [name-only]
  145. tests/test_chain.py:56 echo [name-only]
  146. tests/test_chain.py:61 echo [name-only]
  147. tests/test_chain.py:76 echo [name-only]
  148. tests/test_chain.py:81 echo [name-only]
  149. tests/test_chain.py:101 echo [name-only]
  150. tests/test_chain.py:115 echo [name-only]
  151. tests/test_chain.py:120 echo [name-only]
  152. tests/test_chain.py:147 echo [name-only]
  153. tests/test_chain.py:210 echo [name-only]
  154. tests/test_chain.py:214 echo [name-only]
  155. tests/test_command_decorators.py:9 echo [name-only]
  156. tests/test_command_decorators.py:29 echo [name-only]
  157. tests/test_command_decorators.py:39 echo [name-only]
  158. tests/test_command_decorators.py:43 echo [name-only]
  159. tests/test_command_decorators.py:47 echo [name-only]
  160. tests/test_command_decorators.py:51 echo [name-only]
  161. tests/test_command_decorators.py:68 echo [name-only]
  162. tests/test_commands.py:17 echo [name-only]
  163. tests/test_commands.py:30 echo [name-only]
  164. tests/test_commands.py:51 echo [name-only]
  165. tests/test_commands.py:58 echo [name-only]
  166. tests/test_commands.py:112 echo [name-only]
  167. tests/test_commands.py:133 echo [name-only]
  168. tests/test_commands.py:137 echo [name-only]
  169. tests/test_commands.py:188 echo [name-only]
  170. tests/test_commands.py:189 echo [name-only]
  171. tests/test_commands.py:190 echo [name-only]
  172. tests/test_commands.py:224 echo [name-only]
  173. tests/test_commands.py:284 echo [name-only]
  174. tests/test_commands.py:287 echo [name-only]
  175. tests/test_commands.py:291 echo [name-only]
  176. tests/test_commands.py:342 echo [name-only]
  177. tests/test_commands.py:431 echo [name-only]
  178. tests/test_commands.py:496 echo [name-only]
  179. tests/test_commands.py:497 echo [name-only]
  180. tests/test_context.py:32 echo [name-only]
  181. tests/test_context.py:55 echo [name-only]
  182. tests/test_context.py:78 echo [name-only]
  183. tests/test_context.py:99 echo [name-only]
  184. tests/test_context.py:190 echo [name-only]
  185. tests/test_context.py:191 echo [name-only]
  186. tests/test_context.py:192 echo [name-only]
  187. tests/test_context.py:194 echo [name-only]
  188. tests/test_context.py:195 echo [name-only]
  189. tests/test_context.py:207 echo [name-only]
  190. tests/test_context.py:208 echo [name-only]
  191. tests/test_context.py:209 echo [name-only]
  192. tests/test_context.py:214 echo [name-only]
  193. tests/test_context.py:215 echo [name-only]
  194. tests/test_context.py:216 echo [name-only]
  195. tests/test_context.py:223 echo [name-only]
  196. tests/test_context.py:224 echo [name-only]
  197. tests/test_context.py:225 echo [name-only]
  198. tests/test_context.py:227 echo [name-only]
  199. tests/test_context.py:228 echo [name-only]
  200. tests/test_context.py:247 echo [name-only]

truncated: true, total_matches: 200

index_age_ms: …

How it works

From source bytes to a 700-token answer.

What

Each .py, .ts, .tsx and .js file is parsed from bytes into symbols and raw references.

Why

tree-sitter tolerates broken code, so the index survives half-written files.

  • 4 grammars
  • functions · classes · methods · consts · imports
  • files over 1 MiB skipped

The invariant

The incremental index always equals a cold rebuild.

A property-based test suite throws random edits, deletes and renames at the index and asserts it still matches a fresh build. Run the same check on your repo any time.

cidx check --repo .
no drift: the index matches a cold rebuild

Measured, not claimed

The numbers, including the misses.

Every figure is reproducible. Missed targets are published too.

  • 30.5 sto index Django

    3,043 files, 76,166 symbols

  • 0.95 sto index click

    78 files, 2,909 symbols

  • 238 mssave to queryable on click

    Django: 0.33 s isolated, 1.77 s back to back

  • 263tests, green on 3 OSes

    Including the property-based convergence suite

Query latency on Django, p95

60 samples each.

Ranked search and the repo map miss the target at this size. Published as measured.

django/django@2b30f62, 2026-09-16, Windows 11 laptop, Python 3.13.7: docs/verification.md. click: 2026-10-01, cidx 0.1.0a2, 12th Gen Intel(R) Core(TM) i5-12500H.

Known limitations

What it does not do.

  • Two languages

    Python and TypeScript/JavaScript only.

  • No semantic search

    Names and structure, not concepts.

  • No type checker

    Matches carry a confidence tag instead.

  • TypeScript types are skipped

    interface, type and enum are not indexed yet.

  • Namespace imports

    import * as x is not followed.

  • Freshness cost grows

    Each save re-resolves the whole index: about 1.5 s on Django.

Give your agent an index.

pip install cidx