ctxman 3.0.0 → 3.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 (223) 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 +380 -136
  16. package/bin/cli.js +1880 -529
  17. package/bin/cm-ask.js +18 -13
  18. package/bin/cm-audit.js +124 -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 +683 -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 +675 -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 +113 -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 +294 -0
  93. package/lib/policy/enforce.js +125 -0
  94. package/lib/policy/ignore-files.js +169 -0
  95. package/lib/policy/index.js +52 -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/sources/FileDataSource.js +6 -6
  108. package/lib/rag/stores/BaseVectorStore.js +5 -5
  109. package/lib/rag/stores/ChromaDBStore.js +10 -10
  110. package/lib/rag/stores/LanceDBStore.js +29 -19
  111. package/lib/rag/stores/PineconeStore.js +11 -9
  112. package/lib/search/bm25.js +433 -0
  113. package/lib/search/code-units.js +201 -0
  114. package/lib/search/symbols.js +189 -0
  115. package/lib/ui/dashboard.js +120 -89
  116. package/lib/ui/index.js +1 -6
  117. package/lib/ui/init-wizard-ui.js +550 -0
  118. package/lib/ui/progress-bar.js +94 -88
  119. package/lib/ui/select-input.js +40 -32
  120. package/lib/ui/template-selector.js +99 -0
  121. package/lib/ui/wizard.js +374 -329
  122. package/lib/utils/claude-token-counter.js +151 -0
  123. package/lib/utils/cli-output.js +17 -0
  124. package/lib/utils/clipboard-utils.js +49 -50
  125. package/lib/utils/config-utils.js +81 -66
  126. package/lib/utils/context-versioning.js +532 -0
  127. package/lib/utils/error-handler.js +112 -112
  128. package/lib/utils/file-utils.js +153 -69
  129. package/lib/utils/format-converter.js +356 -283
  130. package/lib/utils/git-utils.js +366 -294
  131. package/lib/utils/llm-cost-estimator.js +281 -0
  132. package/lib/utils/llm-detector.js +242 -78
  133. package/lib/utils/logger.js +243 -237
  134. package/lib/utils/messages.js +355 -0
  135. package/lib/utils/multi-repo-manager.js +364 -0
  136. package/lib/utils/profile-manager.js +380 -0
  137. package/lib/utils/safe-read.js +61 -0
  138. package/lib/utils/secret-redactor.js +61 -0
  139. package/lib/utils/snapshot-manager.js +679 -0
  140. package/lib/utils/template-manager.js +378 -0
  141. package/lib/utils/token-utils.js +201 -170
  142. package/lib/utils/tokenizer-adapter.js +421 -377
  143. package/lib/utils/updater.js +539 -506
  144. package/lib/watch/FileWatcher.js +12 -8
  145. package/lib/watch/IncrementalAnalyzer.js +7 -7
  146. package/lib/why/coupling.js +81 -0
  147. package/lib/why/errors.js +23 -0
  148. package/lib/why/git.js +169 -0
  149. package/lib/why/history.js +108 -0
  150. package/lib/why/index.js +720 -0
  151. package/lib/why/ownership.js +79 -0
  152. package/lib/why/paths.js +207 -0
  153. package/lib/why/references.js +259 -0
  154. package/lib/why/render.js +722 -0
  155. package/lib/why/search.js +358 -0
  156. package/lib/why/target.js +389 -0
  157. package/lib/wizards/config-generator.js +255 -0
  158. package/lib/wizards/project-detector.js +571 -0
  159. package/package.json +44 -10
  160. package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
  161. package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
  162. package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
  163. package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
  164. package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
  165. package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
  166. package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
  167. package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
  168. package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
  169. package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
  170. package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
  171. package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
  172. package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
  173. package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
  174. package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
  175. package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
  176. package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
  177. package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
  178. package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
  179. package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
  180. package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
  181. package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
  182. package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
  183. package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
  184. package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
  185. package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
  186. package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
  187. package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
  188. package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
  189. package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
  190. package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
  191. package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
  192. package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
  193. package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
  194. package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
  195. package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
  196. package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
  197. package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
  198. package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
  199. package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
  200. package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
  201. package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
  202. package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
  203. package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
  204. package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
  205. package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
  206. package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
  207. package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
  208. package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
  209. package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
  210. package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
  211. package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
  212. package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
  213. package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
  214. package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
  215. package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
  216. package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
  217. package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
  218. package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
  219. package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
  220. package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
  221. package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
  222. package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
  223. package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
@@ -1,339 +1,702 @@
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 { loadPolicy, guardExport } from '../../policy/index.js';
27
+ import fs from 'fs';
28
+ import path from 'path';
29
+ import { createRequire } from 'module';
30
+
31
+ const { version } = createRequire(import.meta.url)('../../../package.json');
32
+
33
+ // Tools whose `path` may also name a single file
34
+ const FILE_PATH_TOOLS = new Set(['list_methods']);
21
35
 
22
36
  class MCPServer {
23
- constructor() {
24
- this.server = new Server(
25
- {
26
- name: "ctxman",
27
- version: "3.1.0",
37
+ /**
38
+ * @param {Object} [options]
39
+ * @param {string} [options.root] - Confine every tool and prompt path to this directory
40
+ */
41
+ constructor(options = {}) {
42
+ this.root = options.root ? resolveRootDir(options.root) : null;
43
+ this.server = new Server(
44
+ {
45
+ name: 'ctxman',
46
+ version,
47
+ },
48
+ {
49
+ // No subscribe or listChanged: the server has no subscription handler and sends no
50
+ // change notifications, so clients must not wait for them
51
+ capabilities: {
52
+ tools: {},
53
+ resources: {},
54
+ prompts: {},
55
+ },
56
+ }
57
+ );
58
+
59
+ this.analyzer = new Analyzer();
60
+ this.contextBuilder = new ContextBuilder();
61
+ this.resourceProvider = new ResourceProvider(this.root ?? undefined);
62
+ this.promptProvider = new PromptProvider(this.analyzer, this.contextBuilder);
63
+ this.setupHandlers();
64
+ this.setupErrorHandling();
65
+ }
66
+
67
+ setupHandlers() {
68
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
69
+ return {
70
+ tools: [
71
+ {
72
+ name: 'analyze_codebase',
73
+ description: 'Analyze the codebase and return statistics and token counts',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: {
77
+ path: {
78
+ type: 'string',
79
+ description: 'Absolute path to the project root',
80
+ },
81
+ },
82
+ required: ['path'],
28
83
  },
29
- {
30
- capabilities: {
31
- tools: {},
32
- resources: {
33
- subscribe: true,
34
- listChanged: true
35
- },
36
- prompts: {
37
- listChanged: true
38
- },
84
+ },
85
+ {
86
+ name: 'generate_context',
87
+ description: 'Generate optimized context for LLMs from the codebase',
88
+ inputSchema: {
89
+ type: 'object',
90
+ properties: {
91
+ path: {
92
+ type: 'string',
93
+ description: 'Absolute path to the project root',
39
94
  },
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
- }
95
+ template: {
96
+ type: 'string',
97
+ description: 'Context template (bug-fix, feature, etc.)',
98
+ enum: ['bug-fix', 'feature', 'review', 'refactor'],
99
+ },
100
+ maxTokens: {
101
+ type: 'number',
102
+ description: 'Maximum tokens for the context',
103
+ },
104
+ },
105
+ required: ['path'],
106
+ },
107
+ },
108
+ {
109
+ name: 'git_diff',
110
+ description: 'Analyze git changes and return diff context',
111
+ inputSchema: {
112
+ type: 'object',
113
+ properties: {
114
+ path: { type: 'string', description: 'Project root' },
115
+ branch: {
116
+ type: 'string',
117
+ description: 'Branch to compare against (default: main)',
118
+ },
119
+ },
120
+ required: ['path'],
121
+ },
122
+ },
123
+ {
124
+ name: 'search_code',
125
+ description:
126
+ '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.',
127
+ inputSchema: {
128
+ type: 'object',
129
+ properties: {
130
+ path: { type: 'string', description: 'Project root' },
131
+ query: { type: 'string', description: 'Search query' },
132
+ type: { type: 'string', enum: ['regex', 'semantic', 'ranked'], default: 'regex' },
133
+ limit: {
134
+ type: 'number',
135
+ description: `ranked: maximum number of chunks (default ${DEFAULT_LIMIT})`,
136
+ },
137
+ maxTokens: {
138
+ type: 'number',
139
+ description: `ranked: token budget for the returned source (default ${DEFAULT_MAX_TOKENS})`,
140
+ },
141
+ redact: {
142
+ type: 'boolean',
143
+ description: 'ranked: redact secrets in the returned source (default true)',
144
+ },
145
+ },
146
+ required: ['path', 'query'],
147
+ },
148
+ },
149
+ {
150
+ name: 'list_methods',
151
+ description: 'List all methods/functions in a given file or project',
152
+ inputSchema: {
153
+ type: 'object',
154
+ properties: {
155
+ path: {
156
+ type: 'string',
157
+ description: 'Absolute path to the project root or a specific file',
158
+ },
159
+ file: {
160
+ type: 'string',
161
+ description: 'Optional: Relative path to a specific file within the project',
162
+ },
163
+ },
164
+ required: ['path'],
165
+ },
166
+ },
167
+ {
168
+ name: 'repo_map',
169
+ description:
170
+ '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',
171
+ inputSchema: {
172
+ type: 'object',
173
+ properties: {
174
+ path: { type: 'string', description: 'Absolute path to the project root' },
175
+ tokens: {
176
+ type: 'number',
177
+ description: 'Token budget for the map (default 2048)',
178
+ },
179
+ focus: {
180
+ type: 'array',
181
+ items: { type: 'string' },
182
+ description:
183
+ 'Project-relative files or folders to rank higher, with the files next to them in the import graph',
184
+ },
185
+ },
186
+ required: ['path'],
187
+ },
188
+ },
189
+ {
190
+ name: 'pr_context',
191
+ description:
192
+ 'Review context for a branch: summary, commit log, changed files, full source of changed methods, unified diff and related tests (secrets redacted)',
193
+ inputSchema: {
194
+ type: 'object',
195
+ properties: {
196
+ path: { type: 'string', description: 'Project directory inside a git work tree' },
197
+ base: {
198
+ type: 'string',
199
+ description:
200
+ 'Base branch or commit (default: origin/HEAD, else main, else master)',
201
+ },
202
+ head: { type: 'string', description: 'Head branch or commit (default: HEAD)' },
203
+ budget: { type: 'number', description: 'Maximum tokens of the result' },
204
+ style: { type: 'string', enum: ['markdown', 'xml', 'json'], default: 'markdown' },
205
+ },
206
+ required: ['path'],
207
+ },
208
+ },
209
+ {
210
+ name: 'get_symbol',
211
+ description:
212
+ '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.',
213
+ inputSchema: {
214
+ type: 'object',
215
+ properties: {
216
+ path: { type: 'string', description: 'Project root' },
217
+ name: { type: 'string', description: 'Symbol name, or Class.method' },
218
+ file: {
219
+ type: 'string',
220
+ description: 'Optional: only look in this project-relative file',
221
+ },
222
+ redact: {
223
+ type: 'boolean',
224
+ description: 'Redact secrets in the source (default true)',
225
+ },
226
+ },
227
+ required: ['path', 'name'],
228
+ },
229
+ },
230
+ {
231
+ name: 'count_tokens',
232
+ description:
233
+ "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`",
234
+ inputSchema: {
235
+ type: 'object',
236
+ properties: {
237
+ path: {
238
+ type: 'string',
239
+ description: 'Project root (ignored when text is given)',
240
+ },
241
+ text: { type: 'string', description: 'Count this text instead of a project' },
242
+ model: {
243
+ type: 'string',
244
+ description: 'Model id from `ctxman --list-llms` (default: claude-sonnet-5-5)',
245
+ },
246
+ top: {
247
+ type: 'number',
248
+ description: 'How many of the largest files to list (default 10)',
249
+ },
250
+ },
251
+ },
252
+ },
253
+ {
254
+ name: 'explain_history',
255
+ description:
256
+ '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.',
257
+ inputSchema: {
258
+ type: 'object',
259
+ properties: {
260
+ path: {
261
+ type: 'string',
262
+ description: 'Absolute path to the project root (inside a git repository)',
263
+ },
264
+ target: {
265
+ type: 'string',
266
+ description:
267
+ '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"',
268
+ },
269
+ limit: {
270
+ type: 'number',
271
+ description: 'Maximum number of commits (default 20)',
272
+ },
273
+ budget: {
274
+ type: 'number',
275
+ description:
276
+ 'Maximum tokens of the result (default 20000); patches, related text, tests, coupling details and older commits are dropped first',
277
+ },
278
+ since: {
279
+ type: 'string',
280
+ description: 'Only commits after this date ("2024-01-01", "6 months ago")',
281
+ },
282
+ patches: {
283
+ type: 'boolean',
284
+ description: "Include each commit's diff of the target (default false)",
285
+ },
286
+ },
287
+ required: ['path', 'target'],
288
+ },
289
+ },
290
+ {
291
+ name: 'pack_context',
292
+ description:
293
+ '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.',
294
+ inputSchema: {
295
+ type: 'object',
296
+ properties: {
297
+ path: { type: 'string', description: 'Project root' },
298
+ include: {
299
+ type: 'array',
300
+ items: { type: 'string' },
301
+ description: 'Gitignore-style globs: only matching files (e.g. "src/**/*.ts")',
302
+ },
303
+ exclude: {
304
+ type: 'array',
305
+ items: { type: 'string' },
306
+ description: 'Gitignore-style globs: leave matching files out',
307
+ },
308
+ files: {
309
+ type: 'array',
310
+ items: { type: 'string' },
311
+ description: 'Project-relative files to pack instead of scanning the project',
312
+ },
313
+ style: {
314
+ type: 'string',
315
+ enum: ['xml', 'markdown', 'plain', 'json'],
316
+ default: 'xml',
317
+ },
318
+ compress: {
319
+ type: 'boolean',
320
+ description: 'Keep signatures, replace function bodies with a placeholder',
321
+ },
322
+ lineNumbers: { type: 'boolean', description: 'Number every line' },
323
+ tree: {
324
+ type: 'boolean',
325
+ description: 'Directory tree at the top (default true)',
326
+ },
327
+ instructions: {
328
+ type: 'string',
329
+ description: 'Text placed at the end, after the files',
330
+ },
331
+ budget: {
332
+ type: 'number',
333
+ description: `Maximum tokens of the document (default ${TOOL_DEFAULT_BUDGET})`,
334
+ },
335
+ targetModel: {
336
+ type: 'string',
337
+ description: "Count with this model's tokenizer encoding and report the fit",
338
+ },
339
+ },
340
+ required: ['path'],
341
+ },
342
+ },
343
+ ],
344
+ };
345
+ });
346
+
347
+ // Resource handlers
348
+ this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
349
+ const { cursor } = request.params || {};
350
+ return await this.resourceProvider.listResources(cursor);
351
+ });
352
+
353
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
354
+ const { uri } = request.params;
355
+ return await this.resourceProvider.readResource(uri);
356
+ });
357
+
358
+ this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
359
+ return {
360
+ resourceTemplates: this.resourceProvider.getResourceTemplates(),
361
+ };
362
+ });
363
+
364
+ // Prompt handlers
365
+ this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
366
+ const { cursor } = request.params || {};
367
+ return this.promptProvider.listPrompts(cursor);
368
+ });
369
+
370
+ this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
371
+ const { name } = request.params;
372
+ let args = request.params.arguments;
373
+ // With a root, the paths prompts read must lie inside it too
374
+ if (this.root && args) {
375
+ args = { ...args };
376
+ if (args.projectPath !== undefined) {
377
+ args.projectPath = resolveSandboxPath(args.projectPath, {
378
+ root: this.root,
379
+ name: 'projectPath',
380
+ });
381
+ }
382
+ if (args.filePath !== undefined) {
383
+ args.filePath = resolveSandboxPath(args.filePath, {
384
+ root: this.root,
385
+ expect: 'file',
386
+ name: 'filePath',
387
+ });
388
+ }
389
+ }
390
+ return await this.promptProvider.getPrompt(name, args);
391
+ });
392
+
393
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
394
+ const { name } = request.params;
395
+ let args = request.params.arguments ?? {};
396
+
397
+ try {
398
+ // `path` must name an existing directory (inside the root, when there is one)
399
+ if (args.path !== undefined || this.root) {
400
+ args = {
401
+ ...args,
402
+ path: resolveSandboxPath(args.path, {
403
+ root: this.root,
404
+ expect: FILE_PATH_TOOLS.has(name) ? 'any' : 'directory',
405
+ }),
406
+ };
407
+ }
50
408
 
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
- });
409
+ switch (name) {
410
+ case 'analyze_codebase': {
411
+ const projectPath = args.path;
412
+
413
+ // Update resource provider project path
414
+ this.resourceProvider.projectPath = projectPath;
138
415
 
139
- // Resource handlers
140
- this.server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
141
- const { cursor } = request.params || {};
142
- return await this.resourceProvider.listResources(cursor);
143
- });
416
+ const scanner = new Scanner(projectPath);
417
+ const files = scanner.scan();
418
+ const analysis = await this.analyzer.analyze(files);
144
419
 
145
- this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
146
- const { uri } = request.params;
147
- return await this.resourceProvider.readResource(uri);
148
- });
420
+ // Cache analysis result as a resource
421
+ const analysisId = Date.now().toString();
422
+ this.resourceProvider.cacheAnalysis(analysisId, analysis);
149
423
 
150
- this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
151
424
  return {
152
- resourceTemplates: this.resourceProvider.getResourceTemplates()
425
+ content: [
426
+ {
427
+ type: 'text',
428
+ text: JSON.stringify(analysis, null, 2),
429
+ },
430
+ ],
153
431
  };
154
- });
432
+ }
433
+ case 'generate_context': {
434
+ const projectPath = args.path;
155
435
 
156
- // Prompt handlers
157
- this.server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
158
- const { cursor } = request.params || {};
159
- return this.promptProvider.listPrompts(cursor);
160
- });
436
+ // Update resource provider project path
437
+ this.resourceProvider.projectPath = projectPath;
161
438
 
162
- this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
163
- const { name, arguments: args } = request.params;
164
- return await this.promptProvider.getPrompt(name, args);
165
- });
439
+ const scanner = new Scanner(projectPath);
440
+ const files = scanner.scan();
441
+ const analysis = await this.analyzer.analyze(files);
442
+
443
+ // Configure ContextBuilder based on args
444
+ const builderOptions = {
445
+ targetTokens: args.maxTokens,
446
+ useCase: args.template || 'custom',
447
+ };
448
+ const contextBuilder = new ContextBuilder(builderOptions);
449
+ const context = contextBuilder.build(analysis);
450
+
451
+ // Cache context as a resource
452
+ const templateName = args.template || 'custom';
453
+ const contextText = JSON.stringify(context, null, 2);
454
+ // Export policy: throws PolicyViolationError (an isError result) and logs the export
455
+ guardExport(loadPolicy(projectPath), {
456
+ command: 'mcp generate_context',
457
+ root: projectPath,
458
+ destination: 'mcp',
459
+ format: 'json',
460
+ files: Object.values(context.files || {}).flat(),
461
+ text: contextText,
462
+ });
463
+ this.resourceProvider.cacheContext(templateName, contextText);
166
464
 
167
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
168
- const { name, arguments: args } = request.params;
465
+ return {
466
+ content: [
467
+ {
468
+ type: 'text',
469
+ text: contextText,
470
+ },
471
+ ],
472
+ };
473
+ }
474
+ case 'git_diff': {
475
+ const projectPath = args.path;
476
+ const gitClient = new GitClient(projectPath);
477
+ const branch = args.branch || 'main';
478
+
479
+ if (!isValidGitRef(branch)) {
480
+ return {
481
+ content: [
482
+ { type: 'text', text: `Invalid branch or ref: ${JSON.stringify(branch)}` },
483
+ ],
484
+ isError: true,
485
+ };
486
+ }
169
487
 
488
+ let diff;
170
489
  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) {
490
+ // With a root, keep the diff to that directory even if the repository is larger
491
+ diff = gitClient.exec(['diff', branch, '--', ...(this.root ? ['.'] : [])]);
492
+ } catch (e) {
493
+ return {
494
+ content: [{ type: 'text', text: `Error executing git diff: ${e.message}` }],
495
+ isError: true,
496
+ };
497
+ }
498
+
499
+ return {
500
+ content: [{ type: 'text', text: diff || 'No changes detected.' }],
501
+ };
502
+ }
503
+ case 'search_code': {
504
+ const { path: projectPath, query, type = 'regex' } = args;
505
+ if (type === 'ranked') {
506
+ const result = searchProject(projectPath, query, {
507
+ limit: args.limit ?? DEFAULT_LIMIT,
508
+ maxTokens: args.maxTokens ?? DEFAULT_MAX_TOKENS,
509
+ redact: args.redact !== false,
510
+ });
511
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
512
+ }
513
+ let pattern = null;
514
+ if (type === 'regex') {
515
+ try {
516
+ pattern = new RegExp(query);
517
+ } catch (e) {
310
518
  return {
311
- content: [
312
- {
313
- type: "text",
314
- text: `Error: ${error.message}`,
315
- },
316
- ],
317
- isError: true,
519
+ content: [{ type: 'text', text: `Invalid regular expression: ${e.message}` }],
520
+ isError: true,
318
521
  };
522
+ }
523
+ }
524
+ const scanner = new Scanner(projectPath);
525
+ const files = scanner.scan();
526
+ const results = [];
527
+
528
+ for (const file of files) {
529
+ try {
530
+ const content = fs.readFileSync(file.path, 'utf-8');
531
+ const lines = content.split('\n');
532
+
533
+ lines.forEach((line, index) => {
534
+ const match = pattern ? pattern.test(line) : line.includes(query);
535
+
536
+ if (match) {
537
+ results.push({
538
+ file: file.relativePath,
539
+ line: index + 1,
540
+ content: line.trim(),
541
+ });
542
+ }
543
+ });
544
+ } catch (_e) {
545
+ // Skip read errors
546
+ }
319
547
  }
320
- });
321
- }
322
548
 
323
- setupErrorHandling() {
324
- this.server.onerror = (error) => {
325
- console.error("[MCP Error]", error);
326
- };
327
- }
549
+ return {
550
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
551
+ };
552
+ }
553
+ case 'list_methods': {
554
+ let projectPath = args.path;
555
+ let file = args.file;
556
+ // `path` may name a single file instead of the project root
557
+ if (!file && fs.statSync(projectPath, { throwIfNoEntry: false })?.isFile()) {
558
+ file = path.basename(projectPath);
559
+ projectPath = path.dirname(projectPath);
560
+ }
561
+ const scanner = new Scanner(projectPath);
562
+ // Optional `file`: one project-relative file instead of the whole project
563
+ const target = file ? path.normalize(file) : null;
564
+ const files = scanner
565
+ .scan()
566
+ .filter((file) => !target || path.normalize(file.relativePath) === target);
567
+ if (target && files.length === 0) {
568
+ return {
569
+ content: [{ type: 'text', text: `File not found in project: ${file}` }],
570
+ isError: true,
571
+ };
572
+ }
573
+
574
+ // Use Analyzer with methodLevel: true
575
+ const methodAnalyzer = new Analyzer({ methodLevel: true });
576
+ const analysis = await methodAnalyzer.analyze(files);
577
+
578
+ const methods = [];
579
+ if (analysis.files) {
580
+ for (const file of analysis.files) {
581
+ if (file.methods && file.methods.length > 0) {
582
+ methods.push({
583
+ file: file.relativePath,
584
+ methods: file.methods,
585
+ });
586
+ }
587
+ }
588
+ }
328
589
 
329
- async start(transport) {
330
- if (!transport) {
331
- const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
332
- transport = new StdioServerTransport();
590
+ return {
591
+ content: [{ type: 'text', text: JSON.stringify(methods, null, 2) }],
592
+ };
593
+ }
594
+ case 'repo_map': {
595
+ const map = renderRepoMap(args.path, {
596
+ tokens: args.tokens,
597
+ focus: args.focus,
598
+ });
599
+ const notes = map.warnings.map((warning) => `Note: ${warning}`).join('\n');
600
+ return {
601
+ content: [
602
+ {
603
+ type: 'text',
604
+ text: [map.output.trimEnd(), notes].filter(Boolean).join('\n\n') || 'Empty map.',
605
+ },
606
+ ],
607
+ };
608
+ }
609
+ case 'pr_context': {
610
+ const result = await buildPrContext(args.path, {
611
+ base: args.base,
612
+ head: args.head,
613
+ budget: args.budget == null ? undefined : Math.floor(args.budget),
614
+ style: args.style,
615
+ });
616
+ return { content: [{ type: 'text', text: result.output }] };
617
+ }
618
+ case 'get_symbol': {
619
+ const result = getSymbol(args.path, args.name, {
620
+ file: args.file,
621
+ redact: args.redact !== false,
622
+ });
623
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
624
+ }
625
+ case 'count_tokens': {
626
+ if (args.text === undefined && args.path === undefined) {
627
+ throw new Error('Give a project path or a text to count');
628
+ }
629
+ const { largestFiles, ...summary } = await countForModel(args.path, {
630
+ model: args.model,
631
+ text: args.text,
632
+ });
633
+ const top = Number.isInteger(args.top) && args.top >= 0 ? args.top : 10;
634
+ const result = { ...summary, largestFiles: largestFiles.slice(0, top) };
635
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
636
+ }
637
+ case 'explain_history': {
638
+ const whole = (value) => (typeof value === 'number' ? Math.floor(value) : value);
639
+ const result = await explainHistory(args.path, args.target, {
640
+ limit: whole(args.limit),
641
+ budget: whole(args.budget ?? 20000),
642
+ since: args.since,
643
+ patches: args.patches === true,
644
+ });
645
+ return { content: [{ type: 'text', text: result.text }] };
646
+ }
647
+ case 'pack_context': {
648
+ const result = await packContext(args.path, {
649
+ include: args.include,
650
+ exclude: args.exclude,
651
+ // Relative to the project; packContext rejects paths that leave it
652
+ files: args.files,
653
+ cwd: args.path,
654
+ style: args.style,
655
+ compress: args.compress,
656
+ lineNumbers: args.lineNumbers,
657
+ tree: args.tree,
658
+ instructions: args.instructions,
659
+ budget: args.budget == null ? TOOL_DEFAULT_BUDGET : Math.floor(args.budget),
660
+ targetModel: args.targetModel,
661
+ });
662
+ return {
663
+ content: [
664
+ { type: 'text', text: result.text },
665
+ { type: 'text', text: describePack(result) },
666
+ ],
667
+ };
668
+ }
669
+ default:
670
+ throw new Error(`Unknown tool: ${name}`);
333
671
  }
334
- await this.server.connect(transport);
335
- console.error("Ctxman MCP Server running");
672
+ } catch (error) {
673
+ return {
674
+ content: [
675
+ {
676
+ type: 'text',
677
+ text: `Error: ${error.message}`,
678
+ },
679
+ ],
680
+ isError: true,
681
+ };
682
+ }
683
+ });
684
+ }
685
+
686
+ setupErrorHandling() {
687
+ this.server.onerror = (error) => {
688
+ console.error('[MCP Error]', error);
689
+ };
690
+ }
691
+
692
+ async start(transport) {
693
+ if (!transport) {
694
+ const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js');
695
+ transport = new StdioServerTransport();
336
696
  }
697
+ await this.server.connect(transport);
698
+ console.error('Ctxman MCP Server running');
699
+ }
337
700
  }
338
701
 
339
702
  export default MCPServer;