clast-ai 0.1.2 → 0.2.1

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.
Files changed (294) hide show
  1. package/.claude-plugin/marketplace.json +14 -0
  2. package/.claude-plugin/plugin.json +11 -9
  3. package/README.md +468 -398
  4. package/bench/ab/onair-concept.json +22 -0
  5. package/bench/ab/onair-deadcode.json +7 -0
  6. package/bench/ab/onair-hard.json +32 -0
  7. package/bench/ab/onair-impact.json +17 -0
  8. package/bench/ab/onair-refactors.json +18 -0
  9. package/bench/ab/onair-survey.json +17 -0
  10. package/bench/ab/onair-tasks.json +20 -0
  11. package/bench/ab/onair.json +52 -0
  12. package/bench/ab/vscode-concept.json +22 -0
  13. package/bench/ab/vscode-hard.json +32 -0
  14. package/bench/ab/vscode-impact.json +17 -0
  15. package/bench/ab/vscode-refactors.json +18 -0
  16. package/bench/ab/vscode-tasks.json +20 -0
  17. package/bench/ab/vscode.json +52 -0
  18. package/bench/ab.mjs +213 -0
  19. package/bench/bench.mjs +98 -0
  20. package/bench/resolution.mjs +75 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +266 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/config/defaults.d.ts +12 -2
  25. package/dist/config/defaults.js +38 -31
  26. package/dist/config/defaults.js.map +1 -1
  27. package/dist/config/index.d.ts +1 -1
  28. package/dist/config/index.js +26 -14
  29. package/dist/config/index.js.map +1 -1
  30. package/dist/db/index.d.ts +73 -13
  31. package/dist/db/index.js +481 -39
  32. package/dist/db/index.js.map +1 -1
  33. package/dist/engine.d.ts +15 -0
  34. package/dist/engine.js +28 -0
  35. package/dist/engine.js.map +1 -0
  36. package/dist/indexer/imports.d.ts +6 -0
  37. package/dist/indexer/imports.js +254 -0
  38. package/dist/indexer/imports.js.map +1 -0
  39. package/dist/indexer/index.d.ts +63 -0
  40. package/dist/indexer/index.js +360 -0
  41. package/dist/indexer/index.js.map +1 -0
  42. package/dist/indexer/process.d.ts +22 -0
  43. package/dist/indexer/process.js +42 -0
  44. package/dist/indexer/process.js.map +1 -0
  45. package/dist/indexer/scan.d.ts +43 -0
  46. package/dist/indexer/scan.js +223 -0
  47. package/dist/indexer/scan.js.map +1 -0
  48. package/dist/indexer/worker.d.ts +1 -0
  49. package/dist/indexer/worker.js +23 -0
  50. package/dist/indexer/worker.js.map +1 -0
  51. package/dist/llm/comment-generator.d.ts +17 -19
  52. package/dist/llm/comment-generator.js +46 -80
  53. package/dist/llm/comment-generator.js.map +1 -1
  54. package/dist/llm/index.d.ts +0 -1
  55. package/dist/llm/index.js.map +1 -1
  56. package/dist/llm/prompts.d.ts +0 -1
  57. package/dist/llm/prompts.js +16 -16
  58. package/dist/migrate.d.ts +35 -0
  59. package/dist/migrate.js +194 -0
  60. package/dist/migrate.js.map +1 -0
  61. package/dist/parser/extractor.d.ts +21 -16
  62. package/dist/parser/extractor.js +700 -228
  63. package/dist/parser/extractor.js.map +1 -1
  64. package/dist/parser/index.d.ts +17 -21
  65. package/dist/parser/index.js +115 -171
  66. package/dist/parser/index.js.map +1 -1
  67. package/dist/parser/langs/c.d.ts +3 -0
  68. package/dist/parser/langs/c.js +879 -0
  69. package/dist/parser/langs/c.js.map +1 -0
  70. package/dist/parser/langs/csharp.d.ts +2 -0
  71. package/dist/parser/langs/csharp.js +548 -0
  72. package/dist/parser/langs/csharp.js.map +1 -0
  73. package/dist/parser/langs/dart.d.ts +2 -0
  74. package/dist/parser/langs/dart.js +893 -0
  75. package/dist/parser/langs/dart.js.map +1 -0
  76. package/dist/parser/langs/go.d.ts +2 -0
  77. package/dist/parser/langs/go.js +455 -0
  78. package/dist/parser/langs/go.js.map +1 -0
  79. package/dist/parser/langs/java.d.ts +2 -0
  80. package/dist/parser/langs/java.js +390 -0
  81. package/dist/parser/langs/java.js.map +1 -0
  82. package/dist/parser/langs/php.d.ts +2 -0
  83. package/dist/parser/langs/php.js +486 -0
  84. package/dist/parser/langs/php.js.map +1 -0
  85. package/dist/parser/langs/python.d.ts +2 -0
  86. package/dist/parser/langs/python.js +216 -0
  87. package/dist/parser/langs/python.js.map +1 -0
  88. package/dist/parser/langs/ruby.d.ts +2 -0
  89. package/dist/parser/langs/ruby.js +602 -0
  90. package/dist/parser/langs/ruby.js.map +1 -0
  91. package/dist/parser/langs/rust.d.ts +2 -0
  92. package/dist/parser/langs/rust.js +931 -0
  93. package/dist/parser/langs/rust.js.map +1 -0
  94. package/dist/parser/langs/swift.d.ts +2 -0
  95. package/dist/parser/langs/swift.js +616 -0
  96. package/dist/parser/langs/swift.js.map +1 -0
  97. package/dist/parser/langs/typescript.d.ts +4 -0
  98. package/dist/parser/langs/typescript.js +535 -0
  99. package/dist/parser/langs/typescript.js.map +1 -0
  100. package/dist/parser/registry.d.ts +16 -0
  101. package/dist/parser/registry.js +87 -0
  102. package/dist/parser/registry.js.map +1 -0
  103. package/dist/parser/returns.d.ts +9 -0
  104. package/dist/parser/returns.js +152 -0
  105. package/dist/parser/returns.js.map +1 -0
  106. package/dist/parser/spec.d.ts +165 -0
  107. package/dist/parser/spec.js +188 -0
  108. package/dist/parser/spec.js.map +1 -0
  109. package/dist/query/arity.d.ts +27 -0
  110. package/dist/query/arity.js +187 -0
  111. package/dist/query/arity.js.map +1 -0
  112. package/dist/query/callgraph.d.ts +113 -0
  113. package/dist/query/callgraph.js +332 -0
  114. package/dist/query/callgraph.js.map +1 -0
  115. package/dist/query/format.d.ts +36 -0
  116. package/dist/query/format.js +136 -0
  117. package/dist/query/format.js.map +1 -0
  118. package/dist/query/hierarchy.d.ts +44 -0
  119. package/dist/query/hierarchy.js +441 -0
  120. package/dist/query/hierarchy.js.map +1 -0
  121. package/dist/query/resolve.d.ts +265 -0
  122. package/dist/query/resolve.js +1957 -0
  123. package/dist/query/resolve.js.map +1 -0
  124. package/dist/query/search.d.ts +35 -0
  125. package/dist/query/search.js +379 -0
  126. package/dist/query/search.js.map +1 -0
  127. package/dist/query/words.d.ts +30 -0
  128. package/dist/query/words.js +201 -0
  129. package/dist/query/words.js.map +1 -0
  130. package/dist/server/index.d.ts +1 -3
  131. package/dist/server/index.js +313 -73
  132. package/dist/server/index.js.map +1 -1
  133. package/dist/tools/context.d.ts +35 -0
  134. package/dist/tools/context.js +79 -0
  135. package/dist/tools/context.js.map +1 -0
  136. package/dist/tools/explore.d.ts +51 -0
  137. package/dist/tools/explore.js +779 -0
  138. package/dist/tools/explore.js.map +1 -0
  139. package/dist/tools/impact.d.ts +21 -0
  140. package/dist/tools/impact.js +743 -0
  141. package/dist/tools/impact.js.map +1 -0
  142. package/dist/tools/search.d.ts +59 -0
  143. package/dist/tools/search.js +843 -0
  144. package/dist/tools/search.js.map +1 -0
  145. package/dist/tools/stats.d.ts +26 -0
  146. package/dist/tools/stats.js +604 -0
  147. package/dist/tools/stats.js.map +1 -0
  148. package/dist/types.d.ts +95 -40
  149. package/dist/types.js +9 -1
  150. package/dist/types.js.map +1 -1
  151. package/dist/version.d.ts +2 -0
  152. package/dist/version.js +3 -0
  153. package/dist/version.js.map +1 -0
  154. package/dist/watcher/index.d.ts +18 -16
  155. package/dist/watcher/index.js +94 -77
  156. package/dist/watcher/index.js.map +1 -1
  157. package/package.json +21 -24
  158. package/plugin/server.mjs +72 -0
  159. package/skills/audit/SKILL.md +37 -0
  160. package/skills/deadcode/SKILL.md +33 -0
  161. package/skills/explore/SKILL.md +54 -0
  162. package/skills/impact/SKILL.md +24 -0
  163. package/skills/status/SKILL.md +11 -0
  164. package/src/cli.ts +265 -0
  165. package/src/config/defaults.ts +50 -32
  166. package/src/config/index.ts +23 -14
  167. package/src/db/index.ts +540 -43
  168. package/src/engine.ts +30 -0
  169. package/src/indexer/imports.ts +241 -0
  170. package/src/indexer/index.ts +369 -0
  171. package/src/indexer/process.ts +63 -0
  172. package/src/indexer/scan.ts +226 -0
  173. package/src/indexer/worker.ts +24 -0
  174. package/src/llm/comment-generator.ts +52 -103
  175. package/src/llm/index.ts +80 -80
  176. package/src/llm/prompts.ts +95 -95
  177. package/src/migrate.ts +213 -0
  178. package/src/parser/extractor.ts +643 -256
  179. package/src/parser/index.ts +105 -205
  180. package/src/parser/langs/c.ts +860 -0
  181. package/src/parser/langs/csharp.ts +519 -0
  182. package/src/parser/langs/dart.ts +820 -0
  183. package/src/parser/langs/go.ts +437 -0
  184. package/src/parser/langs/java.ts +361 -0
  185. package/src/parser/langs/php.ts +450 -0
  186. package/src/parser/langs/python.ts +206 -0
  187. package/src/parser/langs/ruby.ts +554 -0
  188. package/src/parser/langs/rust.ts +904 -0
  189. package/src/parser/langs/swift.ts +590 -0
  190. package/src/parser/langs/typescript.ts +499 -0
  191. package/src/parser/registry.ts +88 -0
  192. package/src/parser/returns.ts +137 -0
  193. package/src/parser/spec.ts +309 -0
  194. package/src/query/arity.ts +173 -0
  195. package/src/query/callgraph.ts +473 -0
  196. package/src/query/format.ts +136 -0
  197. package/src/query/hierarchy.ts +447 -0
  198. package/src/query/resolve.ts +1972 -0
  199. package/src/query/search.ts +430 -0
  200. package/src/query/words.ts +183 -0
  201. package/src/server/index.ts +390 -79
  202. package/src/tools/context.ts +82 -0
  203. package/src/tools/explore.ts +860 -0
  204. package/src/tools/impact.ts +748 -0
  205. package/src/tools/search.ts +886 -0
  206. package/src/tools/stats.ts +633 -0
  207. package/src/types.ts +113 -45
  208. package/src/version.ts +2 -0
  209. package/src/watcher/index.ts +82 -88
  210. package/tsconfig.json +6 -3
  211. package/.mcp.json +0 -8
  212. package/commands/clast-status.md +0 -9
  213. package/dist/config/defaults.d.ts.map +0 -1
  214. package/dist/config/index.d.ts.map +0 -1
  215. package/dist/db/index.d.ts.map +0 -1
  216. package/dist/db/queries.d.ts +0 -44
  217. package/dist/db/queries.d.ts.map +0 -1
  218. package/dist/db/queries.js +0 -251
  219. package/dist/db/queries.js.map +0 -1
  220. package/dist/db/schema.d.ts +0 -3
  221. package/dist/db/schema.d.ts.map +0 -1
  222. package/dist/db/schema.js +0 -94
  223. package/dist/db/schema.js.map +0 -1
  224. package/dist/db/types.d.ts +0 -44
  225. package/dist/db/types.d.ts.map +0 -1
  226. package/dist/db/types.js +0 -2
  227. package/dist/db/types.js.map +0 -1
  228. package/dist/llm/comment-generator.d.ts.map +0 -1
  229. package/dist/llm/index.d.ts.map +0 -1
  230. package/dist/llm/prompts.d.ts.map +0 -1
  231. package/dist/parser/call-resolver.d.ts +0 -15
  232. package/dist/parser/call-resolver.d.ts.map +0 -1
  233. package/dist/parser/call-resolver.js +0 -16
  234. package/dist/parser/call-resolver.js.map +0 -1
  235. package/dist/parser/extractor.d.ts.map +0 -1
  236. package/dist/parser/index.d.ts.map +0 -1
  237. package/dist/parser/languages.d.ts +0 -11
  238. package/dist/parser/languages.d.ts.map +0 -1
  239. package/dist/parser/languages.js +0 -74
  240. package/dist/parser/languages.js.map +0 -1
  241. package/dist/server/index.d.ts.map +0 -1
  242. package/dist/server/register-tools.d.ts +0 -7
  243. package/dist/server/register-tools.d.ts.map +0 -1
  244. package/dist/server/register-tools.js +0 -17
  245. package/dist/server/register-tools.js.map +0 -1
  246. package/dist/server/tools/clast-call-graph.d.ts +0 -4
  247. package/dist/server/tools/clast-call-graph.d.ts.map +0 -1
  248. package/dist/server/tools/clast-call-graph.js +0 -70
  249. package/dist/server/tools/clast-call-graph.js.map +0 -1
  250. package/dist/server/tools/clast-comment.d.ts +0 -5
  251. package/dist/server/tools/clast-comment.d.ts.map +0 -1
  252. package/dist/server/tools/clast-comment.js +0 -64
  253. package/dist/server/tools/clast-comment.js.map +0 -1
  254. package/dist/server/tools/clast-file-summary.d.ts +0 -4
  255. package/dist/server/tools/clast-file-summary.d.ts.map +0 -1
  256. package/dist/server/tools/clast-file-summary.js +0 -42
  257. package/dist/server/tools/clast-file-summary.js.map +0 -1
  258. package/dist/server/tools/clast-get-context.d.ts +0 -4
  259. package/dist/server/tools/clast-get-context.d.ts.map +0 -1
  260. package/dist/server/tools/clast-get-context.js +0 -80
  261. package/dist/server/tools/clast-get-context.js.map +0 -1
  262. package/dist/server/tools/clast-reindex.d.ts +0 -5
  263. package/dist/server/tools/clast-reindex.d.ts.map +0 -1
  264. package/dist/server/tools/clast-reindex.js +0 -36
  265. package/dist/server/tools/clast-reindex.js.map +0 -1
  266. package/dist/server/tools/clast-search.d.ts +0 -4
  267. package/dist/server/tools/clast-search.d.ts.map +0 -1
  268. package/dist/server/tools/clast-search.js +0 -29
  269. package/dist/server/tools/clast-search.js.map +0 -1
  270. package/dist/server/tools/clast-status.d.ts +0 -5
  271. package/dist/server/tools/clast-status.d.ts.map +0 -1
  272. package/dist/server/tools/clast-status.js +0 -23
  273. package/dist/server/tools/clast-status.js.map +0 -1
  274. package/dist/types.d.ts.map +0 -1
  275. package/dist/watcher/gitignore.d.ts +0 -9
  276. package/dist/watcher/gitignore.d.ts.map +0 -1
  277. package/dist/watcher/gitignore.js +0 -38
  278. package/dist/watcher/gitignore.js.map +0 -1
  279. package/dist/watcher/index.d.ts.map +0 -1
  280. package/skills/clast-explore/SKILL.md +0 -41
  281. package/src/db/queries.ts +0 -332
  282. package/src/db/schema.ts +0 -106
  283. package/src/db/types.ts +0 -47
  284. package/src/parser/call-resolver.ts +0 -17
  285. package/src/parser/languages.ts +0 -86
  286. package/src/server/register-tools.ts +0 -30
  287. package/src/server/tools/clast-call-graph.ts +0 -82
  288. package/src/server/tools/clast-comment.ts +0 -76
  289. package/src/server/tools/clast-file-summary.ts +0 -59
  290. package/src/server/tools/clast-get-context.ts +0 -97
  291. package/src/server/tools/clast-reindex.ts +0 -52
  292. package/src/server/tools/clast-search.ts +0 -38
  293. package/src/server/tools/clast-status.ts +0 -37
  294. package/src/watcher/gitignore.ts +0 -42
@@ -1,109 +1,420 @@
1
- #!/usr/bin/env node
2
-
3
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
3
  import { resolve } from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+ import { z } from 'zod';
6
6
 
7
- import { loadConfig, resolveDbPath } from '../config/index.js';
8
- import { ClastDatabase } from '../db/index.js';
9
- import { ParserManager } from '../parser/index.js';
7
+ import { Clast } from '../engine.js';
10
8
  import { FileWatcher } from '../watcher/index.js';
11
- import { LlmClient } from '../llm/index.js';
12
9
  import { CommentGenerator } from '../llm/comment-generator.js';
13
- import { registerAllTools } from './register-tools.js';
10
+ import { SYMBOL_KINDS } from '../types.js';
11
+ import { findSymbols } from '../query/resolve.js';
12
+ import { symLine } from '../query/format.js';
13
+ import { ToolContext } from '../tools/context.js';
14
+ import { callGraph, getContext, hierarchy, search } from '../tools/search.js';
15
+ import { changes, fileSummary, grep, overview, references, reindex, status } from '../tools/explore.js';
16
+ import { impact, unused } from '../tools/impact.js';
17
+ import { stats } from '../tools/stats.js';
18
+ import { VERSION } from '../version.js';
19
+
20
+ const INSTRUCTIONS = `Clast is a pre-built, always-fresh code index for this repository. One call answers what otherwise takes several grep + read round trips, with exact file:line locations. Use it first for code navigation:
21
+ - Unfamiliar repo or directory -> clast_overview, then clast_file_summary.
22
+ - Where is X / what is it called -> clast_search (names, partial names, Class.method, or words describing behaviour).
23
+ - Show me X, or before editing X -> clast_get_context (source of that symbol plus callers and callees).
24
+ - Who calls X -> clast_call_graph (depth N), clast_references (every use, including types and imports).
25
+ - Before changing or deleting X: what else must change, and which tests to run -> clast_impact.
26
+ - Dead code, cleanup after a refactor -> clast_unused.
27
+ - What implements / extends / overrides X, which base class to extend -> clast_hierarchy.
28
+ - How many X, how big, what is it made of -> clast_stats (exact totals; do not count search results by hand).
29
+ - Strings, comments, config keys -> clast_grep. After a change -> clast_changes (touched symbols, affected callers, tests).
30
+
31
+ When implementing a change: find the sibling implementations with clast_hierarchy or clast_search, read one with clast_get_context, and list every caller or switch you must update with clast_call_graph / clast_references before editing.
32
+ Most tools take several names at once (arrays). Symbols: name, Container.name, or path/to/file.ext:name. "?" marks a name-based guess. The index follows file changes, including your own edits.`;
33
+
34
+ const text = (t: string) => ({ content: [{ type: 'text' as const, text: t }] });
35
+
36
+ const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false } as const;
37
+
38
+ // Claude Code defers MCP tools behind its tool search by default. Measured on vscode and OnAir
39
+ // (bench/ab.mjs): with the tools deferred the agent answered every question with Grep and never
40
+ // called clast (0 of 60 sessions); loaded up front it used clast in 8 of 10. This asks the client
41
+ // to keep the navigation tools loaded.
42
+ const ALWAYS_LOAD = { 'anthropic/alwaysLoad': true } as const;
14
43
 
15
- async function main() {
16
- // Determine project root
44
+ export async function startServer(): Promise<void> {
17
45
  const projectRoot = resolve(
18
- process.env.CLAST_PROJECT_DIR ??
19
- process.env.CLAUDE_PROJECT_DIR ??
20
- process.cwd()
46
+ process.env.CLAST_PROJECT_DIR ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd()
21
47
  );
48
+ const log = (msg: string): void => {
49
+ console.error(`[clast] ${msg}`);
50
+ };
51
+ log(`v${VERSION} project root: ${projectRoot}`);
22
52
 
23
- console.error(`[clast] Project root: ${projectRoot}`);
53
+ const clast = new Clast(projectRoot);
54
+ const ctx = new ToolContext(clast);
55
+ const watcher = new FileWatcher(clast.indexer, clast.config.watch.debounceMs);
56
+ const commentGen = new CommentGenerator(clast.db, clast.config, ctx.lines);
24
57
 
25
- // Load configuration
26
- const config = loadConfig(projectRoot);
27
- console.error(`[clast] Config loaded. Languages: ${config.languages.join(', ')}`);
58
+ const server = new McpServer({ name: 'clast', version: VERSION }, { instructions: INSTRUCTIONS });
28
59
 
29
- // Open database (async — sql.js uses WASM)
30
- const dbPath = resolveDbPath(projectRoot, config);
31
- console.error(`[clast] Database: ${dbPath}`);
32
- const db = await ClastDatabase.open(dbPath);
60
+ const names = z.union([z.string(), z.array(z.string()).max(20)]);
61
+ const guard = <A>(fn: (args: A) => Promise<string>, gated = true) => async (args: A) => {
62
+ try {
63
+ if (gated) {
64
+ const building = await ctx.gate();
65
+ if (building) return text(building);
66
+ }
67
+ return text(await fn(args));
68
+ } catch (err) {
69
+ log(`tool error: ${err instanceof Error ? err.stack ?? err.message : err}`);
70
+ return { ...text(`error: ${err instanceof Error ? err.message : String(err)}`), isError: true };
71
+ }
72
+ };
33
73
 
34
- // Create parser (async — web-tree-sitter uses WASM)
35
- const parser = new ParserManager(config);
36
- await parser.init();
74
+ server.registerTool(
75
+ 'clast_search',
76
+ {
77
+ title: 'Search symbols',
78
+ description:
79
+ 'Ranked symbol search. Query by exact, partial or abbreviated name ("parseConf", "pcfg"), typo, "Class.method", or plain words describing behaviour (matched against names, signatures, docs and the identifiers used in function bodies). Returns kind, qualified name, file:line, signature and doc summary. Prefer over grep for finding definitions. Array = several searches.',
80
+ inputSchema: {
81
+ query: names.describe('Name, partial name, Container.name, or descriptive words. Array = several searches.'),
82
+ kind: z.enum(SYMBOL_KINDS as [string, ...string[]]).optional().describe('Only this kind of symbol'),
83
+ path: z.string().optional().describe('Only symbols in files under / matching this path (e.g. "src/db" or "parser.ts")'),
84
+ lang: z.string().optional().describe('Only this language id (typescript, python, go, ...)'),
85
+ exported: z.boolean().optional().describe('Only exported / public symbols'),
86
+ limit: z.number().int().min(1).max(100).default(10).describe('Max results per query'),
87
+ },
88
+ annotations: READ_ONLY,
89
+ _meta: ALWAYS_LOAD,
90
+ },
91
+ guard(args => search(ctx, args))
92
+ );
37
93
 
38
- // Create watcher
39
- const watcher = new FileWatcher(projectRoot, config, db, parser);
94
+ server.registerTool(
95
+ 'clast_get_context',
96
+ {
97
+ title: 'Symbol source and context',
98
+ description:
99
+ 'One symbol in one call: its exact source with line numbers plus callers, callees, overloads, overrides and, for classes, members and sub/supertypes. Use INSTEAD of reading a whole file, and before editing a symbol. Address as "name", "Class.method", "path/file.ext:name" or "path/file.ext:<line>". Array = several symbols.',
100
+ inputSchema: {
101
+ name: names.describe('Symbol name, Container.name, or path:name. Array = several symbols.'),
102
+ path: z.string().optional().describe('Disambiguate by file or directory path'),
103
+ includeBody: z.boolean().default(true).describe('Include source code (false = signature + docs only)'),
104
+ maxLines: z.number().int().min(5).max(2000).default(120).describe('Max source lines per symbol; large classes fall back to a member outline'),
105
+ includeCalls: z.boolean().default(true).describe('Include callers and callees'),
106
+ },
107
+ annotations: READ_ONLY,
108
+ _meta: ALWAYS_LOAD,
109
+ },
110
+ guard(args => getContext(ctx, args))
111
+ );
40
112
 
41
- // Create LLM client + comment generator
42
- const llmClient = new LlmClient(config.llm.endpoint, config.llm.model, config.llm.apiKey);
43
- const commentGen = new CommentGenerator(llmClient, db.queries, config);
113
+ server.registerTool(
114
+ 'clast_call_graph',
115
+ {
116
+ title: 'Call graph',
117
+ description:
118
+ 'Callers and/or callees as a tree with file:line per edge; depth > 1 follows the chain. Use for the blast radius before a change (every caller to update) or to trace a path. Resolved by receiver type, imports and inheritance; "?" marks name-only matches. Test callers are summarised. Library names list their call sites.',
119
+ inputSchema: {
120
+ name: z.string().describe('Symbol name, Container.name, or path:name'),
121
+ path: z.string().optional().describe('Disambiguate by file or directory path'),
122
+ direction: z.enum(['callers', 'callees', 'both']).default('both'),
123
+ depth: z.number().int().min(1).max(6).default(1).describe('Levels to traverse (1 = direct only; 2-3 to follow a chain)'),
124
+ limit: z.number().int().min(1).max(2000).default(50).describe('Max nodes per direction'),
125
+ includeTests: z.boolean().optional().describe('true = list test callers in full, false = drop them; default: one summary line naming the test files'),
126
+ within: z.string().optional().describe('Only callers located under this path (e.g. "src/server")'),
127
+ uncertain: z.boolean().default(false).describe('List every unconfirmed caller instead of a short summary'),
128
+ },
129
+ annotations: READ_ONLY,
130
+ _meta: ALWAYS_LOAD,
131
+ },
132
+ guard(args => callGraph(ctx, args))
133
+ );
44
134
 
45
- if (llmClient.isConfigured) {
46
- console.error(`[clast] LLM configured: ${config.llm.model} @ ${config.llm.endpoint}`);
47
- } else {
48
- console.error('[clast] LLM not configured (comment generation disabled)');
49
- }
135
+ server.registerTool(
136
+ 'clast_hierarchy',
137
+ {
138
+ title: 'Type hierarchy',
139
+ description:
140
+ 'Supertypes and all direct and indirect subtypes of a class, interface, struct or trait as a tree (including Go interface satisfaction). For a method: the same method on related types (declaration, overrides, implementations). Use for "what implements X", "which classes override Y", "which base class do the others extend".',
141
+ inputSchema: {
142
+ name: z.string().describe('Type or method: name, Container.name, or path:name'),
143
+ path: z.string().optional().describe('Disambiguate by file or directory path'),
144
+ direction: z.enum(['supertypes', 'subtypes', 'both']).default('both'),
145
+ depth: z.number().int().min(1).max(8).default(3).describe('Levels to expand'),
146
+ limit: z.number().int().min(1).max(1000).default(80).describe('Max lines per direction'),
147
+ within: z.string().optional().describe('Only subtypes / overrides located under this path'),
148
+ },
149
+ annotations: READ_ONLY,
150
+ _meta: ALWAYS_LOAD,
151
+ },
152
+ guard(args => hierarchy(ctx, args))
153
+ );
50
154
 
51
- // Create MCP server
52
- const server = new McpServer({
53
- name: 'clast',
54
- version: '0.1.0',
55
- });
155
+ server.registerTool(
156
+ 'clast_references',
157
+ {
158
+ title: 'Find references',
159
+ description:
160
+ 'Every use of an identifier (calls, type annotations, imports, assignments) grouped by file, each line with its enclosing function/class. Matches real identifiers only, not substrings, strings or comments. Use for variables, types, fields, constants and enum members, or when clast_call_graph is too narrow.',
161
+ inputSchema: {
162
+ name: z.string().describe('Identifier (or Container.name; matching is by the last name segment)'),
163
+ path: z.string().optional().describe('Only references in files under / matching this path'),
164
+ limit: z.number().int().min(1).max(500).default(40).describe('Max reference lines'),
165
+ includeTests: z.boolean().default(true),
166
+ },
167
+ annotations: READ_ONLY,
168
+ _meta: ALWAYS_LOAD,
169
+ },
170
+ guard(args => references(ctx, args))
171
+ );
56
172
 
57
- // Register all tools
58
- registerAllTools(server, db, parser, watcher, commentGen, projectRoot);
173
+ server.registerTool(
174
+ 'clast_file_summary',
175
+ {
176
+ title: 'File or directory outline',
177
+ description:
178
+ 'Outline of a file: symbols as a tree of signatures with line ranges and doc summaries, plus its imports and importers. Use INSTEAD of reading a file to learn its structure. For a directory: its files with main symbols and sub-directories with sizes. Partial paths work ("db/queries").',
179
+ inputSchema: {
180
+ path: z.string().describe('File or directory path (relative; partial paths are matched)'),
181
+ members: z
182
+ .enum(['auto', 'all', 'callable'])
183
+ .default('auto')
184
+ .describe('Whether to list fields/properties/constants inside classes (auto hides them in very large files)'),
185
+ limit: z.number().int().min(20).max(5000).default(300).describe('Max outline lines for a file'),
186
+ },
187
+ annotations: READ_ONLY,
188
+ _meta: ALWAYS_LOAD,
189
+ },
190
+ guard(args => fileSummary(ctx, args))
191
+ );
59
192
 
60
- // Perform initial index
61
- console.error('[clast] Starting initial index...');
62
- const indexResult = await parser.fullIndex(projectRoot, db, (file, current, total) => {
63
- if (current % 50 === 0 || current === total) {
64
- console.error(`[clast] Indexing: ${current}/${total} files`);
65
- }
66
- });
67
- console.error(
68
- `[clast] Index complete: ${indexResult.filesProcessed} files, ` +
69
- `${indexResult.nodesFound} nodes, ${indexResult.edgesResolved} edges resolved ` +
70
- `(${indexResult.durationMs}ms)`
193
+ server.registerTool(
194
+ 'clast_overview',
195
+ {
196
+ title: 'Repository map',
197
+ description:
198
+ 'Map of the repository or a sub-directory: languages, layout with sizes, most referenced classes/functions, hub files, entry points. Call first in an unfamiliar codebase.',
199
+ inputSchema: {
200
+ path: z.string().optional().describe('Sub-directory to map (default: whole repository)'),
201
+ },
202
+ annotations: READ_ONLY,
203
+ _meta: ALWAYS_LOAD,
204
+ },
205
+ guard(args => overview(ctx, args))
71
206
  );
72
207
 
73
- // Start file watcher
74
- watcher.start();
75
- watcher.on('indexed', ({ filePath, nodesFound }: { filePath: string; nodesFound: number }) => {
76
- console.error(`[clast] Updated: ${filePath} (${nodesFound} nodes)`);
77
- });
78
- watcher.on('deleted', ({ filePath }: { filePath: string }) => {
79
- console.error(`[clast] Removed: ${filePath}`);
80
- });
81
- watcher.on('error', ({ filePath, error }: { filePath: string; error: Error }) => {
82
- console.error(`[clast] Error processing ${filePath}:`, error.message);
83
- });
208
+ server.registerTool(
209
+ 'clast_grep',
210
+ {
211
+ title: 'Text search with symbol context',
212
+ description:
213
+ 'Text search over all text files (literal or regex, smart-case); each hit is labelled with its enclosing function/class. Use for strings, comments, error messages and config keys; for identifiers prefer clast_search / clast_references.',
214
+ inputSchema: {
215
+ pattern: z.string().describe('Text to find (literal unless regex=true)'),
216
+ regex: z.boolean().default(false).describe('Interpret pattern as a JavaScript regular expression'),
217
+ path: z.string().optional().describe('Only files under / matching this path'),
218
+ lang: z.string().optional().describe('Only this language id'),
219
+ caseSensitive: z.boolean().optional().describe('Default: case-insensitive unless the pattern contains uppercase'),
220
+ limit: z.number().int().min(1).max(500).default(50).describe('Max matching lines'),
221
+ includeTests: z.boolean().default(true),
222
+ },
223
+ annotations: READ_ONLY,
224
+ _meta: ALWAYS_LOAD,
225
+ },
226
+ guard(args => grep(ctx, args))
227
+ );
228
+
229
+ server.registerTool(
230
+ 'clast_stats',
231
+ {
232
+ title: 'Count and aggregate',
233
+ description:
234
+ 'Exact counts for "how many ..." questions, grouped by directory, file, kind or name; do not tally search results by hand. what = inventory (size, languages, production vs tests, symbols by kind, longest functions), symbols (name pattern "*Receiver|*Service" and/or kind), subtypes (direct and indirect subclasses / implementers), calls (call sites of "query|execute", optionally on a receiver), text (regex), imports (external dependencies), endpoints (route declarations of common web frameworks), database (SQL, schema, ORM models, query calls; heuristic).',
235
+ inputSchema: {
236
+ what: z.enum(['inventory', 'symbols', 'subtypes', 'calls', 'text', 'imports', 'endpoints', 'database']).optional()
237
+ .describe('What to count (default inventory, or symbols when name is given)'),
238
+ name: z.string().optional().describe('symbols: name pattern with * and | ; subtypes: base type; calls: called name(s); text: regular expression'),
239
+ kind: z.string().optional().describe('symbols: only these kinds, comma separated (class, interface, function, method, ...)'),
240
+ on: z.string().optional().describe('calls: only on receivers / receiver types matching this pattern'),
241
+ path: z.string().optional().describe('Only files under / matching this path'),
242
+ lang: z.string().optional().describe('Only this language id'),
243
+ groupBy: z.enum(['dir', 'file', 'kind', 'lang', 'name', 'container', 'callee', 'caller', 'receiver', 'match']).optional()
244
+ .describe('Grouping of the main table (default dir)'),
245
+ depth: z.number().int().min(1).max(12).optional().describe('Directory depth for groupBy=dir (default 2)'),
246
+ limit: z.number().int().min(1).max(500).optional().describe('Rows per table (default 25)'),
247
+ includeTests: z.boolean().optional().describe('Include tests/examples in the tables (default: counted separately)'),
248
+ },
249
+ annotations: READ_ONLY,
250
+ _meta: ALWAYS_LOAD,
251
+ },
252
+ guard(args => stats(ctx, args))
253
+ );
84
254
 
85
- if (watcher.isActive) {
86
- console.error('[clast] File watcher active');
87
- }
255
+ server.registerTool(
256
+ 'clast_impact',
257
+ {
258
+ title: 'Impact of changing a symbol',
259
+ description:
260
+ 'Before changing a signature, behaviour or deleting a symbol: one answer with the declarations / overrides / implementations that must change together, every direct caller by file and line, how far the change reaches through indirect callers, and the test files to run (those that call it directly or through its callers).',
261
+ inputSchema: {
262
+ name: z.string().describe('Symbol: name, Container.name, or path:name'),
263
+ path: z.string().optional().describe('Disambiguate by file or directory path'),
264
+ depth: z.number().int().min(1).max(6).optional().describe('Caller levels to follow (default 3)'),
265
+ limit: z.number().int().min(1).max(500).optional().describe('Max lines per section (default 30)'),
266
+ },
267
+ annotations: READ_ONLY,
268
+ _meta: ALWAYS_LOAD,
269
+ },
270
+ guard(args => impact(ctx, args))
271
+ );
272
+
273
+ server.registerTool(
274
+ 'clast_unused',
275
+ {
276
+ title: 'Unused code',
277
+ description:
278
+ 'Dead code: functions, methods and types that nothing references (or that only other unused code references), grouped by file with line ranges; plus code used only by tests, and names that only appear in strings or comments. Entry points, decorated / attribute-marked symbols and overrides of library types are left out. Use after a refactor or to clean up a directory.',
279
+ inputSchema: {
280
+ path: z.string().optional().describe('Only judge symbols under / matching this path'),
281
+ lang: z.string().optional().describe('Only this language id'),
282
+ kind: z.string().optional().describe('Kinds to judge, comma separated (default function, method, class, struct, interface, trait, enum, type)'),
283
+ exported: z.boolean().optional().describe('false: leave out exported / public symbols'),
284
+ limit: z.number().int().min(1).max(1000).optional().describe('Max symbols listed (default 60)'),
285
+ },
286
+ annotations: READ_ONLY,
287
+ _meta: ALWAYS_LOAD,
288
+ },
289
+ guard(args => unused(ctx, args))
290
+ );
88
291
 
89
- // Connect to stdio transport
292
+ server.registerTool(
293
+ 'clast_changes',
294
+ {
295
+ title: 'Changed symbols and impact',
296
+ description:
297
+ 'Map the current git diff to code structure: which functions/classes were touched (uncommitted changes by default, or relative to a base ref such as "main"), which callers outside the change may be affected, and which test files exercise the changed symbols. Use when reviewing a change, before committing, or to decide which tests to run.',
298
+ inputSchema: {
299
+ base: z.string().optional().describe('Git ref to diff against (default HEAD = uncommitted changes; e.g. "main", "HEAD~3")'),
300
+ },
301
+ annotations: { readOnlyHint: true, openWorldHint: false },
302
+ },
303
+ guard(args => changes(ctx, args))
304
+ );
305
+
306
+ server.registerTool(
307
+ 'clast_status',
308
+ {
309
+ title: 'Index status',
310
+ description: 'Index statistics and health: files, symbols, languages, whether indexing is in progress, watcher state.',
311
+ inputSchema: {},
312
+ annotations: READ_ONLY,
313
+ },
314
+ guard(() => status(ctx, watcher), false)
315
+ );
316
+
317
+ server.registerTool(
318
+ 'clast_reindex',
319
+ {
320
+ title: 'Re-index',
321
+ description:
322
+ 'Re-scan the repository (incremental: only changed files are re-parsed) or force re-index one file. Rarely needed - the index follows file changes automatically and re-checks files when they are queried. Use force=true to rebuild from scratch.',
323
+ inputSchema: {
324
+ path: z.string().optional().describe('Single file to re-index (default: whole repository)'),
325
+ force: z.boolean().default(false).describe('Re-parse every file even if unchanged'),
326
+ },
327
+ annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false },
328
+ },
329
+ guard(args => reindex(ctx, args), false)
330
+ );
331
+
332
+ server.registerTool(
333
+ 'clast_comment',
334
+ {
335
+ title: 'Get or generate documentation',
336
+ description:
337
+ 'Return the documentation comment of a symbol. With generate=true and a local LLM configured (llm.model in clast.config.json), generates and stores a doc comment for undocumented symbols.',
338
+ inputSchema: {
339
+ name: z.string().describe('Symbol name, Container.name, or path:name'),
340
+ generate: z.boolean().default(false).describe('Generate with the configured LLM if no comment exists'),
341
+ },
342
+ annotations: { readOnlyHint: false, openWorldHint: true },
343
+ },
344
+ guard(async ({ name, generate }: { name: string; generate: boolean }) => {
345
+ await ctx.ready();
346
+ const matches = findSymbols(ctx.db, name);
347
+ if (matches.length === 0) return `"${name}": no symbol with this name.`;
348
+ const out: string[] = [];
349
+ for (const sym of matches.slice(0, 5)) {
350
+ out.push(symLine(sym));
351
+ const stored = commentGen.stored(sym);
352
+ if (sym.doc) out.push(sym.doc);
353
+ else if (stored) out.push(`[generated] ${stored}`);
354
+ else if (generate) {
355
+ if (!commentGen.isAvailable) out.push('(no comment; LLM not configured - set llm.model in clast.config.json)');
356
+ else out.push((await commentGen.generate(sym)) ?? '(generation failed; see server log)');
357
+ } else out.push('(no comment)');
358
+ }
359
+ return out.join('\n');
360
+ })
361
+ );
362
+
363
+ // Connect first so the client never waits on indexing; the index builds
364
+ // (or revalidates) in the background and tools report progress meanwhile.
90
365
  const transport = new StdioServerTransport();
91
366
  await server.connect(transport);
92
- console.error('[clast] MCP server running on stdio');
93
-
94
- // Graceful shutdown
95
- const shutdown = () => {
96
- console.error('[clast] Shutting down...');
97
- watcher.stop();
98
- db.close();
99
- process.exit(0);
100
- };
367
+ log('MCP server running on stdio');
368
+
369
+ clast.indexer
370
+ .indexAll()
371
+ .then(r => {
372
+ const s = clast.db.stats();
373
+ log(
374
+ `index ready: scanned ${r.scanned}, parsed ${r.parsed}, removed ${r.removed} in ${r.durationMs}ms ` +
375
+ `(${s.files} files, ${s.symbols} symbols)`
376
+ );
377
+ })
378
+ .catch(err => log(`indexing failed: ${err instanceof Error ? err.stack : err}`))
379
+ .finally(() => {
380
+ if (clast.config.watch.enabled) {
381
+ try {
382
+ watcher.start();
383
+ log('file watcher active');
384
+ } catch (err) {
385
+ log(`watcher failed to start: ${err instanceof Error ? err.message : err}`);
386
+ }
387
+ }
388
+ });
101
389
 
390
+ watcher.on('error', ({ filePath, error }: { filePath: string; error: Error }) => {
391
+ log(`watch error ${filePath}: ${error.message}`);
392
+ });
393
+
394
+ let closing = false;
395
+ const shutdown = (): void => {
396
+ if (closing) return;
397
+ closing = true;
398
+ watcher
399
+ .stop()
400
+ .catch(() => undefined)
401
+ .finally(() => {
402
+ try { clast.close(); } catch { /* already closed */ }
403
+ process.exit(0);
404
+ });
405
+ };
102
406
  process.on('SIGINT', shutdown);
103
407
  process.on('SIGTERM', shutdown);
408
+ // The client closing stdin is the normal way an MCP server is stopped.
409
+ process.stdin.on('close', shutdown);
104
410
  }
105
411
 
106
- main().catch((err) => {
107
- console.error('[clast] Fatal error:', err);
108
- process.exit(1);
109
- });
412
+ // Backwards compatibility: configurations written for 0.1.x launch this file
413
+ // directly (`node dist/server/index.js`). Keep that working.
414
+ const invoked = process.argv[1] ? pathToFileURL(process.argv[1]).href : '';
415
+ if (invoked === import.meta.url) {
416
+ startServer().catch(err => {
417
+ console.error('[clast] Fatal error:', err);
418
+ process.exit(1);
419
+ });
420
+ }
@@ -0,0 +1,82 @@
1
+ import type { Clast } from '../engine.js';
2
+ import { LineCache } from '../query/format.js';
3
+ import { Resolver } from '../query/resolve.js';
4
+
5
+ /**
6
+ * Per-engine state shared by all tool implementations.
7
+ */
8
+ export class ToolContext {
9
+ readonly lines: LineCache;
10
+ private resolverGen = -1;
11
+ private resolverCache: Resolver | null = null;
12
+ /** How long a query waits for an in-flight bulk index before answering from a partial index. */
13
+ waitMs = 8000;
14
+
15
+ constructor(readonly clast: Clast) {
16
+ this.lines = new LineCache(clast.root);
17
+ }
18
+
19
+ get db() {
20
+ return this.clast.db;
21
+ }
22
+
23
+ get indexer() {
24
+ return this.clast.indexer;
25
+ }
26
+
27
+ /** Resolver whose caches are valid for the current database generation. */
28
+ get resolver(): Resolver {
29
+ if (!this.resolverCache || this.resolverGen !== this.db.generation) {
30
+ this.resolverCache = new Resolver(this.db);
31
+ this.resolverGen = this.db.generation;
32
+ }
33
+ return this.resolverCache;
34
+ }
35
+
36
+ /**
37
+ * Called at the start of every query tool. Waits briefly for a running
38
+ * bulk index; returns a note to prepend when results may be incomplete.
39
+ */
40
+ async ready(): Promise<string> {
41
+ if (!this.indexer.isIndexing) return '';
42
+ const done = await this.indexer.whenIdle(this.waitMs);
43
+ if (done) return '';
44
+ const p = this.indexer.progress;
45
+ return `note: index is still being built (${p.done}/${p.total} files parsed) - results may be incomplete; retry shortly.\n\n`;
46
+ }
47
+
48
+ /**
49
+ * Gate for query tools: waits briefly for a running index. If a FIRST-TIME
50
+ * build is still going after that, returns a message to send instead of
51
+ * running the query (the half-loaded index has no lookup indexes yet, so a
52
+ * query would be slow and incomplete). Otherwise returns null.
53
+ */
54
+ async gate(): Promise<string | null> {
55
+ if (!this.indexer.isIndexing) return null;
56
+ const done = await this.indexer.whenIdle(this.waitMs);
57
+ if (done || !this.indexer.coldBuild) return null;
58
+ const p = this.indexer.progress;
59
+ const pct = p.total ? Math.floor((100 * p.done) / p.total) : 0;
60
+ return (
61
+ `clast is building its index for the first time (${p.done}/${p.total} files, ${pct}%). ` +
62
+ `This takes seconds to about half a minute depending on repository size; call again shortly. ` +
63
+ `(Later sessions start from the saved index and are ready immediately.)`
64
+ );
65
+ }
66
+
67
+ /** Re-index any of these files that changed on disk since they were indexed. */
68
+ async fresh(paths: Iterable<string>): Promise<void> {
69
+ const list = [...paths];
70
+ const updated = await this.indexer.ensureFresh(list);
71
+ if (updated > 0) {
72
+ for (const p of list) this.lines.invalidate(p);
73
+ }
74
+ }
75
+ }
76
+
77
+ /** Accept `"a"`, `["a", "b"]` or `"a, b"` for batched lookups. */
78
+ export function toList(value: string | string[] | undefined, splitCommas = false): string[] {
79
+ if (value === undefined) return [];
80
+ const arr = Array.isArray(value) ? value : splitCommas ? value.split(',') : [value];
81
+ return arr.map(s => s.trim()).filter(Boolean);
82
+ }