seer-mcp 0.1.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.
Files changed (371) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/LICENSE +176 -0
  3. package/README.md +272 -0
  4. package/README_dev.md +199 -0
  5. package/dist/bundle/ci.d.ts +47 -0
  6. package/dist/bundle/ci.d.ts.map +1 -0
  7. package/dist/bundle/ci.js +113 -0
  8. package/dist/bundle/ci.js.map +1 -0
  9. package/dist/bundle/contract.d.ts +111 -0
  10. package/dist/bundle/contract.d.ts.map +1 -0
  11. package/dist/bundle/contract.js +352 -0
  12. package/dist/bundle/contract.js.map +1 -0
  13. package/dist/bundle/export.d.ts +36 -0
  14. package/dist/bundle/export.d.ts.map +1 -0
  15. package/dist/bundle/export.js +152 -0
  16. package/dist/bundle/export.js.map +1 -0
  17. package/dist/bundle/external.d.ts +66 -0
  18. package/dist/bundle/external.d.ts.map +1 -0
  19. package/dist/bundle/external.js +238 -0
  20. package/dist/bundle/external.js.map +1 -0
  21. package/dist/bundle/format.d.ts +94 -0
  22. package/dist/bundle/format.d.ts.map +1 -0
  23. package/dist/bundle/format.js +42 -0
  24. package/dist/bundle/format.js.map +1 -0
  25. package/dist/bundle/import.d.ts +49 -0
  26. package/dist/bundle/import.d.ts.map +1 -0
  27. package/dist/bundle/import.js +116 -0
  28. package/dist/bundle/import.js.map +1 -0
  29. package/dist/cli/index.d.ts +3 -0
  30. package/dist/cli/index.d.ts.map +1 -0
  31. package/dist/cli/index.js +1402 -0
  32. package/dist/cli/index.js.map +1 -0
  33. package/dist/cli/init.d.ts +48 -0
  34. package/dist/cli/init.d.ts.map +1 -0
  35. package/dist/cli/init.js +284 -0
  36. package/dist/cli/init.js.map +1 -0
  37. package/dist/db/schema.d.ts +3 -0
  38. package/dist/db/schema.d.ts.map +1 -0
  39. package/dist/db/schema.js +616 -0
  40. package/dist/db/schema.js.map +1 -0
  41. package/dist/db/store.d.ts +1011 -0
  42. package/dist/db/store.d.ts.map +1 -0
  43. package/dist/db/store.js +3888 -0
  44. package/dist/db/store.js.map +1 -0
  45. package/dist/graph/pagerank.d.ts +9 -0
  46. package/dist/graph/pagerank.d.ts.map +1 -0
  47. package/dist/graph/pagerank.js +47 -0
  48. package/dist/graph/pagerank.js.map +1 -0
  49. package/dist/indexer/architecture.d.ts +72 -0
  50. package/dist/indexer/architecture.d.ts.map +1 -0
  51. package/dist/indexer/architecture.js +112 -0
  52. package/dist/indexer/architecture.js.map +1 -0
  53. package/dist/indexer/behavior.d.ts +75 -0
  54. package/dist/indexer/behavior.d.ts.map +1 -0
  55. package/dist/indexer/behavior.js +395 -0
  56. package/dist/indexer/behavior.js.map +1 -0
  57. package/dist/indexer/boundaries.d.ts +60 -0
  58. package/dist/indexer/boundaries.d.ts.map +1 -0
  59. package/dist/indexer/boundaries.js +366 -0
  60. package/dist/indexer/boundaries.js.map +1 -0
  61. package/dist/indexer/churn.d.ts +15 -0
  62. package/dist/indexer/churn.d.ts.map +1 -0
  63. package/dist/indexer/churn.js +49 -0
  64. package/dist/indexer/churn.js.map +1 -0
  65. package/dist/indexer/classify.d.ts +9 -0
  66. package/dist/indexer/classify.d.ts.map +1 -0
  67. package/dist/indexer/classify.js +90 -0
  68. package/dist/indexer/classify.js.map +1 -0
  69. package/dist/indexer/context.d.ts +176 -0
  70. package/dist/indexer/context.d.ts.map +1 -0
  71. package/dist/indexer/context.js +193 -0
  72. package/dist/indexer/context.js.map +1 -0
  73. package/dist/indexer/continuity.d.ts +67 -0
  74. package/dist/indexer/continuity.d.ts.map +1 -0
  75. package/dist/indexer/continuity.js +288 -0
  76. package/dist/indexer/continuity.js.map +1 -0
  77. package/dist/indexer/detectchanges.d.ts +32 -0
  78. package/dist/indexer/detectchanges.d.ts.map +1 -0
  79. package/dist/indexer/detectchanges.js +74 -0
  80. package/dist/indexer/detectchanges.js.map +1 -0
  81. package/dist/indexer/discovery.d.ts +37 -0
  82. package/dist/indexer/discovery.d.ts.map +1 -0
  83. package/dist/indexer/discovery.js +136 -0
  84. package/dist/indexer/discovery.js.map +1 -0
  85. package/dist/indexer/externaldeps.d.ts +18 -0
  86. package/dist/indexer/externaldeps.d.ts.map +1 -0
  87. package/dist/indexer/externaldeps.js +288 -0
  88. package/dist/indexer/externaldeps.js.map +1 -0
  89. package/dist/indexer/freshness.d.ts +48 -0
  90. package/dist/indexer/freshness.d.ts.map +1 -0
  91. package/dist/indexer/freshness.js +128 -0
  92. package/dist/indexer/freshness.js.map +1 -0
  93. package/dist/indexer/git.d.ts +144 -0
  94. package/dist/indexer/git.d.ts.map +1 -0
  95. package/dist/indexer/git.js +444 -0
  96. package/dist/indexer/git.js.map +1 -0
  97. package/dist/indexer/index.d.ts +145 -0
  98. package/dist/indexer/index.d.ts.map +1 -0
  99. package/dist/indexer/index.js +930 -0
  100. package/dist/indexer/index.js.map +1 -0
  101. package/dist/indexer/modules.d.ts +62 -0
  102. package/dist/indexer/modules.d.ts.map +1 -0
  103. package/dist/indexer/modules.js +293 -0
  104. package/dist/indexer/modules.js.map +1 -0
  105. package/dist/indexer/preflight.d.ts +154 -0
  106. package/dist/indexer/preflight.d.ts.map +1 -0
  107. package/dist/indexer/preflight.js +399 -0
  108. package/dist/indexer/preflight.js.map +1 -0
  109. package/dist/indexer/protoScanner.d.ts +34 -0
  110. package/dist/indexer/protoScanner.d.ts.map +1 -0
  111. package/dist/indexer/protoScanner.js +133 -0
  112. package/dist/indexer/protoScanner.js.map +1 -0
  113. package/dist/indexer/risk.d.ts +115 -0
  114. package/dist/indexer/risk.d.ts.map +1 -0
  115. package/dist/indexer/risk.js +194 -0
  116. package/dist/indexer/risk.js.map +1 -0
  117. package/dist/indexer/serviceHostScanner.d.ts +25 -0
  118. package/dist/indexer/serviceHostScanner.d.ts.map +1 -0
  119. package/dist/indexer/serviceHostScanner.js +95 -0
  120. package/dist/indexer/serviceHostScanner.js.map +1 -0
  121. package/dist/indexer/serviceLinks.d.ts +105 -0
  122. package/dist/indexer/serviceLinks.d.ts.map +1 -0
  123. package/dist/indexer/serviceLinks.js +509 -0
  124. package/dist/indexer/serviceLinks.js.map +1 -0
  125. package/dist/indexer/shapehash.d.ts +98 -0
  126. package/dist/indexer/shapehash.d.ts.map +1 -0
  127. package/dist/indexer/shapehash.js +354 -0
  128. package/dist/indexer/shapehash.js.map +1 -0
  129. package/dist/indexer/skeleton.d.ts +15 -0
  130. package/dist/indexer/skeleton.d.ts.map +1 -0
  131. package/dist/indexer/skeleton.js +136 -0
  132. package/dist/indexer/skeleton.js.map +1 -0
  133. package/dist/indexer/symbolhistory.d.ts +41 -0
  134. package/dist/indexer/symbolhistory.d.ts.map +1 -0
  135. package/dist/indexer/symbolhistory.js +124 -0
  136. package/dist/indexer/symbolhistory.js.map +1 -0
  137. package/dist/indexer/watcher.d.ts +68 -0
  138. package/dist/indexer/watcher.d.ts.map +1 -0
  139. package/dist/indexer/watcher.js +179 -0
  140. package/dist/indexer/watcher.js.map +1 -0
  141. package/dist/mcp/server.d.ts +80 -0
  142. package/dist/mcp/server.d.ts.map +1 -0
  143. package/dist/mcp/server.js +1610 -0
  144. package/dist/mcp/server.js.map +1 -0
  145. package/dist/parser/index.d.ts +8 -0
  146. package/dist/parser/index.d.ts.map +1 -0
  147. package/dist/parser/index.js +33 -0
  148. package/dist/parser/index.js.map +1 -0
  149. package/dist/parser/languages/cpp.d.ts +3 -0
  150. package/dist/parser/languages/cpp.d.ts.map +1 -0
  151. package/dist/parser/languages/cpp.js +350 -0
  152. package/dist/parser/languages/cpp.js.map +1 -0
  153. package/dist/parser/languages/csharp.d.ts +3 -0
  154. package/dist/parser/languages/csharp.d.ts.map +1 -0
  155. package/dist/parser/languages/csharp.js +239 -0
  156. package/dist/parser/languages/csharp.js.map +1 -0
  157. package/dist/parser/languages/go.d.ts +3 -0
  158. package/dist/parser/languages/go.d.ts.map +1 -0
  159. package/dist/parser/languages/go.js +259 -0
  160. package/dist/parser/languages/go.js.map +1 -0
  161. package/dist/parser/languages/java.d.ts +3 -0
  162. package/dist/parser/languages/java.d.ts.map +1 -0
  163. package/dist/parser/languages/java.js +391 -0
  164. package/dist/parser/languages/java.js.map +1 -0
  165. package/dist/parser/languages/python.d.ts +3 -0
  166. package/dist/parser/languages/python.d.ts.map +1 -0
  167. package/dist/parser/languages/python.js +396 -0
  168. package/dist/parser/languages/python.js.map +1 -0
  169. package/dist/parser/languages/rust.d.ts +3 -0
  170. package/dist/parser/languages/rust.d.ts.map +1 -0
  171. package/dist/parser/languages/rust.js +159 -0
  172. package/dist/parser/languages/rust.js.map +1 -0
  173. package/dist/parser/languages/typescript.d.ts +3 -0
  174. package/dist/parser/languages/typescript.d.ts.map +1 -0
  175. package/dist/parser/languages/typescript.js +1442 -0
  176. package/dist/parser/languages/typescript.js.map +1 -0
  177. package/dist/parser/parserContext.d.ts +77 -0
  178. package/dist/parser/parserContext.d.ts.map +1 -0
  179. package/dist/parser/parserContext.js +354 -0
  180. package/dist/parser/parserContext.js.map +1 -0
  181. package/dist/parser/walker.d.ts +81 -0
  182. package/dist/parser/walker.d.ts.map +1 -0
  183. package/dist/parser/walker.js +217 -0
  184. package/dist/parser/walker.js.map +1 -0
  185. package/dist/parser/worker.d.ts +66 -0
  186. package/dist/parser/worker.d.ts.map +1 -0
  187. package/dist/parser/worker.js +129 -0
  188. package/dist/parser/worker.js.map +1 -0
  189. package/dist/parser/workerpool.d.ts +107 -0
  190. package/dist/parser/workerpool.d.ts.map +1 -0
  191. package/dist/parser/workerpool.js +383 -0
  192. package/dist/parser/workerpool.js.map +1 -0
  193. package/dist/scip/format.d.ts +87 -0
  194. package/dist/scip/format.d.ts.map +1 -0
  195. package/dist/scip/format.js +31 -0
  196. package/dist/scip/format.js.map +1 -0
  197. package/dist/scip/import.d.ts +37 -0
  198. package/dist/scip/import.d.ts.map +1 -0
  199. package/dist/scip/import.js +180 -0
  200. package/dist/scip/import.js.map +1 -0
  201. package/dist/types.d.ts +392 -0
  202. package/dist/types.d.ts.map +1 -0
  203. package/dist/types.js +4 -0
  204. package/dist/types.js.map +1 -0
  205. package/docs/architecture.md +105 -0
  206. package/docs/benchmarks/methodology.md +134 -0
  207. package/docs/benchmarks/raw-results.md +71 -0
  208. package/docs/benchmarks.md +74 -0
  209. package/docs/cli.md +148 -0
  210. package/docs/examples/behavior-tests.md +70 -0
  211. package/docs/examples/change-history.md +85 -0
  212. package/docs/examples/pre-edit-context.md +81 -0
  213. package/docs/examples/service-links.md +88 -0
  214. package/docs/examples.md +80 -0
  215. package/docs/faq.md +70 -0
  216. package/docs/internals.md +104 -0
  217. package/docs/languages.md +70 -0
  218. package/docs/limits.md +52 -0
  219. package/docs/mcp.md +199 -0
  220. package/docs/quickstart.md +119 -0
  221. package/docs/testing.md +123 -0
  222. package/docs/tools.md +115 -0
  223. package/package.json +52 -0
  224. package/research-codebase.md +578 -0
  225. package/seer-cli-docs.md +326 -0
  226. package/seer-master-guide.md +246 -0
  227. package/src/bundle/ci.ts +141 -0
  228. package/src/bundle/contract.ts +387 -0
  229. package/src/bundle/export.ts +175 -0
  230. package/src/bundle/external.ts +285 -0
  231. package/src/bundle/format.ts +92 -0
  232. package/src/bundle/import.ts +157 -0
  233. package/src/cli/index.ts +1249 -0
  234. package/src/cli/init.ts +389 -0
  235. package/src/db/schema.ts +614 -0
  236. package/src/db/store.ts +4306 -0
  237. package/src/graph/pagerank.ts +53 -0
  238. package/src/indexer/architecture.ts +148 -0
  239. package/src/indexer/behavior.ts +466 -0
  240. package/src/indexer/boundaries.ts +374 -0
  241. package/src/indexer/churn.ts +58 -0
  242. package/src/indexer/classify.ts +96 -0
  243. package/src/indexer/context.ts +340 -0
  244. package/src/indexer/continuity.ts +322 -0
  245. package/src/indexer/detectchanges.ts +94 -0
  246. package/src/indexer/discovery.ts +176 -0
  247. package/src/indexer/externaldeps.ts +243 -0
  248. package/src/indexer/freshness.ts +166 -0
  249. package/src/indexer/git.ts +453 -0
  250. package/src/indexer/index.ts +1092 -0
  251. package/src/indexer/modules.ts +358 -0
  252. package/src/indexer/preflight.ts +548 -0
  253. package/src/indexer/protoScanner.ts +147 -0
  254. package/src/indexer/risk.ts +304 -0
  255. package/src/indexer/serviceHostScanner.ts +92 -0
  256. package/src/indexer/serviceLinks.ts +543 -0
  257. package/src/indexer/shapehash.ts +370 -0
  258. package/src/indexer/skeleton.ts +169 -0
  259. package/src/indexer/symbolhistory.ts +172 -0
  260. package/src/indexer/watcher.ts +206 -0
  261. package/src/mcp/server.ts +1659 -0
  262. package/src/parser/index.ts +37 -0
  263. package/src/parser/languages/cpp.ts +361 -0
  264. package/src/parser/languages/csharp.ts +235 -0
  265. package/src/parser/languages/go.ts +259 -0
  266. package/src/parser/languages/java.ts +382 -0
  267. package/src/parser/languages/python.ts +370 -0
  268. package/src/parser/languages/rust.ts +164 -0
  269. package/src/parser/languages/typescript.ts +1435 -0
  270. package/src/parser/parserContext.ts +392 -0
  271. package/src/parser/walker.ts +306 -0
  272. package/src/parser/worker.ts +181 -0
  273. package/src/parser/workerpool.ts +448 -0
  274. package/src/scip/format.ts +83 -0
  275. package/src/scip/import.ts +216 -0
  276. package/src/types.ts +457 -0
  277. package/tests/benchmark-service-links.ts +244 -0
  278. package/tests/bug-regressions.ts +626 -0
  279. package/tests/filters.ts +264 -0
  280. package/tests/fixtures/Counter.tsx +38 -0
  281. package/tests/fixtures/caller.ts +7 -0
  282. package/tests/fixtures/collisions.ts +23 -0
  283. package/tests/fixtures/local_helper.ts +5 -0
  284. package/tests/fixtures/overloads.java +17 -0
  285. package/tests/fixtures/remote_helper.ts +4 -0
  286. package/tests/fixtures/sample.c +15 -0
  287. package/tests/fixtures/sample.cpp +47 -0
  288. package/tests/fixtures/sample.cs +62 -0
  289. package/tests/fixtures/sample.go +68 -0
  290. package/tests/fixtures/sample.h +30 -0
  291. package/tests/fixtures/sample.java +85 -0
  292. package/tests/fixtures/sample.py +46 -0
  293. package/tests/fixtures/sample.rs +78 -0
  294. package/tests/fixtures/sample.ts +76 -0
  295. package/tests/fixtures-service/HttpClients.cs +30 -0
  296. package/tests/fixtures-service/HttpClients.java +24 -0
  297. package/tests/fixtures-service/billing.ts +15 -0
  298. package/tests/fixtures-service/docker-compose.yml +15 -0
  299. package/tests/fixtures-service/gateway.ts +10 -0
  300. package/tests/fixtures-service/get_user.ts +11 -0
  301. package/tests/fixtures-service/graphql_client.ts +63 -0
  302. package/tests/fixtures-service/graphql_server.ts +30 -0
  303. package/tests/fixtures-service/grpc_client.go +30 -0
  304. package/tests/fixtures-service/http_clients.go +23 -0
  305. package/tests/fixtures-service/http_clients.py +38 -0
  306. package/tests/fixtures-service/http_clients.ts +49 -0
  307. package/tests/fixtures-service/k8s/payment-service.yaml +22 -0
  308. package/tests/fixtures-service/k8s_calls.ts +20 -0
  309. package/tests/fixtures-service/messaging.ts +87 -0
  310. package/tests/fixtures-service/trpc_client.ts +39 -0
  311. package/tests/fixtures-service/trpc_server.ts +39 -0
  312. package/tests/fixtures-service/user_service.proto +33 -0
  313. package/tests/fixtures-trackcd/Cargo.toml +11 -0
  314. package/tests/fixtures-trackcd/SpringController.java +36 -0
  315. package/tests/fixtures-trackcd/auth_service.ts +19 -0
  316. package/tests/fixtures-trackcd/complex_module.py +50 -0
  317. package/tests/fixtures-trackcd/express_app.js +30 -0
  318. package/tests/fixtures-trackcd/fastapi_app.py +49 -0
  319. package/tests/fixtures-trackcd/fastify_object_routes.js +32 -0
  320. package/tests/fixtures-trackcd/go.mod +8 -0
  321. package/tests/fixtures-trackcd/package.json +15 -0
  322. package/tests/fixtures-trackcd/requirements.txt +4 -0
  323. package/tests/fixtures-trackcd/tests/auth_service.test.ts +13 -0
  324. package/tests/fixtures-tracke/auth/AuthService.ts +23 -0
  325. package/tests/fixtures-tracke/auth/crypto.ts +7 -0
  326. package/tests/fixtures-tracke/billing/Billing.ts +20 -0
  327. package/tests/fixtures-tracke/billing/Invoice.ts +10 -0
  328. package/tests/fixtures-tracke/billing/server.ts +17 -0
  329. package/tests/fixtures-tracke/package.json +7 -0
  330. package/tests/fixtures-tracke/tests/auth.test.ts +23 -0
  331. package/tests/fixtures-tracke/tests/billing.test.ts +14 -0
  332. package/tests/fixtures-trackf/package.json +5 -0
  333. package/tests/fixtures-trackf/src/auth.ts +26 -0
  334. package/tests/fixtures-trackf/src/handlers.ts +35 -0
  335. package/tests/fixtures-tracki/billing/routes.ts +12 -0
  336. package/tests/fixtures-tracki/gateway/client.ts +13 -0
  337. package/tests/git-features.ts +267 -0
  338. package/tests/init.ts +141 -0
  339. package/tests/mcp-jit.ts +130 -0
  340. package/tests/mcp-smoke.ts +191 -0
  341. package/tests/mcp-trackcd.ts +169 -0
  342. package/tests/mcp-tracke.ts +229 -0
  343. package/tests/mcp-trackf.ts +330 -0
  344. package/tests/mcp-trackg.ts +219 -0
  345. package/tests/mcp-tracki.ts +174 -0
  346. package/tests/mcp-watcher.ts +126 -0
  347. package/tests/optspec.ts +194 -0
  348. package/tests/parallel-index.ts +333 -0
  349. package/tests/parallel-read.ts +125 -0
  350. package/tests/parallel-recovery.ts +241 -0
  351. package/tests/perf-callers.ts +145 -0
  352. package/tests/query-parity.ts +184 -0
  353. package/tests/query-perf.ts +55 -0
  354. package/tests/scale-parallel-parity.ts +225 -0
  355. package/tests/scale-test.ts +523 -0
  356. package/tests/smoke.ts +396 -0
  357. package/tests/trackcd.ts +325 -0
  358. package/tests/tracke-collisions.ts +255 -0
  359. package/tests/tracke.ts +314 -0
  360. package/tests/trackf-bugs.ts +406 -0
  361. package/tests/trackf.ts +390 -0
  362. package/tests/trackg.ts +1372 -0
  363. package/tests/tracki-boundaries.ts +202 -0
  364. package/tests/tracki-continuity.ts +253 -0
  365. package/tests/tracki-contract-diff.ts +249 -0
  366. package/tests/tracki-external-bundles.ts +341 -0
  367. package/tests/tracki-preflight.ts +251 -0
  368. package/tests/verify-roles.ts +51 -0
  369. package/tests/worker-parity.ts +286 -0
  370. package/tests/worker-pool.ts +262 -0
  371. package/tsconfig.json +20 -0
@@ -0,0 +1,1659 @@
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
5
+ import { z } from 'zod';
6
+ import { Store } from '../db/store.js';
7
+ import { Indexer } from '../indexer/index.js';
8
+ import { jitSync } from '../indexer/freshness.js';
9
+ import { SeerWatcher } from '../indexer/watcher.js';
10
+ import { buildArchitecture } from '../indexer/architecture.js';
11
+ import { detectChanges } from '../indexer/detectchanges.js';
12
+ import { collectChurn } from '../indexer/churn.js';
13
+ import { buildSymbolHistory } from '../indexer/symbolhistory.js';
14
+ import { buildModules } from '../indexer/modules.js';
15
+ import { rankedBehavior } from '../indexer/behavior.js';
16
+ import { computeRisk } from '../indexer/risk.js';
17
+ import { buildContext } from '../indexer/context.js';
18
+ import { exportBundle } from '../bundle/export.js';
19
+ import { importBundle, readBundleManifest } from '../bundle/import.js';
20
+ import { importExternalBundle } from '../bundle/external.js';
21
+ import { contractDiff } from '../bundle/contract.js';
22
+ import { preflight } from '../indexer/preflight.js';
23
+ import { getContinuityForSymbol } from '../indexer/continuity.js';
24
+ import { importScip } from '../scip/import.js';
25
+ import { findDuplicates, buildShapeHashes } from '../indexer/shapehash.js';
26
+ import { buildSkeleton } from '../indexer/skeleton.js';
27
+
28
+ /**
29
+ * Seer MCP server.
30
+ *
31
+ * Tool surface (Track-B baseline + Track-C/D additions):
32
+ * - seer_health freshness + schema state
33
+ * - seer_stats counts (files/symbols/edges + role + Track-C totals)
34
+ * - seer_symbols symbol search (BM25 / LIKE)
35
+ * - seer_definition exact symbol definition lookup
36
+ * - seer_file_symbols list symbols in a file
37
+ * - seer_callers direct callers, bounded with true total
38
+ * - seer_callees direct callees, bounded
39
+ * - seer_search combined symbol + file path BM25 search,
40
+ * enriched with containing-symbol context
41
+ * - seer_reindex explicit reindex
42
+ *
43
+ * v4 additions:
44
+ * - seer_routes list HTTP routes detected in source
45
+ * - seer_dependencies list external dependencies from manifests
46
+ * - seer_config list config / env reads
47
+ * - seer_complexity rank symbols by cyclomatic/cognitive complexity
48
+ * - seer_behavior tests that exercise a given symbol
49
+ * - seer_trace_path bounded BFS shortest call path A → B
50
+ * - seer_architecture one-page codebase snapshot
51
+ * - seer_detect_changes blast-radius for current diff
52
+ * - seer_churn file-level git churn pass (opt-in)
53
+ * - seer_history per-symbol git history
54
+ * - seer_symbol_history (action) build symbol history index
55
+ */
56
+
57
+ export interface McpServerOptions {
58
+ workspace: string;
59
+ dbPath?: string;
60
+ watch?: boolean;
61
+ jit?: boolean;
62
+ }
63
+
64
+ export class SeerMcpServer {
65
+ private store!: Store;
66
+ private indexer!: Indexer;
67
+ private watcher: SeerWatcher | null = null;
68
+ private mcp: McpServer;
69
+ private startedAt = Date.now();
70
+ private workspace: string;
71
+ private dbPath: string;
72
+ private jitEnabled: boolean;
73
+ private watchEnabled: boolean;
74
+ private jitPromise: Promise<void> | null = null;
75
+
76
+ constructor(options: McpServerOptions) {
77
+ this.workspace = path.resolve(options.workspace);
78
+ this.dbPath = options.dbPath ?? path.join(this.workspace, '.seer', 'graph.db');
79
+ this.jitEnabled = options.jit ?? true;
80
+ this.watchEnabled = options.watch ?? true;
81
+
82
+ this.mcp = new McpServer({ name: 'seer', version: '0.1.0' });
83
+ this.registerTools();
84
+ }
85
+
86
+ async start(): Promise<void> {
87
+ const dir = path.dirname(this.dbPath);
88
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
89
+
90
+ this.store = new Store(this.dbPath);
91
+ this.indexer = new Indexer(this.store);
92
+
93
+ const stats = this.store.getStats();
94
+ if (stats.files === 0) {
95
+ process.stderr.write(`[seer-mcp] empty index; running initial index...\n`);
96
+ const r = await this.indexer.indexDirectory(this.workspace, { quiet: true });
97
+ process.stderr.write(`[seer-mcp] initial index: ${r.filesIndexed} files, ${r.symbols} symbols, ${r.elapsedMs}ms\n`);
98
+ }
99
+
100
+ if (this.watchEnabled) {
101
+ this.watcher = new SeerWatcher(this.workspace, this.store, this.indexer, {
102
+ log: (m) => process.stderr.write(`[watcher] ${m}\n`),
103
+ });
104
+ this.watcher.start();
105
+ }
106
+
107
+ const transport = new StdioServerTransport();
108
+ await this.mcp.connect(transport);
109
+ process.stderr.write(`[seer-mcp] ready workspace=${this.workspace}\n`);
110
+ }
111
+
112
+ async stop(): Promise<void> {
113
+ if (this.watcher) await this.watcher.stop();
114
+ try { this.store.close(); } catch { /* */ }
115
+ }
116
+
117
+ private async ensureFresh(): Promise<void> {
118
+ if (!this.jitEnabled) return;
119
+ if (this.jitPromise) { await this.jitPromise; return; }
120
+ this.jitPromise = (async () => {
121
+ try { await jitSync(this.store, this.indexer, this.workspace, { maxDirty: 200 }); }
122
+ catch (err) { process.stderr.write(`[seer-mcp] JIT failed: ${err}\n`); }
123
+ finally { this.jitPromise = null; }
124
+ })();
125
+ await this.jitPromise;
126
+ }
127
+
128
+ private text(obj: unknown): { content: Array<{ type: 'text'; text: string }> } {
129
+ return { content: [{ type: 'text', text: JSON.stringify(obj, null, 2) }] };
130
+ }
131
+
132
+ /**
133
+ * Registry of every tool handler, keyed by name. Mirrors the MCP registration
134
+ * so seer_batch can dispatch internally without a second round-trip. The
135
+ * wrapper stores the raw handler and forwards to the SDK unchanged.
136
+ */
137
+ private handlers = new Map<string, (args: any) => Promise<any>>();
138
+
139
+ private registerTool(
140
+ name: string,
141
+ def: { description?: string; inputSchema?: Record<string, any>; [k: string]: any },
142
+ handler: (args: any) => Promise<any>,
143
+ ): void {
144
+ this.handlers.set(name, handler);
145
+ (this.mcp.registerTool as any)(name, def, handler);
146
+ }
147
+
148
+ /**
149
+ * Up to 5 deterministic fuzzy suggestions (BM25 over the camel/snake-split
150
+ * FTS index) for a name that resolved to nothing. SUGGESTION-ONLY: callers
151
+ * surface these under a `didYouMean` key, never substitute them for a real
152
+ * lookup. Returning a guessed symbol as if exact would be exactly the
153
+ * "misleading information" Seer's contract forbids.
154
+ */
155
+ private suggestSymbols(name: string): Array<{
156
+ name: string; qualifiedName: string | null; kind: string; file: string; lineStart: number;
157
+ }> {
158
+ let rows;
159
+ try { rows = this.store.searchSymbolsFts(name, { limit: 5 }); }
160
+ catch { return []; }
161
+ return rows.map(r => ({
162
+ name: r.name, qualifiedName: r.qualifiedName, kind: r.kind,
163
+ file: r.filePath, lineStart: r.lineStart,
164
+ }));
165
+ }
166
+
167
+ /**
168
+ * Emit a list response under a deterministic token budget. Items are assumed
169
+ * pre-sorted by relevance, so we prefix-trim: keep appending until the
170
+ * serialized payload would exceed `tokenBudget * 4` chars (~4 chars/token).
171
+ * Without a budget the output is byte-identical to the previous behavior.
172
+ */
173
+ private budgetedText(
174
+ base: Record<string, unknown>,
175
+ items: unknown[],
176
+ tokenBudget?: number,
177
+ key = 'items',
178
+ ): { content: Array<{ type: 'text'; text: string }> } {
179
+ if (!tokenBudget || tokenBudget <= 0) {
180
+ return this.text({ ...base, returned: items.length, [key]: items });
181
+ }
182
+ const budgetChars = tokenBudget * 4;
183
+ const kept: unknown[] = [];
184
+ for (const it of items) {
185
+ kept.push(it);
186
+ const len = JSON.stringify({ ...base, returned: kept.length, [key]: kept }).length;
187
+ // Always keep at least one item; stop once we cross the budget.
188
+ if (len > budgetChars) break;
189
+ }
190
+ const truncated = kept.length < items.length;
191
+ const out: Record<string, unknown> = { ...base, returned: kept.length, [key]: kept };
192
+ if (truncated) {
193
+ out.truncated = true;
194
+ out.omitted = items.length - kept.length;
195
+ out.tokenBudget = tokenBudget;
196
+ out.note = `Output trimmed to ~${tokenBudget} tokens (${kept.length}/${items.length} items shown). Raise tokenBudget, add a filter, or paginate for the rest.`;
197
+ }
198
+ return this.text(out);
199
+ }
200
+
201
+ // ── Lazy lifecycle resolution (AI-agent optimization §5a) ────────────────
202
+ // Derived passes (modules / shape-hash / symbol-history) normally run during
203
+ // indexing. When the DB was produced some other way (bundle import, partial
204
+ // index) the dependent tools used to silently return nothing until the agent
205
+ // hand-ran a *_build tool. These guards extend the JIT-freshness philosophy
206
+ // to those passes: build on first dependent query, once per process.
207
+ private autoBuilt = { modules: false, shapes: false, history: false };
208
+
209
+ private ensureModules(): void {
210
+ if (this.autoBuilt.modules) return;
211
+ this.autoBuilt.modules = true;
212
+ try { if (this.store.countModules() === 0) buildModules(this.store); }
213
+ catch (err) { process.stderr.write(`[seer-mcp] auto modules build skipped: ${err}\n`); }
214
+ }
215
+
216
+ private ensureShapeHashes(): void {
217
+ if (this.autoBuilt.shapes) return;
218
+ this.autoBuilt.shapes = true;
219
+ try {
220
+ const row = this.store.rawDb()
221
+ .prepare('SELECT COUNT(*) AS c FROM symbols WHERE shape_hash IS NOT NULL')
222
+ .get() as { c: number };
223
+ if (Number(row.c) === 0) buildShapeHashes(this.store, {});
224
+ } catch (err) { process.stderr.write(`[seer-mcp] auto shape-hash build skipped: ${err}\n`); }
225
+ }
226
+
227
+ private async ensureSymbolHistory(): Promise<void> {
228
+ if (this.autoBuilt.history) return;
229
+ this.autoBuilt.history = true;
230
+ try {
231
+ const row = this.store.rawDb()
232
+ .prepare('SELECT COUNT(*) AS c FROM symbol_history')
233
+ .get() as { c: number };
234
+ if (Number(row.c) === 0) {
235
+ await buildSymbolHistory(this.workspace, this.store, {
236
+ maxCommitsPerFile: 200, skipIfHeadUnchanged: true,
237
+ });
238
+ }
239
+ } catch (err) { process.stderr.write(`[seer-mcp] auto symbol-history build skipped: ${err}\n`); }
240
+ }
241
+
242
+ private registerTools(): void {
243
+ this.registerTool('seer_health', {
244
+ description: 'Server health, schema, file/symbol counts, watcher status. Cheap; no JIT.',
245
+ inputSchema: {},
246
+ }, async () => {
247
+ const schema = this.store.schemaInfo();
248
+ const stats = this.store.getStats();
249
+ const watcher = this.watcher ? this.watcher.syncStatus() : null;
250
+ return this.text({
251
+ workspace: this.workspace,
252
+ dbPath: this.dbPath,
253
+ schemaVersion: schema.dbVersion,
254
+ buildSchemaVersion: schema.buildVersion,
255
+ schemaCurrent: schema.current,
256
+ files: stats.files, symbols: stats.symbols, edges: stats.edges,
257
+ resolvedEdges: stats.resolvedEdges,
258
+ roles: stats.roles, languages: stats.languages,
259
+ routes: stats.routes,
260
+ externalDependencies: stats.externalDependencies,
261
+ configKeys: stats.configKeys,
262
+ symbolHistory: stats.symbolHistory,
263
+ modules: stats.modules ?? 0,
264
+ scipImports: stats.scipImports ?? 0,
265
+ shapeHashed: stats.shapeHashed ?? 0,
266
+ provenance: stats.provenance,
267
+ watcher, jitEnabled: this.jitEnabled,
268
+ uptimeMs: Date.now() - this.startedAt,
269
+ });
270
+ });
271
+
272
+ this.registerTool('seer_stats', {
273
+ description: 'Index statistics: counts, languages, roles, routes, deps, config keys. Runs JIT.',
274
+ inputSchema: {},
275
+ }, async () => {
276
+ await this.ensureFresh();
277
+ return this.text(this.store.getStats());
278
+ });
279
+
280
+ this.registerTool('seer_symbols', {
281
+ description: 'Search symbols by name (BM25 over name/qualified_name/signature with camelCase/snake_case split). Returns top by PageRank when query omitted. Excludes vendor/generated/test/declaration rows by default; pass include* to widen.',
282
+ inputSchema: {
283
+ query: z.string().optional(),
284
+ top: z.number().int().positive().max(500).optional(),
285
+ limit: z.number().int().positive().max(500).optional(),
286
+ includeVendor: z.boolean().optional(),
287
+ includeGenerated: z.boolean().optional(),
288
+ includeTests: z.boolean().optional(),
289
+ includeDeclarations: z.boolean().optional(),
290
+ includeTypeRefs: z.boolean().optional(),
291
+ tokenBudget: z.number().int().positive().max(50000).optional()
292
+ .describe('Soft cap (~4 chars/token) that prefix-trims items, keeping the highest-ranked rows.'),
293
+ },
294
+ }, async ({ query, top, limit, includeVendor, includeGenerated, includeTests, includeDeclarations, includeTypeRefs, tokenBudget }) => {
295
+ await this.ensureFresh();
296
+ const opts = {
297
+ includeVendor: includeVendor ?? false,
298
+ includeGenerated: includeGenerated ?? false,
299
+ includeTests: includeTests ?? false,
300
+ includeDeclarations: includeDeclarations ?? false,
301
+ includeTypeRefs: includeTypeRefs ?? false,
302
+ };
303
+ let rows;
304
+ let total: number | null = null;
305
+ if (query) {
306
+ rows = this.store.searchSymbolsFts(query, { ...opts, limit: limit ?? 50 });
307
+ total = this.store.countSymbols(query, opts);
308
+ } else {
309
+ rows = this.store.getTopSymbols(top ?? 20, opts);
310
+ }
311
+ if (query && rows.length === 0) {
312
+ const didYouMean = this.suggestSymbols(query);
313
+ return this.text({ total, returned: 0, items: [], source: 'tree-sitter',
314
+ ...(didYouMean.length > 0 ? { didYouMean } : {}) });
315
+ }
316
+ const items = rows.map(r => ({
317
+ id: r.id, name: r.name, qualifiedName: r.qualifiedName, kind: r.kind,
318
+ file: r.filePath, lineStart: r.lineStart, lineEnd: r.lineEnd,
319
+ pagerank: r.pagerank, signature: r.signature,
320
+ loc: r.loc, cyclomatic: r.cyclomatic, cognitive: r.cognitive,
321
+ symbolRole: r.symbolRole,
322
+ }));
323
+ return this.budgetedText({ total, source: 'tree-sitter' }, items, tokenBudget);
324
+ });
325
+
326
+ this.registerTool('seer_definition', {
327
+ description: 'Look up an exact symbol by name or qualified name. The optional `file` accepts an absolute path, the exact rel_path, OR a trailing path fragment on a segment boundary (e.g. "service.ts" or "auth/service.ts"). Excludes vendor/generated/test/declaration rows by default; pass include* to widen.',
328
+ inputSchema: {
329
+ name: z.string(),
330
+ file: z.string().optional(),
331
+ includeVendor: z.boolean().optional(),
332
+ includeGenerated: z.boolean().optional(),
333
+ includeTests: z.boolean().optional(),
334
+ includeDeclarations: z.boolean().optional(),
335
+ includeTypeRefs: z.boolean().optional(),
336
+ tokenBudget: z.number().int().positive().max(50000).optional()
337
+ .describe('Soft cap (~4 chars/token) that prefix-trims items, keeping the highest-PageRank rows.'),
338
+ },
339
+ }, async ({ name, file, includeVendor, includeGenerated, includeTests, includeDeclarations, includeTypeRefs, tokenBudget }) => {
340
+ await this.ensureFresh();
341
+ const rows = this.store.getDefinition(name, {
342
+ filePath: file,
343
+ includeVendor: includeVendor ?? false,
344
+ includeGenerated: includeGenerated ?? false,
345
+ includeTests: includeTests ?? false,
346
+ includeDeclarations: includeDeclarations ?? false,
347
+ includeTypeRefs: includeTypeRefs ?? false,
348
+ });
349
+ // Suggestion-only fuzzy fallback: never substitute, just hint.
350
+ if (rows.length === 0) {
351
+ const didYouMean = this.suggestSymbols(name);
352
+ return this.text({ total: 0, items: [], source: 'tree-sitter',
353
+ ...(didYouMean.length > 0 ? { didYouMean } : {}) });
354
+ }
355
+ const items = rows.map(r => ({
356
+ id: r.id, name: r.name, qualifiedName: r.qualifiedName, kind: r.kind,
357
+ file: r.filePath, lineStart: r.lineStart, lineEnd: r.lineEnd,
358
+ pagerank: r.pagerank, signature: r.signature,
359
+ loc: r.loc, cyclomatic: r.cyclomatic, cognitive: r.cognitive,
360
+ symbolRole: r.symbolRole,
361
+ }));
362
+ return this.budgetedText({ total: rows.length, source: 'tree-sitter' }, items, tokenBudget);
363
+ });
364
+
365
+ this.registerTool('seer_file_symbols', {
366
+ description: 'List symbols defined in a file (sorted by line).',
367
+ inputSchema: {
368
+ file: z.string(),
369
+ limit: z.number().int().positive().max(2000).optional(),
370
+ },
371
+ }, async ({ file, limit }) => {
372
+ await this.ensureFresh();
373
+ const rows = this.store.listSymbolsInFile(file, limit ?? 200);
374
+ return this.text({
375
+ file, total: rows.length,
376
+ items: rows.map(r => ({
377
+ id: r.id, name: r.name, qualifiedName: r.qualifiedName, kind: r.kind,
378
+ lineStart: r.lineStart, lineEnd: r.lineEnd, pagerank: r.pagerank,
379
+ signature: r.signature, loc: r.loc,
380
+ cyclomatic: r.cyclomatic, cognitive: r.cognitive,
381
+ })),
382
+ });
383
+ });
384
+
385
+ this.registerTool('seer_callers', {
386
+ description: 'Direct callers of a symbol, bounded preview + true total.',
387
+ inputSchema: {
388
+ symbol: z.string(),
389
+ limit: z.number().int().positive().max(500).optional(),
390
+ tokenBudget: z.number().int().positive().max(50000).optional()
391
+ .describe('Soft cap (~4 chars/token) that prefix-trims the (already limit-bounded) caller list.'),
392
+ },
393
+ }, async ({ symbol, limit, tokenBudget }) => {
394
+ await this.ensureFresh();
395
+ const total = this.store.countCallers(symbol);
396
+ const items = this.store.findCallers(symbol, limit ?? 40).map(c => ({
397
+ callerName: c.callerName, callerQualifiedName: c.callerQualifiedName,
398
+ callerKind: c.callerKind, file: c.callerFile, line: c.callerLine,
399
+ edgeKind: c.edgeKind,
400
+ }));
401
+ if (total === 0) {
402
+ const didYouMean = this.suggestSymbols(symbol);
403
+ return this.text({ symbol, total: 0, returned: 0, items: [], source: 'tree-sitter',
404
+ ...(didYouMean.length > 0 ? { didYouMean } : {}) });
405
+ }
406
+ return this.budgetedText({ symbol, total, source: 'tree-sitter' }, items, tokenBudget);
407
+ });
408
+
409
+ this.registerTool('seer_callees', {
410
+ description: 'Direct callees of a symbol.',
411
+ inputSchema: {
412
+ symbol: z.string(),
413
+ limit: z.number().int().positive().max(500).optional(),
414
+ tokenBudget: z.number().int().positive().max(50000).optional()
415
+ .describe('Soft cap (~4 chars/token) that prefix-trims the callee list.'),
416
+ },
417
+ }, async ({ symbol, limit, tokenBudget }) => {
418
+ await this.ensureFresh();
419
+ const all = this.store.findCallees(symbol);
420
+ const max = Math.min(all.length, limit ?? 40);
421
+ const items = all.slice(0, max).map(c => ({
422
+ calleeName: c.calleeName, calleeKind: c.calleeKind,
423
+ file: c.calleeFile, lineStart: c.calleeLineStart,
424
+ edgeKind: c.edgeKind,
425
+ source: c.calleeFile ? 'tree-sitter' : 'unresolved',
426
+ }));
427
+ return this.budgetedText({ symbol, total: all.length }, items, tokenBudget);
428
+ });
429
+
430
+ // Search: BM25 across symbols + files. Each symbol hit also gets enriched
431
+ // with the containing symbol when the match is non-symbol (e.g. file).
432
+ this.registerTool('seer_search', {
433
+ description: 'Combined BM25 search across symbol names and file paths. Use this first; follow up with seer_definition / seer_file_symbols. Excludes vendor/generated/test/declaration rows by default.',
434
+ inputSchema: {
435
+ query: z.string().min(1),
436
+ limit: z.number().int().positive().max(200).optional(),
437
+ includeVendor: z.boolean().optional(),
438
+ includeGenerated: z.boolean().optional(),
439
+ includeTests: z.boolean().optional(),
440
+ includeDeclarations: z.boolean().optional(),
441
+ includeTypeRefs: z.boolean().optional(),
442
+ },
443
+ }, async ({ query, limit, includeVendor, includeGenerated, includeTests, includeDeclarations, includeTypeRefs }) => {
444
+ await this.ensureFresh();
445
+ const opts = {
446
+ includeVendor: includeVendor ?? false,
447
+ includeGenerated: includeGenerated ?? false,
448
+ includeTests: includeTests ?? false,
449
+ includeDeclarations: includeDeclarations ?? false,
450
+ includeTypeRefs: includeTypeRefs ?? false,
451
+ };
452
+ const symHits = this.store.searchSymbolsFts(query, { ...opts, limit: limit ?? 30 });
453
+ const symbolTotal = this.store.countSymbols(query, opts);
454
+ const fileHits = this.store.searchFilesFts(query, limit ?? 30, {
455
+ includeVendor: opts.includeVendor,
456
+ includeGenerated: opts.includeGenerated,
457
+ includeTests: opts.includeTests,
458
+ });
459
+ return this.text({
460
+ query,
461
+ symbolHits: {
462
+ total: symbolTotal, returned: symHits.length,
463
+ items: symHits.map(r => ({
464
+ id: r.id, name: r.name, qualifiedName: r.qualifiedName,
465
+ kind: r.kind, file: r.filePath, lineStart: r.lineStart,
466
+ pagerank: r.pagerank, symbolRole: r.symbolRole,
467
+ })),
468
+ },
469
+ fileHits: {
470
+ total: fileHits.length,
471
+ items: fileHits.map(f => ({ path: f.path, relPath: f.relPath, language: f.language, role: f.role })),
472
+ },
473
+ source: 'tree-sitter',
474
+ note: 'Search-first: call seer_definition or seer_file_symbols on the chosen hit.',
475
+ });
476
+ });
477
+
478
+ this.registerTool('seer_reindex', {
479
+ description: 'Reindex the workspace (incremental). Pass reset=true to wipe.',
480
+ inputSchema: { reset: z.boolean().optional() },
481
+ }, async ({ reset }) => {
482
+ if (reset) {
483
+ this.store.close();
484
+ try { fs.unlinkSync(this.dbPath); } catch { /* */ }
485
+ try { fs.unlinkSync(this.dbPath + '-wal'); } catch { /* */ }
486
+ try { fs.unlinkSync(this.dbPath + '-shm'); } catch { /* */ }
487
+ this.store = new Store(this.dbPath);
488
+ this.indexer = new Indexer(this.store);
489
+ if (this.watcher) {
490
+ await this.watcher.stop();
491
+ this.watcher = new SeerWatcher(this.workspace, this.store, this.indexer, {
492
+ log: (m) => process.stderr.write(`[watcher] ${m}\n`),
493
+ });
494
+ this.watcher.start();
495
+ }
496
+ }
497
+ const r = await this.indexer.indexDirectory(this.workspace, { quiet: true });
498
+ return this.text({
499
+ reset: Boolean(reset),
500
+ filesIndexed: r.filesIndexed,
501
+ filesReusedFromCache: r.filesReusedFromCache,
502
+ symbols: r.symbols, edges: r.edges, resolvedEdges: r.resolvedEdges,
503
+ externalDependencies: r.externalDependencies,
504
+ testEdgesAdded: r.testEdgesAdded,
505
+ routesResolved: r.routesResolved,
506
+ elapsedMs: r.elapsedMs,
507
+ pagerankRecomputed: r.pagerankRecomputed,
508
+ });
509
+ });
510
+
511
+ // ── Track-C tools ───────────────────────────────────────────────────────
512
+
513
+ this.registerTool('seer_routes', {
514
+ description: 'List HTTP routes detected in source (Express/Fastify/FastAPI/Flask/Spring).',
515
+ inputSchema: {
516
+ method: z.string().optional(),
517
+ framework: z.string().optional(),
518
+ pathSubstr: z.string().optional(),
519
+ limit: z.number().int().positive().max(500).optional(),
520
+ },
521
+ }, async ({ method, framework, pathSubstr, limit }) => {
522
+ await this.ensureFresh();
523
+ const rows = this.store.listRoutes({ method, framework, pathSubstr, limit: limit ?? 100 });
524
+ return this.text({
525
+ total: this.store.countRoutes(),
526
+ returned: rows.length,
527
+ items: rows,
528
+ source: 'tree-sitter',
529
+ });
530
+ });
531
+
532
+ this.registerTool('seer_service_calls', {
533
+ description:
534
+ 'v9 Track H — List outbound HTTP/tRPC/GraphQL/gRPC/messaging service client calls. ' +
535
+ 'Each row is AST-attributed to its enclosing function/method. Pagination via limit/offset; ' +
536
+ 'filter by protocol, method, framework, path substring, caller symbol, or min confidence.',
537
+ inputSchema: {
538
+ protocol: z.string().optional(),
539
+ method: z.string().optional(),
540
+ framework: z.string().optional(),
541
+ pathSubstr: z.string().optional(),
542
+ callerSymbolId: z.number().int().nonnegative().optional(),
543
+ minConfidence: z.number().min(0).max(1).optional(),
544
+ limit: z.number().int().positive().max(1000).optional(),
545
+ offset: z.number().int().nonnegative().optional(),
546
+ summaryOnly: z.boolean().optional(),
547
+ tokenBudget: z.number().int().positive().max(50000).optional()
548
+ .describe('Soft cap (~4 chars/token) that prefix-trims the returned items.'),
549
+ },
550
+ }, async (args) => {
551
+ await this.ensureFresh();
552
+ const limit = args.limit ?? 100;
553
+ const rows = this.store.listServiceCalls({ ...args, limit });
554
+ const total = this.store.countServiceCalls();
555
+ if (args.summaryOnly) {
556
+ return this.text({ total, returned: rows.length, source: 'tree-sitter' });
557
+ }
558
+ return this.budgetedText({ total, offset: args.offset ?? 0, source: 'tree-sitter' }, rows, args.tokenBudget);
559
+ });
560
+
561
+ this.registerTool('seer_service_links', {
562
+ description:
563
+ 'v9 Track H — List deterministic service-link rendezvous between client calls and route handlers. ' +
564
+ 'Each link carries match_kind (literal_path / env_base / service_host / route_pattern / ' +
565
+ 'trpc_procedure / graphql_operation / grpc_method / topic_match / queue_match / exchange_match), confidence, ' +
566
+ 'and an evidence_json blob enumerating ambiguity candidates. Filter by protocol, method, path, ' +
567
+ 'caller/handler symbol id, match_kind, or min confidence.',
568
+ inputSchema: {
569
+ protocol: z.string().optional(),
570
+ method: z.string().optional(),
571
+ pathSubstr: z.string().optional(),
572
+ callerSymbolId: z.number().int().nonnegative().optional(),
573
+ handlerSymbolId: z.number().int().nonnegative().optional(),
574
+ matchKind: z.string().optional(),
575
+ minConfidence: z.number().min(0).max(1).optional(),
576
+ limit: z.number().int().positive().max(1000).optional(),
577
+ offset: z.number().int().nonnegative().optional(),
578
+ summaryOnly: z.boolean().optional(),
579
+ tokenBudget: z.number().int().positive().max(50000).optional()
580
+ .describe('Soft cap (~4 chars/token) that prefix-trims the returned items.'),
581
+ },
582
+ }, async (args) => {
583
+ await this.ensureFresh();
584
+ const limit = args.limit ?? 100;
585
+ const rows = this.store.listServiceLinks({ ...args, limit });
586
+ const total = this.store.countServiceLinks();
587
+ if (args.summaryOnly) {
588
+ return this.text({ total, returned: rows.length, source: 'tree-sitter' });
589
+ }
590
+ return this.budgetedText({ total, offset: args.offset ?? 0, source: 'tree-sitter' }, rows, args.tokenBudget);
591
+ });
592
+
593
+ this.registerTool('seer_trace_service_path', {
594
+ description:
595
+ 'v8 Track G — Shortest service-link path between two symbols (bounded BFS). ' +
596
+ 'Treats each service_link as a directed edge caller→handler. Returns the chain of ' +
597
+ 'symbol ids and names; empty when unreachable within maxDepth.',
598
+ inputSchema: {
599
+ from: z.string().describe('Source symbol name or qualified name'),
600
+ to: z.string().describe('Target symbol name or qualified name'),
601
+ maxDepth: z.number().int().positive().max(20).optional(),
602
+ },
603
+ }, async ({ from, to, maxDepth }) => {
604
+ await this.ensureFresh();
605
+ const fRows = this.store.getDefinition(from);
606
+ const tRows = this.store.getDefinition(to);
607
+ if (fRows.length === 0) return this.text({ ok: false, error: `Source symbol not found: ${from}` });
608
+ if (tRows.length === 0) return this.text({ ok: false, error: `Target symbol not found: ${to}` });
609
+ const ids = this.store.traceServicePath(fRows[0].id, tRows[0].id, maxDepth ?? 6);
610
+ if (ids.length === 0) return this.text({ ok: true, found: false, path: [] });
611
+ const items = ids.map(id => {
612
+ const row = this.store.rawDb().prepare(
613
+ `SELECT id, name, qualified_name AS qualifiedName, kind FROM symbols WHERE id = ?`,
614
+ ).get(id) as { id: unknown; name: unknown; qualifiedName: unknown; kind: unknown };
615
+ return {
616
+ id: Number(row.id),
617
+ name: String(row.name),
618
+ qualifiedName: row.qualifiedName == null ? null : String(row.qualifiedName),
619
+ kind: String(row.kind),
620
+ };
621
+ });
622
+ return this.text({ ok: true, found: true, hops: items.length - 1, path: items });
623
+ });
624
+
625
+ this.registerTool('seer_trace_service_dependencies', {
626
+ description:
627
+ 'v9 Track H — Bounded BFS over service-link edges from one symbol. ' +
628
+ 'Returns every handler reachable within maxDepth/maxNodes/maxFanout, ' +
629
+ 'each with its depth, the protocols carrying traffic, and the hop chain. ' +
630
+ '`cutoff` reports which limit fired (maxNodes / maxDepth / maxFanout) if any.',
631
+ inputSchema: {
632
+ from: z.string().describe('Source symbol name or qualified name'),
633
+ maxDepth: z.number().int().positive().max(20).optional(),
634
+ maxNodes: z.number().int().positive().max(2000).optional(),
635
+ maxFanout: z.number().int().positive().max(200).optional(),
636
+ },
637
+ }, async ({ from, maxDepth, maxNodes, maxFanout }) => {
638
+ await this.ensureFresh();
639
+ const fRows = this.store.getDefinition(from);
640
+ if (fRows.length === 0) return this.text({ ok: false, error: `Source symbol not found: ${from}` });
641
+ const r = this.store.traceServiceDependencies(fRows[0].id, { maxDepth, maxNodes, maxFanout });
642
+ const items = r.reached.map(x => {
643
+ const row = this.store.rawDb().prepare(
644
+ `SELECT id, name, qualified_name AS qualifiedName, kind FROM symbols WHERE id = ?`,
645
+ ).get(x.symbolId) as { id: unknown; name: unknown; qualifiedName: unknown; kind: unknown } | undefined;
646
+ return {
647
+ symbolId: x.symbolId,
648
+ name: row ? String(row.name) : null,
649
+ qualifiedName: row?.qualifiedName == null ? null : String(row.qualifiedName),
650
+ kind: row ? String(row.kind) : null,
651
+ depth: x.depth,
652
+ protocols: x.protocols,
653
+ matchKinds: x.matchKinds,
654
+ hops: x.hops,
655
+ };
656
+ });
657
+ return this.text({
658
+ ok: true,
659
+ from: { id: fRows[0].id, name: fRows[0].name, qualifiedName: fRows[0].qualifiedName },
660
+ reached: items.length,
661
+ cutoff: r.cutoff,
662
+ items,
663
+ });
664
+ });
665
+
666
+ this.registerTool('seer_trace_module_service_dependencies', {
667
+ description:
668
+ 'v9 Track H — Bounded BFS over cross-module service-link edges from one ' +
669
+ 'module. Returns each downstream module with hop depth, the protocols ' +
670
+ 'carrying traffic, and the total cross-module link weight feeding it.',
671
+ inputSchema: {
672
+ moduleId: z.number().int().nonnegative(),
673
+ maxDepth: z.number().int().positive().max(10).optional(),
674
+ maxNodes: z.number().int().positive().max(500).optional(),
675
+ },
676
+ }, async ({ moduleId, maxDepth, maxNodes }) => {
677
+ await this.ensureFresh();
678
+ const r = this.store.traceModuleServiceDependencies(moduleId, { maxDepth, maxNodes });
679
+ // Hydrate module metadata for the response so callers don't need a
680
+ // follow-up tool call.
681
+ const ids = r.reached.map(x => x.moduleId);
682
+ let metaById = new Map<number, { label: string; sizeFiles: number }>();
683
+ if (ids.length > 0) {
684
+ const rows = this.store.rawDb().prepare(
685
+ `SELECT id, label, size_files AS sizeFiles FROM modules WHERE id IN (${ids.map(() => '?').join(',')})`,
686
+ ).all(...ids) as Array<{ id: unknown; label: unknown; sizeFiles: unknown }>;
687
+ for (const row of rows) {
688
+ metaById.set(Number(row.id), { label: String(row.label), sizeFiles: Number(row.sizeFiles) });
689
+ }
690
+ }
691
+ const items = r.reached.map(x => ({
692
+ moduleId: x.moduleId,
693
+ label: metaById.get(x.moduleId)?.label ?? null,
694
+ sizeFiles: metaById.get(x.moduleId)?.sizeFiles ?? 0,
695
+ depth: x.depth,
696
+ protocols: x.protocols,
697
+ viaLinks: x.viaLinks,
698
+ }));
699
+ return this.text({
700
+ ok: true,
701
+ fromModuleId: moduleId,
702
+ reached: items.length,
703
+ cutoff: r.cutoff,
704
+ items,
705
+ });
706
+ });
707
+
708
+ this.registerTool('seer_dependencies', {
709
+ description: 'List external dependencies declared in package manifests / lockfiles.',
710
+ inputSchema: {
711
+ ecosystem: z.string().optional(),
712
+ nameSubstr: z.string().optional(),
713
+ limit: z.number().int().positive().max(2000).optional(),
714
+ },
715
+ }, async ({ ecosystem, nameSubstr, limit }) => {
716
+ await this.ensureFresh();
717
+ const rows = this.store.listExternalDeps({ ecosystem, nameSubstr, limit: limit ?? 200 });
718
+ return this.text({
719
+ total: this.store.countExternalDeps(),
720
+ returned: rows.length,
721
+ items: rows,
722
+ });
723
+ });
724
+
725
+ this.registerTool('seer_config', {
726
+ description: 'List static env/config reads detected in source (process.env, os.getenv, System.getenv).',
727
+ inputSchema: {
728
+ key: z.string().optional(),
729
+ source: z.string().optional(),
730
+ limit: z.number().int().positive().max(2000).optional(),
731
+ },
732
+ }, async ({ key, source, limit }) => {
733
+ await this.ensureFresh();
734
+ const rows = this.store.listConfigKeys({ key, source, limit: limit ?? 200 });
735
+ return this.text({
736
+ total: this.store.countConfigKeys(),
737
+ returned: rows.length,
738
+ items: rows,
739
+ });
740
+ });
741
+
742
+ this.registerTool('seer_complexity', {
743
+ description: 'Rank functions/methods by complexity. Useful for risk-aware editing. Excludes vendor/generated/test/declaration rows by default.',
744
+ inputSchema: {
745
+ by: z.enum(['cyclomatic', 'cognitive', 'loc', 'max_nesting']).optional(),
746
+ minValue: z.number().int().nonnegative().optional(),
747
+ limit: z.number().int().positive().max(500).optional(),
748
+ includeVendor: z.boolean().optional(),
749
+ includeGenerated: z.boolean().optional(),
750
+ includeTests: z.boolean().optional(),
751
+ includeDeclarations: z.boolean().optional(),
752
+ tokenBudget: z.number().int().positive().max(50000).optional()
753
+ .describe('Soft cap (~4 chars/token) that prefix-trims items, keeping the most complex rows.'),
754
+ },
755
+ }, async ({ by, minValue, limit, includeVendor, includeGenerated, includeTests, includeDeclarations, tokenBudget }) => {
756
+ await this.ensureFresh();
757
+ const col = by ?? 'cyclomatic';
758
+ const min = minValue ?? 1;
759
+ const lim = limit ?? 50;
760
+ const conds: string[] = [`s.${col} >= ?`];
761
+ const args: unknown[] = [min];
762
+ if (!includeVendor) conds.push('f.is_vendor = 0');
763
+ if (!includeGenerated) conds.push('f.is_generated = 0');
764
+ if (!includeTests) conds.push(`f.role <> 'test'`);
765
+ if (!includeDeclarations) conds.push(`(s.symbol_role IS NULL OR s.symbol_role <> 'declaration')`);
766
+ args.push(lim);
767
+ const sql = `
768
+ SELECT s.id, s.name, s.qualified_name AS qualifiedName, s.kind,
769
+ f.path AS file, s.line_start AS lineStart, s.line_end AS lineEnd,
770
+ s.loc, s.cyclomatic, s.cognitive, s.max_nesting AS maxNesting,
771
+ s.pagerank
772
+ FROM symbols s JOIN files f ON f.id = s.file_id
773
+ WHERE ${conds.join(' AND ')}
774
+ ORDER BY s.${col} DESC, s.pagerank DESC
775
+ LIMIT ?
776
+ `;
777
+ const rows = (this.store as any).rawDb().prepare(sql).all(...args);
778
+ return this.budgetedText({ by: col, minValue: min }, rows, tokenBudget);
779
+ });
780
+
781
+ this.registerTool('seer_behavior', {
782
+ description: 'Ranked behavioral contract for a symbol: direct/indirect/naming-convention/same-file tests with assertion counts, graph distance, and recency. Use this BEFORE editing a symbol to find the tests that describe its expected behavior.',
783
+ inputSchema: {
784
+ symbol: z.string(),
785
+ limit: z.number().int().positive().max(200).optional(),
786
+ indirectDepth: z.number().int().nonnegative().max(4).optional()
787
+ .describe('BFS depth for indirect coverage (callers that transitively reach the symbol). 0 disables indirect.'),
788
+ includeNamingConvention: z.boolean().optional(),
789
+ includeSameFile: z.boolean().optional(),
790
+ },
791
+ }, async ({ symbol, limit, indirectDepth, includeNamingConvention, includeSameFile }) => {
792
+ await this.ensureFresh();
793
+ const result = rankedBehavior(this.store, symbol, {
794
+ limit: limit ?? 30,
795
+ indirectDepth: indirectDepth ?? 2,
796
+ includeNamingConvention: includeNamingConvention ?? true,
797
+ includeSameFile: includeSameFile ?? true,
798
+ });
799
+ if (!result) {
800
+ const didYouMean = this.suggestSymbols(symbol);
801
+ return this.text({ symbol, total: 0, direct: 0, indirect: 0, tests: [], reason: `no symbol "${symbol}"`,
802
+ ...(didYouMean.length > 0 ? { didYouMean } : {}) });
803
+ }
804
+ return this.text(result);
805
+ });
806
+
807
+ this.registerTool('seer_trace_path', {
808
+ description: 'Bounded BFS shortest call path from one symbol to another.',
809
+ inputSchema: {
810
+ from: z.string(),
811
+ to: z.string(),
812
+ maxDepth: z.number().int().positive().max(12).optional(),
813
+ },
814
+ }, async ({ from, to, maxDepth }) => {
815
+ await this.ensureFresh();
816
+ const fromCandidates = this.store.getDefinition(from);
817
+ const toCandidates = this.store.getDefinition(to);
818
+ if (fromCandidates.length === 0) return this.text({ found: false, reason: `no symbol "${from}"` });
819
+ if (toCandidates.length === 0) return this.text({ found: false, reason: `no symbol "${to}"` });
820
+ // Try the highest-PageRank pair first.
821
+ for (const f of fromCandidates.slice(0, 5)) {
822
+ for (const t of toCandidates.slice(0, 5)) {
823
+ const p = this.store.tracePath(f.id, t.id, maxDepth ?? 6);
824
+ if (p) return this.text({ found: true, depth: p.length - 1, path: p });
825
+ }
826
+ }
827
+ return this.text({ found: false, reason: `no path within depth ${maxDepth ?? 6}` });
828
+ });
829
+
830
+ this.registerTool('seer_trace_callers', {
831
+ description: 'Bounded reverse-reachable callers of a symbol (transitive blast radius). Returns each caller with the BFS depth at which it was found.',
832
+ inputSchema: {
833
+ symbol: z.string(),
834
+ maxDepth: z.number().int().positive().max(8).optional(),
835
+ maxNodes: z.number().int().positive().max(50000).optional(),
836
+ limit: z.number().int().positive().max(500).optional(),
837
+ },
838
+ }, async ({ symbol, maxDepth, maxNodes, limit }) => {
839
+ await this.ensureFresh();
840
+ const defs = this.store.getDefinition(symbol);
841
+ if (defs.length === 0) return this.text({ found: false, reason: `no symbol "${symbol}"` });
842
+ const target = defs[0];
843
+ const hits = this.store.reverseReachableWithDepth(target.id, maxDepth ?? 4, maxNodes ?? 20000);
844
+ const lim = Math.min(hits.length, limit ?? 100);
845
+ const ids = hits.slice(0, lim).map(h => h.id);
846
+ if (ids.length === 0) {
847
+ return this.text({ symbol: { id: target.id, name: target.name }, maxDepth: maxDepth ?? 4, total: 0, items: [] });
848
+ }
849
+ const ph = ids.map(() => '?').join(',');
850
+ const rows = (this.store as any).rawDb().prepare(`
851
+ SELECT s.id, s.name, s.qualified_name AS qualifiedName, s.kind,
852
+ f.path AS file, s.line_start AS lineStart, s.pagerank
853
+ FROM symbols s JOIN files f ON f.id = s.file_id
854
+ WHERE s.id IN (${ph})
855
+ `).all(...ids) as any[];
856
+ const byId = new Map(rows.map(r => [Number(r.id), r]));
857
+ const items = hits.slice(0, lim).map(h => {
858
+ const r = byId.get(h.id);
859
+ return r ? {
860
+ id: Number(r.id), name: String(r.name),
861
+ qualifiedName: r.qualifiedName == null ? null : String(r.qualifiedName),
862
+ kind: String(r.kind), file: String(r.file), lineStart: Number(r.lineStart),
863
+ pagerank: Number(r.pagerank), depth: h.depth,
864
+ } : { id: h.id, name: '', qualifiedName: null, kind: '', file: '', lineStart: 0, pagerank: 0, depth: h.depth };
865
+ });
866
+ items.sort((a, b) => a.depth - b.depth || b.pagerank - a.pagerank);
867
+ return this.text({
868
+ symbol: { id: target.id, name: target.name, qualifiedName: target.qualifiedName },
869
+ maxDepth: maxDepth ?? 4, total: hits.length, returned: items.length,
870
+ items, source: 'tree-sitter',
871
+ });
872
+ });
873
+
874
+ this.registerTool('seer_trace_callees', {
875
+ description: 'Bounded forward-reachable callees of a symbol (everything its call graph reaches within depth N).',
876
+ inputSchema: {
877
+ symbol: z.string(),
878
+ maxDepth: z.number().int().positive().max(8).optional(),
879
+ maxNodes: z.number().int().positive().max(50000).optional(),
880
+ limit: z.number().int().positive().max(500).optional(),
881
+ },
882
+ }, async ({ symbol, maxDepth, maxNodes, limit }) => {
883
+ await this.ensureFresh();
884
+ const defs = this.store.getDefinition(symbol);
885
+ if (defs.length === 0) return this.text({ found: false, reason: `no symbol "${symbol}"` });
886
+ const target = defs[0];
887
+ const hits = this.store.forwardReachableWithDepth(target.id, maxDepth ?? 4, maxNodes ?? 20000);
888
+ const lim = Math.min(hits.length, limit ?? 100);
889
+ const ids = hits.slice(0, lim).map(h => h.id);
890
+ if (ids.length === 0) {
891
+ return this.text({ symbol: { id: target.id, name: target.name }, maxDepth: maxDepth ?? 4, total: 0, items: [] });
892
+ }
893
+ const ph = ids.map(() => '?').join(',');
894
+ const rows = (this.store as any).rawDb().prepare(`
895
+ SELECT s.id, s.name, s.qualified_name AS qualifiedName, s.kind,
896
+ f.path AS file, s.line_start AS lineStart, s.pagerank
897
+ FROM symbols s JOIN files f ON f.id = s.file_id
898
+ WHERE s.id IN (${ph})
899
+ `).all(...ids) as any[];
900
+ const byId = new Map(rows.map(r => [Number(r.id), r]));
901
+ const items = hits.slice(0, lim).map(h => {
902
+ const r = byId.get(h.id);
903
+ return r ? {
904
+ id: Number(r.id), name: String(r.name),
905
+ qualifiedName: r.qualifiedName == null ? null : String(r.qualifiedName),
906
+ kind: String(r.kind), file: String(r.file), lineStart: Number(r.lineStart),
907
+ pagerank: Number(r.pagerank), depth: h.depth,
908
+ } : { id: h.id, name: '', qualifiedName: null, kind: '', file: '', lineStart: 0, pagerank: 0, depth: h.depth };
909
+ });
910
+ items.sort((a, b) => a.depth - b.depth || b.pagerank - a.pagerank);
911
+ return this.text({
912
+ symbol: { id: target.id, name: target.name, qualifiedName: target.qualifiedName },
913
+ maxDepth: maxDepth ?? 4, total: hits.length, returned: items.length,
914
+ items, source: 'tree-sitter',
915
+ });
916
+ });
917
+
918
+ this.registerTool('seer_architecture', {
919
+ description: 'One-page snapshot of the codebase: languages, modules, top symbols, entry points, hotspots, deps.',
920
+ inputSchema: {},
921
+ }, async () => {
922
+ await this.ensureFresh();
923
+ return this.text(buildArchitecture(this.workspace, this.store));
924
+ });
925
+
926
+ this.registerTool('seer_detect_changes', {
927
+ description: 'Compute blast-radius of an uncommitted (or between-refs) diff. Direct + transitive callers.',
928
+ inputSchema: {
929
+ fromRef: z.string().optional(),
930
+ toRef: z.string().optional(),
931
+ callerDepth: z.number().int().positive().max(6).optional(),
932
+ },
933
+ }, async ({ fromRef, toRef, callerDepth }) => {
934
+ await this.ensureFresh();
935
+ return this.text(detectChanges(this.workspace, this.store, { fromRef, toRef, callerDepth }));
936
+ });
937
+
938
+ this.registerTool('seer_churn', {
939
+ description: 'Run a file-level git churn pass (commit counts, last commit, authors). Idempotent.',
940
+ inputSchema: {},
941
+ }, async () => {
942
+ return this.text(await collectChurn(this.workspace, this.store));
943
+ });
944
+
945
+ // ── Track-D tools ───────────────────────────────────────────────────────
946
+
947
+ this.registerTool('seer_history', {
948
+ description: 'Per-symbol git history. Returns commits whose hunks overlap the symbol\'s line range.',
949
+ inputSchema: {
950
+ symbol: z.string(),
951
+ limit: z.number().int().positive().max(200).optional(),
952
+ since: z.number().int().optional().describe('Unix-seconds lower bound on committed_at'),
953
+ file: z.string().optional(),
954
+ },
955
+ }, async ({ symbol, limit, since, file }) => {
956
+ await this.ensureFresh();
957
+ await this.ensureSymbolHistory();
958
+ const candidates = this.store.getDefinition(symbol, { filePath: file });
959
+ const items: any[] = [];
960
+ for (const c of candidates.slice(0, 5)) {
961
+ const history = this.store.getSymbolHistory(c.id, { limit: limit ?? 50, since });
962
+ const total = this.store.countSymbolHistory(c.id);
963
+ const continuity = getContinuityForSymbol(this.store, c.id);
964
+ items.push({
965
+ symbol: { id: c.id, name: c.name, qualifiedName: c.qualifiedName, kind: c.kind, file: c.filePath },
966
+ total,
967
+ returned: history.length,
968
+ commits: history.map(h => ({
969
+ sha: h.commitSha,
970
+ author: h.authorName, email: h.authorEmail,
971
+ committedAt: h.committedAt,
972
+ message: h.message,
973
+ linesAdded: h.linesAdded, linesRemoved: h.linesRemoved,
974
+ prNumber: h.prNumber, prUrl: h.prUrl,
975
+ matchStrategy: h.matchStrategy, confidence: h.confidence,
976
+ })),
977
+ continuity,
978
+ });
979
+ }
980
+ return this.text({
981
+ symbol, returned: items.length, results: items,
982
+ note: 'Honest limits: file renames followed via --follow; symbol renames cut off history at the rename commit. Confidence drops with commit age.',
983
+ });
984
+ });
985
+
986
+ this.registerTool('seer_symbol_history_build', {
987
+ description: '(Advanced — usually unnecessary.) seer_history auto-builds this index on first use. Call only to force a refresh or set a custom maxCommitsPerFile. Can take minutes on large repos.',
988
+ inputSchema: {
989
+ maxCommitsPerFile: z.number().int().positive().max(2000).optional(),
990
+ force: z.boolean().optional(),
991
+ },
992
+ }, async ({ maxCommitsPerFile, force }) => {
993
+ const r = await buildSymbolHistory(this.workspace, this.store, {
994
+ maxCommitsPerFile: maxCommitsPerFile ?? 200,
995
+ skipIfHeadUnchanged: !force,
996
+ });
997
+ return this.text(r);
998
+ });
999
+
1000
+ // ── Track-E tools ───────────────────────────────────────────────────────
1001
+
1002
+ this.registerTool('seer_modules', {
1003
+ description: 'List modules (Louvain clusters of files) — agents should start here to orient before reading files. Each module reports size, primary language, cohesion (intra-module edges / total), and centrality (sum of member PageRank).',
1004
+ inputSchema: {
1005
+ limit: z.number().int().positive().max(500).optional(),
1006
+ sortBy: z.enum(['centrality', 'size', 'label']).optional(),
1007
+ },
1008
+ }, async ({ limit, sortBy }) => {
1009
+ await this.ensureFresh();
1010
+ this.ensureModules();
1011
+ const modules = this.store.listModules({ limit: limit ?? 50, sortBy });
1012
+ return this.text({
1013
+ total: this.store.countModules(),
1014
+ returned: modules.length,
1015
+ items: modules,
1016
+ source: 'tree-sitter',
1017
+ });
1018
+ });
1019
+
1020
+ this.registerTool('seer_module_members', {
1021
+ description: 'List files and top-PageRank symbols inside a module. Address the module by `id` or `label`.',
1022
+ inputSchema: {
1023
+ id: z.number().int().positive().optional(),
1024
+ label: z.string().optional(),
1025
+ fileLimit: z.number().int().positive().max(5000).optional(),
1026
+ symbolLimit: z.number().int().positive().max(500).optional(),
1027
+ },
1028
+ }, async ({ id, label, fileLimit, symbolLimit }) => {
1029
+ await this.ensureFresh();
1030
+ this.ensureModules();
1031
+ const mod = id != null ? this.store.getModuleById(id)
1032
+ : label != null ? this.store.getModuleByLabel(label)
1033
+ : null;
1034
+ if (!mod) return this.text({ found: false, reason: id != null ? `no module #${id}` : `no module "${label}"` });
1035
+ const files = this.store.listModuleMembers(mod.id, fileLimit ?? 500);
1036
+ const symbols = this.store.listModuleTopSymbols(mod.id, symbolLimit ?? 25);
1037
+ return this.text({
1038
+ module: mod,
1039
+ files: { total: files.length, items: files },
1040
+ topSymbols: { returned: symbols.length, items: symbols.map(s => ({
1041
+ id: s.id, name: s.name, qualifiedName: s.qualifiedName,
1042
+ kind: s.kind, file: s.filePath, lineStart: s.lineStart,
1043
+ pagerank: s.pagerank,
1044
+ })) },
1045
+ source: 'tree-sitter',
1046
+ });
1047
+ });
1048
+
1049
+ this.registerTool('seer_symbol_module', {
1050
+ description: 'Look up the module a symbol belongs to. Helpful for "what part of the codebase does X live in?".',
1051
+ inputSchema: {
1052
+ symbol: z.string(),
1053
+ file: z.string().optional(),
1054
+ },
1055
+ }, async ({ symbol, file }) => {
1056
+ await this.ensureFresh();
1057
+ this.ensureModules();
1058
+ const defs = this.store.getDefinition(symbol, { filePath: file });
1059
+ if (defs.length === 0) {
1060
+ const didYouMean = this.suggestSymbols(symbol);
1061
+ return this.text({ found: false, reason: `no symbol "${symbol}"`,
1062
+ ...(didYouMean.length > 0 ? { didYouMean } : {}) });
1063
+ }
1064
+ const out: Array<{ symbol: any; module: any }> = [];
1065
+ for (const d of defs.slice(0, 5)) {
1066
+ const mod = this.store.moduleForFile(d.fileId);
1067
+ out.push({
1068
+ symbol: { id: d.id, name: d.name, qualifiedName: d.qualifiedName, kind: d.kind, file: d.filePath },
1069
+ module: mod,
1070
+ });
1071
+ }
1072
+ return this.text({ matches: out, source: 'tree-sitter' });
1073
+ });
1074
+
1075
+ this.registerTool('seer_module_dependencies', {
1076
+ description: 'List module-to-module dependency edges. Direction "out" = modules this one calls/imports/tests into (default). "in" = modules that depend on this one. Edges are aggregated cross-module weights for calls / imports / tests.',
1077
+ inputSchema: {
1078
+ id: z.number().int().positive().optional(),
1079
+ label: z.string().optional(),
1080
+ direction: z.enum(['in', 'out']).optional(),
1081
+ limit: z.number().int().positive().max(500).optional(),
1082
+ },
1083
+ }, async ({ id, label, direction, limit }) => {
1084
+ await this.ensureFresh();
1085
+ this.ensureModules();
1086
+ const mod = id != null ? this.store.getModuleById(id)
1087
+ : label != null ? this.store.getModuleByLabel(label)
1088
+ : null;
1089
+ if (!mod) return this.text({ found: false, reason: id != null ? `no module #${id}` : `no module "${label}"` });
1090
+ const deps = this.store.moduleDependencies(mod.id, {
1091
+ direction: direction ?? 'out',
1092
+ limit: limit ?? 100,
1093
+ });
1094
+ return this.text({
1095
+ module: mod, direction: direction ?? 'out',
1096
+ returned: deps.length, items: deps, source: 'tree-sitter',
1097
+ });
1098
+ });
1099
+
1100
+ this.registerTool('seer_trace_file_dependencies', {
1101
+ description: 'Bounded BFS over the resolved import graph starting at a file. Returns each reachable file with the depth at which it was first seen.',
1102
+ inputSchema: {
1103
+ file: z.string(),
1104
+ maxDepth: z.number().int().positive().max(8).optional(),
1105
+ maxNodes: z.number().int().positive().max(20000).optional(),
1106
+ },
1107
+ }, async ({ file, maxDepth, maxNodes }) => {
1108
+ await this.ensureFresh();
1109
+ const files = this.store.listFiles();
1110
+ const norm = (p: string): string => p.replace(/\\/g, '/').toLowerCase();
1111
+ const match = files.find(f =>
1112
+ norm(f.path) === norm(file) || norm(f.relPath) === norm(file)
1113
+ || norm(f.path).endsWith(norm(file)) || norm(f.relPath).endsWith(norm(file)),
1114
+ );
1115
+ if (!match) return this.text({ found: false, reason: `no indexed file matching "${file}"` });
1116
+ const closure = this.store.fileImportClosure(match.id, maxDepth ?? 4, maxNodes ?? 5000);
1117
+ closure.sort((a, b) => a.depth - b.depth || (a.relPath < b.relPath ? -1 : 1));
1118
+ return this.text({
1119
+ from: { id: match.id, path: match.path, relPath: match.relPath, language: match.language },
1120
+ maxDepth: maxDepth ?? 4,
1121
+ totalReachable: closure.length,
1122
+ items: closure.map(c => ({ id: c.id, relPath: c.relPath, language: c.language, depth: c.depth })),
1123
+ source: 'tree-sitter',
1124
+ });
1125
+ });
1126
+
1127
+ this.registerTool('seer_trace_module_dependencies', {
1128
+ description: 'Bounded BFS over the module dependency graph. Returns each reachable module with the depth at which it was first seen.',
1129
+ inputSchema: {
1130
+ id: z.number().int().positive().optional(),
1131
+ label: z.string().optional(),
1132
+ maxDepth: z.number().int().positive().max(8).optional(),
1133
+ direction: z.enum(['in', 'out']).optional(),
1134
+ },
1135
+ }, async ({ id, label, maxDepth, direction }) => {
1136
+ await this.ensureFresh();
1137
+ this.ensureModules();
1138
+ const mod = id != null ? this.store.getModuleById(id)
1139
+ : label != null ? this.store.getModuleByLabel(label)
1140
+ : null;
1141
+ if (!mod) return this.text({ found: false, reason: id != null ? `no module #${id}` : `no module "${label}"` });
1142
+ const depth = Math.min(maxDepth ?? 4, 8);
1143
+ const dir = direction ?? 'out';
1144
+ const seen = new Map<number, number>([[mod.id, 0]]);
1145
+ const queue: Array<{ id: number; depth: number }> = [{ id: mod.id, depth: 0 }];
1146
+ while (queue.length > 0) {
1147
+ const cur = queue.shift()!;
1148
+ if (cur.depth >= depth) continue;
1149
+ const deps = this.store.moduleDependencies(cur.id, { direction: dir, limit: 500 });
1150
+ for (const d of deps) {
1151
+ if (seen.has(d.moduleId)) continue;
1152
+ seen.set(d.moduleId, cur.depth + 1);
1153
+ queue.push({ id: d.moduleId, depth: cur.depth + 1 });
1154
+ }
1155
+ }
1156
+ seen.delete(mod.id);
1157
+ const items = Array.from(seen.entries()).map(([mid, d]) => {
1158
+ const m = this.store.getModuleById(mid);
1159
+ return { id: mid, label: m?.label ?? null, depth: d };
1160
+ });
1161
+ items.sort((a, b) => a.depth - b.depth || ((a.label ?? '') < (b.label ?? '') ? -1 : 1));
1162
+ return this.text({
1163
+ from: mod, direction: dir, maxDepth: depth,
1164
+ totalReachable: items.length, items, source: 'tree-sitter',
1165
+ });
1166
+ });
1167
+
1168
+ this.registerTool('seer_modules_build', {
1169
+ description: '(Advanced — usually unnecessary.) Module clustering (Louvain) runs automatically during indexing and auto-builds on first seer_modules* query. Call only to force a rebuild. Idempotent.',
1170
+ inputSchema: {},
1171
+ }, async () => {
1172
+ const r = buildModules(this.store);
1173
+ return this.text(r);
1174
+ });
1175
+
1176
+ this.registerTool('seer_risk', {
1177
+ description: 'Deterministic edit-risk profile for a symbol. Returns a decomposed score with per-signal contributions: fan-in, route exposure, test coverage, complexity, churn, config reads, and module-boundary crossings. The verdict (low/medium/high) is for triage; the signals are the evidence.',
1178
+ inputSchema: {
1179
+ symbol: z.string(),
1180
+ callerDepth: z.number().int().positive().max(6).optional(),
1181
+ },
1182
+ }, async ({ symbol, callerDepth }) => {
1183
+ await this.ensureFresh();
1184
+ const r = computeRisk(this.store, symbol, { callerDepth: callerDepth ?? 3 });
1185
+ if (!r) {
1186
+ const didYouMean = this.suggestSymbols(symbol);
1187
+ return this.text({ found: false, reason: `no symbol "${symbol}"`,
1188
+ ...(didYouMean.length > 0 ? { didYouMean } : {}) });
1189
+ }
1190
+ return this.text(r);
1191
+ });
1192
+
1193
+ // ── Track-F tools (portability + precision) ─────────────────────────────
1194
+
1195
+ this.registerTool('seer_bundle_export', {
1196
+ description: 'Export the current index as a portable .seerbundle file. Use this in CI or to share a pre-built index with teammates so they skip the cold-start indexing cost.',
1197
+ inputSchema: {
1198
+ out: z.string().optional().describe('Output path (default: <workspace>/.seer/index.seerbundle)'),
1199
+ compressionLevel: z.number().int().min(0).max(9).optional(),
1200
+ builtAt: z.number().int().optional().describe('Pin manifest.builtAt (Unix millis) for reproducible bundle bytes.'),
1201
+ },
1202
+ }, async ({ out, compressionLevel, builtAt }) => {
1203
+ const r = await exportBundle(this.dbPath, this.workspace, {
1204
+ out, compressionLevel, builtAt,
1205
+ });
1206
+ return this.text({
1207
+ bundlePath: r.bundlePath, bytes: r.bytes,
1208
+ manifest: r.manifest, elapsedMs: r.elapsedMs,
1209
+ });
1210
+ });
1211
+
1212
+ this.registerTool('seer_bundle_info', {
1213
+ description: 'Read a bundle\'s manifest without unpacking the DB (schema version, file count, symbol/edge totals, SCIP layers).',
1214
+ inputSchema: { bundle: z.string() },
1215
+ }, async ({ bundle }) => {
1216
+ try {
1217
+ return this.text(readBundleManifest(path.resolve(bundle)));
1218
+ } catch (err) {
1219
+ return this.text({ ok: false, reason: (err as Error).message });
1220
+ }
1221
+ });
1222
+
1223
+ this.registerTool('seer_bundle_import', {
1224
+ description: 'Import a .seerbundle. Defaults to destructive whole-index restore. Pass external=true to import additively as a read-only external layer (peer-repo evidence) that does not replace any local rows.',
1225
+ inputSchema: {
1226
+ bundle: z.string(),
1227
+ overwrite: z.boolean().optional(),
1228
+ skipIntegrityCheck: z.boolean().optional(),
1229
+ skipSchemaCheck: z.boolean().optional(),
1230
+ external: z.boolean().optional().describe('Additive external import — never replaces the local DB.'),
1231
+ alias: z.string().optional().describe('External-only: alias for the imported layer.'),
1232
+ force: z.boolean().optional().describe('External-only: force re-import even if the same hash is already present.'),
1233
+ },
1234
+ }, async ({ bundle, overwrite, skipIntegrityCheck, skipSchemaCheck, external, alias, force }) => {
1235
+ if (external) {
1236
+ try {
1237
+ const r = await importExternalBundle(path.resolve(bundle), this.store, {
1238
+ alias, force,
1239
+ });
1240
+ return this.text({
1241
+ ok: true, external: true,
1242
+ bundleId: r.bundleId, externalProject: r.externalProject,
1243
+ externalHash: r.externalHash, schemaVersion: r.schemaVersion,
1244
+ routesImported: r.routesImported,
1245
+ serviceEndpointsImported: r.serviceEndpointsImported,
1246
+ alreadyImported: r.alreadyImported,
1247
+ elapsedMs: r.elapsedMs,
1248
+ });
1249
+ } catch (err) {
1250
+ return this.text({ ok: false, external: true, reason: (err as Error).message });
1251
+ }
1252
+ }
1253
+ try {
1254
+ // Closing the store ensures the file isn't locked when we overwrite.
1255
+ const wasWatchEnabled = this.watcher != null;
1256
+ if (this.watcher) { await this.watcher.stop(); this.watcher = null; }
1257
+ this.store.close();
1258
+ // Use this.dbPath so a server started with `--db custom.db` keeps
1259
+ // serving the same file after import. Without this override the
1260
+ // bundle would land at <workspace>/.seer/graph.db (the default in
1261
+ // importBundle) while the server kept reading from `custom.db`.
1262
+ const r = await importBundle(path.resolve(bundle), {
1263
+ repoRoot: this.workspace, overwrite,
1264
+ skipIntegrityCheck, skipSchemaCheck,
1265
+ dbOut: this.dbPath,
1266
+ });
1267
+ // Re-open against the freshly imported DB.
1268
+ this.store = new Store(this.dbPath);
1269
+ this.indexer = new Indexer(this.store);
1270
+ if (wasWatchEnabled) {
1271
+ this.watcher = new SeerWatcher(this.workspace, this.store, this.indexer, {
1272
+ log: (m) => process.stderr.write(`[watcher] ${m}\n`),
1273
+ });
1274
+ this.watcher.start();
1275
+ }
1276
+ return this.text({
1277
+ ok: true, dbPath: r.dbPath, manifest: r.manifest, elapsedMs: r.elapsedMs,
1278
+ });
1279
+ } catch (err) {
1280
+ return this.text({ ok: false, reason: (err as Error).message });
1281
+ }
1282
+ });
1283
+
1284
+ this.registerTool('seer_continuity', {
1285
+ description: 'v10 — Rename/move continuity candidates for a symbol. When the exact symbol_key history walk terminates at a rename/move boundary, this tool surfaces honest, confidence-labelled candidates for the previous identity (shape_hash exact / close match, same containing scope, similar name). Always advisory — confidence < 1.0 reflects ambiguity.',
1286
+ inputSchema: {
1287
+ symbol: z.string(),
1288
+ file: z.string().optional(),
1289
+ },
1290
+ }, async ({ symbol, file }) => {
1291
+ await this.ensureFresh();
1292
+ this.ensureShapeHashes();
1293
+ const defs = this.store.getDefinition(symbol, { filePath: file });
1294
+ if (defs.length === 0) {
1295
+ const didYouMean = this.suggestSymbols(symbol);
1296
+ return this.text({ ok: false, reason: `no symbol "${symbol}"`,
1297
+ ...(didYouMean.length > 0 ? { didYouMean } : {}) });
1298
+ }
1299
+ const items = defs.slice(0, 5).map(d => ({
1300
+ symbol: { id: d.id, name: d.name, qualifiedName: d.qualifiedName, kind: d.kind, file: d.filePath },
1301
+ candidates: getContinuityForSymbol(this.store, d.id),
1302
+ }));
1303
+ return this.text({ ok: true, results: items });
1304
+ });
1305
+
1306
+ this.registerTool('seer_boundaries', {
1307
+ description: 'v10 — List monorepo package/service boundaries detected from manifests (package.json/pyproject.toml/Cargo.toml/go.mod/composer.json) and the services/* / packages/* / apps/* / libs/* fallback. Strictly advisory.',
1308
+ inputSchema: {
1309
+ limit: z.number().int().positive().max(1000).optional(),
1310
+ },
1311
+ }, async ({ limit }) => {
1312
+ await this.ensureFresh();
1313
+ const items = this.store.listBoundaries(limit ?? 100);
1314
+ return this.text({
1315
+ total: this.store.countBoundaries(),
1316
+ returned: items.length,
1317
+ items,
1318
+ source: 'tree-sitter',
1319
+ });
1320
+ });
1321
+
1322
+ this.registerTool('seer_boundary_for_file', {
1323
+ description: 'v10 — Look up the boundary that owns a file. Returns null when no boundary matched (file lives outside any detected package/service root).',
1324
+ inputSchema: {
1325
+ file: z.string(),
1326
+ },
1327
+ }, async ({ file }) => {
1328
+ await this.ensureFresh();
1329
+ const files = this.store.listFiles();
1330
+ const norm = (p: string): string => p.replace(/\\/g, '/').toLowerCase();
1331
+ const match = files.find(f =>
1332
+ norm(f.path) === norm(file) || norm(f.relPath) === norm(file)
1333
+ || norm(f.path).endsWith(norm(file)) || norm(f.relPath).endsWith(norm(file)));
1334
+ if (!match) return this.text({ ok: false, reason: `no indexed file matching "${file}"` });
1335
+ const boundary = this.store.boundaryForFile(match.id);
1336
+ return this.text({
1337
+ ok: true,
1338
+ file: { id: match.id, relPath: match.relPath },
1339
+ boundary,
1340
+ });
1341
+ });
1342
+
1343
+ this.registerTool('seer_boundary_dependencies', {
1344
+ description: 'v10 — Cross-boundary dependency edges from a given boundary (aggregated cross-boundary call/import/service-link weights).',
1345
+ inputSchema: {
1346
+ boundaryId: z.number().int().nonnegative(),
1347
+ direction: z.enum(['in', 'out']).optional(),
1348
+ limit: z.number().int().positive().max(500).optional(),
1349
+ },
1350
+ }, async ({ boundaryId, direction, limit }) => {
1351
+ await this.ensureFresh();
1352
+ const items = this.store.boundaryDependencies(boundaryId, {
1353
+ direction: direction ?? 'out',
1354
+ limit: limit ?? 100,
1355
+ });
1356
+ return this.text({
1357
+ boundaryId, direction: direction ?? 'out',
1358
+ returned: items.length,
1359
+ items,
1360
+ });
1361
+ });
1362
+
1363
+ this.registerTool('seer_preflight', {
1364
+ description: 'Compact "should I edit this?" evidence packet. Pass `symbol` for a single-symbol packet (risk, likely tests, service impact, history), or `fromRef`/`toRef` for a diff-range packet (touched symbols, aggregated risk, likely tests, service impact). Optional `oldBundle`/`newBundle` adds a contract diff to the packet. Output is structured facts only — no AI prose.',
1365
+ inputSchema: {
1366
+ symbol: z.string().optional(),
1367
+ file: z.string().optional(),
1368
+ fromRef: z.string().optional(),
1369
+ toRef: z.string().optional(),
1370
+ oldBundle: z.string().optional(),
1371
+ newBundle: z.string().optional(),
1372
+ maxSymbols: z.number().int().positive().max(50).optional(),
1373
+ maxTests: z.number().int().positive().max(50).optional(),
1374
+ maxHistory: z.number().int().positive().max(50).optional(),
1375
+ callerDepth: z.number().int().positive().max(6).optional(),
1376
+ },
1377
+ }, async (args) => {
1378
+ await this.ensureFresh();
1379
+ const r = await preflight(this.store, {
1380
+ symbol: args.symbol,
1381
+ filePath: args.file,
1382
+ fromRef: args.fromRef,
1383
+ toRef: args.toRef,
1384
+ workspace: this.workspace,
1385
+ oldBundle: args.oldBundle,
1386
+ newBundle: args.newBundle,
1387
+ maxSymbols: args.maxSymbols,
1388
+ maxTests: args.maxTests,
1389
+ maxHistory: args.maxHistory,
1390
+ callerDepth: args.callerDepth,
1391
+ });
1392
+ return this.text(r);
1393
+ });
1394
+
1395
+ this.registerTool('seer_contract_diff', {
1396
+ description: 'Diff API/service contracts between two .seerbundle artifacts (routes, tRPC/GraphQL/gRPC operations, topics, queues). Advisory only — never raises an error for breaking changes. Pass includeAffectedCallers to enrich the diff with service-link evidence when both bundles contain it.',
1397
+ inputSchema: {
1398
+ oldBundle: z.string(),
1399
+ newBundle: z.string(),
1400
+ includeAffectedCallers: z.boolean().optional(),
1401
+ },
1402
+ }, async ({ oldBundle, newBundle, includeAffectedCallers }) => {
1403
+ try {
1404
+ const diff = await contractDiff(
1405
+ path.resolve(oldBundle),
1406
+ path.resolve(newBundle),
1407
+ { includeAffectedCallers },
1408
+ );
1409
+ return this.text({ ok: true, ...diff });
1410
+ } catch (err) {
1411
+ return this.text({ ok: false, reason: (err as Error).message });
1412
+ }
1413
+ });
1414
+
1415
+ this.registerTool('seer_external_bundles', {
1416
+ description: 'List external .seerbundle layers imported into this workspace. Each entry carries the source bundle path, external project alias, manifest hash, schemaVersion, and the rendezvous counts (routes / service endpoints) contributed by that layer.',
1417
+ inputSchema: {
1418
+ includeRoutes: z.boolean().optional().describe('When true, also returns a bounded preview of the external routes contributed by each layer.'),
1419
+ routesPreviewLimit: z.number().int().positive().max(500).optional(),
1420
+ },
1421
+ }, async ({ includeRoutes, routesPreviewLimit }) => {
1422
+ const layers = this.store.listExternalBundles();
1423
+ const previewLimit = routesPreviewLimit ?? 25;
1424
+ const items = layers.map(layer => {
1425
+ const base = {
1426
+ id: layer.id,
1427
+ sourceKind: layer.sourceKind,
1428
+ bundlePath: layer.bundlePath,
1429
+ externalProject: layer.externalProject,
1430
+ externalVersion: layer.externalVersion,
1431
+ externalHash: layer.externalHash,
1432
+ schemaVersion: layer.schemaVersion,
1433
+ importedAt: layer.importedAt,
1434
+ routesImported: layer.routesImported,
1435
+ serviceCallsImported: layer.serviceCallsImported,
1436
+ serviceLinksImported: layer.serviceLinksImported,
1437
+ };
1438
+ if (!includeRoutes) return base;
1439
+ const routes = this.store.listExternalRoutes({ bundleId: layer.id, limit: previewLimit });
1440
+ return { ...base, routesPreview: routes };
1441
+ });
1442
+ return this.text({
1443
+ total: items.length,
1444
+ items,
1445
+ source: 'external-bundle',
1446
+ });
1447
+ });
1448
+
1449
+ this.registerTool('seer_scip_import', {
1450
+ description: 'Import a SCIP precision index. Adds source-labelled precise edges (provenance="scip") over the tree-sitter baseline. Tree-sitter rows are never deleted; overlapping rows are tagged "scip-merge" instead.',
1451
+ inputSchema: {
1452
+ scipPath: z.string(),
1453
+ requireFileInIndex: z.boolean().optional().describe('Skip SCIP docs whose file isn\'t already indexed (default: true)'),
1454
+ },
1455
+ }, async ({ scipPath, requireFileInIndex }) => {
1456
+ try {
1457
+ const r = await importScip(path.resolve(scipPath), this.store, {
1458
+ repoRoot: this.workspace,
1459
+ requireFileInIndex: requireFileInIndex ?? true,
1460
+ });
1461
+ return this.text(r);
1462
+ } catch (err) {
1463
+ return this.text({ ok: false, reason: (err as Error).message });
1464
+ }
1465
+ });
1466
+
1467
+ this.registerTool('seer_scip_imports', {
1468
+ description: 'List every SCIP index that\'s been folded into this DB. Each entry includes the producer tool, sha256, and per-import symbol/ref counts so agents can see exactly which precision layers contributed.',
1469
+ inputSchema: {},
1470
+ }, async () => {
1471
+ return this.text({
1472
+ items: this.store.listScipImports(),
1473
+ provenance: this.store.getProvenanceCounts(),
1474
+ });
1475
+ });
1476
+
1477
+ this.registerTool('seer_provenance', {
1478
+ description: 'Breakdown of symbols + edges by provenance (tree-sitter / scip / scip-merge). Lets agents tell which signals came from a precise indexer vs the tree-sitter baseline.',
1479
+ inputSchema: {},
1480
+ }, async () => {
1481
+ await this.ensureFresh();
1482
+ return this.text({
1483
+ provenance: this.store.getProvenanceCounts(),
1484
+ scipImports: this.store.listScipImports(),
1485
+ });
1486
+ });
1487
+
1488
+ this.registerTool('seer_duplicates', {
1489
+ description: 'Find clusters of structurally near-duplicate functions/methods (SimHash over the body token shape, identifier-folded so renames still match). Returns each cluster sorted by size with Hamming distance from the cluster anchor.',
1490
+ inputSchema: {
1491
+ maxDistance: z.number().int().nonnegative().max(32).optional()
1492
+ .describe('Max Hamming distance for clustering (default: 6).'),
1493
+ minLoc: z.number().int().positive().optional()
1494
+ .describe('Minimum LOC for a symbol to count (default: 4).'),
1495
+ includeTests: z.boolean().optional(),
1496
+ limit: z.number().int().positive().max(1000).optional(),
1497
+ },
1498
+ }, async ({ maxDistance, minLoc, includeTests, limit }) => {
1499
+ await this.ensureFresh();
1500
+ this.ensureShapeHashes();
1501
+ const clusters = findDuplicates(this.store, {
1502
+ maxDistance: maxDistance ?? 6,
1503
+ minLoc: minLoc ?? 4,
1504
+ includeTests: includeTests ?? false,
1505
+ maxClusters: limit ?? 50,
1506
+ });
1507
+ // bigint isn't JSON-serializable — render as hex.
1508
+ return this.text({
1509
+ clusters: clusters.length,
1510
+ items: clusters.map(c => ({
1511
+ fingerprint: c.fingerprint.toString(16),
1512
+ size: c.symbols.length,
1513
+ symbols: c.symbols,
1514
+ })),
1515
+ source: 'tree-sitter',
1516
+ });
1517
+ });
1518
+
1519
+ this.registerTool('seer_shape_hash_build', {
1520
+ description: '(Advanced — usually unnecessary.) The shape-hash pass (Track-F SimHash) runs automatically during indexing and auto-builds on first seer_duplicates / seer_continuity query. Call only to force a re-hash. Idempotent.',
1521
+ inputSchema: {
1522
+ force: z.boolean().optional().describe('Re-hash symbols that already have a hash.'),
1523
+ minLoc: z.number().int().positive().optional(),
1524
+ },
1525
+ }, async ({ force, minLoc }) => {
1526
+ const r = buildShapeHashes(this.store, { force, minLoc });
1527
+ return this.text(r);
1528
+ });
1529
+
1530
+ this.registerTool('seer_context', {
1531
+ description: 'One compact pre-edit packet for a symbol: definition, callers, callees, routes, config, behavioral tests, recent history, complexity, module, blast radius, and deterministic risk. Use this as the first call before editing a symbol — then drill in with seer_callers / seer_history / seer_behavior as needed.',
1532
+ inputSchema: {
1533
+ symbol: z.string(),
1534
+ file: z.string().optional(),
1535
+ callerLimit: z.number().int().positive().max(100).optional(),
1536
+ calleeLimit: z.number().int().positive().max(100).optional(),
1537
+ testLimit: z.number().int().positive().max(100).optional(),
1538
+ historyLimit: z.number().int().positive().max(50).optional(),
1539
+ callerDepth: z.number().int().positive().max(6).optional(),
1540
+ affectedLimit: z.number().int().positive().max(100).optional(),
1541
+ },
1542
+ }, async ({ symbol, file, callerLimit, calleeLimit, testLimit, historyLimit, callerDepth, affectedLimit }) => {
1543
+ await this.ensureFresh();
1544
+ const packet = buildContext(this.store, symbol, {
1545
+ filePath: file,
1546
+ callerLimit, calleeLimit, testLimit, historyLimit,
1547
+ callerDepth, affectedLimit,
1548
+ });
1549
+ if (!packet) {
1550
+ const didYouMean = this.suggestSymbols(symbol);
1551
+ return this.text({ found: false, reason: `no symbol "${symbol}"`,
1552
+ ...(didYouMean.length > 0 ? { didYouMean } : {}) });
1553
+ }
1554
+ return this.text(packet);
1555
+ });
1556
+
1557
+ // ── AI-agent optimization tools ─────────────────────────────────────────
1558
+
1559
+ this.registerTool('seer_skeleton', {
1560
+ description: 'Render a file as a structural skeleton: every symbol signature is kept, bodies are collapsed to fold markers carrying the exact collapsed line count. Deterministic source elision (not AI summarization) — a token-cheap way to grasp a file\'s shape before reading it in full. Pass `focusSymbol` to expand one symbol\'s real body inline while everything else stays collapsed.',
1561
+ inputSchema: {
1562
+ file: z.string().describe('Absolute path, exact rel_path, or a trailing path fragment on a / boundary.'),
1563
+ focusSymbol: z.string().optional().describe('Expand this symbol\'s body verbatim; collapse the rest.'),
1564
+ },
1565
+ }, async ({ file, focusSymbol }) => {
1566
+ await this.ensureFresh();
1567
+ return this.text(buildSkeleton(this.store, file, { focusSymbol }));
1568
+ });
1569
+
1570
+ this.registerTool('seer_trace', {
1571
+ description:
1572
+ 'Unified graph-trace entry point. Set `scope` and pass the matching `args`:\n' +
1573
+ '• callers {symbol, maxDepth?, maxNodes?, limit?} — transitive reverse callers (blast radius)\n' +
1574
+ '• callees {symbol, maxDepth?, maxNodes?, limit?} — transitive forward callees\n' +
1575
+ '• path {from, to, maxDepth?} — shortest call path A→B\n' +
1576
+ '• file {file, maxDepth?, maxNodes?} — import-graph closure from a file\n' +
1577
+ '• module {id|label, maxDepth?, direction?} — module dependency reachability\n' +
1578
+ '• service {from, maxDepth?, maxNodes?, maxFanout?} — service-link reachability\n' +
1579
+ '• service_path {from, to, maxDepth?} — shortest service-link path\n' +
1580
+ '• module_service {moduleId, maxDepth?, maxNodes?} — cross-module service-link reachability\n' +
1581
+ 'Delegates to the specific seer_trace_* tool (each still available for direct use).',
1582
+ inputSchema: {
1583
+ scope: z.enum([
1584
+ 'callers', 'callees', 'path', 'file',
1585
+ 'module', 'service', 'service_path', 'module_service',
1586
+ ]),
1587
+ args: z.any().optional(),
1588
+ },
1589
+ }, async ({ scope, args }) => {
1590
+ const map: Record<string, string> = {
1591
+ callers: 'seer_trace_callers',
1592
+ callees: 'seer_trace_callees',
1593
+ path: 'seer_trace_path',
1594
+ file: 'seer_trace_file_dependencies',
1595
+ module: 'seer_trace_module_dependencies',
1596
+ service: 'seer_trace_service_dependencies',
1597
+ service_path: 'seer_trace_service_path',
1598
+ module_service: 'seer_trace_module_service_dependencies',
1599
+ };
1600
+ const target = map[scope];
1601
+ const h = target ? this.handlers.get(target) : undefined;
1602
+ if (!h) return this.text({ ok: false, error: `unsupported scope "${scope}"` });
1603
+ // The umbrella accepts `args` as opaque (z.any()), so the delegate's own
1604
+ // required-param schema isn't enforced by the SDK here. Catch its throws
1605
+ // and return a clean, advisory error rather than a raw binding failure.
1606
+ try {
1607
+ return await h(args ?? {});
1608
+ } catch (err) {
1609
+ return this.text({ ok: false, scope, error: `seer_trace[${scope}] failed: ${(err as Error).message}` });
1610
+ }
1611
+ });
1612
+
1613
+ this.registerTool('seer_batch', {
1614
+ description:
1615
+ 'Run several read-only Seer tools in one call and get all results back together. ' +
1616
+ 'Saves turns when the fan-out is known up front (e.g. definition + callers + behavior + risk for one symbol). ' +
1617
+ 'Each entry is {tool, args}. Calls run sequentially in one process; one failure never aborts the rest. ' +
1618
+ 'seer_batch cannot nest, and it is intended for read-only tools.',
1619
+ inputSchema: {
1620
+ calls: z.array(z.object({
1621
+ tool: z.string(),
1622
+ args: z.any().optional(),
1623
+ })).min(1).max(25),
1624
+ },
1625
+ }, async ({ calls }) => {
1626
+ const results: Array<{ tool: string | null; ok: boolean; result?: unknown; error?: string }> = [];
1627
+ for (const c of calls) {
1628
+ const toolName = c && typeof c.tool === 'string' ? c.tool : null;
1629
+ if (!toolName || toolName === 'seer_batch') {
1630
+ results.push({ tool: toolName, ok: false, error: 'missing tool name or nested seer_batch (disallowed)' });
1631
+ continue;
1632
+ }
1633
+ const h = this.handlers.get(toolName);
1634
+ if (!h) { results.push({ tool: toolName, ok: false, error: `unknown tool "${toolName}"` }); continue; }
1635
+ try {
1636
+ const r = await h(c.args ?? {});
1637
+ const raw = r?.content?.[0]?.text;
1638
+ let parsed: unknown;
1639
+ try { parsed = raw != null ? JSON.parse(raw) : null; } catch { parsed = raw ?? null; }
1640
+ results.push({ tool: toolName, ok: true, result: parsed });
1641
+ } catch (err) {
1642
+ results.push({ tool: toolName, ok: false, error: (err as Error).message });
1643
+ }
1644
+ }
1645
+ return this.text({ batch: true, count: results.length, results });
1646
+ });
1647
+ }
1648
+ }
1649
+
1650
+ export async function runMcp(options: McpServerOptions): Promise<void> {
1651
+ const server = new SeerMcpServer(options);
1652
+ const shutdown = async (): Promise<void> => {
1653
+ try { await server.stop(); } catch { /* */ }
1654
+ process.exit(0);
1655
+ };
1656
+ process.on('SIGINT', shutdown);
1657
+ process.on('SIGTERM', shutdown);
1658
+ await server.start();
1659
+ }