ctxman 3.0.0 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/.ctxman/llm-profiles.json +1067 -39
  2. package/.ctxman/profiles/backend-team.json +13 -0
  3. package/.ctxman/profiles/default.json +12 -0
  4. package/.ctxman/profiles/devops-team.json +21 -0
  5. package/.ctxman/profiles/frontend-team.json +20 -0
  6. package/.ctxman/templates/bug-fix.json +17 -0
  7. package/.ctxman/templates/code-review.json +9 -0
  8. package/.ctxman/templates/documentation.json +9 -0
  9. package/.ctxman/templates/feature.json +9 -0
  10. package/.ctxman/templates/full-context.json +9 -0
  11. package/.ctxman/templates/refactoring.json +18 -0
  12. package/.ctxman/templates/testing.json +9 -0
  13. package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
  14. package/LICENSE +21 -0
  15. package/README.md +381 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -67
  18. package/bin/cm-audit.js +121 -0
  19. package/bin/cm-count.js +151 -0
  20. package/bin/cm-gitingest.js +128 -115
  21. package/bin/cm-map.js +242 -0
  22. package/bin/cm-mcp.js +293 -0
  23. package/bin/cm-pack.js +405 -0
  24. package/bin/cm-policy.js +170 -0
  25. package/bin/cm-pr.js +248 -0
  26. package/bin/cm-search.js +178 -0
  27. package/bin/cm-update.js +128 -128
  28. package/bin/cm-why.js +197 -0
  29. package/bin/mcp-server.js +8 -24
  30. package/ctxman.js +237 -183
  31. package/examples/README.md +19 -4
  32. package/examples/github-actions/ctxman-check.yml +49 -0
  33. package/examples/wizard-profiles/security-audit/profile.json +7 -1
  34. package/index.d.ts +1422 -0
  35. package/index.js +55 -31
  36. package/lib/agents/pr-failure-agent.js +48 -45
  37. package/lib/analyzers/ai-suggester.js +451 -0
  38. package/lib/analyzers/dependency-scanner.js +505 -0
  39. package/lib/analyzers/duplicate-detector.js +264 -0
  40. package/lib/analyzers/go-method-analyzer.js +151 -110
  41. package/lib/analyzers/import-tracker.js +431 -0
  42. package/lib/analyzers/method-analyzer.js +1055 -434
  43. package/lib/analyzers/performance-dashboard.js +687 -0
  44. package/lib/analyzers/token-calculator.js +895 -504
  45. package/lib/analyzers/unused-exports-detector.js +336 -0
  46. package/lib/api/mcp/install.js +424 -0
  47. package/lib/api/mcp/prompts.js +281 -281
  48. package/lib/api/mcp/resources.js +300 -241
  49. package/lib/api/mcp/sandbox.js +89 -0
  50. package/lib/api/mcp/server.js +772 -312
  51. package/lib/api/mcp/start.js +125 -0
  52. package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
  53. package/lib/api/rest/server.js +105 -28
  54. package/lib/cache/CacheManager.js +9 -17
  55. package/lib/core/Analyzer.js +16 -10
  56. package/lib/core/ContextBuilder.js +19 -28
  57. package/lib/core/Reporter.js +15 -14
  58. package/lib/core/Scanner.js +81 -12
  59. package/lib/count/index.js +100 -0
  60. package/lib/formatters/format-registry.js +394 -372
  61. package/lib/formatters/gitingest-formatter.js +656 -621
  62. package/lib/formatters/toon-formatter-v1.3.js +637 -632
  63. package/lib/formatters/toon-formatter.js +239 -240
  64. package/lib/integrations/git/BlameTracker.js +4 -4
  65. package/lib/integrations/git/DiffAnalyzer.js +38 -31
  66. package/lib/integrations/git/GitClient.js +71 -53
  67. package/lib/lsp/server.js +311 -0
  68. package/lib/map/graph.js +299 -0
  69. package/lib/map/imports.js +233 -0
  70. package/lib/map/index.js +33 -0
  71. package/lib/map/rank.js +253 -0
  72. package/lib/map/render.js +346 -0
  73. package/lib/map/resolver.js +574 -0
  74. package/lib/pack/budget.js +111 -0
  75. package/lib/pack/code-lexer.js +360 -0
  76. package/lib/pack/compress.js +414 -0
  77. package/lib/pack/index.js +354 -0
  78. package/lib/pack/renderers/common.js +134 -0
  79. package/lib/pack/renderers/index.js +24 -0
  80. package/lib/pack/renderers/json.js +47 -0
  81. package/lib/pack/renderers/markdown.js +54 -0
  82. package/lib/pack/renderers/plain.js +38 -0
  83. package/lib/pack/renderers/xml.js +85 -0
  84. package/lib/pack/select.js +177 -0
  85. package/lib/pack/split.js +182 -0
  86. package/lib/pack/tree.js +35 -0
  87. package/lib/parsers/gitignore-parser.js +116 -107
  88. package/lib/parsers/method-filter-parser.js +50 -33
  89. package/lib/plugins/ExporterPlugin.js +3 -3
  90. package/lib/plugins/LanguagePlugin.js +3 -3
  91. package/lib/plugins/PluginManager.js +7 -13
  92. package/lib/policy/audit.js +312 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +53 -0
  96. package/lib/policy/policy.js +489 -0
  97. package/lib/pr/diff-parser.js +267 -0
  98. package/lib/pr/method-mapper.js +148 -0
  99. package/lib/pr/pr-context.js +829 -0
  100. package/lib/pr/pr-git.js +149 -0
  101. package/lib/pr/pr-renderer.js +644 -0
  102. package/lib/pr/related-tests.js +296 -0
  103. package/lib/rag/DataSourcePlugin.js +5 -5
  104. package/lib/rag/EmbeddingProvider.js +19 -8
  105. package/lib/rag/Indexer.js +14 -5
  106. package/lib/rag/VectorStoreFactory.js +3 -3
  107. package/lib/rag/ask.js +122 -0
  108. package/lib/rag/sources/FileDataSource.js +12 -9
  109. package/lib/rag/stores/BaseVectorStore.js +5 -5
  110. package/lib/rag/stores/ChromaDBStore.js +10 -10
  111. package/lib/rag/stores/LanceDBStore.js +29 -19
  112. package/lib/rag/stores/PineconeStore.js +11 -9
  113. package/lib/search/bm25.js +433 -0
  114. package/lib/search/code-units.js +201 -0
  115. package/lib/search/symbols.js +189 -0
  116. package/lib/ui/dashboard.js +120 -89
  117. package/lib/ui/index.js +1 -6
  118. package/lib/ui/init-wizard-ui.js +550 -0
  119. package/lib/ui/progress-bar.js +94 -88
  120. package/lib/ui/select-input.js +40 -32
  121. package/lib/ui/template-selector.js +99 -0
  122. package/lib/ui/wizard.js +374 -329
  123. package/lib/utils/claude-token-counter.js +151 -0
  124. package/lib/utils/cli-output.js +17 -0
  125. package/lib/utils/clipboard-utils.js +49 -50
  126. package/lib/utils/config-utils.js +81 -66
  127. package/lib/utils/context-versioning.js +532 -0
  128. package/lib/utils/error-handler.js +112 -112
  129. package/lib/utils/file-utils.js +153 -69
  130. package/lib/utils/format-converter.js +356 -283
  131. package/lib/utils/git-utils.js +366 -294
  132. package/lib/utils/llm-cost-estimator.js +281 -0
  133. package/lib/utils/llm-detector.js +242 -78
  134. package/lib/utils/logger.js +246 -237
  135. package/lib/utils/messages.js +355 -0
  136. package/lib/utils/multi-repo-manager.js +364 -0
  137. package/lib/utils/profile-manager.js +380 -0
  138. package/lib/utils/safe-read.js +61 -0
  139. package/lib/utils/secret-redactor.js +61 -0
  140. package/lib/utils/snapshot-manager.js +679 -0
  141. package/lib/utils/template-manager.js +378 -0
  142. package/lib/utils/token-utils.js +201 -170
  143. package/lib/utils/tokenizer-adapter.js +421 -377
  144. package/lib/utils/updater.js +539 -506
  145. package/lib/watch/FileWatcher.js +12 -8
  146. package/lib/watch/IncrementalAnalyzer.js +7 -7
  147. package/lib/why/coupling.js +81 -0
  148. package/lib/why/errors.js +23 -0
  149. package/lib/why/git.js +169 -0
  150. package/lib/why/history.js +108 -0
  151. package/lib/why/index.js +720 -0
  152. package/lib/why/ownership.js +79 -0
  153. package/lib/why/paths.js +207 -0
  154. package/lib/why/references.js +259 -0
  155. package/lib/why/render.js +722 -0
  156. package/lib/why/search.js +358 -0
  157. package/lib/why/target.js +389 -0
  158. package/lib/wizards/config-generator.js +257 -0
  159. package/lib/wizards/project-detector.js +572 -0
  160. package/package.json +44 -10
  161. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  224. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -1,339 +1,799 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
 
3
3
  import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- ListResourcesRequestSchema,
7
- ReadResourceRequestSchema,
8
- ListResourceTemplatesRequestSchema,
9
- ListPromptsRequestSchema,
10
- GetPromptRequestSchema,
11
- } from "@modelcontextprotocol/sdk/types.js";
12
- import { z } from "zod";
13
- import { Analyzer } from "../../core/Analyzer.js";
14
- import { ContextBuilder } from "../../core/ContextBuilder.js";
15
- import { Scanner } from "../../core/Scanner.js";
16
- import { GitClient } from "../../integrations/git/GitClient.js";
17
- import { ResourceProvider } from "./resources.js";
18
- import { PromptProvider } from "./prompts.js";
19
- import path from "path";
20
- import fs from "fs";
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ ListResourcesRequestSchema,
7
+ ReadResourceRequestSchema,
8
+ ListResourceTemplatesRequestSchema,
9
+ ListPromptsRequestSchema,
10
+ GetPromptRequestSchema,
11
+ } from '@modelcontextprotocol/sdk/types.js';
12
+ import { Analyzer } from '../../core/Analyzer.js';
13
+ import { ContextBuilder } from '../../core/ContextBuilder.js';
14
+ import { Scanner } from '../../core/Scanner.js';
15
+ import { GitClient, isValidGitRef } from '../../integrations/git/GitClient.js';
16
+ import { renderRepoMap } from '../../map/index.js';
17
+ import { ResourceProvider } from './resources.js';
18
+ import { PromptProvider } from './prompts.js';
19
+ import { buildPrContext } from '../../pr/pr-context.js';
20
+ import { resolveRootDir, resolveSandboxPath } from './sandbox.js';
21
+ import { searchProject, DEFAULT_LIMIT, DEFAULT_MAX_TOKENS } from '../../search/bm25.js';
22
+ import { getSymbol } from '../../search/symbols.js';
23
+ import { countForModel } from '../../count/index.js';
24
+ import { explainHistory } from '../../why/index.js';
25
+ import { packContext, describePack, TOOL_DEFAULT_BUDGET } from '../../pack/index.js';
26
+ import {
27
+ loadPolicy,
28
+ guardExport,
29
+ PolicyConfigError,
30
+ auditLogFiles,
31
+ readAuditLog,
32
+ filterAuditEvents,
33
+ parseSince,
34
+ } from '../../policy/index.js';
35
+ import { askCodebase, DEFAULT_ASK_LIMIT } from '../../rag/ask.js';
36
+ import fs from 'fs';
37
+ import path from 'path';
38
+ import { createRequire } from 'module';
39
+
40
+ const { version } = createRequire(import.meta.url)('../../../package.json');
41
+
42
+ // Tools whose `path` may also name a single file
43
+ const FILE_PATH_TOOLS = new Set(['list_methods']);
44
+
45
+ const DEFAULT_AUDIT_LIMIT = 50;
46
+ // Characters of each ask_codebase snippet (the index stores whole files)
47
+ const ASK_SNIPPET_CHARS = 2000;
21
48
 
22
49
  class MCPServer {
23
- constructor() {
24
- this.server = new Server(
25
- {
26
- name: "ctxman",
27
- version: "3.1.0",
50
+ /**
51
+ * @param {Object} [options]
52
+ * @param {string} [options.root] - Confine every tool and prompt path to this directory
53
+ */
54
+ constructor(options = {}) {
55
+ this.root = options.root ? resolveRootDir(options.root) : null;
56
+ this.server = new Server(
57
+ {
58
+ name: 'ctxman',
59
+ version,
60
+ },
61
+ {
62
+ // No subscribe or listChanged: the server has no subscription handler and sends no
63
+ // change notifications, so clients must not wait for them
64
+ capabilities: {
65
+ tools: {},
66
+ resources: {},
67
+ prompts: {},
68
+ },
69
+ }
70
+ );
71
+
72
+ this.analyzer = new Analyzer();
73
+ this.contextBuilder = new ContextBuilder();
74
+ this.resourceProvider = new ResourceProvider(this.root ?? undefined);
75
+ this.promptProvider = new PromptProvider(this.analyzer, this.contextBuilder);
76
+ this.setupHandlers();
77
+ this.setupErrorHandling();
78
+ }
79
+
80
+ setupHandlers() {
81
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
82
+ return {
83
+ tools: [
84
+ {
85
+ name: 'analyze_codebase',
86
+ description: 'Analyze the codebase and return statistics and token counts',
87
+ inputSchema: {
88
+ type: 'object',
89
+ properties: {
90
+ path: {
91
+ type: 'string',
92
+ description: 'Absolute path to the project root',
93
+ },
94
+ },
95
+ required: ['path'],
28
96
  },
29
- {
30
- capabilities: {
31
- tools: {},
32
- resources: {
33
- subscribe: true,
34
- listChanged: true
35
- },
36
- prompts: {
37
- listChanged: true
38
- },
97
+ },
98
+ {
99
+ name: 'generate_context',
100
+ description: 'Generate optimized context for LLMs from the codebase',
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: {
104
+ path: {
105
+ type: 'string',
106
+ description: 'Absolute path to the project root',
39
107
  },
40
- }
41
- );
42
-
43
- this.analyzer = new Analyzer();
44
- this.contextBuilder = new ContextBuilder();
45
- this.resourceProvider = new ResourceProvider();
46
- this.promptProvider = new PromptProvider(this.analyzer, this.contextBuilder);
47
- this.setupHandlers();
48
- this.setupErrorHandling();
49
- }
108
+ template: {
109
+ type: 'string',
110
+ description: 'Context template (bug-fix, feature, etc.)',
111
+ enum: ['bug-fix', 'feature', 'review', 'refactor'],
112
+ },
113
+ maxTokens: {
114
+ type: 'number',
115
+ description: 'Maximum tokens for the context',
116
+ },
117
+ },
118
+ required: ['path'],
119
+ },
120
+ },
121
+ {
122
+ name: 'git_diff',
123
+ description: 'Analyze git changes and return diff context',
124
+ inputSchema: {
125
+ type: 'object',
126
+ properties: {
127
+ path: { type: 'string', description: 'Project root' },
128
+ branch: {
129
+ type: 'string',
130
+ description: 'Branch to compare against (default: main)',
131
+ },
132
+ },
133
+ required: ['path'],
134
+ },
135
+ },
136
+ {
137
+ name: 'search_code',
138
+ description:
139
+ 'Search source lines by regular expression (default) or plain substring ("semantic" is currently a substring match). "ranked" returns the methods/functions (or 40-line windows) that best match the query words by BM25, with their source.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ path: { type: 'string', description: 'Project root' },
144
+ query: { type: 'string', description: 'Search query' },
145
+ type: { type: 'string', enum: ['regex', 'semantic', 'ranked'], default: 'regex' },
146
+ limit: {
147
+ type: 'number',
148
+ description: `ranked: maximum number of chunks (default ${DEFAULT_LIMIT})`,
149
+ },
150
+ maxTokens: {
151
+ type: 'number',
152
+ description: `ranked: token budget for the returned source (default ${DEFAULT_MAX_TOKENS})`,
153
+ },
154
+ redact: {
155
+ type: 'boolean',
156
+ description: 'ranked: redact secrets in the returned source (default true)',
157
+ },
158
+ },
159
+ required: ['path', 'query'],
160
+ },
161
+ },
162
+ {
163
+ name: 'list_methods',
164
+ description: 'List all methods/functions in a given file or project',
165
+ inputSchema: {
166
+ type: 'object',
167
+ properties: {
168
+ path: {
169
+ type: 'string',
170
+ description: 'Absolute path to the project root or a specific file',
171
+ },
172
+ file: {
173
+ type: 'string',
174
+ description: 'Optional: Relative path to a specific file within the project',
175
+ },
176
+ },
177
+ required: ['path'],
178
+ },
179
+ },
180
+ {
181
+ name: 'repo_map',
182
+ description:
183
+ 'Ranked repository map: the most important files (PageRank over the import graph, git change frequency, symbol references) with one line per function signature, trimmed to a token budget',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ path: { type: 'string', description: 'Absolute path to the project root' },
188
+ tokens: {
189
+ type: 'number',
190
+ description: 'Token budget for the map (default 2048)',
191
+ },
192
+ focus: {
193
+ type: 'array',
194
+ items: { type: 'string' },
195
+ description:
196
+ 'Project-relative files or folders to rank higher, with the files next to them in the import graph',
197
+ },
198
+ },
199
+ required: ['path'],
200
+ },
201
+ },
202
+ {
203
+ name: 'pr_context',
204
+ description:
205
+ 'Review context for a branch: summary, commit log, changed files, full source of changed methods, unified diff and related tests (secrets redacted)',
206
+ inputSchema: {
207
+ type: 'object',
208
+ properties: {
209
+ path: { type: 'string', description: 'Project directory inside a git work tree' },
210
+ base: {
211
+ type: 'string',
212
+ description:
213
+ 'Base branch or commit (default: origin/HEAD, else main, else master)',
214
+ },
215
+ head: { type: 'string', description: 'Head branch or commit (default: HEAD)' },
216
+ budget: { type: 'number', description: 'Maximum tokens of the result' },
217
+ style: { type: 'string', enum: ['markdown', 'xml', 'json'], default: 'markdown' },
218
+ },
219
+ required: ['path'],
220
+ },
221
+ },
222
+ {
223
+ name: 'get_symbol',
224
+ description:
225
+ 'Full source of a method, function or class by name, with file and line range. Exact matches first, then case-insensitive; similar names are suggested when nothing matches. "Class.method" narrows to methods of a class.',
226
+ inputSchema: {
227
+ type: 'object',
228
+ properties: {
229
+ path: { type: 'string', description: 'Project root' },
230
+ name: { type: 'string', description: 'Symbol name, or Class.method' },
231
+ file: {
232
+ type: 'string',
233
+ description: 'Optional: only look in this project-relative file',
234
+ },
235
+ redact: {
236
+ type: 'boolean',
237
+ description: 'Redact secrets in the source (default true)',
238
+ },
239
+ },
240
+ required: ['path', 'name'],
241
+ },
242
+ },
243
+ {
244
+ name: 'count_tokens',
245
+ description:
246
+ "Token count of a project (or of a text) for a model, with the model's context window and whether it fits. Exact for OpenAI models; approximate for models with their own tokenizer (Claude, Gemini, ...). Nothing leaves the machine; for Claude's exact count run `ctxman count --model <id> --api`",
247
+ inputSchema: {
248
+ type: 'object',
249
+ properties: {
250
+ path: {
251
+ type: 'string',
252
+ description: 'Project root (ignored when text is given)',
253
+ },
254
+ text: { type: 'string', description: 'Count this text instead of a project' },
255
+ model: {
256
+ type: 'string',
257
+ description: 'Model id from `ctxman --list-llms` (default: claude-sonnet-5-5)',
258
+ },
259
+ top: {
260
+ type: 'number',
261
+ description: 'How many of the largest files to list (default 10)',
262
+ },
263
+ },
264
+ },
265
+ },
266
+ {
267
+ name: 'explain_history',
268
+ description:
269
+ 'Explain why code looks the way it does, from the local git history (offline): commits that changed a file, symbol or line range with their messages, referenced issues/PRs/tickets, blame ownership, files that change together with it, related docs/ADRs/comments, TODO notes and tests. Returns Markdown.',
270
+ inputSchema: {
271
+ type: 'object',
272
+ properties: {
273
+ path: {
274
+ type: 'string',
275
+ description: 'Absolute path to the project root (inside a git repository)',
276
+ },
277
+ target: {
278
+ type: 'string',
279
+ description:
280
+ 'File relative to path, optionally with a symbol or line range: "src/cart.js", "src/cart.js:total", "src/cart.js:Cart.total", "src/cart.js#L10-L40"',
281
+ },
282
+ limit: {
283
+ type: 'number',
284
+ description: 'Maximum number of commits (default 20)',
285
+ },
286
+ budget: {
287
+ type: 'number',
288
+ description:
289
+ 'Maximum tokens of the result (default 20000); patches, related text, tests, coupling details and older commits are dropped first',
290
+ },
291
+ since: {
292
+ type: 'string',
293
+ description: 'Only commits after this date ("2024-01-01", "6 months ago")',
294
+ },
295
+ patches: {
296
+ type: 'boolean',
297
+ description: "Include each commit's diff of the target (default false)",
298
+ },
299
+ },
300
+ required: ['path', 'target'],
301
+ },
302
+ },
303
+ {
304
+ name: 'pack_context',
305
+ description:
306
+ 'File contents of a project in one LLM-ready document (directory tree, then every file; Anthropic <documents> XML by default), honouring .gitignore/.contextignore/.contextinclude, with secrets redacted. Trimmed to a token budget (default 20000): the highest-ranked files first, the rest listed with their token counts.',
307
+ inputSchema: {
308
+ type: 'object',
309
+ properties: {
310
+ path: { type: 'string', description: 'Project root' },
311
+ include: {
312
+ type: 'array',
313
+ items: { type: 'string' },
314
+ description: 'Gitignore-style globs: only matching files (e.g. "src/**/*.ts")',
315
+ },
316
+ exclude: {
317
+ type: 'array',
318
+ items: { type: 'string' },
319
+ description: 'Gitignore-style globs: leave matching files out',
320
+ },
321
+ files: {
322
+ type: 'array',
323
+ items: { type: 'string' },
324
+ description: 'Project-relative files to pack instead of scanning the project',
325
+ },
326
+ style: {
327
+ type: 'string',
328
+ enum: ['xml', 'markdown', 'plain', 'json'],
329
+ default: 'xml',
330
+ },
331
+ compress: {
332
+ type: 'boolean',
333
+ description: 'Keep signatures, replace function bodies with a placeholder',
334
+ },
335
+ lineNumbers: { type: 'boolean', description: 'Number every line' },
336
+ tree: {
337
+ type: 'boolean',
338
+ description: 'Directory tree at the top (default true)',
339
+ },
340
+ instructions: {
341
+ type: 'string',
342
+ description: 'Text placed at the end, after the files',
343
+ },
344
+ budget: {
345
+ type: 'number',
346
+ description: `Maximum tokens of the document (default ${TOOL_DEFAULT_BUDGET})`,
347
+ },
348
+ targetModel: {
349
+ type: 'string',
350
+ description: "Count with this model's tokenizer encoding and report the fit",
351
+ },
352
+ },
353
+ required: ['path'],
354
+ },
355
+ },
356
+ {
357
+ name: 'check_policy',
358
+ description:
359
+ 'The effective export policy of a project (.ctxman/policy.json plus the organisation policy named by CTXMAN_POLICY): denied paths, redaction and token limits, allowed target models, AI-tool ignore files and audit logging. Reports whether the policy files are valid and, if not, every error.',
360
+ inputSchema: {
361
+ type: 'object',
362
+ properties: {
363
+ path: { type: 'string', description: 'Project root' },
364
+ },
365
+ required: ['path'],
366
+ },
367
+ },
368
+ {
369
+ name: 'audit_log',
370
+ description:
371
+ "Exports recorded in the project's audit log while the export policy enables audit: when, by whom, which command, where to, which files, token count and a SHA-256 of the exported text. Newest last.",
372
+ inputSchema: {
373
+ type: 'object',
374
+ properties: {
375
+ path: { type: 'string', description: 'Project root' },
376
+ since: {
377
+ type: 'string',
378
+ description:
379
+ 'Only exports at or after this date (2026-10-01), ISO time or age (30m, 12h, 7d, 2w)',
380
+ },
381
+ limit: {
382
+ type: 'number',
383
+ description: `The last N events (default ${DEFAULT_AUDIT_LIMIT}; 0 = all)`,
384
+ },
385
+ },
386
+ required: ['path'],
387
+ },
388
+ },
389
+ {
390
+ name: 'ask_codebase',
391
+ description:
392
+ 'Semantic search: the files whose content is closest in meaning to a question, from a local embedding index (.ctxman/self-knowledge, built on first use; the first call downloads the embedding model and can take minutes). Needs the optional @xenova/transformers and @lancedb/lancedb packages. For exact names or keywords, search_code is faster.',
393
+ inputSchema: {
394
+ type: 'object',
395
+ properties: {
396
+ path: { type: 'string', description: 'Project root' },
397
+ query: { type: 'string', description: 'The question, in natural language' },
398
+ limit: {
399
+ type: 'number',
400
+ description: `How many files (default ${DEFAULT_ASK_LIMIT})`,
401
+ },
402
+ },
403
+ required: ['path', 'query'],
404
+ },
405
+ },
406
+ ],
407
+ };
408
+ });
409
+
410
+ // Resource handlers
411
+ this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
412
+ const { cursor } = request.params || {};
413
+ return await this.resourceProvider.listResources(cursor);
414
+ });
415
+
416
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
417
+ const { uri } = request.params;
418
+ return await this.resourceProvider.readResource(uri);
419
+ });
420
+
421
+ this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
422
+ return {
423
+ resourceTemplates: this.resourceProvider.getResourceTemplates(),
424
+ };
425
+ });
426
+
427
+ // Prompt handlers
428
+ this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
429
+ const { cursor } = request.params || {};
430
+ return this.promptProvider.listPrompts(cursor);
431
+ });
432
+
433
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
434
+ const { name } = request.params;
435
+ let args = request.params.arguments;
436
+ // With a root, the paths prompts read must lie inside it too
437
+ if (this.root && args) {
438
+ args = { ...args };
439
+ if (args.projectPath !== undefined) {
440
+ args.projectPath = resolveSandboxPath(args.projectPath, {
441
+ root: this.root,
442
+ name: 'projectPath',
443
+ });
444
+ }
445
+ if (args.filePath !== undefined) {
446
+ args.filePath = resolveSandboxPath(args.filePath, {
447
+ root: this.root,
448
+ expect: 'file',
449
+ name: 'filePath',
450
+ });
451
+ }
452
+ }
453
+ return await this.promptProvider.getPrompt(name, args);
454
+ });
455
+
456
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
457
+ const { name } = request.params;
458
+ let args = request.params.arguments ?? {};
459
+
460
+ try {
461
+ // `path` must name an existing directory (inside the root, when there is one)
462
+ if (args.path !== undefined || this.root) {
463
+ args = {
464
+ ...args,
465
+ path: resolveSandboxPath(args.path, {
466
+ root: this.root,
467
+ expect: FILE_PATH_TOOLS.has(name) ? 'any' : 'directory',
468
+ }),
469
+ };
470
+ }
50
471
 
51
- setupHandlers() {
52
- this.server.setRequestHandler(ListToolsRequestSchema, async () => {
53
- return {
54
- tools: [
55
- {
56
- name: "analyze_codebase",
57
- description: "Analyze the codebase and return statistics and token counts",
58
- inputSchema: {
59
- type: "object",
60
- properties: {
61
- path: {
62
- type: "string",
63
- description: "Absolute path to the project root",
64
- },
65
- },
66
- required: ["path"],
67
- },
68
- },
69
- {
70
- name: "generate_context",
71
- description: "Generate optimized context for LLMs from the codebase",
72
- inputSchema: {
73
- type: "object",
74
- properties: {
75
- path: {
76
- type: "string",
77
- description: "Absolute path to the project root",
78
- },
79
- template: {
80
- type: "string",
81
- description: "Context template (bug-fix, feature, etc.)",
82
- enum: ["bug-fix", "feature", "review", "refactor"],
83
- },
84
- maxTokens: {
85
- type: "number",
86
- description: "Maximum tokens for the context",
87
- },
88
- },
89
- required: ["path"],
90
- },
91
- },
92
- {
93
- name: "git_diff",
94
- description: "Analyze git changes and return diff context",
95
- inputSchema: {
96
- type: "object",
97
- properties: {
98
- path: { type: "string", description: "Project root" },
99
- branch: { type: "string", description: "Branch to compare against (default: main)" }
100
- },
101
- required: ["path"]
102
- }
103
- },
104
- {
105
- name: "search_code",
106
- description: "Search for code using regex or semantic search",
107
- inputSchema: {
108
- type: "object",
109
- properties: {
110
- path: { type: "string", description: "Project root" },
111
- query: { type: "string", description: "Search query" },
112
- type: { type: "string", enum: ["regex", "semantic"], default: "regex" }
113
- },
114
- required: ["path", "query"]
115
- }
116
- },
117
- {
118
- name: "list_methods",
119
- description: "List all methods/functions in a given file or project",
120
- inputSchema: {
121
- type: "object",
122
- properties: {
123
- path: {
124
- type: "string",
125
- description: "Absolute path to the project root or a specific file"
126
- },
127
- file: {
128
- type: "string",
129
- description: "Optional: Relative path to a specific file within the project"
130
- }
131
- },
132
- required: ["path"]
133
- }
134
- },
135
- ],
136
- };
137
- });
472
+ switch (name) {
473
+ case 'analyze_codebase': {
474
+ const projectPath = args.path;
475
+
476
+ // Update resource provider project path
477
+ this.resourceProvider.projectPath = projectPath;
138
478
 
139
- // Resource handlers
140
- this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
141
- const { cursor } = request.params || {};
142
- return await this.resourceProvider.listResources(cursor);
143
- });
479
+ const scanner = new Scanner(projectPath);
480
+ const files = scanner.scan();
481
+ const analysis = await this.analyzer.analyze(files);
144
482
 
145
- this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
146
- const { uri } = request.params;
147
- return await this.resourceProvider.readResource(uri);
148
- });
483
+ // Cache analysis result as a resource
484
+ const analysisId = Date.now().toString();
485
+ this.resourceProvider.cacheAnalysis(analysisId, analysis);
149
486
 
150
- this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
151
487
  return {
152
- resourceTemplates: this.resourceProvider.getResourceTemplates()
488
+ content: [
489
+ {
490
+ type: 'text',
491
+ text: JSON.stringify(analysis, null, 2),
492
+ },
493
+ ],
153
494
  };
154
- });
495
+ }
496
+ case 'generate_context': {
497
+ const projectPath = args.path;
155
498
 
156
- // Prompt handlers
157
- this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
158
- const { cursor } = request.params || {};
159
- return this.promptProvider.listPrompts(cursor);
160
- });
499
+ // Update resource provider project path
500
+ this.resourceProvider.projectPath = projectPath;
161
501
 
162
- this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
163
- const { name, arguments: args } = request.params;
164
- return await this.promptProvider.getPrompt(name, args);
165
- });
502
+ const scanner = new Scanner(projectPath);
503
+ const files = scanner.scan();
504
+ const analysis = await this.analyzer.analyze(files);
166
505
 
167
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
168
- const { name, arguments: args } = request.params;
506
+ // Configure ContextBuilder based on args
507
+ const builderOptions = {
508
+ targetTokens: args.maxTokens,
509
+ useCase: args.template || 'custom',
510
+ };
511
+ const contextBuilder = new ContextBuilder(builderOptions);
512
+ const context = contextBuilder.build(analysis);
513
+
514
+ // Cache context as a resource
515
+ const templateName = args.template || 'custom';
516
+ const contextText = JSON.stringify(context, null, 2);
517
+ // Export policy: throws PolicyViolationError (an isError result) and logs the export
518
+ guardExport(loadPolicy(projectPath), {
519
+ command: 'mcp generate_context',
520
+ root: projectPath,
521
+ destination: 'mcp',
522
+ format: 'json',
523
+ files: Object.values(context.files || {}).flat(),
524
+ text: contextText,
525
+ });
526
+ this.resourceProvider.cacheContext(templateName, contextText);
169
527
 
528
+ return {
529
+ content: [
530
+ {
531
+ type: 'text',
532
+ text: contextText,
533
+ },
534
+ ],
535
+ };
536
+ }
537
+ case 'git_diff': {
538
+ const projectPath = args.path;
539
+ const gitClient = new GitClient(projectPath);
540
+ const branch = args.branch || 'main';
541
+
542
+ if (!isValidGitRef(branch)) {
543
+ return {
544
+ content: [
545
+ { type: 'text', text: `Invalid branch or ref: ${JSON.stringify(branch)}` },
546
+ ],
547
+ isError: true,
548
+ };
549
+ }
550
+
551
+ let diff;
170
552
  try {
171
- switch (name) {
172
- case "analyze_codebase": {
173
- const projectPath = args.path;
174
-
175
- // Update resource provider project path
176
- this.resourceProvider.projectPath = projectPath;
177
-
178
- const scanner = new Scanner(projectPath);
179
- const files = scanner.scan();
180
- const analysis = await this.analyzer.analyze(files);
181
-
182
- // Cache analysis result as a resource
183
- const analysisId = Date.now().toString();
184
- this.resourceProvider.cacheAnalysis(analysisId, analysis);
185
-
186
- return {
187
- content: [
188
- {
189
- type: "text",
190
- text: JSON.stringify(analysis, null, 2),
191
- },
192
- ],
193
- };
194
- }
195
- case "generate_context": {
196
- const projectPath = args.path;
197
-
198
- // Update resource provider project path
199
- this.resourceProvider.projectPath = projectPath;
200
-
201
- const scanner = new Scanner(projectPath);
202
- const files = scanner.scan();
203
- const analysis = await this.analyzer.analyze(files);
204
-
205
- // Configure ContextBuilder based on args
206
- const builderOptions = {
207
- targetTokens: args.maxTokens,
208
- useCase: args.template || "custom"
209
- };
210
- const contextBuilder = new ContextBuilder(builderOptions);
211
- const context = contextBuilder.build(analysis);
212
-
213
- // Cache context as a resource
214
- const templateName = args.template || "custom";
215
- const contextText = JSON.stringify(context, null, 2);
216
- this.resourceProvider.cacheContext(templateName, contextText);
217
-
218
- return {
219
- content: [
220
- {
221
- type: "text",
222
- text: contextText,
223
- }
224
- ]
225
- }
226
- }
227
- case "git_diff": {
228
- const projectPath = args.path;
229
- const gitClient = new GitClient(projectPath);
230
- const branch = args.branch || "main";
231
-
232
- let diff;
233
- try {
234
- diff = gitClient.exec(`diff ${branch}`);
235
- } catch (e) {
236
- diff = `Error executing git diff: ${e.message}`;
237
- }
238
-
239
- return {
240
- content: [{ type: "text", text: diff || "No changes detected." }]
241
- };
242
- }
243
- case "search_code": {
244
- const { path: projectPath, query, type } = args;
245
- const scanner = new Scanner(projectPath);
246
- const files = scanner.scan();
247
- const results = [];
248
-
249
- for (const file of files) {
250
- try {
251
- const content = fs.readFileSync(file.path, "utf-8");
252
- const lines = content.split("\n");
253
-
254
- lines.forEach((line, index) => {
255
- let match = false;
256
- if (type === "regex") {
257
- try {
258
- if (new RegExp(query).test(line)) match = true;
259
- } catch (e) { /* ignore invalid regex */ }
260
- } else {
261
- if (line.includes(query)) match = true;
262
- }
263
-
264
- if (match) {
265
- results.push({
266
- file: file.relativePath,
267
- line: index + 1,
268
- content: line.trim()
269
- });
270
- }
271
- });
272
- } catch (e) {
273
- // Skip read errors
274
- }
275
- }
276
-
277
- return {
278
- content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
279
- };
280
- }
281
- case "list_methods": {
282
- const projectPath = args.path;
283
- const scanner = new Scanner(projectPath);
284
- const files = scanner.scan();
285
-
286
- // Use Analyzer with methodLevel: true
287
- const methodAnalyzer = new Analyzer({ methodLevel: true });
288
- const analysis = await methodAnalyzer.analyze(files);
289
-
290
- const methods = [];
291
- if (analysis.files) {
292
- for (const file of analysis.files) {
293
- if (file.methods && file.methods.length > 0) {
294
- methods.push({
295
- file: file.relativePath,
296
- methods: file.methods
297
- });
298
- }
299
- }
300
- }
301
-
302
- return {
303
- content: [{ type: "text", text: JSON.stringify(methods, null, 2) }]
304
- };
305
- }
306
- default:
307
- throw new Error(`Unknown tool: ${name}`);
308
- }
309
- } catch (error) {
553
+ // With a root, keep the diff to that directory even if the repository is larger
554
+ diff = gitClient.exec(['diff', branch, '--', ...(this.root ? ['.'] : [])]);
555
+ } catch (e) {
556
+ return {
557
+ content: [{ type: 'text', text: `Error executing git diff: ${e.message}` }],
558
+ isError: true,
559
+ };
560
+ }
561
+
562
+ return {
563
+ content: [{ type: 'text', text: diff || 'No changes detected.' }],
564
+ };
565
+ }
566
+ case 'search_code': {
567
+ const { path: projectPath, query, type = 'regex' } = args;
568
+ if (type === 'ranked') {
569
+ const result = searchProject(projectPath, query, {
570
+ limit: args.limit ?? DEFAULT_LIMIT,
571
+ maxTokens: args.maxTokens ?? DEFAULT_MAX_TOKENS,
572
+ redact: args.redact !== false,
573
+ });
574
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
575
+ }
576
+ let pattern = null;
577
+ if (type === 'regex') {
578
+ try {
579
+ pattern = new RegExp(query);
580
+ } catch (e) {
310
581
  return {
311
- content: [
312
- {
313
- type: "text",
314
- text: `Error: ${error.message}`,
315
- },
316
- ],
317
- isError: true,
582
+ content: [{ type: 'text', text: `Invalid regular expression: ${e.message}` }],
583
+ isError: true,
318
584
  };
585
+ }
586
+ }
587
+ const scanner = new Scanner(projectPath);
588
+ const files = scanner.scan();
589
+ const results = [];
590
+
591
+ for (const file of files) {
592
+ try {
593
+ const content = fs.readFileSync(file.path, 'utf-8');
594
+ const lines = content.split('\n');
595
+
596
+ lines.forEach((line, index) => {
597
+ const match = pattern ? pattern.test(line) : line.includes(query);
598
+
599
+ if (match) {
600
+ results.push({
601
+ file: file.relativePath,
602
+ line: index + 1,
603
+ content: line.trim(),
604
+ });
605
+ }
606
+ });
607
+ } catch (_e) {
608
+ // Skip read errors
609
+ }
319
610
  }
320
- });
321
- }
322
611
 
323
- setupErrorHandling() {
324
- this.server.onerror = (error) => {
325
- console.error("[MCP Error]", error);
326
- };
327
- }
612
+ return {
613
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
614
+ };
615
+ }
616
+ case 'list_methods': {
617
+ let projectPath = args.path;
618
+ let file = args.file;
619
+ // `path` may name a single file instead of the project root
620
+ if (!file && fs.statSync(projectPath, { throwIfNoEntry: false })?.isFile()) {
621
+ file = path.basename(projectPath);
622
+ projectPath = path.dirname(projectPath);
623
+ }
624
+ const scanner = new Scanner(projectPath);
625
+ // Optional `file`: one project-relative file instead of the whole project
626
+ const target = file ? path.normalize(file) : null;
627
+ const files = scanner
628
+ .scan()
629
+ .filter((file) => !target || path.normalize(file.relativePath) === target);
630
+ if (target && files.length === 0) {
631
+ return {
632
+ content: [{ type: 'text', text: `File not found in project: ${file}` }],
633
+ isError: true,
634
+ };
635
+ }
328
636
 
329
- async start(transport) {
330
- if (!transport) {
331
- const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
332
- transport = new StdioServerTransport();
637
+ // Use Analyzer with methodLevel: true
638
+ const methodAnalyzer = new Analyzer({ methodLevel: true });
639
+ const analysis = await methodAnalyzer.analyze(files);
640
+
641
+ const methods = [];
642
+ if (analysis.files) {
643
+ for (const file of analysis.files) {
644
+ if (file.methods && file.methods.length > 0) {
645
+ methods.push({
646
+ file: file.relativePath,
647
+ methods: file.methods,
648
+ });
649
+ }
650
+ }
651
+ }
652
+
653
+ return {
654
+ content: [{ type: 'text', text: JSON.stringify(methods, null, 2) }],
655
+ };
656
+ }
657
+ case 'repo_map': {
658
+ const map = renderRepoMap(args.path, {
659
+ tokens: args.tokens,
660
+ focus: args.focus,
661
+ });
662
+ const notes = map.warnings.map((warning) => `Note: ${warning}`).join('\n');
663
+ return {
664
+ content: [
665
+ {
666
+ type: 'text',
667
+ text: [map.output.trimEnd(), notes].filter(Boolean).join('\n\n') || 'Empty map.',
668
+ },
669
+ ],
670
+ };
671
+ }
672
+ case 'pr_context': {
673
+ const result = await buildPrContext(args.path, {
674
+ base: args.base,
675
+ head: args.head,
676
+ budget: args.budget == null ? undefined : Math.floor(args.budget),
677
+ style: args.style,
678
+ });
679
+ return { content: [{ type: 'text', text: result.output }] };
680
+ }
681
+ case 'get_symbol': {
682
+ const result = getSymbol(args.path, args.name, {
683
+ file: args.file,
684
+ redact: args.redact !== false,
685
+ });
686
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
687
+ }
688
+ case 'count_tokens': {
689
+ if (args.text === undefined && args.path === undefined) {
690
+ throw new Error('Give a project path or a text to count');
691
+ }
692
+ const { largestFiles, ...summary } = await countForModel(args.path, {
693
+ model: args.model,
694
+ text: args.text,
695
+ });
696
+ const top = Number.isInteger(args.top) && args.top >= 0 ? args.top : 10;
697
+ const result = { ...summary, largestFiles: largestFiles.slice(0, top) };
698
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
699
+ }
700
+ case 'explain_history': {
701
+ const whole = (value) => (typeof value === 'number' ? Math.floor(value) : value);
702
+ const result = await explainHistory(args.path, args.target, {
703
+ limit: whole(args.limit),
704
+ budget: whole(args.budget ?? 20000),
705
+ since: args.since,
706
+ patches: args.patches === true,
707
+ });
708
+ return { content: [{ type: 'text', text: result.text }] };
709
+ }
710
+ case 'pack_context': {
711
+ const result = await packContext(args.path, {
712
+ include: args.include,
713
+ exclude: args.exclude,
714
+ // Relative to the project; packContext rejects paths that leave it
715
+ files: args.files,
716
+ cwd: args.path,
717
+ style: args.style,
718
+ compress: args.compress,
719
+ lineNumbers: args.lineNumbers,
720
+ tree: args.tree,
721
+ instructions: args.instructions,
722
+ budget: args.budget == null ? TOOL_DEFAULT_BUDGET : Math.floor(args.budget),
723
+ targetModel: args.targetModel,
724
+ });
725
+ return {
726
+ content: [
727
+ { type: 'text', text: result.text },
728
+ { type: 'text', text: describePack(result) },
729
+ ],
730
+ };
731
+ }
732
+ case 'check_policy': {
733
+ let result;
734
+ try {
735
+ result = { valid: true, ...loadPolicy(args.path).toJSON() };
736
+ } catch (error) {
737
+ if (!(error instanceof PolicyConfigError)) throw error;
738
+ result = { valid: false, file: error.file, errors: error.errors };
739
+ }
740
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
741
+ }
742
+ case 'audit_log': {
743
+ const since = args.since === undefined ? null : parseSince(String(args.since));
744
+ const limit =
745
+ Number.isInteger(args.limit) && args.limit >= 0 ? args.limit : DEFAULT_AUDIT_LIMIT;
746
+ // Only the policy's own logs: a tool argument never names a file to read
747
+ const { files, error } = auditLogFiles(args.path);
748
+ const { events, skipped, missing } = readAuditLog(files);
749
+ const matching = filterAuditEvents(events, { since });
750
+ const result = {
751
+ files: files.filter((file) => !missing.includes(file)),
752
+ total: matching.length,
753
+ events: filterAuditEvents(matching, { limit }),
754
+ skippedLines: skipped,
755
+ };
756
+ if (error) result.policyError = error.message;
757
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
758
+ }
759
+ case 'ask_codebase': {
760
+ const result = await askCodebase(args.path, args.query, {
761
+ limit: Number.isInteger(args.limit) ? args.limit : undefined,
762
+ maxChars: ASK_SNIPPET_CHARS,
763
+ });
764
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
765
+ }
766
+ default:
767
+ throw new Error(`Unknown tool: ${name}`);
333
768
  }
334
- await this.server.connect(transport);
335
- console.error("Ctxman MCP Server running");
769
+ } catch (error) {
770
+ return {
771
+ content: [
772
+ {
773
+ type: 'text',
774
+ text: `Error: ${error.message}`,
775
+ },
776
+ ],
777
+ isError: true,
778
+ };
779
+ }
780
+ });
781
+ }
782
+
783
+ setupErrorHandling() {
784
+ this.server.onerror = (error) => {
785
+ console.error('[MCP Error]', error);
786
+ };
787
+ }
788
+
789
+ async start(transport) {
790
+ if (!transport) {
791
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
792
+ transport = new StdioServerTransport();
336
793
  }
794
+ await this.server.connect(transport);
795
+ console.error('Ctxman MCP Server running');
796
+ }
337
797
  }
338
798
 
339
799
  export default MCPServer;