@maxgfr/codeindex 2.18.0 → 2.19.0

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.
@@ -0,0 +1,541 @@
1
+ // The MCP tool catalogue: the 26 tool definitions, their display metadata, and
2
+ // the per-protocol-version view of the list a client actually receives.
3
+ //
4
+ // Split out of mcp.ts because it is pure data plus one projection function —
5
+ // nothing here scans a repo or touches the wire — and because it is the part a
6
+ // reader most often wants to consult on its own.
7
+ import { ANNOTATIONS_SINCE, PROTOCOL_VERSIONS, RICH_TOOLS_SINCE } from "./protocol.js";
8
+
9
+ const repoProp = { repo: { type: "string", description: "Absolute path to the repository root" } };
10
+ const scopeProps = {
11
+ scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
12
+ include: { type: "array", items: { type: "string" }, description: "Include globs" },
13
+ exclude: { type: "array", items: { type: "string" }, description: "Exclude globs" },
14
+ };
15
+
16
+ export const TOOLS = [
17
+ {
18
+ name: "scan_summary",
19
+ description:
20
+ "Deterministically scan a repository: file count, per-language file histogram, HEAD commit, and whether the walk was capped. Fast first look at any codebase.",
21
+ inputSchema: { type: "object", properties: { ...repoProp, ...scopeProps }, required: ["repo"] },
22
+ },
23
+ {
24
+ name: "graph",
25
+ description:
26
+ "Build the full typed cross-file link-graph (import/call/use/doc-link/mention edges, module grouping, PageRank centrality, Louvain communities, tests-map). Returns graph.json. Large on big repos — prefer scan_summary/symbols/callers for targeted questions.",
27
+ inputSchema: { type: "object", properties: { ...repoProp, ...scopeProps }, required: ["repo"] },
28
+ },
29
+ {
30
+ name: "symbols",
31
+ description:
32
+ "Where is a symbol defined and which files reference it? Returns the definition sites (file, line, kind, exported) and referencing files. Omit `name` for the full symbol index.",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: { ...repoProp, name: { type: "string", description: "Symbol name to look up" } },
36
+ required: ["repo"],
37
+ },
38
+ },
39
+ {
40
+ name: "callers",
41
+ description:
42
+ "Who calls a function? Per-symbol caller index: each defined symbol with the exact (file, line) call sites that bind to it. Omit `name` for the full index.",
43
+ inputSchema: {
44
+ type: "object",
45
+ properties: {
46
+ ...repoProp,
47
+ name: { type: "string", description: "Symbol name to look up" },
48
+ recall: {
49
+ type: "boolean",
50
+ description:
51
+ "Recall-oriented binding: relax the JS/TS import gate to unique repo-wide names, labelling each site corroborated|unique-name (default false = precision)",
52
+ },
53
+ },
54
+ required: ["repo"],
55
+ },
56
+ },
57
+ {
58
+ name: "workspaces",
59
+ description:
60
+ "Detect monorepo packages (npm/pnpm/yarn/lerna/nx/cargo/go.work/maven) with the workspace dependency graph, one cycle if present, and a topological build order.",
61
+ inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] },
62
+ },
63
+ {
64
+ name: "churn",
65
+ description: "Per-file git commit counts (whole history, or since a ref) — the churn half of hotspot analysis.",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: { ...repoProp, since: { type: "string", description: "Only count commits after this ref" } },
69
+ required: ["repo"],
70
+ },
71
+ },
72
+ {
73
+ name: "symbols_overview",
74
+ description:
75
+ "All symbols declared in ONE file (name, kind, line span, exported, parent), in declaration order — the fastest way to understand a file without reading it.",
76
+ inputSchema: {
77
+ type: "object",
78
+ properties: { ...repoProp, file: { type: "string", description: "Repo-relative file path" } },
79
+ required: ["repo", "file"],
80
+ },
81
+ },
82
+ {
83
+ name: "find_symbol",
84
+ description:
85
+ "Find symbol declarations by name or name path ('Class/method' matches a method inside Class). Options: substring matching, includeBody to return the declaration's source. Exact-name matches rank first.",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ ...repoProp,
90
+ namePath: { type: "string", description: "Symbol name or Parent/child path" },
91
+ substring: { type: "boolean" },
92
+ includeBody: { type: "boolean" },
93
+ maxResults: { type: "number", description: "Cap matches (default 50)" },
94
+ },
95
+ required: ["repo", "namePath"],
96
+ },
97
+ },
98
+ {
99
+ name: "find_references",
100
+ description:
101
+ "Who references a symbol? Three labeled tiers: defs (declarations), callSites (line-precise, import-corroborated call bindings), referencingFiles (file-level identifier/doc mentions — may include homonyms). Confidence decreases across tiers; the labels let you decide what to trust.",
102
+ inputSchema: {
103
+ type: "object",
104
+ properties: { ...repoProp, name: { type: "string", description: "Symbol name" } },
105
+ required: ["repo", "name"],
106
+ },
107
+ },
108
+ {
109
+ name: "repo_map",
110
+ description:
111
+ "Token-budgeted map of the repository: the highest-PageRank files with their key exported signatures, deterministically rendered to fit `budgetTokens` (default 1024). The densest single read to understand an unfamiliar codebase.",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: { ...repoProp, budgetTokens: { type: "number", description: "Approximate token budget (default 1024)" } },
115
+ required: ["repo"],
116
+ },
117
+ },
118
+ {
119
+ name: "hotspots",
120
+ description:
121
+ "Where does work concentrate? Files ranked by git churn × size (commits × log2 lines). High-scoring files are where changes and defects cluster.",
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: { ...repoProp, since: { type: "string", description: "Only count commits after this ref" } },
125
+ required: ["repo"],
126
+ },
127
+ },
128
+ {
129
+ name: "coupling",
130
+ description:
131
+ "Change coupling: pairs of files that repeatedly change in the same commits — hidden dependencies no import shows. strength 1.0 = every change to one touched the other.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: { ...repoProp, since: { type: "string", description: "Only mine commits after this ref" } },
135
+ required: ["repo"],
136
+ },
137
+ },
138
+ {
139
+ name: "replace_symbol_body",
140
+ description:
141
+ "WRITE: replace a symbol's whole declaration with `body` (verbatim, supply full indentation). The symbol is resolved by name path ('Class/method'); ambiguity errors list the candidates — qualify with `file`. Line spans come from the AST index.",
142
+ inputSchema: {
143
+ type: "object",
144
+ properties: {
145
+ ...repoProp,
146
+ namePath: { type: "string" },
147
+ body: { type: "string" },
148
+ file: { type: "string", description: "Disambiguate: repo-relative file containing the symbol" },
149
+ },
150
+ required: ["repo", "namePath", "body"],
151
+ },
152
+ },
153
+ {
154
+ name: "insert_after_symbol",
155
+ description:
156
+ "WRITE: insert `body` after a symbol's declaration (blank-line separation preserved for definition-like kinds). Resolved like replace_symbol_body.",
157
+ inputSchema: {
158
+ type: "object",
159
+ properties: { ...repoProp, namePath: { type: "string" }, body: { type: "string" }, file: { type: "string" } },
160
+ required: ["repo", "namePath", "body"],
161
+ },
162
+ },
163
+ {
164
+ name: "insert_before_symbol",
165
+ description:
166
+ "WRITE: insert `body` before a symbol's declaration (blank-line separation preserved). Resolved like replace_symbol_body.",
167
+ inputSchema: {
168
+ type: "object",
169
+ properties: { ...repoProp, namePath: { type: "string" }, body: { type: "string" }, file: { type: "string" } },
170
+ required: ["repo", "namePath", "body"],
171
+ },
172
+ },
173
+ {
174
+ name: "write_memory",
175
+ description:
176
+ "Persist a named markdown note under <repo>/.codeindex/memories/ (names may use topic/name form). Write small, focused notes: project map, build commands, conventions.",
177
+ inputSchema: {
178
+ type: "object",
179
+ properties: { ...repoProp, name: { type: "string" }, content: { type: "string" } },
180
+ required: ["repo", "name", "content"],
181
+ },
182
+ },
183
+ {
184
+ name: "read_memory",
185
+ description: "Read one persisted memory by name.",
186
+ inputSchema: {
187
+ type: "object",
188
+ properties: { ...repoProp, name: { type: "string" } },
189
+ required: ["repo", "name"],
190
+ },
191
+ },
192
+ {
193
+ name: "list_memories",
194
+ description: "List persisted memory names — load this first, then read individual memories on relevance.",
195
+ inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] },
196
+ },
197
+ {
198
+ name: "delete_memory",
199
+ description: "Delete one persisted memory by name.",
200
+ inputSchema: {
201
+ type: "object",
202
+ properties: { ...repoProp, name: { type: "string" } },
203
+ required: ["repo", "name"],
204
+ },
205
+ },
206
+ {
207
+ name: "dead_code",
208
+ description:
209
+ "Dead-code candidates in two labeled tiers: 'unreferenced' (no call site binds AND nothing references the name) and 'uncalled' (referenced somewhere — re-export, type position — but never called). Exported symbols only; test files and entrypoint-looking files excluded as roots. On a large repo this list runs to thousands of entries — pass `limit`, or `scope` to one subdirectory.",
210
+ inputSchema: {
211
+ type: "object",
212
+ properties: {
213
+ ...repoProp,
214
+ ...scopeProps,
215
+ limit: { type: "number", description: "Cap entries (default: all)" },
216
+ },
217
+ required: ["repo"],
218
+ },
219
+ },
220
+ {
221
+ name: "complexity",
222
+ description:
223
+ "Cyclomatic-complexity estimates (branch-token counting over AST line spans), most-complex first. Pass `file` for one file's symbols, omit for the repo-wide top. Combine with hotspots: the `risk` field of this tool's sibling ranks complexity × churn.",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: { ...repoProp, file: { type: "string" }, risk: { type: "boolean", description: "Return complexity × git-churn risk ranking instead" } },
227
+ required: ["repo"],
228
+ },
229
+ },
230
+ {
231
+ name: "mermaid",
232
+ description:
233
+ "Mermaid diagram of the module graph (renders inline in Claude/GitHub — no graph database). Optionally scoped to one module's neighborhood.",
234
+ inputSchema: {
235
+ type: "object",
236
+ properties: { ...repoProp, module: { type: "string", description: "Module slug to focus on" } },
237
+ required: ["repo"],
238
+ },
239
+ },
240
+ {
241
+ name: "grep",
242
+ description:
243
+ "Search file contents (ripgrep when available, deterministic JS fallback otherwise). Returns sorted (file, line, text) hits.",
244
+ inputSchema: {
245
+ type: "object",
246
+ properties: {
247
+ ...repoProp,
248
+ pattern: { type: "string", description: "Regular expression to search for" },
249
+ scope: { type: "string", description: "Restrict to one directory (repo-relative)" },
250
+ globs: { type: "array", items: { type: "string" }, description: "Restrict to matching paths" },
251
+ ignoreCase: { type: "boolean" },
252
+ maxHits: { type: "number" },
253
+ },
254
+ required: ["repo", "pattern"],
255
+ },
256
+ },
257
+ {
258
+ name: "search",
259
+ description:
260
+ 'Natural-language-ish lexical search: BM25 ranking (k1=1.2, b=0.75) over symbol names (camelCase/snake_case subtokens), file path segments, markdown headings and summary lines. NOT embeddings by default — deterministic, diacritic-folded, zero API keys. Answers "where is auth handled?"-style queries with ranked files, matched terms and top symbols. Query terms with zero document frequency get a deterministic trigram-fuzzy fallback (typo-tolerant) unless `fuzzy: false`. Set `semantic: true` to RRF-fuse an embedding tier (HTTP endpoint, else a local static model) with lexical — the response then wraps the ranked list as `{ results, tier, degradedReason? }`, `tier` being "endpoint"/"static" when fusion happened or "lexical" (with `degradedReason`) when it did not (see embed_status). Without `semantic`, the response is the bare ranked array, unchanged.',
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ ...repoProp,
265
+ ...scopeProps,
266
+ query: { type: "string", description: "Natural-language or identifier query" },
267
+ limit: { type: "number", description: "Max results (default 20)" },
268
+ fuzzy: {
269
+ type: "boolean",
270
+ description:
271
+ "Trigram fuzzy fallback for query terms with zero document frequency (default true)",
272
+ },
273
+ semantic: {
274
+ type: "boolean",
275
+ description:
276
+ 'RRF-fuse an embedding tier with lexical (default false). Precedence: the HTTP endpoint (CODEINDEX_EMBED_ENDPOINT) if set, else a local static model. The response reports the effective tier as a top-level `tier` field ("endpoint"/"static" on success, "lexical" plus `degradedReason` when neither is available/reachable) instead of degrading silently — see embed_status.',
277
+ },
278
+ },
279
+ required: ["repo", "query"],
280
+ },
281
+ },
282
+ {
283
+ name: "embed_status",
284
+ description:
285
+ "Report the embedding tier: the effective mode (none/static/endpoint; endpoint > static model), the resolved model (opt-in, never shipped in the package) with its modelId/dim, EMBED_VERSION, and the configured HTTP endpoint with its reachability. Use to check whether `search` with semantic:true will fuse embeddings or degrade to lexical.",
286
+ inputSchema: { type: "object", properties: { ...repoProp }, required: ["repo"] },
287
+ },
288
+ {
289
+ name: "check_rules",
290
+ description:
291
+ 'Validate dependency-cruiser-style architecture rules against the link-graph. Rules (inline JSON array): forbidden edges {name, from, to, kind?, severity?, comment?} with glob paths, plus builtins {name, builtin: "cycles"|"orphans"} (module-level import cycles; edge-less code files). Returns deterministic violations with severity error|warn — a CI gate.',
292
+ inputSchema: {
293
+ type: "object",
294
+ properties: {
295
+ ...repoProp,
296
+ ...scopeProps,
297
+ rules: { type: "array", description: "Rules array (inline JSON — see description)" },
298
+ configPath: {
299
+ type: "string",
300
+ description:
301
+ "Read the rules from this JSON file instead (repo-relative or absolute) — the CLI's --config. Ignored when `rules` is given.",
302
+ },
303
+ },
304
+ required: ["repo"],
305
+ },
306
+ },
307
+ ] as const;
308
+
309
+
310
+ // --- output schemas ----------------------------------------------------------
311
+ // `outputSchema` (protocol 2025-06-18) lets a client validate and type a tool's
312
+ // result instead of re-parsing an opaque string, and `structuredContent` carries
313
+ // that result alongside the text block.
314
+ //
315
+ // Only tools whose response is a JSON OBJECT for EVERY argument combination are
316
+ // declared here, because the spec requires structuredContent to be an object and
317
+ // requires it to conform whenever an outputSchema is present. That rules out:
318
+ //
319
+ // * array responses — symbols_overview, find_symbol, grep, check_rules,
320
+ // list_memories. Wrapping them in `{ items: [...] }` would make
321
+ // structuredContent diverge from the text block, and changing the text block
322
+ // itself would break every existing client. Omitting the schema is the only
323
+ // option that breaks neither.
324
+ // * argument-dependent shapes — dead_code (array, object with `limit`),
325
+ // complexity (array, object with `risk`), search (array, object with
326
+ // `semantic`). A schema that cannot describe every response is worse than
327
+ // none: it would make a conforming client reject valid output.
328
+ // * text responses — repo_map, mermaid, read_memory, which are not JSON.
329
+ //
330
+ // Shapes are deliberately open (no `additionalProperties: false`): a later
331
+ // engine adding a field must not turn a strict client's success into a failure.
332
+ const strArr = { type: "array", items: { type: "string" } };
333
+ const anyObj = { type: "object" };
334
+
335
+ export const OUTPUT_SCHEMAS: Record<string, Record<string, unknown>> = {
336
+ scan_summary: {
337
+ type: "object",
338
+ properties: {
339
+ engineVersion: { type: "string" },
340
+ commit: { type: "string" },
341
+ fileCount: { type: "integer" },
342
+ languages: { type: "object", additionalProperties: { type: "integer" } },
343
+ capped: { type: "boolean" },
344
+ },
345
+ required: ["engineVersion", "fileCount", "languages", "capped"],
346
+ },
347
+ graph: {
348
+ type: "object",
349
+ properties: {
350
+ schemaVersion: { type: "integer" },
351
+ version: { type: "string" },
352
+ commit: { type: "string" },
353
+ fileCount: { type: "integer" },
354
+ languages: { type: "object", additionalProperties: { type: "integer" } },
355
+ files: { type: "array", items: anyObj },
356
+ modules: { type: "array", items: anyObj },
357
+ fileEdges: { type: "array", items: anyObj },
358
+ moduleEdges: { type: "array", items: anyObj },
359
+ },
360
+ required: ["schemaVersion", "files", "fileEdges", "modules", "moduleEdges"],
361
+ },
362
+ // Two shapes, both objects: the whole index, or one symbol's entry.
363
+ symbols: {
364
+ oneOf: [
365
+ {
366
+ type: "object",
367
+ properties: { schemaVersion: { type: "integer" }, defs: anyObj, refs: anyObj },
368
+ required: ["schemaVersion", "defs"],
369
+ },
370
+ {
371
+ type: "object",
372
+ properties: { name: { type: "string" }, defs: { type: "array", items: anyObj }, refs: strArr },
373
+ required: ["name", "defs", "refs"],
374
+ },
375
+ ],
376
+ },
377
+ // The whole index (symbol name -> entry), one entry, or the not-found notice.
378
+ callers: {
379
+ oneOf: [
380
+ { type: "object", additionalProperties: anyObj },
381
+ { type: "object", properties: { error: { type: "string" } }, required: ["error"] },
382
+ ],
383
+ },
384
+ workspaces: {
385
+ type: "object",
386
+ properties: {
387
+ packages: { type: "array", items: anyObj },
388
+ cycle: { type: ["array", "null"], items: { type: "string" } },
389
+ topoOrder: strArr,
390
+ },
391
+ required: ["packages", "topoOrder"],
392
+ },
393
+ churn: {
394
+ type: "object",
395
+ properties: { ok: { type: "boolean" }, churn: { type: "object", additionalProperties: { type: "integer" } } },
396
+ required: ["ok", "churn"],
397
+ },
398
+ find_references: {
399
+ type: "object",
400
+ properties: {
401
+ defs: { type: "array", items: anyObj },
402
+ callSites: { type: "array", items: anyObj },
403
+ referencingFiles: strArr,
404
+ },
405
+ required: ["defs", "callSites", "referencingFiles"],
406
+ },
407
+ hotspots: {
408
+ type: "object",
409
+ properties: { churnOk: { type: "boolean" }, hotspots: { type: "array", items: anyObj } },
410
+ required: ["churnOk", "hotspots"],
411
+ },
412
+ coupling: {
413
+ type: "object",
414
+ properties: { ok: { type: "boolean" }, couplings: { type: "array", items: anyObj } },
415
+ required: ["ok", "couplings"],
416
+ },
417
+ embed_status: {
418
+ type: "object",
419
+ properties: {
420
+ embedVersion: { type: "integer" },
421
+ mode: { type: "string", enum: ["none", "static", "endpoint"] },
422
+ model: {},
423
+ endpoint: {},
424
+ endpointReachable: { type: "boolean" },
425
+ },
426
+ required: ["embedVersion", "mode"],
427
+ },
428
+ write_memory: {
429
+ type: "object",
430
+ properties: { written: { type: "string" } },
431
+ required: ["written"],
432
+ },
433
+ delete_memory: {
434
+ type: "object",
435
+ properties: { deleted: { type: "boolean" } },
436
+ required: ["deleted"],
437
+ },
438
+ };
439
+
440
+ // The three symbolic edits share one result shape (see src/edit.ts EditResult).
441
+ for (const name of ["replace_symbol_body", "insert_after_symbol", "insert_before_symbol"]) {
442
+ OUTPUT_SCHEMAS[name] = {
443
+ type: "object",
444
+ properties: {
445
+ file: { type: "string" },
446
+ symbol: { type: "string" },
447
+ startLine: { type: "integer" },
448
+ endLine: { type: "integer" },
449
+ },
450
+ required: ["file"],
451
+ };
452
+ }
453
+
454
+ // Per-tool display title and behaviour hints.
455
+ //
456
+ // The hints matter operationally: they are what lets a host auto-approve the 23
457
+ // read-only tools and hold a confirmation for the 5 that write. Without them a
458
+ // client must treat `scan_summary` and `replace_symbol_body` alike.
459
+ //
460
+ // openWorldHint is true only where a call can leave this machine — `search`
461
+ // with semantic:true and `embed_status` may contact CODEINDEX_EMBED_ENDPOINT.
462
+ // Everything else reads the repo and nothing but the repo.
463
+ interface ToolMeta {
464
+ title: string;
465
+ write?: boolean;
466
+ destructive?: boolean;
467
+ idempotent?: boolean;
468
+ openWorld?: boolean;
469
+ }
470
+
471
+ export const TOOL_META: Record<string, ToolMeta> = {
472
+ scan_summary: { title: "Scan summary" },
473
+ graph: { title: "Link graph" },
474
+ symbols: { title: "Symbol index" },
475
+ callers: { title: "Caller index" },
476
+ workspaces: { title: "Monorepo workspaces" },
477
+ churn: { title: "Git churn" },
478
+ symbols_overview: { title: "File symbol overview" },
479
+ find_symbol: { title: "Find symbol" },
480
+ find_references: { title: "Find references" },
481
+ repo_map: { title: "Repository map" },
482
+ hotspots: { title: "Hotspots" },
483
+ coupling: { title: "Change coupling" },
484
+ replace_symbol_body: { title: "Replace symbol body", write: true, destructive: true, idempotent: true },
485
+ insert_after_symbol: { title: "Insert after symbol", write: true, destructive: false, idempotent: false },
486
+ insert_before_symbol: { title: "Insert before symbol", write: true, destructive: false, idempotent: false },
487
+ write_memory: { title: "Write memory", write: true, destructive: false, idempotent: true },
488
+ read_memory: { title: "Read memory" },
489
+ list_memories: { title: "List memories" },
490
+ delete_memory: { title: "Delete memory", write: true, destructive: true, idempotent: true },
491
+ dead_code: { title: "Dead-code candidates" },
492
+ complexity: { title: "Complexity" },
493
+ mermaid: { title: "Mermaid module diagram" },
494
+ grep: { title: "Grep file contents" },
495
+ search: { title: "Lexical search", openWorld: true },
496
+ embed_status: { title: "Embedding tier status", openWorld: true },
497
+ check_rules: { title: "Check architecture rules" },
498
+ };
499
+
500
+ export function annotationsFor(name: string): Record<string, boolean> | undefined {
501
+ const meta = TOOL_META[name];
502
+ if (!meta) return undefined;
503
+ return {
504
+ readOnlyHint: !meta.write,
505
+ ...(meta.write ? { destructiveHint: meta.destructive === true, idempotentHint: meta.idempotent === true } : {}),
506
+ openWorldHint: meta.openWorld === true,
507
+ };
508
+ }
509
+
510
+ // The advertised tool list, for one negotiated protocol version.
511
+ //
512
+ // Without a server-level repo pin and on 2024-11-05 this is TOOLS verbatim —
513
+ // byte-compat for every existing consumer. A pin drops `repo` from each
514
+ // `required` set and documents the default, so a client that omits it is
515
+ // spec-correct rather than relying on the server being lenient. Newer protocol
516
+ // revisions additionally get `title` and `annotations`.
517
+ export function toolsFor(defaultRepo?: string, protocolVersion: string = PROTOCOL_VERSIONS[0]): readonly unknown[] {
518
+ const withAnnotations = protocolVersion >= ANNOTATIONS_SINCE;
519
+ // Tool.title and Tool.outputSchema both arrive in 2025-06-18.
520
+ const withRich = protocolVersion >= RICH_TOOLS_SINCE;
521
+ if (!defaultRepo && !withAnnotations && !withRich) return TOOLS;
522
+ return TOOLS.map((t) => ({
523
+ ...t,
524
+ ...(withRich && TOOL_META[t.name] ? { title: TOOL_META[t.name]!.title } : {}),
525
+ ...(withRich && OUTPUT_SCHEMAS[t.name] ? { outputSchema: OUTPUT_SCHEMAS[t.name] } : {}),
526
+ ...(withAnnotations ? { annotations: annotationsFor(t.name) } : {}),
527
+ inputSchema: !defaultRepo
528
+ ? t.inputSchema
529
+ : {
530
+ ...t.inputSchema,
531
+ properties: {
532
+ ...t.inputSchema.properties,
533
+ repo: {
534
+ type: "string",
535
+ description: `Absolute path to the repository root (optional — defaults to ${defaultRepo})`,
536
+ },
537
+ },
538
+ required: (t.inputSchema.required as readonly string[]).filter((r) => r !== "repo"),
539
+ },
540
+ }));
541
+ }