@esankhan3/code-search-mcp 1.1.4 → 1.1.6
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.
- package/README.md +294 -405
- package/dist/__tests__/config-resolver.test.js +33 -1
- package/dist/core/config.js +6 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,495 +1,384 @@
|
|
|
1
|
-
#
|
|
1
|
+
# code-search-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@esankhan3/code-search-mcp)
|
|
4
|
+
[](./LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
behind an MCP-compliant tool surface. Point Claude Code, Claude
|
|
7
|
-
Desktop, or Cursor at it and your agent gains hybrid search,
|
|
8
|
-
caller tracing, and impact analysis across every repo you've
|
|
9
|
-
indexed — local or remote.
|
|
6
|
+
**Code search for AI agents, across every repository you own.**
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
`code-search-mcp` is a Model Context Protocol server that indexes many
|
|
9
|
+
repositories at once and gives any MCP client (Claude Code, Claude Desktop,
|
|
10
|
+
Cursor, or your own agent) three things that `grep` and plain vector search
|
|
11
|
+
can't:
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
- **Hybrid search that finds the right code.** It handles exact identifiers,
|
|
14
|
+
partial names, error strings and plain-English questions in one query path.
|
|
15
|
+
- **A code graph.** Agents can see callers, callees, call chains, dead code,
|
|
16
|
+
and the blast radius of a diff.
|
|
17
|
+
- **A system graph across repositories.** It links the HTTP routes, gRPC
|
|
18
|
+
services, Kafka topics, database tables, env vars and shared types that
|
|
19
|
+
connect your services, so "who consumes this topic?" has an answer.
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
AST-aware chunks, cross-repo graph traversal, and a whole-project
|
|
24
|
-
view — without you writing a single tool.
|
|
21
|
+
It runs on a laptop over stdio, or as a shared HTTP server for a whole team.
|
|
22
|
+
It is tested at over a million indexed code chunks.
|
|
25
23
|
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
"code-search": {
|
|
31
|
-
"command": "code-search-mcp",
|
|
32
|
-
"args": ["--local", "/path/to/your/projects"]
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
24
|
+
```sh
|
|
25
|
+
npm install -g @esankhan3/code-search-mcp
|
|
26
|
+
ollama pull bge-m3 # or use an OpenAI / Voyage / Mistral key
|
|
27
|
+
claude mcp add code-search -- code-search-mcp --local ~/code
|
|
36
28
|
```
|
|
37
29
|
|
|
38
|
-
That's
|
|
30
|
+
That's it. Your agent now knows your codebase. The **[step-by-step guide](./RUNNING.md)**
|
|
31
|
+
covers every way to run it.
|
|
39
32
|
|
|
40
33
|
---
|
|
41
34
|
|
|
42
|
-
##
|
|
35
|
+
## Contents
|
|
36
|
+
|
|
37
|
+
- [What your agent can ask](#what-your-agent-can-ask)
|
|
38
|
+
- [Quick start](#quick-start)
|
|
39
|
+
- [Tools](#tools)
|
|
40
|
+
- [How it works](#how-it-works)
|
|
41
|
+
- [Performance at scale](#performance-at-scale)
|
|
42
|
+
- [Ways to run it](#ways-to-run-it)
|
|
43
|
+
- [Configuration](#configuration)
|
|
44
|
+
- [Security and operations](#security-and-operations)
|
|
45
|
+
- [Supported languages](#supported-languages)
|
|
46
|
+
- [Design principles](#design-principles)
|
|
47
|
+
- [Development](#development)
|
|
43
48
|
|
|
44
|
-
|
|
45
|
-
job — none of them need a separate install.
|
|
49
|
+
---
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
## What your agent can ask
|
|
52
|
+
|
|
53
|
+
| Question | Tool the agent reaches for |
|
|
48
54
|
|---|---|
|
|
49
|
-
|
|
|
50
|
-
| `
|
|
51
|
-
|
|
|
55
|
+
| "Where do we verify JWTs?" | `search_code`: semantic + keyword + symbol search, fused |
|
|
56
|
+
| "Find `CompanySearchResponse`." / "Anything named `*SearchResponse`?" | `search_exact`: exact and partial identifier match, no embedder needed |
|
|
57
|
+
| "Who calls `createInvoice`, and from which repos?" | `find_callers` |
|
|
58
|
+
| "If I change `billing/tax.ts`, what breaks?" | `impact_analysis` (follows dependents across repos) |
|
|
59
|
+
| "What does this diff touch?" | `detect_changes`: diff hunks → changed entities → their dependents |
|
|
60
|
+
| "How does a request get from `checkout` to `ledgerWrite`?" | `trace_path`: shortest call chain |
|
|
61
|
+
| "Which services read the `orders.created` topic?" | `get_cross_repo_edges` |
|
|
62
|
+
| "What does this repo do and how is it laid out?" | `get_repo_profile` / `get_architecture` |
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
**1. Install.** Requires Node.js 20 or newer and `git`.
|
|
52
69
|
|
|
53
|
-
### Standalone CLI
|
|
54
|
-
```sh
|
|
55
|
-
code-search index . # index current dir
|
|
56
|
-
code-search query "where do we verify JWTs" # hybrid by default
|
|
57
|
-
code-search query "createPet" --mode bm25 --top-k 5 # exact-identifier mode
|
|
58
|
-
code-search query --project petshop --format json "adopt pet" # flags in any order
|
|
59
|
-
code-search status --project petshop # JSON report
|
|
60
|
-
code-search reset --project petshop # drop the index
|
|
61
|
-
code-search --print-config # resolved config (redacted)
|
|
62
|
-
```
|
|
63
|
-
Every `CodeSearchConfig` leaf is settable as `--<dotted-path>` —
|
|
64
|
-
e.g. `--embedding.provider codestral`, `--retrieval.max-chunks 12`,
|
|
65
|
-
`--no-indexing.auto-index`. An unknown flag (or an invalid `--mode` /
|
|
66
|
-
`--format` / `--top-k`) exits with status 2 instead of being ignored.
|
|
67
|
-
`query`, `status` and `--print-config` read the cwd's
|
|
68
|
-
`.code-search.yaml`; `index <path>` reads the one in `<path>`.
|
|
69
|
-
`status` reports where the KB lives (`dataDir`, `kbPath`), whether the
|
|
70
|
-
project is indexed, and whether a daemon answers on its socket.
|
|
71
|
-
`reset` only removes `<dataDir>/<project>` and rejects project names that
|
|
72
|
-
aren't a single path segment (or are the reserved `daemon` / `projects`).
|
|
73
|
-
|
|
74
|
-
### Daemon mode (long-running indexer)
|
|
75
70
|
```sh
|
|
76
|
-
code-search-
|
|
77
|
-
code-search daemon --workspace . # same, via the CLI wrapper
|
|
71
|
+
npm install -g @esankhan3/code-search-mcp
|
|
78
72
|
```
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
|
|
74
|
+
This installs three commands: `code-search` (CLI), `code-search-mcp`
|
|
75
|
+
(MCP server) and `code-search-daemon` (watch-and-reindex).
|
|
76
|
+
|
|
77
|
+
**2. Pick an embedder.** The default (`auto`) uses the first one it finds:
|
|
78
|
+
|
|
79
|
+
| You have | It uses |
|
|
80
|
+
|---|---|
|
|
81
|
+
| `CODE_SEARCH_EMBEDDING_BASE_URL` set | any OpenAI-compatible endpoint |
|
|
82
|
+
| Ollama running locally | Ollama, model `bge-m3` (fully offline) |
|
|
83
|
+
| `MISTRAL_API_KEY` | Codestral Embed |
|
|
84
|
+
| `OPENAI_API_KEY` | OpenAI |
|
|
85
|
+
| `VOYAGE_API_KEY` | Voyage |
|
|
86
|
+
| `GEMINI_API_KEY` / `GOOGLE_API_KEY` or a logged-in Gemini CLI | Gemini |
|
|
87
|
+
|
|
88
|
+
**3. Index and query from the terminal.**
|
|
89
|
+
|
|
94
90
|
```sh
|
|
95
|
-
code-search
|
|
96
|
-
code-search
|
|
91
|
+
code-search index ~/code --project acme # one repo, or a folder full of repos
|
|
92
|
+
code-search query --project acme "where do we retry failed webhooks"
|
|
93
|
+
code-search query --project acme "WebhookRetryPolicy" --mode bm25
|
|
97
94
|
```
|
|
98
|
-
Spawns a stdio MCP server that forwards to a remote HTTP server.
|
|
99
|
-
Zero local setup, zero local index — perfect for hosted
|
|
100
|
-
deployments where the index lives in the cloud and dev machines
|
|
101
|
-
stay light. Auth via `CODE_SEARCH_API_KEY`.
|
|
102
95
|
|
|
103
|
-
**
|
|
96
|
+
**4. Connect your MCP client.**
|
|
97
|
+
|
|
104
98
|
```sh
|
|
105
|
-
|
|
106
|
-
code-search-mcp --local
|
|
99
|
+
# Claude Code
|
|
100
|
+
claude mcp add code-search -- code-search-mcp --local ~/code --project acme
|
|
107
101
|
```
|
|
108
|
-
Discovers every repo under a path (or clones a GitHub org), builds
|
|
109
|
-
the knowledge base, and serves over stdio. Works fully offline if
|
|
110
|
-
your embedder + reranker are local (Ollama).
|
|
111
102
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
```jsonc
|
|
104
|
+
// Claude Desktop (claude_desktop_config.json) or Cursor (~/.cursor/mcp.json)
|
|
105
|
+
{
|
|
106
|
+
"mcpServers": {
|
|
107
|
+
"code-search": {
|
|
108
|
+
"command": "code-search-mcp",
|
|
109
|
+
"args": ["--local", "/Users/me/code", "--project", "acme"]
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
115
113
|
```
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`POST /index`. The port is bound before the startup index runs, so
|
|
120
|
-
probes answer from the start (`/ready` is 503 until the index is
|
|
121
|
-
servable). `--serve <path>`, `--serve --local <path>` and
|
|
122
|
-
`--local <path> --serve` are equivalent; the project is `default`
|
|
123
|
-
unless `--project` says otherwise. Use this to host one index for a
|
|
124
|
-
whole team — every dev points their client at the same URL.
|
|
125
|
-
|
|
126
|
-
`code-search mcp [path]` is the stdio local mode over the cwd (or
|
|
127
|
-
`path`); `code-search mcp --remote URL` is the remote proxy.
|
|
114
|
+
|
|
115
|
+
The guide covers team servers, Docker, GitHub orgs, object storage and
|
|
116
|
+
troubleshooting: **[RUNNING.md](./RUNNING.md)**.
|
|
128
117
|
|
|
129
118
|
---
|
|
130
119
|
|
|
131
|
-
##
|
|
120
|
+
## Tools
|
|
132
121
|
|
|
133
|
-
Seventeen tools
|
|
134
|
-
|
|
135
|
-
retrieval pipeline that powers the Anvil dashboard.
|
|
122
|
+
Seventeen tools in four groups. Every tool validates its arguments against
|
|
123
|
+
its schema and returns `isError: true` with a reason when they are wrong.
|
|
136
124
|
|
|
137
125
|
### Search
|
|
126
|
+
|
|
138
127
|
| Tool | What it does |
|
|
139
128
|
|---|---|
|
|
140
|
-
| `search_code` | Hybrid
|
|
141
|
-
| `search_semantic` | Vector-only
|
|
142
|
-
| `search_exact` |
|
|
143
|
-
| `get_code_snippet` |
|
|
129
|
+
| `search_code` | Hybrid search: vector + keyword + symbol + phrase tiers, fused, then expanded through the code graph and optionally reranked |
|
|
130
|
+
| `search_semantic` | Vector-only: paraphrases and intent |
|
|
131
|
+
| `search_exact` | Literal search led by exact and partial symbol-name and exact-phrase hits; works without an embedder |
|
|
132
|
+
| `get_code_snippet` | The source of one entity by name, cheaper than reading the whole file |
|
|
144
133
|
|
|
145
134
|
### Graph
|
|
135
|
+
|
|
146
136
|
| Tool | What it does |
|
|
147
137
|
|---|---|
|
|
148
|
-
| `
|
|
149
|
-
| `
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `
|
|
154
|
-
| `
|
|
155
|
-
| `
|
|
156
|
-
| `
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
### Profiles
|
|
165
|
-
| Tool | What it does |
|
|
166
|
-
|---|---|
|
|
167
|
-
| `list_repos` | All indexed repos with profiles |
|
|
168
|
-
| `get_repo_profile` | Single repo's tech stack, structure, key entry points |
|
|
138
|
+
| `find_callers` / `find_dependencies` | Who calls a symbol, and what it calls. `calls` edges by default; `relations` adds imports, uses, type refs and inheritance |
|
|
139
|
+
| `trace_path` | Shortest call chain between two symbols, or everything reachable from one |
|
|
140
|
+
| `impact_analysis` | Dependents of a file or entity, up to 5 hops, across repositories |
|
|
141
|
+
| `detect_changes` | Maps a git diff to the entities whose lines changed, plus their dependents |
|
|
142
|
+
| `search_graph` | Structural query: nodes by name (regex), type, file or repo, ranked by connectivity |
|
|
143
|
+
| `find_dead_code` | Entities nothing references (heuristic) |
|
|
144
|
+
| `get_repo_graph` | One repository's entities and edges |
|
|
145
|
+
| `get_cross_repo_edges` | Links between repositories, each with the evidence that produced it |
|
|
146
|
+
| `get_architecture` | Project overview: repository roles, key flows, connections |
|
|
147
|
+
|
|
148
|
+
Symbols are matched by exact name or full `repo::file::entity` id.
|
|
149
|
+
`fuzzy: true` matches a case-insensitive substring. Multi-hop traversals are
|
|
150
|
+
bounded (50k nodes, 1 s), so a hub symbol in a large org gets a partial
|
|
151
|
+
answer that says so, never a hang.
|
|
152
|
+
|
|
153
|
+
### Profiles and index
|
|
169
154
|
|
|
170
|
-
### Index
|
|
171
155
|
| Tool | What it does |
|
|
172
156
|
|---|---|
|
|
173
|
-
| `
|
|
174
|
-
|
|
175
|
-
Plus four MCP resources via `code-search://`:
|
|
176
|
-
`repos`, `system-graph`, `repo/{name}/profile`, `repo/{name}/graph`.
|
|
177
|
-
|
|
178
|
-
---
|
|
157
|
+
| `list_repos` / `get_repo_profile` | Each repository's tech stack, structure and entry points |
|
|
158
|
+
| `index_status` | Chunks per repo, embedder, last index time, vector-index health |
|
|
179
159
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
This isn't a thin wrapper. The retrieval pipeline runs:
|
|
183
|
-
|
|
184
|
-
1. **Vector ⫽ BM25 in parallel** — semantic + lexical recall.
|
|
185
|
-
2. **Reciprocal Rank Fusion** — combine without one dominating.
|
|
186
|
-
3. **AST tripartite expansion** — pull in callers, callees, type
|
|
187
|
-
refs via the project graph.
|
|
188
|
-
4. **Cross-encoder rerank** — Qwen3-Reranker by default; Cohere /
|
|
189
|
-
Voyage / OpenAI-compatible swappable.
|
|
190
|
-
|
|
191
|
-
A query classifier picks adaptive weights — identifier queries
|
|
192
|
-
lean BM25, natural-language leans vector, error codes lean both.
|
|
193
|
-
You don't tune anything; the retriever does it per query.
|
|
160
|
+
Plus MCP resources: `code-search://repos`, `code-search://system-graph`,
|
|
161
|
+
`code-search://repo/{name}/profile`, `code-search://repo/{name}/graph`.
|
|
194
162
|
|
|
195
163
|
---
|
|
196
164
|
|
|
197
|
-
##
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
165
|
+
## How it works
|
|
166
|
+
|
|
167
|
+
```mermaid
|
|
168
|
+
flowchart LR
|
|
169
|
+
subgraph Index["Indexing (incremental)"]
|
|
170
|
+
direction TB
|
|
171
|
+
A[Discover repos] --> B[Change check per repo<br/>git SHA + working-tree snapshot]
|
|
172
|
+
B --> C[AST chunking<br/>tree-sitter, 8 languages]
|
|
173
|
+
C --> D[Per-repo code graph<br/>calls, imports, types, inheritance]
|
|
174
|
+
C --> E[Cross-repo signals<br/>routes, topics, tables, env vars, ...]
|
|
175
|
+
E --> F[Hash-join correlation<br/>14 strategies]
|
|
176
|
+
D & F --> G[(System graph<br/>SQLite)]
|
|
177
|
+
C --> H[Embed only changed chunks<br/>reuse vectors by content hash]
|
|
178
|
+
H --> I[(LanceDB<br/>vectors + full-text + trigram indexes)]
|
|
179
|
+
end
|
|
180
|
+
subgraph Query["Query"]
|
|
181
|
+
direction TB
|
|
182
|
+
Q[Query] --> K[Classify:<br/>identifier, path, error, natural language]
|
|
183
|
+
K --> V[Vector] & BM[Keyword BM25<br/>tokens + identifier parts] & S[Symbol names<br/>exact + trigram substring] & P[Exact phrase]
|
|
184
|
+
V & BM & S & P --> R[Weighted rank fusion<br/>+ exact-symbol pin + dedupe]
|
|
185
|
+
R --> X[Graph expansion<br/>callers, callees, type refs]
|
|
186
|
+
X --> RR[Optional cross-encoder rerank]
|
|
187
|
+
end
|
|
188
|
+
I --> V & BM & S & P
|
|
189
|
+
G --> X
|
|
190
|
+
```
|
|
204
191
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
192
|
+
**Chunks follow the code's structure.** Files split at definition boundaries
|
|
193
|
+
(functions, classes, methods, top-level constants), not every N lines. Each
|
|
194
|
+
chunk carries a context prefix (file path, module and imports) that is
|
|
195
|
+
embedded with it.
|
|
196
|
+
|
|
197
|
+
**Four retrieval tiers run in parallel.** Vector search catches intent.
|
|
198
|
+
BM25 runs over the raw tokens and over the parts of compound identifiers, so
|
|
199
|
+
"expected backlog count" finds `verifyExpectedBacklogCount`. A symbol tier
|
|
200
|
+
matches entity names exactly and as substrings through a trigram index, so
|
|
201
|
+
`SearchResponse` finds `CompanySearchResponse`. A phrase tier matches error
|
|
202
|
+
strings word for word. A query classifier weights the tiers per query:
|
|
203
|
+
identifiers lean on keywords, questions lean on vectors.
|
|
204
|
+
|
|
205
|
+
**The graph does more than draw diagrams.** It feeds retrieval (seed hits pull in
|
|
206
|
+
their callers and callees) and answers structural questions directly.
|
|
207
|
+
Cross-repository edges come from 14 detectors: npm and workspace
|
|
208
|
+
dependencies, shared types, env vars, Kafka and other topics, HTTP routes
|
|
209
|
+
(including template-literal client URLs), gRPC services, database schemas,
|
|
210
|
+
Redis keys, S3 buckets, OpenAPI contracts, docker-compose and Kubernetes
|
|
211
|
+
service references, and shared constants.
|
|
212
|
+
|
|
213
|
+
**The index maintains itself.**
|
|
214
|
+
- Each cycle touches only what changed:
|
|
215
|
+
- repositories are checked concurrently;
|
|
216
|
+
- only changed files are re-chunked;
|
|
217
|
+
- only chunks whose text changed are re-embedded;
|
|
218
|
+
- the system graph is updated in place.
|
|
219
|
+
- The vector index retrains itself as the corpus grows.
|
|
220
|
+
- The number of IVF partitions probed per query is auto-calibrated to hit
|
|
221
|
+
0.95 recall against an exact scan.
|
|
222
|
+
- The embedder is pinned per index: querying with a different model or
|
|
223
|
+
dimension size is an error, never silently wrong results.
|
|
212
224
|
|
|
213
225
|
---
|
|
214
226
|
|
|
215
|
-
##
|
|
227
|
+
## Performance at scale
|
|
216
228
|
|
|
217
|
-
|
|
218
|
-
|
|
229
|
+
Measured on one 12-core laptop, over 1.15 million chunks across 360
|
|
230
|
+
repositories, with 600 mixed queries (exact identifiers, partial names,
|
|
231
|
+
natural language and phrases). The corpus was built by replicating real
|
|
232
|
+
repositories. A stub embedder was used, so the timings exclude
|
|
233
|
+
embedding-provider latency.
|
|
219
234
|
|
|
220
|
-
|
|
|
235
|
+
| | |
|
|
221
236
|
|---|---|
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
|
|
|
225
|
-
|
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
are PII-shaped).
|
|
237
|
+
| `search_code` latency (p50, 1 client) | **36 ms** |
|
|
238
|
+
| `search_exact` latency (p50, 1 client) | **17 ms** |
|
|
239
|
+
| Throughput, 8 concurrent clients | **26 q/s** (`search_code`), **39 q/s** (`search_exact`) |
|
|
240
|
+
| Vector recall@10 vs exact scan (auto-calibrated) | **0.977** |
|
|
241
|
+
| Re-index, nothing changed | **~15 s** |
|
|
242
|
+
| Re-index after a one-file edit (chunks, graph, embeddings) | **~30–70 s** |
|
|
243
|
+
| Cross-repository edges in the system graph | **454k** |
|
|
244
|
+
|
|
245
|
+
On the same queries, the expected file ranked in the top 10 for 100% of
|
|
246
|
+
exact-identifier queries in both search tools. For natural-language
|
|
247
|
+
questions it was 91% with `search_code` and 80% with `search_exact`.
|
|
234
248
|
|
|
235
249
|
---
|
|
236
250
|
|
|
237
|
-
##
|
|
251
|
+
## Ways to run it
|
|
238
252
|
|
|
239
|
-
|
|
253
|
+
| Mode | Command | Use it for |
|
|
254
|
+
|---|---|---|
|
|
255
|
+
| **Local, stdio** | `code-search-mcp --local ~/code` | One developer; the index lives on your machine |
|
|
256
|
+
| **GitHub org** | `code-search-mcp --local "github:my-org/api-*"` | Clone or refresh matching repos, then serve them |
|
|
257
|
+
| **Team server** | `code-search-mcp --serve ~/code --auth api-key` | One index for everyone, over Streamable HTTP at `/mcp` |
|
|
258
|
+
| **Remote proxy** | `code-search-mcp --remote https://search.internal` | A thin local stdio bridge to a team server; nothing indexed locally |
|
|
259
|
+
| **Daemon** | `code-search-daemon --workspace ~/code` | Watch files and re-index on change; the CLI queries it over a local socket |
|
|
260
|
+
| **Docker** | `docker build -f Dockerfile.all-in-one …` | One image with Ollama, the embedder and reranker models baked in |
|
|
240
261
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
max_embedding_cost_usd: 5
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
The HTTP server reads the entry of the project it serves once at
|
|
250
|
-
boot and enforces it on every `/mcp` request: the caller's scopes
|
|
251
|
-
must match `scopes` (`*` is admin; API keys are admin; a JWT's
|
|
252
|
-
scopes come from its `scope` claim) or it gets 403, and
|
|
253
|
-
`max_queries_per_minute` caps requests per `(identity, project)` in
|
|
254
|
-
a sliding 1-minute window (429 + `Retry-After`). Not enforced:
|
|
255
|
-
`repos`, `max_embedding_cost_usd` / `max_llm_cost_usd`, and any
|
|
256
|
-
`config:` block (per-project config comes from the workspace's
|
|
257
|
-
`.code-search.yaml`).
|
|
258
|
-
|
|
259
|
-
---
|
|
262
|
+
The team server binds its port before the first index runs, so health
|
|
263
|
+
probes answer immediately (`/ready` is 503 until the index can serve).
|
|
264
|
+
It serves every `/mcp` request statelessly, which makes restarts free and
|
|
265
|
+
lets it scale across cores with `CODE_SEARCH_WORKERS=N`. In that mode one
|
|
266
|
+
primary does all writes and N workers serve.
|
|
260
267
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
### Auth, three flavors
|
|
264
|
-
`none` (local-only, default 127.0.0.1 binding), `api-key`
|
|
265
|
-
(`Authorization: Bearer ...`, timing-safe compare), `jwt` (HS256;
|
|
266
|
-
signature, `iss`, a required `exp` and, when present, `nbf`
|
|
267
|
-
validated; the server refuses to start in `jwt` mode without
|
|
268
|
-
`CODE_SEARCH_AUTH_JWT_SECRET`). Public binding without auth logs a
|
|
269
|
-
warning, because that's what should happen.
|
|
270
|
-
|
|
271
|
-
### Rate limiting
|
|
272
|
-
`CODE_SEARCH_RATE_LIMIT_PER_MINUTE` (default 100) requests per
|
|
273
|
-
identity in a sliding 1-minute window (each API key counts on its
|
|
274
|
-
own); 429 responses carry `Retry-After`. In cluster mode the limit
|
|
275
|
-
applies to the whole server, not per worker.
|
|
276
|
-
|
|
277
|
-
### Auto-reindex
|
|
278
|
-
`CODE_SEARCH_REINDEX_INTERVAL=30m` (or `1h`, `6h`, `0` to disable;
|
|
279
|
-
also `indexing.reindex_interval_ms` in YAML or
|
|
280
|
-
`--indexing.reindex-interval-ms`) runs an incremental re-index in
|
|
281
|
-
the background. Anything else (e.g. `90s`) is reported as invalid and
|
|
282
|
-
disables it. Skips itself if a manual index is already running. The
|
|
283
|
-
timer is `unref`'d so it doesn't keep the process alive.
|
|
284
|
-
|
|
285
|
-
### Status tracking
|
|
286
|
-
Every index call lands in a 50-entry FIFO history with start
|
|
287
|
-
timestamp, success / error, last duration. Available at
|
|
288
|
-
`GET /status`.
|
|
289
|
-
|
|
290
|
-
### Stateless Streamable HTTP
|
|
291
|
-
Every `/mcp` request is served statelessly by a fresh Server
|
|
292
|
-
instance: modern (2026-07-28) clients negotiate via
|
|
293
|
-
`server/discover`; 2025-era clients are answered by the SDK's
|
|
294
|
-
per-request legacy fallback. No session map, no TTL.
|
|
295
|
-
|
|
296
|
-
### Admin index endpoint
|
|
297
|
-
`POST /index` with `{ "path": "/repos", "force"?: true }` (re)indexes
|
|
298
|
-
the served project from `path`, which then becomes its directory for
|
|
299
|
-
scheduled reindexes. `project`, if given, must be the served project
|
|
300
|
-
(a server never switches projects — 400 otherwise). Invalid JSON or a
|
|
301
|
-
missing `path` → 400, a body over 64 KiB → 413, an index already
|
|
302
|
-
running → 409. Auth-required when auth is on. The response reports
|
|
303
|
-
chunks, repos and cross-repo edges.
|
|
304
|
-
|
|
305
|
-
### Cluster mode
|
|
306
|
-
`CODE_SEARCH_WORKERS=N` (2–16) forks N HTTP workers sharing the port.
|
|
307
|
-
The primary is the only writer (startup index, `POST /index` forwarded
|
|
308
|
-
over IPC, scheduled reindex) and pushes its indexing state to the
|
|
309
|
-
workers, so `/status`, `/ready` and `/admin/api/status` look the same
|
|
310
|
-
on every worker; rate limits and quotas are counted once for the whole
|
|
311
|
-
server.
|
|
268
|
+
For stateless pods, vectors can live in S3 or MinIO (`s3://` LanceDB URIs) and
|
|
269
|
+
index artifacts in MongoDB. See [RUNNING.md § Object storage](./RUNNING.md#9-stateless-deployments-object-storage).
|
|
312
270
|
|
|
313
271
|
---
|
|
314
272
|
|
|
315
|
-
##
|
|
273
|
+
## Configuration
|
|
316
274
|
|
|
317
|
-
|
|
318
|
-
┌─────────────────────────┐
|
|
319
|
-
│ MCP client │
|
|
320
|
-
│ (Claude Code, │
|
|
321
|
-
│ Claude Desktop, │
|
|
322
|
-
│ Cursor, …) │
|
|
323
|
-
└────────────┬────────────┘
|
|
324
|
-
│ stdio / HTTP
|
|
325
|
-
▼
|
|
326
|
-
┌─────────────────────────┐
|
|
327
|
-
│ code-search-mcp │
|
|
328
|
-
│ (this package) │
|
|
329
|
-
│ tools + resources │
|
|
330
|
-
└────────────┬────────────┘
|
|
331
|
-
│ wraps
|
|
332
|
-
▼
|
|
333
|
-
┌─────────────────────────┐
|
|
334
|
-
│ @anvil/knowledge-core │
|
|
335
|
-
│ AST chunks + │
|
|
336
|
-
│ project graph + │
|
|
337
|
-
│ hybrid retriever │
|
|
338
|
-
└────────────┬────────────┘
|
|
339
|
-
│ on disk
|
|
340
|
-
▼
|
|
341
|
-
<dataDir>/<project>/
|
|
342
|
-
(default ~/.anvil/knowledge-base/)
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
Three different fronts, one knowledge stack:
|
|
275
|
+
Five layers, lowest to highest precedence:
|
|
346
276
|
|
|
347
|
-
-
|
|
348
|
-
|
|
349
|
-
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
client.
|
|
277
|
+
1. built-in defaults
|
|
278
|
+
2. `~/.code-search/config.yaml` (per user)
|
|
279
|
+
3. `<workspace>/.code-search.yaml` (per workspace)
|
|
280
|
+
4. `CODE_SEARCH_*` environment variables
|
|
281
|
+
5. CLI flags: any setting as `--<dotted.path> <value>`, e.g. `--retrieval.max-chunks 12`
|
|
353
282
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
LanceDB store, same graph files.
|
|
357
|
-
|
|
358
|
-
---
|
|
359
|
-
|
|
360
|
-
## Configuration — defaults → file → env → CLI flags
|
|
361
|
-
|
|
362
|
-
Single resolver in `src/core/config.ts`. Five precedence layers
|
|
363
|
-
(lowest → highest):
|
|
364
|
-
|
|
365
|
-
1. compiled-in `DEFAULTS`
|
|
366
|
-
2. `~/.code-search/config.yaml` (user-global)
|
|
367
|
-
3. `<workspaceDir>/.code-search.yaml` (per-workspace overlay)
|
|
368
|
-
4. `CODE_SEARCH_*` env vars
|
|
369
|
-
5. CLI flags via `--<dotted-path>` (e.g. `--embedding.provider codestral`)
|
|
370
|
-
|
|
371
|
-
Run `code-search --print-config` or `code-search-mcp --print-config`
|
|
372
|
-
any time to see the resolved struct (secrets redacted — including
|
|
373
|
-
passwords in URIs and OTEL header values). `--port`, `--host`,
|
|
374
|
-
`--transport` and `--auth` are shorthands for `server.port`,
|
|
375
|
-
`server.host`, `server.transport` and `auth.mode`.
|
|
376
|
-
|
|
377
|
-
| Var (`CODE_SEARCH_*`) | Effect |
|
|
378
|
-
|---|---|
|
|
379
|
-
| `SERVER` | Remote URL (proxy mode) |
|
|
380
|
-
| `API_KEY` | API key for proxy or serve modes |
|
|
381
|
-
| `DATA_DIR` | Index storage root (default `~/.anvil/knowledge-base`; also `storage.data_dir` / `--storage.data-dir`). Daemon sockets live under `<dataDir>/daemon/` |
|
|
382
|
-
| `PORT` / `HOST` / `TRANSPORT` | HTTP server bind |
|
|
383
|
-
| `AUTH_MODE` / `AUTH_API_KEYS` / `AUTH_JWT_SECRET` | Server auth |
|
|
384
|
-
| `REINDEX_INTERVAL` | `30m` / `1h` / `6h` / `0`; invalid → disabled |
|
|
385
|
-
| `RATE_LIMIT_PER_MINUTE` | Per-identity request limit (sliding window) |
|
|
386
|
-
| `WORKERS` | `>1` → cluster mode |
|
|
387
|
-
| `EMBEDDING_PROVIDER` | `auto` / `codestral` / `voyage` / `openai` / `ollama` / `openai-compatible` / `custom` / `gemini-oauth` |
|
|
388
|
-
| `EMBEDDING_MODEL` / `EMBEDDING_DIMENSIONS` | Model + dims |
|
|
389
|
-
| `EMBEDDING_API_KEY` / `EMBEDDING_BASE_URL` | Credentials |
|
|
390
|
-
| `OLLAMA_HOST` | Default `http://localhost:11434` |
|
|
391
|
-
| `RERANKER_PROVIDER` | `ollama` (default) / `cohere` / `voyage` / `openai-compatible` / `custom` / `none` |
|
|
392
|
-
| `RERANKER_MODEL` / `RERANKER_API_KEY` / `RERANKER_BASE_URL` | Reranker config |
|
|
393
|
-
| `RETRIEVAL_MAX_CHUNKS` / `RETRIEVAL_MAX_TOKENS` | Retrieval tuning |
|
|
394
|
-
| `AUTO_INDEX` | `false` to disable startup auto-index |
|
|
395
|
-
| `DAEMON_DISABLED` | `code-search query` never uses a daemon |
|
|
396
|
-
| `LLM_MODE` / `LLM_PROVIDER` / `LLM_MODEL` / `LLM_API_KEY` / `LLM_BASE_URL` | Profiling + service-mesh LLM (`LLM_MODE` defaults to `none`). Pre-promoted to `ANVIL_LLM_*` so the agent-core integration is silent. |
|
|
397
|
-
| `INDEXING_DEBOUNCE_MS` / `CHUNKING_MAX_TOKENS` | Indexer knobs |
|
|
398
|
-
| `METRICS_ENABLED` / `STRUCTURED_LOGS` / `TELEMETRY_RECORD_QUERIES` | Observability |
|
|
399
|
-
|
|
400
|
-
### Per-workspace config
|
|
401
|
-
|
|
402
|
-
Drop a `.code-search.yaml` at the root of your workspace:
|
|
283
|
+
`code-search --print-config` prints the resolved result with secrets
|
|
284
|
+
redacted. An unknown flag exits with status 2 instead of being ignored.
|
|
403
285
|
|
|
404
286
|
```yaml
|
|
287
|
+
# .code-search.yaml
|
|
405
288
|
embedding:
|
|
406
|
-
provider: codestral
|
|
407
|
-
model:
|
|
408
|
-
dimensions:
|
|
289
|
+
provider: openai # auto | ollama | openai | voyage | codestral | openai-compatible | gemini-oauth | custom
|
|
290
|
+
model: text-embedding-3-small
|
|
291
|
+
dimensions: 1536
|
|
409
292
|
reranker:
|
|
410
|
-
provider: ollama
|
|
411
|
-
model: qwen2.5-coder:7b # the default that actually answers
|
|
293
|
+
provider: none # ollama (default, qwen2.5-coder:7b) | cohere | voyage | openai-compatible | none
|
|
412
294
|
retrieval:
|
|
413
295
|
max_chunks: 12
|
|
414
|
-
hybrid_weights: { vector: 0.55, bm25: 0.3, graph: 0.15 }
|
|
415
296
|
indexing:
|
|
416
|
-
|
|
417
|
-
debounce_ms: 500
|
|
297
|
+
reindex_interval_ms: 1800000 # team server: re-index every 30 min
|
|
418
298
|
```
|
|
419
299
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
`
|
|
300
|
+
| Variable (`CODE_SEARCH_…`) | Purpose |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `EMBEDDING_PROVIDER` / `_MODEL` / `_DIMENSIONS` / `_API_KEY` / `_BASE_URL` | Embedder |
|
|
303
|
+
| `RERANKER_PROVIDER` / `_MODEL` / `_API_KEY` / `_BASE_URL` | Reranker (`none` to disable) |
|
|
304
|
+
| `DATA_DIR` | Where indexes live (default `~/.anvil/knowledge-base`) |
|
|
305
|
+
| `PORT` / `HOST` | HTTP bind (default `127.0.0.1:3100`) |
|
|
306
|
+
| `AUTH_MODE` / `AUTH_API_KEYS` / `AUTH_JWT_SECRET` | `none` / `api-key` / `jwt` |
|
|
307
|
+
| `REINDEX_INTERVAL` | `30m`, `1h`, `6h` or `0` |
|
|
308
|
+
| `WORKERS` | HTTP worker processes (2–16) |
|
|
309
|
+
| `SERVER` / `API_KEY` | Remote-proxy target and its key |
|
|
310
|
+
| `VECTOR_NPROBES` | Fix IVF probes per query (overrides auto-calibration; lower = faster, less recall) |
|
|
311
|
+
| `STORAGE_VECTOR_URI`, `STORAGE_BLOB_BACKEND`, `STORAGE_MONGO_*`, `STORAGE_S3_*` | Object-storage deployment |
|
|
312
|
+
|
|
313
|
+
The full list is in [RUNNING.md § Configuration reference](./RUNNING.md#configuration-reference).
|
|
423
314
|
|
|
424
315
|
---
|
|
425
316
|
|
|
426
|
-
##
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
317
|
+
## Security and operations
|
|
318
|
+
|
|
319
|
+
- **Auth.**
|
|
320
|
+
- `none`: binds to `127.0.0.1` only.
|
|
321
|
+
- `api-key`: `Authorization: Bearer …`, compared in constant time.
|
|
322
|
+
- `jwt`: HS256; signature, issuer and a required `exp` are verified, plus `nbf` when present. The server refuses to start without a secret.
|
|
323
|
+
- A public bind without auth logs a warning.
|
|
324
|
+
- **Limits.** A per-identity sliding-window rate limit (default 100/min).
|
|
325
|
+
Per-project scopes and query quotas come from
|
|
326
|
+
`<dataDir>/projects/<name>/project.yaml`.
|
|
327
|
+
- **Endpoints.**
|
|
328
|
+
- `GET /health`, `/ready`, `/version`, `/status`.
|
|
329
|
+
- `GET /metrics`: Prometheus metrics for query counts, latency histogram, errors, index age, chunks per repo and vector-index health.
|
|
330
|
+
- `GET /admin/api/status`.
|
|
331
|
+
- `POST /index`: re-index on demand.
|
|
332
|
+
- **Logs and tracing.** `CODE_SEARCH_STRUCTURED_LOGS=1` emits JSON log lines.
|
|
333
|
+
Query text is kept out of traces unless you opt in with
|
|
334
|
+
`CODE_SEARCH_TELEMETRY_RECORD_QUERIES=1`.
|
|
430
335
|
|
|
431
|
-
|
|
432
|
-
semantic edges) routes through `@anvil/agent-core`'s single-shot —
|
|
433
|
-
the same router, retries, and cost ledger as the rest of Anvil.
|
|
336
|
+
---
|
|
434
337
|
|
|
435
|
-
|
|
436
|
-
solo, serve for teams. The dispatcher is `src/index.ts:argv`.
|
|
338
|
+
## Supported languages
|
|
437
339
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
340
|
+
| Support | Languages / files |
|
|
341
|
+
|---|---|
|
|
342
|
+
| AST chunking, code graph, cross-repo signals | TypeScript, TSX, JavaScript, Python, Go, Java, Rust, PHP |
|
|
343
|
+
| Indexed for search | the above, plus shell, YAML, Terraform/HCL, TOML, `Dockerfile`, `Makefile`, `Jenkinsfile` |
|
|
441
344
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
signature + exp (required) + nbf + iss validated. Public binding
|
|
445
|
-
without auth logs a warning instead of pretending it's fine.
|
|
345
|
+
`.gitignore` is respected, and `node_modules`, `dist`, `build`, `vendor`,
|
|
346
|
+
`target` and similar directories are skipped.
|
|
446
347
|
|
|
447
348
|
---
|
|
448
349
|
|
|
449
|
-
##
|
|
350
|
+
## Design principles
|
|
450
351
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
352
|
+
- **Multi-repo first.** The interesting questions cross repository
|
|
353
|
+
boundaries, so the index, the graph and the tools all do too.
|
|
354
|
+
- **Scale by doing less.** Every index cycle costs in proportion to what
|
|
355
|
+
changed, not to the size of the corpus. Every query tier is index-backed.
|
|
356
|
+
Every graph traversal is bounded.
|
|
357
|
+
- **Never silently wrong.** An embedder mismatch is a hard error. Truncated
|
|
358
|
+
traversals say they were truncated. Bad tool arguments return the reason.
|
|
359
|
+
- **Local by default, shared when you want.** The same binary serves one
|
|
360
|
+
laptop over stdio or a whole team over HTTP.
|
|
361
|
+
- **Few moving parts.** Embedded LanceDB and SQLite, no database server to
|
|
362
|
+
run, a hand-rolled HTTP layer and zero-dependency metrics.
|
|
454
363
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
| **T2** | `code-search index` produces a real Ollama-backed index; no stale legacy-env warning |
|
|
459
|
-
| **T3** | `code-search query --mode vector` and `--mode bm25` both return the right file |
|
|
460
|
-
| **T4** | `code-search status` reports provider + chunk count |
|
|
461
|
-
| **T5** | Issue-#6 reproduction: `CODE_SEARCH_EMBEDDING_PROVIDER=openai` actually constructs the OpenAI embedder (skipped when `OPENAI_API_KEY` is unset) |
|
|
462
|
-
| **T6** | Vector-space mismatch guard hard-errors on cross-provider query |
|
|
463
|
-
| **T7** | Daemon round-trip: spawn, UDS socket, query through RPC, file-watcher debounce-reindex |
|
|
464
|
-
| **T8** | HTTP serve: `/health` `/ready` `/version` `/metrics` `/admin/api/status` all 200; Prom text format |
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Development
|
|
465
367
|
|
|
466
|
-
|
|
368
|
+
This package lives in a monorepo alongside its retrieval engine,
|
|
369
|
+
`@esankhan3/anvil-knowledge-core`.
|
|
467
370
|
|
|
468
371
|
```sh
|
|
469
|
-
npm
|
|
470
|
-
npm -w @esankhan3/
|
|
372
|
+
npm install # at the repo root
|
|
373
|
+
npm -w @esankhan3/anvil-knowledge-core run build
|
|
374
|
+
npm -w @esankhan3/code-search-mcp run build # esbuild bundle → dist/
|
|
375
|
+
npm -w @esankhan3/code-search-mcp test
|
|
376
|
+
npm -w @esankhan3/code-search-mcp run smoke # end-to-end, needs Ollama with bge-m3
|
|
471
377
|
```
|
|
472
378
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
provider-switch path against the real OpenAI endpoint.
|
|
379
|
+
Internals: [ARCHITECTURE.md](./ARCHITECTURE.md). Retrieval engine:
|
|
380
|
+
[knowledge-core](../knowledge-core/ARCHITECTURE.md).
|
|
476
381
|
|
|
477
|
-
|
|
478
|
-
so it's a hand-rolled local check, not a release gate. The release
|
|
479
|
-
workflow does enforce a static guard that every bin entry
|
|
480
|
-
(`code-search-mcp` / `code-search` / `code-search-daemon`) is in the
|
|
481
|
-
built tarball before publishing.
|
|
482
|
-
|
|
483
|
-
---
|
|
484
|
-
|
|
485
|
-
## Status
|
|
486
|
-
|
|
487
|
-
Stable. The retrieval and graph layers move with
|
|
488
|
-
`@anvil/knowledge-core`; the MCP surface is locked. New tools
|
|
489
|
-
land additively. P0–P8 of the standalone-product plan
|
|
490
|
-
(`docs/CODE-SEARCH-MCP-STANDALONE-PLAN.md`) have landed; F1–F4
|
|
491
|
-
follow-up fixes verified by `scripts/smoke/run-smoke.sh`.
|
|
492
|
-
|
|
493
|
-
---
|
|
382
|
+
## License
|
|
494
383
|
|
|
495
|
-
|
|
384
|
+
[MIT](./LICENSE)
|
|
@@ -20,7 +20,10 @@ const ENV_KEYS = [
|
|
|
20
20
|
"CODE_SEARCH_AUTH_MODE",
|
|
21
21
|
"CODE_SEARCH_AUTH_API_KEYS",
|
|
22
22
|
"CODE_SEARCH_DATA_DIR",
|
|
23
|
-
"CODE_SEARCH_REINDEX_INTERVAL"
|
|
23
|
+
"CODE_SEARCH_REINDEX_INTERVAL",
|
|
24
|
+
"CODE_SEARCH_EMBEDDING_HEADERS",
|
|
25
|
+
"CODE_SEARCH_LLM_HEADERS",
|
|
26
|
+
"ANVIL_LLM_HEADERS"
|
|
24
27
|
];
|
|
25
28
|
let tmp = "";
|
|
26
29
|
const STASH = {};
|
|
@@ -105,6 +108,35 @@ describe("toKnowledgeConfig (P3 adapter)", () => {
|
|
|
105
108
|
assert.equal(r.provider, cfg.reranker.provider);
|
|
106
109
|
});
|
|
107
110
|
});
|
|
111
|
+
describe("gateway request headers", () => {
|
|
112
|
+
const RAW = "CF-Access-Client-Id=id.access,CF-Access-Client-Secret=s3cret";
|
|
113
|
+
const PARSED = { "CF-Access-Client-Id": "id.access", "CF-Access-Client-Secret": "s3cret" };
|
|
114
|
+
it("CODE_SEARCH_EMBEDDING_HEADERS reach the embedder config", () => {
|
|
115
|
+
process.env.CODE_SEARCH_EMBEDDING_HEADERS = RAW;
|
|
116
|
+
const cfg = resolveCodeSearchConfig({ configPath: join(tmp, "missing.yaml") });
|
|
117
|
+
assert.deepEqual(cfg.embedding.headers, PARSED);
|
|
118
|
+
assert.deepEqual(toKnowledgeConfig(cfg).embedding.headers, PARSED);
|
|
119
|
+
});
|
|
120
|
+
it("CODE_SEARCH_LLM_HEADERS reach agent-core as ANVIL_LLM_HEADERS", () => {
|
|
121
|
+
process.env.CODE_SEARCH_LLM_HEADERS = RAW;
|
|
122
|
+
const cfg = resolveCodeSearchConfig({ configPath: join(tmp, "missing.yaml") });
|
|
123
|
+
assert.deepEqual(cfg.llm.headers, PARSED);
|
|
124
|
+
assert.equal(process.env.ANVIL_LLM_HEADERS, RAW);
|
|
125
|
+
});
|
|
126
|
+
it("llm.headers from YAML reach agent-core too", () => {
|
|
127
|
+
const file = join(tmp, "config.yaml");
|
|
128
|
+
writeFileSync(file, "llm:\n headers:\n CF-Access-Client-Id: id.access\n CF-Access-Client-Secret: s3cret\n");
|
|
129
|
+
resolveCodeSearchConfig({ configPath: file });
|
|
130
|
+
assert.equal(process.env.ANVIL_LLM_HEADERS, RAW);
|
|
131
|
+
});
|
|
132
|
+
it("--print-config redacts every header value", () => {
|
|
133
|
+
const out = redactSecrets({ embedding: { headers: PARSED }, llm: { headers: PARSED } });
|
|
134
|
+
for (const h of [out.embedding.headers, out.llm.headers]) {
|
|
135
|
+
assert.match(h["CF-Access-Client-Id"], /redacted/);
|
|
136
|
+
assert.match(h["CF-Access-Client-Secret"], /redacted/);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|
|
108
140
|
describe("redactSecrets (P3 --print-config)", () => {
|
|
109
141
|
it("redacts apiKey/jwtSecret/token fields, preserves shape", () => {
|
|
110
142
|
const input = {
|
package/dist/core/config.js
CHANGED
|
@@ -241,6 +241,7 @@ function envLayer() {
|
|
|
241
241
|
dimensions: embedDims ? parseInt(embedDims, 10) : void 0,
|
|
242
242
|
apiKey: env("EMBEDDING_API_KEY"),
|
|
243
243
|
baseUrl: env("EMBEDDING_BASE_URL"),
|
|
244
|
+
headers: parseHeaders(env("EMBEDDING_HEADERS")),
|
|
244
245
|
ollamaHost: env("OLLAMA_HOST") ?? process.env.OLLAMA_HOST
|
|
245
246
|
},
|
|
246
247
|
reranker: {
|
|
@@ -268,6 +269,7 @@ function envLayer() {
|
|
|
268
269
|
model: env("LLM_MODEL"),
|
|
269
270
|
apiKey: env("LLM_API_KEY") ?? process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY,
|
|
270
271
|
baseUrl: env("LLM_BASE_URL"),
|
|
272
|
+
headers: parseHeaders(env("LLM_HEADERS")),
|
|
271
273
|
claudeBin: env("CLAUDE_BIN") ?? process.env.CLAUDE_BIN
|
|
272
274
|
},
|
|
273
275
|
github: {
|
|
@@ -420,6 +422,9 @@ function bridgeLegacyEnvVars(cfg) {
|
|
|
420
422
|
process.env.ANVIL_LLM_MODEL ??= process.env.CODE_SEARCH_LLM_MODEL ?? cfg.llm.model;
|
|
421
423
|
if (cfg.llm.apiKey) process.env.ANVIL_LLM_API_KEY ??= cfg.llm.apiKey;
|
|
422
424
|
if (cfg.llm.baseUrl) process.env.ANVIL_LLM_BASE_URL ??= cfg.llm.baseUrl;
|
|
425
|
+
if (cfg.llm.headers) {
|
|
426
|
+
process.env.ANVIL_LLM_HEADERS ??= Object.entries(cfg.llm.headers).map(([k, v]) => `${k}=${v}`).join(",");
|
|
427
|
+
}
|
|
423
428
|
if (cfg.llm.claudeBin) process.env.ANVIL_CLAUDE_BIN ??= cfg.llm.claudeBin;
|
|
424
429
|
}
|
|
425
430
|
function toKnowledgeConfig(c) {
|
|
@@ -454,7 +459,7 @@ function redactSecrets(value, parentKey = "") {
|
|
|
454
459
|
if (isPlainObject(value)) {
|
|
455
460
|
const out = {};
|
|
456
461
|
for (const [k, v] of Object.entries(value)) {
|
|
457
|
-
const secret = SECRET_KEY.test(k) || parentKey === "otelHeaders";
|
|
462
|
+
const secret = SECRET_KEY.test(k) || parentKey === "otelHeaders" || parentKey === "headers";
|
|
458
463
|
if (secret && typeof v === "string" && v.length > 0) {
|
|
459
464
|
out[k] = redactString(v);
|
|
460
465
|
} else if (secret && Array.isArray(v)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@esankhan3/code-search-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
4
4
|
"description": "MCP server for multi-repo code search \u2014 GitHub org or local path, zero config",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
"smoke": "bash scripts/smoke/run-smoke.sh"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@esankhan3/anvil-agent-core": "^0.5.
|
|
27
|
-
"@esankhan3/anvil-knowledge-core": "^0.4.
|
|
26
|
+
"@esankhan3/anvil-agent-core": "^0.5.4",
|
|
27
|
+
"@esankhan3/anvil-knowledge-core": "^0.4.12",
|
|
28
28
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
29
29
|
"@modelcontextprotocol/server": "^2.0.0"
|
|
30
30
|
},
|