codebase-brain-mcp 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/PKG-INFO +64 -11
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/README.md +63 -10
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/pyproject.toml +1 -1
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/pyproject.toml.orig +1 -1
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/server.py +130 -42
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/sources.py +1 -1
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/LICENSE +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/__init__.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/__main__.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/analysis.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/brain.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/chunking.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/chunking_js.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/config.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/embeddings.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/evals.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/history.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/py.typed +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/remote.py +0 -0
- {codebase_brain_mcp-0.2.0 → codebase_brain_mcp-0.2.1}/src/codebase_brain_mcp/store.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codebase-brain-mcp
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: An MCP server that gives AI agents a cited, searchable memory of a codebase.
|
|
5
5
|
Keywords: mcp,model-context-protocol,rag,code-search,ai-agents,claude,git
|
|
6
6
|
Author: Noman Nazir
|
|
@@ -103,7 +103,7 @@ A plug-in for any AI assistant that turns a codebase into something it can actua
|
|
|
103
103
|
|
|
104
104
|
| Tool | What it does |
|
|
105
105
|
|---|---|
|
|
106
|
-
| `index_repo(source, name?)` | Index a local path or an `https://` git URL (
|
|
106
|
+
| `index_repo(source, name?)` | Index a local path or an `https://` git URL (blobless clone with full history). Reports progress. |
|
|
107
107
|
| `ask_codebase(question, repo?, k?)` | The most relevant code and docs as cited sources, plus related commits, with instructions to answer only from them. |
|
|
108
108
|
| `search(query, repo?, k?, path_prefix?, kinds?)` | Raw hybrid search, filterable by path and chunk kind. |
|
|
109
109
|
| `explain_history(symbol? \| path, start_line?, end_line?, repo?)` | Every commit that changed a function or line range, newest first, with the relevant diff, current authorship, and PR titles and descriptions (GitHub). |
|
|
@@ -117,15 +117,67 @@ Also included: the resource `brain://repos`, and the prompt `answer_with_citatio
|
|
|
117
117
|
|
|
118
118
|
## Install
|
|
119
119
|
|
|
120
|
-
Codebase Brain runs on your own machine, so there's no server or account, and it's free.
|
|
121
|
-
[uv](https://docs.astral.sh/uv/):
|
|
120
|
+
Codebase Brain runs on your own machine, so there's no server or account, and it's free. It works with any
|
|
121
|
+
MCP-compatible assistant. All you need is [uv](https://docs.astral.sh/uv/), and every client runs the same command:
|
|
122
|
+
`uvx codebase-brain-mcp`.
|
|
123
|
+
|
|
124
|
+
**Claude Code**
|
|
122
125
|
|
|
123
126
|
```bash
|
|
124
|
-
# Claude Code
|
|
125
127
|
claude mcp add codebase-brain -- uvx codebase-brain-mcp
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Cursor:** `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project)
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
{
|
|
134
|
+
"mcpServers": {
|
|
135
|
+
"codebase-brain": { "command": "uvx", "args": ["codebase-brain-mcp"] }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
126
139
|
|
|
127
|
-
|
|
128
|
-
|
|
140
|
+
**Claude Desktop:** Settings → Developer → Edit Config (`claude_desktop_config.json`), then restart the app
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{
|
|
144
|
+
"mcpServers": {
|
|
145
|
+
"codebase-brain": { "command": "uvx", "args": ["codebase-brain-mcp"] }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**VS Code (GitHub Copilot agent mode):** `.vscode/mcp.json`
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"servers": {
|
|
155
|
+
"codebase-brain": { "type": "stdio", "command": "uvx", "args": ["codebase-brain-mcp"] }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
**Windsurf:** `~/.codeium/windsurf/mcp_config.json`. **Gemini CLI:** `~/.gemini/settings.json`. Both use the same
|
|
161
|
+
`mcpServers` block as Cursor.
|
|
162
|
+
|
|
163
|
+
**OpenAI Codex CLI**
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
codex mcp add codebase-brain -- uvx codebase-brain-mcp
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> **Tool doesn't start?** Desktop apps often don't see your shell's `PATH`. Run `which uvx` and put that full
|
|
170
|
+
> path in `"command"`, for example `"/Users/you/.local/bin/uvx"`.
|
|
171
|
+
|
|
172
|
+
Optional: install the [skill](https://github.com/Nomannazir/codebase-brain-mcp/blob/main/skills/codebase-brain/SKILL.md).
|
|
173
|
+
It tells the agent which tool fits which question, to cite every claim, to check history before removing odd code,
|
|
174
|
+
and to run impact analysis before editing. For Claude Code, run the commands below. Other agents that read
|
|
175
|
+
`SKILL.md` files take the same file in their own skills folder.
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
mkdir -p ~/.claude/skills/codebase-brain
|
|
179
|
+
curl -fsSL https://raw.githubusercontent.com/Nomannazir/codebase-brain-mcp/main/skills/codebase-brain/SKILL.md \
|
|
180
|
+
-o ~/.claude/skills/codebase-brain/SKILL.md
|
|
129
181
|
```
|
|
130
182
|
|
|
131
183
|
## Quick start
|
|
@@ -154,7 +206,7 @@ src/itsdangerous/timed.py:72-158
|
|
|
154
206
|
...
|
|
155
207
|
```
|
|
156
208
|
|
|
157
|
-
### Use
|
|
209
|
+
### Use a local checkout instead (for development)
|
|
158
210
|
|
|
159
211
|
```bash
|
|
160
212
|
claude mcp add codebase-brain -- uv run --directory /path/to/codebase-brain-mcp codebase-brain
|
|
@@ -288,13 +340,14 @@ How it's measured:
|
|
|
288
340
|
- **nDCG@10:** each target is credited once, so duplicate chunks don't inflate the score.
|
|
289
341
|
- **Confidence intervals:** 95% bootstrap over questions, seeded.
|
|
290
342
|
- **Relevance:** a result is relevant if it's in the target file and, when the target names a symbol, is that symbol
|
|
291
|
-
or one of its methods.
|
|
343
|
+
or one of its methods. A docs target can name a section (`FAQ > How do I stop retrying early?`), and then that
|
|
344
|
+
section or one of its subsections counts.
|
|
292
345
|
- **Dataset rot:** the harness fails if a target no longer exists at the pinned commit.
|
|
293
346
|
|
|
294
347
|
## How it works
|
|
295
348
|
|
|
296
349
|
```
|
|
297
|
-
source ─► resolve (local dir /
|
|
350
|
+
source ─► resolve (local dir / blobless https clone) ─► list files (.gitignore-aware, skips binaries and lockfiles)
|
|
298
351
|
─► chunk (ast / tree-sitter / headings / windows) ─► embed (fastembed) ─► SQLite: chunks + vectors + FTS5
|
|
299
352
|
query ─► BM25 top 50 ┐
|
|
300
353
|
─► vector top 50 ┴─► reciprocal-rank fusion ─► filters ─► cited sources
|
|
@@ -314,7 +367,7 @@ query ─► BM25 top 50 ┐
|
|
|
314
367
|
## Development
|
|
315
368
|
|
|
316
369
|
```bash
|
|
317
|
-
uv run pytest #
|
|
370
|
+
uv run pytest # 67 tests, offline (hash embedder, temporary git repos, in-process MCP client)
|
|
318
371
|
uv run ruff check . # lint
|
|
319
372
|
uv run ruff format . # format
|
|
320
373
|
uv run pyright # types
|
|
@@ -74,7 +74,7 @@ A plug-in for any AI assistant that turns a codebase into something it can actua
|
|
|
74
74
|
|
|
75
75
|
| Tool | What it does |
|
|
76
76
|
|---|---|
|
|
77
|
-
| `index_repo(source, name?)` | Index a local path or an `https://` git URL (
|
|
77
|
+
| `index_repo(source, name?)` | Index a local path or an `https://` git URL (blobless clone with full history). Reports progress. |
|
|
78
78
|
| `ask_codebase(question, repo?, k?)` | The most relevant code and docs as cited sources, plus related commits, with instructions to answer only from them. |
|
|
79
79
|
| `search(query, repo?, k?, path_prefix?, kinds?)` | Raw hybrid search, filterable by path and chunk kind. |
|
|
80
80
|
| `explain_history(symbol? \| path, start_line?, end_line?, repo?)` | Every commit that changed a function or line range, newest first, with the relevant diff, current authorship, and PR titles and descriptions (GitHub). |
|
|
@@ -88,15 +88,67 @@ Also included: the resource `brain://repos`, and the prompt `answer_with_citatio
|
|
|
88
88
|
|
|
89
89
|
## Install
|
|
90
90
|
|
|
91
|
-
Codebase Brain runs on your own machine, so there's no server or account, and it's free.
|
|
92
|
-
[uv](https://docs.astral.sh/uv/):
|
|
91
|
+
Codebase Brain runs on your own machine, so there's no server or account, and it's free. It works with any
|
|
92
|
+
MCP-compatible assistant. All you need is [uv](https://docs.astral.sh/uv/), and every client runs the same command:
|
|
93
|
+
`uvx codebase-brain-mcp`.
|
|
94
|
+
|
|
95
|
+
**Claude Code**
|
|
93
96
|
|
|
94
97
|
```bash
|
|
95
|
-
# Claude Code
|
|
96
98
|
claude mcp add codebase-brain -- uvx codebase-brain-mcp
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Cursor:** `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project)
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"codebase-brain": { "command": "uvx", "args": ["codebase-brain-mcp"] }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
97
110
|
|
|
98
|
-
|
|
99
|
-
|
|
111
|
+
**Claude Desktop:** Settings → Developer → Edit Config (`claude_desktop_config.json`), then restart the app
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"mcpServers": {
|
|
116
|
+
"codebase-brain": { "command": "uvx", "args": ["codebase-brain-mcp"] }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**VS Code (GitHub Copilot agent mode):** `.vscode/mcp.json`
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"servers": {
|
|
126
|
+
"codebase-brain": { "type": "stdio", "command": "uvx", "args": ["codebase-brain-mcp"] }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Windsurf:** `~/.codeium/windsurf/mcp_config.json`. **Gemini CLI:** `~/.gemini/settings.json`. Both use the same
|
|
132
|
+
`mcpServers` block as Cursor.
|
|
133
|
+
|
|
134
|
+
**OpenAI Codex CLI**
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
codex mcp add codebase-brain -- uvx codebase-brain-mcp
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
> **Tool doesn't start?** Desktop apps often don't see your shell's `PATH`. Run `which uvx` and put that full
|
|
141
|
+
> path in `"command"`, for example `"/Users/you/.local/bin/uvx"`.
|
|
142
|
+
|
|
143
|
+
Optional: install the [skill](https://github.com/Nomannazir/codebase-brain-mcp/blob/main/skills/codebase-brain/SKILL.md).
|
|
144
|
+
It tells the agent which tool fits which question, to cite every claim, to check history before removing odd code,
|
|
145
|
+
and to run impact analysis before editing. For Claude Code, run the commands below. Other agents that read
|
|
146
|
+
`SKILL.md` files take the same file in their own skills folder.
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
mkdir -p ~/.claude/skills/codebase-brain
|
|
150
|
+
curl -fsSL https://raw.githubusercontent.com/Nomannazir/codebase-brain-mcp/main/skills/codebase-brain/SKILL.md \
|
|
151
|
+
-o ~/.claude/skills/codebase-brain/SKILL.md
|
|
100
152
|
```
|
|
101
153
|
|
|
102
154
|
## Quick start
|
|
@@ -125,7 +177,7 @@ src/itsdangerous/timed.py:72-158
|
|
|
125
177
|
...
|
|
126
178
|
```
|
|
127
179
|
|
|
128
|
-
### Use
|
|
180
|
+
### Use a local checkout instead (for development)
|
|
129
181
|
|
|
130
182
|
```bash
|
|
131
183
|
claude mcp add codebase-brain -- uv run --directory /path/to/codebase-brain-mcp codebase-brain
|
|
@@ -259,13 +311,14 @@ How it's measured:
|
|
|
259
311
|
- **nDCG@10:** each target is credited once, so duplicate chunks don't inflate the score.
|
|
260
312
|
- **Confidence intervals:** 95% bootstrap over questions, seeded.
|
|
261
313
|
- **Relevance:** a result is relevant if it's in the target file and, when the target names a symbol, is that symbol
|
|
262
|
-
or one of its methods.
|
|
314
|
+
or one of its methods. A docs target can name a section (`FAQ > How do I stop retrying early?`), and then that
|
|
315
|
+
section or one of its subsections counts.
|
|
263
316
|
- **Dataset rot:** the harness fails if a target no longer exists at the pinned commit.
|
|
264
317
|
|
|
265
318
|
## How it works
|
|
266
319
|
|
|
267
320
|
```
|
|
268
|
-
source ─► resolve (local dir /
|
|
321
|
+
source ─► resolve (local dir / blobless https clone) ─► list files (.gitignore-aware, skips binaries and lockfiles)
|
|
269
322
|
─► chunk (ast / tree-sitter / headings / windows) ─► embed (fastembed) ─► SQLite: chunks + vectors + FTS5
|
|
270
323
|
query ─► BM25 top 50 ┐
|
|
271
324
|
─► vector top 50 ┴─► reciprocal-rank fusion ─► filters ─► cited sources
|
|
@@ -285,7 +338,7 @@ query ─► BM25 top 50 ┐
|
|
|
285
338
|
## Development
|
|
286
339
|
|
|
287
340
|
```bash
|
|
288
|
-
uv run pytest #
|
|
341
|
+
uv run pytest # 67 tests, offline (hash embedder, temporary git repos, in-process MCP client)
|
|
289
342
|
uv run ruff check . # lint
|
|
290
343
|
uv run ruff format . # format
|
|
291
344
|
uv run pyright # types
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import inspect
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Annotated, Any
|
|
8
|
+
|
|
5
9
|
import anyio
|
|
6
10
|
import anyio.from_thread
|
|
7
11
|
import anyio.to_thread
|
|
@@ -29,6 +33,16 @@ READ_ONLY = ToolAnnotations(read_only_hint=True, open_world_hint=False)
|
|
|
29
33
|
MAX_RESULTS = 25
|
|
30
34
|
SNIPPET_CHARS = 1_800
|
|
31
35
|
|
|
36
|
+
# Parameters shared by several tools, described once so every tool explains them the same way.
|
|
37
|
+
Repo = Annotated[
|
|
38
|
+
str | None,
|
|
39
|
+
Field(description="Name of an indexed repo, as list_repos shows it. Leave out when only one repo is indexed."),
|
|
40
|
+
]
|
|
41
|
+
ResultCount = Annotated[int, Field(description=f"How many results to return, 1 to {MAX_RESULTS}.")]
|
|
42
|
+
PathPrefix = Annotated[
|
|
43
|
+
str | None, Field(description='Only consider files under this path, e.g. "src/auth/" or "docs/".')
|
|
44
|
+
]
|
|
45
|
+
|
|
32
46
|
|
|
33
47
|
class IndexResult(BaseModel):
|
|
34
48
|
repo: str
|
|
@@ -164,14 +178,23 @@ def build_server(brain: Brain | None = None) -> MCPServer:
|
|
|
164
178
|
brain = brain or Brain(load_settings())
|
|
165
179
|
server = MCPServer("codebase-brain", instructions=INSTRUCTIONS, version=__version__)
|
|
166
180
|
|
|
167
|
-
@
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
)
|
|
181
|
+
@_tool(
|
|
182
|
+
server,
|
|
183
|
+
ToolAnnotations(read_only_hint=False, destructive_hint=False, idempotent_hint=True, open_world_hint=True),
|
|
171
184
|
)
|
|
172
|
-
async def index_repo(
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
async def index_repo(
|
|
186
|
+
source: Annotated[
|
|
187
|
+
str, Field(description="A local directory, or an https git URL such as https://github.com/owner/repo.")
|
|
188
|
+
],
|
|
189
|
+
ctx: Context,
|
|
190
|
+
name: Annotated[
|
|
191
|
+
str | None, Field(description="Name to index it under. Defaults to the folder or repository name.")
|
|
192
|
+
] = None,
|
|
193
|
+
) -> IndexResult:
|
|
194
|
+
"""Index a repository so the other tools can search it. Use this first, and again after the code
|
|
195
|
+
changes: re-running only re-embeds files whose content changed. A URL is cloned with full history
|
|
196
|
+
(blobless), so explain_history works. Reports progress while it runs, and returns file counts and
|
|
197
|
+
the commit indexed."""
|
|
175
198
|
|
|
176
199
|
def progress(done: int, total: int, message: str) -> None:
|
|
177
200
|
anyio.from_thread.run(ctx.report_progress, done, total, message)
|
|
@@ -182,19 +205,37 @@ def build_server(brain: Brain | None = None) -> MCPServer:
|
|
|
182
205
|
raise ToolError(str(exc)) from exc
|
|
183
206
|
return IndexResult(**report.__dict__)
|
|
184
207
|
|
|
185
|
-
@server
|
|
208
|
+
@_tool(server, READ_ONLY)
|
|
186
209
|
async def search(
|
|
187
|
-
query: str,
|
|
210
|
+
query: Annotated[str, Field(description="An identifier (normalizeRetryOptions) or plain words.")],
|
|
211
|
+
repo: Repo = None,
|
|
212
|
+
k: ResultCount = 8,
|
|
213
|
+
path_prefix: PathPrefix = None,
|
|
214
|
+
kinds: Annotated[
|
|
215
|
+
list[str] | None,
|
|
216
|
+
Field(
|
|
217
|
+
description="Only these chunk kinds: function, class, method, interface, type, enum, "
|
|
218
|
+
"module, section (docs) or code (other files)."
|
|
219
|
+
),
|
|
220
|
+
] = None,
|
|
188
221
|
) -> SearchResult:
|
|
189
|
-
"""Hybrid search (BM25 keywords
|
|
190
|
-
|
|
222
|
+
"""Hybrid search over code and docs (BM25 keywords plus local embeddings, fused by rank). Use this
|
|
223
|
+
for a targeted lookup, or when you need filters by folder or chunk kind; for a question you'll answer
|
|
224
|
+
in prose, use ask_codebase instead. Returns ranked chunks, each with a path:start-end citation,
|
|
225
|
+
its symbol and a snippet."""
|
|
191
226
|
hits = await _search(brain, query, repo, k, path_prefix, kinds)
|
|
192
227
|
return SearchResult(repo=brain.resolve_repo(repo), query=query, results=[_source(h) for h in hits])
|
|
193
228
|
|
|
194
|
-
@server
|
|
195
|
-
async def ask_codebase(
|
|
196
|
-
|
|
197
|
-
|
|
229
|
+
@_tool(server, READ_ONLY)
|
|
230
|
+
async def ask_codebase(
|
|
231
|
+
question: Annotated[str, Field(description='A question in plain words, e.g. "How are sessions expired?"')],
|
|
232
|
+
repo: Repo = None,
|
|
233
|
+
k: ResultCount = 8,
|
|
234
|
+
) -> AskResult:
|
|
235
|
+
"""Gather the code and docs most relevant to a question, plus related commits, as numbered,
|
|
236
|
+
cited sources. Use this to answer "how does X work?" or "where is Y handled?" questions. Answer
|
|
237
|
+
from these sources only and cite each claim as path:start-end; open a source in full with
|
|
238
|
+
read_lines. For filters, use search."""
|
|
198
239
|
hits = await _search(brain, question, repo, k, None, None)
|
|
199
240
|
commits = await _call(lambda: brain.search_history(question, repo, 3))
|
|
200
241
|
instructions = (
|
|
@@ -212,57 +253,95 @@ def build_server(brain: Brain | None = None) -> MCPServer:
|
|
|
212
253
|
related_commits=[_commit(c) for c in commits],
|
|
213
254
|
)
|
|
214
255
|
|
|
215
|
-
@server
|
|
256
|
+
@_tool(server, ToolAnnotations(read_only_hint=True, open_world_hint=True))
|
|
216
257
|
async def explain_history(
|
|
217
|
-
repo:
|
|
218
|
-
symbol:
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
258
|
+
repo: Repo = None,
|
|
259
|
+
symbol: Annotated[
|
|
260
|
+
str | None,
|
|
261
|
+
Field(description='A function, class or method, e.g. "unsign" or "Signer.sign". Use this or path.'),
|
|
262
|
+
] = None,
|
|
263
|
+
path: Annotated[
|
|
264
|
+
str | None, Field(description="A file in the repo. Narrow it with start_line and end_line.")
|
|
265
|
+
] = None,
|
|
266
|
+
start_line: Annotated[int | None, Field(description="First line of the range (with path).")] = None,
|
|
267
|
+
end_line: Annotated[int | None, Field(description="Last line of the range (with path).")] = None,
|
|
268
|
+
max_commits: Annotated[int, Field(description="How many commits to return, newest first.")] = 8,
|
|
223
269
|
) -> HistoryResult:
|
|
224
|
-
"""Why code looks the way it does.
|
|
225
|
-
|
|
226
|
-
(with the relevant diff), and the
|
|
270
|
+
"""Why code looks the way it does. Use this before removing or rewriting code that looks odd, or
|
|
271
|
+
to answer "why is it like this?". Give a symbol, or a path with an optional line range. Returns
|
|
272
|
+
who wrote the current lines, every commit that changed them (with the relevant diff), and the
|
|
273
|
+
title and description of the pull request behind each commit (GitHub only)."""
|
|
227
274
|
report = await _call(lambda: brain.explain_history(repo, path, symbol, start_line, end_line, max_commits))
|
|
228
275
|
return _history(report)
|
|
229
276
|
|
|
230
|
-
@server
|
|
277
|
+
@_tool(server, READ_ONLY)
|
|
231
278
|
async def impact_of_change(
|
|
232
|
-
symbol:
|
|
279
|
+
symbol: Annotated[
|
|
280
|
+
str, Field(description='The function, class or method you plan to change, e.g. "discount" or "Cart.total".')
|
|
281
|
+
],
|
|
282
|
+
repo: Repo = None,
|
|
283
|
+
path: Annotated[
|
|
284
|
+
str | None, Field(description="The file that defines symbol, when several files define that name.")
|
|
285
|
+
] = None,
|
|
286
|
+
depth: Annotated[
|
|
287
|
+
int, Field(description="1 = direct users only; 2 or 3 also follows users of those users.")
|
|
288
|
+
] = 2,
|
|
233
289
|
) -> ImpactResult:
|
|
234
|
-
"""What breaks if
|
|
235
|
-
|
|
290
|
+
"""What breaks if a symbol changes: code that uses it directly, code that uses that code, and the
|
|
291
|
+
tests involved. Use this before editing a shared function, and run or update the tests it lists.
|
|
292
|
+
Python, JavaScript and TypeScript are analysed structurally. Matches are by name, so check the
|
|
293
|
+
same-name notes it returns."""
|
|
236
294
|
report = await _call(lambda: brain.impact_of_change(repo, symbol, path, depth))
|
|
237
295
|
return _impact(report)
|
|
238
296
|
|
|
239
|
-
@server
|
|
297
|
+
@_tool(server, ToolAnnotations(read_only_hint=True, open_world_hint=False))
|
|
240
298
|
async def find_doc_drift(
|
|
241
|
-
repo:
|
|
299
|
+
repo: Repo = None,
|
|
300
|
+
path_prefix: PathPrefix = None,
|
|
301
|
+
check_history: Annotated[
|
|
302
|
+
bool, Field(description="Also flag doc sections last edited long before the code they describe.")
|
|
303
|
+
] = True,
|
|
304
|
+
limit: Annotated[int, Field(description="Most findings to return, 1 to 200, most severe first.")] = 50,
|
|
242
305
|
) -> DriftResult:
|
|
243
|
-
"""Documentation that no longer matches the code
|
|
244
|
-
|
|
245
|
-
|
|
306
|
+
"""Documentation that no longer matches the code. Use this to check a README or docs folder, or
|
|
307
|
+
before a release. Finds files, functions, settings and flags that don't exist; code examples that
|
|
308
|
+
pass parameters a function doesn't take; imports that would fail; and stale sections. Each finding
|
|
309
|
+
has a severity, cites the doc section and suggests a fix. Changelogs are skipped."""
|
|
246
310
|
report = await _call(lambda: brain.find_doc_drift(repo, path_prefix, check_history, max(1, min(limit, 200))))
|
|
247
311
|
return _drift(brain.resolve_repo(repo), report)
|
|
248
312
|
|
|
249
|
-
@server
|
|
250
|
-
async def search_history(
|
|
251
|
-
|
|
313
|
+
@_tool(server, READ_ONLY)
|
|
314
|
+
async def search_history(
|
|
315
|
+
query: Annotated[
|
|
316
|
+
str, Field(description='Words from commit messages, e.g. "fix timezone" or "drop python 3.8".')
|
|
317
|
+
],
|
|
318
|
+
repo: Repo = None,
|
|
319
|
+
k: ResultCount = 5,
|
|
320
|
+
) -> list[CommitInfo]:
|
|
321
|
+
"""Find commits by what their messages say. Use this to answer "when did we change X?" or "which
|
|
322
|
+
commit fixed Y?". For the history of one function or file, use explain_history instead."""
|
|
252
323
|
commits = await _call(lambda: brain.search_history(query, repo, max(1, min(k, MAX_RESULTS))))
|
|
253
324
|
return [_commit(c) for c in commits]
|
|
254
325
|
|
|
255
|
-
@server
|
|
256
|
-
async def read_lines(
|
|
257
|
-
|
|
326
|
+
@_tool(server, READ_ONLY)
|
|
327
|
+
async def read_lines(
|
|
328
|
+
path: Annotated[str, Field(description="The file, relative to the repo root, as a citation shows it.")],
|
|
329
|
+
start: Annotated[int, Field(description="First line to return (1-based).")],
|
|
330
|
+
end: Annotated[int, Field(description="Last line to return (inclusive).")],
|
|
331
|
+
repo: Repo = None,
|
|
332
|
+
) -> str:
|
|
333
|
+
"""Exact, numbered lines from a file in an indexed repo. Use this to open a citation in full
|
|
334
|
+
before quoting or editing it, since search results are trimmed snippets. Refuses paths outside
|
|
335
|
+
the repo."""
|
|
258
336
|
try:
|
|
259
337
|
return brain.read_lines(repo, path, start, end)
|
|
260
338
|
except BrainError as exc:
|
|
261
339
|
raise ToolError(str(exc)) from exc
|
|
262
340
|
|
|
263
|
-
@server
|
|
341
|
+
@_tool(server, READ_ONLY)
|
|
264
342
|
async def list_repos() -> list[RepoSummary]:
|
|
265
|
-
"""Repositories indexed so far, with size, languages and the commit they were indexed at.
|
|
343
|
+
"""Repositories indexed so far, with their size, languages and the commit they were indexed at.
|
|
344
|
+
Use this first to see what's available and to get the repo names the other tools take."""
|
|
266
345
|
return [_summary(info) for info in brain.repos()]
|
|
267
346
|
|
|
268
347
|
@server.resource(
|
|
@@ -288,6 +367,15 @@ def build_server(brain: Brain | None = None) -> MCPServer:
|
|
|
288
367
|
return server
|
|
289
368
|
|
|
290
369
|
|
|
370
|
+
def _tool(server: MCPServer, annotations: ToolAnnotations) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
371
|
+
"""Register a tool with its docstring, dedented: clients show descriptions as they are sent."""
|
|
372
|
+
|
|
373
|
+
def register(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
374
|
+
return server.tool(annotations=annotations, description=inspect.cleandoc(fn.__doc__ or ""))(fn)
|
|
375
|
+
|
|
376
|
+
return register
|
|
377
|
+
|
|
378
|
+
|
|
291
379
|
async def _search(
|
|
292
380
|
brain: Brain, query: str, repo: str | None, k: int, path_prefix: str | None, kinds: list[str] | None
|
|
293
381
|
) -> list[Hit]:
|
|
@@ -97,7 +97,7 @@ def default_name(source: str) -> str:
|
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def resolve(source: str, clone_dir: Path) -> Path:
|
|
100
|
-
"""The repository's root on disk. URLs are
|
|
100
|
+
"""The repository's root on disk. URLs are cloned blobless (or refreshed) under ``clone_dir``."""
|
|
101
101
|
if not is_url(source):
|
|
102
102
|
root = Path(source).expanduser().resolve()
|
|
103
103
|
if not root.is_dir():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|