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.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
package/lib/api/mcp/server.js
CHANGED
|
@@ -1,339 +1,799 @@
|
|
|
1
|
-
import { Server } from
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import { ResourceProvider } from
|
|
18
|
-
import { PromptProvider } from
|
|
19
|
-
import
|
|
20
|
-
import
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
const
|
|
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
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
});
|
|
502
|
+
const scanner = new Scanner(projectPath);
|
|
503
|
+
const files = scanner.scan();
|
|
504
|
+
const analysis = await this.analyzer.analyze(files);
|
|
166
505
|
|
|
167
|
-
|
|
168
|
-
const
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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;
|