@zuvia-software-solutions/code-mapper 1.4.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.
- package/README.md +215 -0
- package/dist/cli/ai-context.d.ts +19 -0
- package/dist/cli/ai-context.js +168 -0
- package/dist/cli/analyze.d.ts +7 -0
- package/dist/cli/analyze.js +325 -0
- package/dist/cli/augment.d.ts +7 -0
- package/dist/cli/augment.js +27 -0
- package/dist/cli/clean.d.ts +5 -0
- package/dist/cli/clean.js +56 -0
- package/dist/cli/eval-server.d.ts +25 -0
- package/dist/cli/eval-server.js +365 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +102 -0
- package/dist/cli/lazy-action.d.ts +6 -0
- package/dist/cli/lazy-action.js +19 -0
- package/dist/cli/list.d.ts +2 -0
- package/dist/cli/list.js +27 -0
- package/dist/cli/mcp.d.ts +8 -0
- package/dist/cli/mcp.js +35 -0
- package/dist/cli/refresh.d.ts +12 -0
- package/dist/cli/refresh.js +165 -0
- package/dist/cli/serve.d.ts +5 -0
- package/dist/cli/serve.js +8 -0
- package/dist/cli/setup.d.ts +6 -0
- package/dist/cli/setup.js +218 -0
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +33 -0
- package/dist/cli/tool.d.ts +28 -0
- package/dist/cli/tool.js +87 -0
- package/dist/config/ignore-service.d.ts +32 -0
- package/dist/config/ignore-service.js +282 -0
- package/dist/config/supported-languages.d.ts +23 -0
- package/dist/config/supported-languages.js +52 -0
- package/dist/core/augmentation/engine.d.ts +22 -0
- package/dist/core/augmentation/engine.js +232 -0
- package/dist/core/embeddings/embedder.d.ts +35 -0
- package/dist/core/embeddings/embedder.js +171 -0
- package/dist/core/embeddings/embedding-pipeline.d.ts +41 -0
- package/dist/core/embeddings/embedding-pipeline.js +402 -0
- package/dist/core/embeddings/index.d.ts +5 -0
- package/dist/core/embeddings/index.js +6 -0
- package/dist/core/embeddings/text-generator.d.ts +20 -0
- package/dist/core/embeddings/text-generator.js +159 -0
- package/dist/core/embeddings/types.d.ts +60 -0
- package/dist/core/embeddings/types.js +23 -0
- package/dist/core/graph/graph.d.ts +4 -0
- package/dist/core/graph/graph.js +65 -0
- package/dist/core/graph/types.d.ts +69 -0
- package/dist/core/graph/types.js +3 -0
- package/dist/core/incremental/child-process.d.ts +8 -0
- package/dist/core/incremental/child-process.js +649 -0
- package/dist/core/incremental/refresh-coordinator.d.ts +32 -0
- package/dist/core/incremental/refresh-coordinator.js +147 -0
- package/dist/core/incremental/types.d.ts +78 -0
- package/dist/core/incremental/types.js +153 -0
- package/dist/core/incremental/watcher.d.ts +63 -0
- package/dist/core/incremental/watcher.js +338 -0
- package/dist/core/ingestion/ast-cache.d.ts +12 -0
- package/dist/core/ingestion/ast-cache.js +34 -0
- package/dist/core/ingestion/call-processor.d.ts +34 -0
- package/dist/core/ingestion/call-processor.js +937 -0
- package/dist/core/ingestion/call-routing.d.ts +40 -0
- package/dist/core/ingestion/call-routing.js +97 -0
- package/dist/core/ingestion/cluster-enricher.d.ts +30 -0
- package/dist/core/ingestion/cluster-enricher.js +151 -0
- package/dist/core/ingestion/community-processor.d.ts +26 -0
- package/dist/core/ingestion/community-processor.js +272 -0
- package/dist/core/ingestion/constants.d.ts +5 -0
- package/dist/core/ingestion/constants.js +8 -0
- package/dist/core/ingestion/entry-point-scoring.d.ts +23 -0
- package/dist/core/ingestion/entry-point-scoring.js +317 -0
- package/dist/core/ingestion/export-detection.d.ts +11 -0
- package/dist/core/ingestion/export-detection.js +203 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +18 -0
- package/dist/core/ingestion/filesystem-walker.js +64 -0
- package/dist/core/ingestion/framework-detection.d.ts +42 -0
- package/dist/core/ingestion/framework-detection.js +405 -0
- package/dist/core/ingestion/heritage-processor.d.ts +15 -0
- package/dist/core/ingestion/heritage-processor.js +237 -0
- package/dist/core/ingestion/import-processor.d.ts +31 -0
- package/dist/core/ingestion/import-processor.js +416 -0
- package/dist/core/ingestion/language-config.d.ts +32 -0
- package/dist/core/ingestion/language-config.js +161 -0
- package/dist/core/ingestion/mro-processor.d.ts +32 -0
- package/dist/core/ingestion/mro-processor.js +343 -0
- package/dist/core/ingestion/named-binding-extraction.d.ts +51 -0
- package/dist/core/ingestion/named-binding-extraction.js +343 -0
- package/dist/core/ingestion/parsing-processor.d.ts +20 -0
- package/dist/core/ingestion/parsing-processor.js +282 -0
- package/dist/core/ingestion/pipeline.d.ts +3 -0
- package/dist/core/ingestion/pipeline.js +416 -0
- package/dist/core/ingestion/process-processor.d.ts +42 -0
- package/dist/core/ingestion/process-processor.js +357 -0
- package/dist/core/ingestion/resolution-context.d.ts +40 -0
- package/dist/core/ingestion/resolution-context.js +171 -0
- package/dist/core/ingestion/resolvers/csharp.d.ts +10 -0
- package/dist/core/ingestion/resolvers/csharp.js +101 -0
- package/dist/core/ingestion/resolvers/go.d.ts +8 -0
- package/dist/core/ingestion/resolvers/go.js +33 -0
- package/dist/core/ingestion/resolvers/index.d.ts +14 -0
- package/dist/core/ingestion/resolvers/index.js +10 -0
- package/dist/core/ingestion/resolvers/jvm.d.ts +9 -0
- package/dist/core/ingestion/resolvers/jvm.js +74 -0
- package/dist/core/ingestion/resolvers/php.d.ts +7 -0
- package/dist/core/ingestion/resolvers/php.js +30 -0
- package/dist/core/ingestion/resolvers/ruby.d.ts +9 -0
- package/dist/core/ingestion/resolvers/ruby.js +13 -0
- package/dist/core/ingestion/resolvers/rust.d.ts +5 -0
- package/dist/core/ingestion/resolvers/rust.js +62 -0
- package/dist/core/ingestion/resolvers/standard.d.ts +16 -0
- package/dist/core/ingestion/resolvers/standard.js +144 -0
- package/dist/core/ingestion/resolvers/utils.d.ts +18 -0
- package/dist/core/ingestion/resolvers/utils.js +113 -0
- package/dist/core/ingestion/structure-processor.d.ts +4 -0
- package/dist/core/ingestion/structure-processor.js +39 -0
- package/dist/core/ingestion/symbol-table.d.ts +34 -0
- package/dist/core/ingestion/symbol-table.js +48 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +20 -0
- package/dist/core/ingestion/tree-sitter-queries.js +691 -0
- package/dist/core/ingestion/type-env.d.ts +52 -0
- package/dist/core/ingestion/type-env.js +349 -0
- package/dist/core/ingestion/type-extractors/c-cpp.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/c-cpp.js +214 -0
- package/dist/core/ingestion/type-extractors/csharp.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/csharp.js +224 -0
- package/dist/core/ingestion/type-extractors/go.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/go.js +261 -0
- package/dist/core/ingestion/type-extractors/index.d.ts +20 -0
- package/dist/core/ingestion/type-extractors/index.js +30 -0
- package/dist/core/ingestion/type-extractors/jvm.d.ts +5 -0
- package/dist/core/ingestion/type-extractors/jvm.js +386 -0
- package/dist/core/ingestion/type-extractors/php.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/php.js +280 -0
- package/dist/core/ingestion/type-extractors/python.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/python.js +175 -0
- package/dist/core/ingestion/type-extractors/ruby.d.ts +12 -0
- package/dist/core/ingestion/type-extractors/ruby.js +218 -0
- package/dist/core/ingestion/type-extractors/rust.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/rust.js +290 -0
- package/dist/core/ingestion/type-extractors/shared.d.ts +81 -0
- package/dist/core/ingestion/type-extractors/shared.js +322 -0
- package/dist/core/ingestion/type-extractors/swift.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/swift.js +140 -0
- package/dist/core/ingestion/type-extractors/types.d.ts +111 -0
- package/dist/core/ingestion/type-extractors/types.js +4 -0
- package/dist/core/ingestion/type-extractors/typescript.d.ts +4 -0
- package/dist/core/ingestion/type-extractors/typescript.js +227 -0
- package/dist/core/ingestion/utils.d.ts +73 -0
- package/dist/core/ingestion/utils.js +992 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +99 -0
- package/dist/core/ingestion/workers/parse-worker.js +1055 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +15 -0
- package/dist/core/ingestion/workers/worker-pool.js +123 -0
- package/dist/core/lbug/csv-generator.d.ts +28 -0
- package/dist/core/lbug/csv-generator.js +355 -0
- package/dist/core/lbug/lbug-adapter.d.ts +96 -0
- package/dist/core/lbug/lbug-adapter.js +753 -0
- package/dist/core/lbug/schema.d.ts +46 -0
- package/dist/core/lbug/schema.js +402 -0
- package/dist/core/search/bm25-index.d.ts +20 -0
- package/dist/core/search/bm25-index.js +123 -0
- package/dist/core/search/hybrid-search.d.ts +32 -0
- package/dist/core/search/hybrid-search.js +131 -0
- package/dist/core/search/query-cache.d.ts +18 -0
- package/dist/core/search/query-cache.js +47 -0
- package/dist/core/search/query-expansion.d.ts +19 -0
- package/dist/core/search/query-expansion.js +75 -0
- package/dist/core/search/reranker.d.ts +29 -0
- package/dist/core/search/reranker.js +122 -0
- package/dist/core/search/types.d.ts +154 -0
- package/dist/core/search/types.js +51 -0
- package/dist/core/semantic/tsgo-service.d.ts +67 -0
- package/dist/core/semantic/tsgo-service.js +355 -0
- package/dist/core/tree-sitter/parser-loader.d.ts +12 -0
- package/dist/core/tree-sitter/parser-loader.js +71 -0
- package/dist/lib/memory-guard.d.ts +35 -0
- package/dist/lib/memory-guard.js +70 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +6 -0
- package/dist/mcp/compatible-stdio-transport.d.ts +32 -0
- package/dist/mcp/compatible-stdio-transport.js +209 -0
- package/dist/mcp/core/embedder.d.ts +24 -0
- package/dist/mcp/core/embedder.js +168 -0
- package/dist/mcp/core/lbug-adapter.d.ts +29 -0
- package/dist/mcp/core/lbug-adapter.js +330 -0
- package/dist/mcp/local/local-backend.d.ts +188 -0
- package/dist/mcp/local/local-backend.js +2759 -0
- package/dist/mcp/resources.d.ts +22 -0
- package/dist/mcp/resources.js +379 -0
- package/dist/mcp/server.d.ts +10 -0
- package/dist/mcp/server.js +217 -0
- package/dist/mcp/staleness.d.ts +10 -0
- package/dist/mcp/staleness.js +25 -0
- package/dist/mcp/tools.d.ts +21 -0
- package/dist/mcp/tools.js +202 -0
- package/dist/server/api.d.ts +5 -0
- package/dist/server/api.js +340 -0
- package/dist/server/mcp-http.d.ts +7 -0
- package/dist/server/mcp-http.js +95 -0
- package/dist/storage/git.d.ts +6 -0
- package/dist/storage/git.js +35 -0
- package/dist/storage/repo-manager.d.ts +87 -0
- package/dist/storage/repo-manager.js +249 -0
- package/dist/types/pipeline.d.ts +35 -0
- package/dist/types/pipeline.js +20 -0
- package/hooks/claude/code-mapper-hook.cjs +238 -0
- package/hooks/claude/pre-tool-use.sh +79 -0
- package/hooks/claude/session-start.sh +42 -0
- package/models/mlx-embedder.py +185 -0
- package/package.json +100 -0
- package/scripts/patch-tree-sitter-swift.cjs +74 -0
- package/vendor/leiden/index.cjs +355 -0
- package/vendor/leiden/utils.cjs +392 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** @file tools.ts
|
|
2
|
+
* @description MCP tool definitions exposed to external AI agents
|
|
3
|
+
* All tools support an optional `repo` parameter for multi-repo setups */
|
|
4
|
+
export interface ToolDefinition {
|
|
5
|
+
name: string;
|
|
6
|
+
description: string;
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object';
|
|
9
|
+
properties: Record<string, {
|
|
10
|
+
type: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
default?: any;
|
|
13
|
+
items?: {
|
|
14
|
+
type: string;
|
|
15
|
+
};
|
|
16
|
+
enum?: string[];
|
|
17
|
+
}>;
|
|
18
|
+
required: string[];
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export declare const CODE_MAPPER_TOOLS: ToolDefinition[];
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// code-mapper/src/mcp/tools.ts
|
|
2
|
+
/** @file tools.ts
|
|
3
|
+
* @description MCP tool definitions exposed to external AI agents
|
|
4
|
+
* All tools support an optional `repo` parameter for multi-repo setups */
|
|
5
|
+
export const CODE_MAPPER_TOOLS = [
|
|
6
|
+
{
|
|
7
|
+
name: 'list_repos',
|
|
8
|
+
description: `List all indexed repositories available to Code Mapper.
|
|
9
|
+
|
|
10
|
+
Returns each repo's name, path, indexed date, last commit, and stats.
|
|
11
|
+
|
|
12
|
+
WHEN TO USE: First step when multiple repos are indexed, or to discover available repos.
|
|
13
|
+
AFTER THIS: READ code-mapper://repo/{name}/context for the repo you want to work with.
|
|
14
|
+
|
|
15
|
+
When multiple repos are indexed, you MUST specify the "repo" parameter
|
|
16
|
+
on other tools (query, context, impact, etc.) to target the correct one.`,
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {},
|
|
20
|
+
required: [],
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'query',
|
|
25
|
+
description: `Query the code knowledge graph for execution flows related to a concept.
|
|
26
|
+
Returns processes (call chains) ranked by relevance, each with its symbols and file locations.
|
|
27
|
+
|
|
28
|
+
WHEN TO USE: Understanding how code works together. Use this when you need execution flows and relationships, not just file matches. Complements grep/IDE search.
|
|
29
|
+
AFTER THIS: Use context() on a specific symbol for 360-degree view (callers, callees, categorized refs).
|
|
30
|
+
|
|
31
|
+
Returns results grouped by process (execution flow):
|
|
32
|
+
- processes: ranked execution flows with relevance priority
|
|
33
|
+
- process_symbols: all symbols in those flows with file locations and module (functional area)
|
|
34
|
+
- definitions: standalone types/interfaces not in any process
|
|
35
|
+
|
|
36
|
+
Hybrid ranking: BM25 keyword + semantic vector search, ranked by Reciprocal Rank Fusion.`,
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
query: { type: 'string', description: 'Natural language or keyword search query' },
|
|
41
|
+
task_context: { type: 'string', description: 'What you are working on (e.g., "adding OAuth support"). Helps ranking.' },
|
|
42
|
+
goal: { type: 'string', description: 'What you want to find (e.g., "existing auth validation logic"). Helps ranking.' },
|
|
43
|
+
file_path: { type: 'string', description: 'Filter results to symbols in this file path (substring match)' },
|
|
44
|
+
limit: { type: 'number', description: 'Max processes to return (default: 5)', default: 5 },
|
|
45
|
+
max_symbols: { type: 'number', description: 'Max symbols per process (default: 10)', default: 10 },
|
|
46
|
+
include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
|
|
47
|
+
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
|
48
|
+
},
|
|
49
|
+
required: ['query'],
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'cypher',
|
|
54
|
+
description: `Execute Cypher query against the code knowledge graph.
|
|
55
|
+
|
|
56
|
+
WHEN TO USE: Complex structural queries that search/explore can't answer. READ code-mapper://repo/{name}/schema first for the full schema.
|
|
57
|
+
AFTER THIS: Use context() on result symbols for deeper context.
|
|
58
|
+
|
|
59
|
+
SCHEMA:
|
|
60
|
+
- Nodes: File, Folder, Function, Class, Interface, Method, CodeElement, Community, Process
|
|
61
|
+
- Multi-language nodes (use backticks): \`Struct\`, \`Enum\`, \`Trait\`, \`Impl\`, etc.
|
|
62
|
+
- All edges via single CodeRelation table with 'type' property
|
|
63
|
+
- Edge types: CONTAINS, DEFINES, CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES, MEMBER_OF, STEP_IN_PROCESS
|
|
64
|
+
- Edge properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)
|
|
65
|
+
|
|
66
|
+
EXAMPLES:
|
|
67
|
+
• Find callers of a function:
|
|
68
|
+
MATCH (a)-[:CodeRelation {type: 'CALLS'}]->(b:Function {name: "validateUser"}) RETURN a.name, a.filePath
|
|
69
|
+
|
|
70
|
+
• Find community members:
|
|
71
|
+
MATCH (f)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community) WHERE c.heuristicLabel = "Auth" RETURN f.name
|
|
72
|
+
|
|
73
|
+
• Trace a process:
|
|
74
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process) WHERE p.heuristicLabel = "UserLogin" RETURN s.name, r.step ORDER BY r.step
|
|
75
|
+
|
|
76
|
+
• Find all methods of a class:
|
|
77
|
+
MATCH (c:Class {name: "UserService"})-[r:CodeRelation {type: 'HAS_METHOD'}]->(m:Method) RETURN m.name, m.parameterCount, m.returnType
|
|
78
|
+
|
|
79
|
+
• Find method overrides (MRO resolution):
|
|
80
|
+
MATCH (winner:Method)-[r:CodeRelation {type: 'OVERRIDES'}]->(loser:Method) RETURN winner.name, winner.filePath, loser.filePath, r.reason
|
|
81
|
+
|
|
82
|
+
• Detect diamond inheritance:
|
|
83
|
+
MATCH (d:Class)-[:CodeRelation {type: 'EXTENDS'}]->(b1), (d)-[:CodeRelation {type: 'EXTENDS'}]->(b2), (b1)-[:CodeRelation {type: 'EXTENDS'}]->(a), (b2)-[:CodeRelation {type: 'EXTENDS'}]->(a) WHERE b1 <> b2 RETURN d.name, b1.name, b2.name, a.name
|
|
84
|
+
|
|
85
|
+
OUTPUT: Returns { markdown, row_count } — results formatted as a Markdown table for easy reading.
|
|
86
|
+
|
|
87
|
+
TIPS:
|
|
88
|
+
- All relationships use single CodeRelation table — filter with {type: 'CALLS'} etc.
|
|
89
|
+
- Community = auto-detected functional area (Leiden algorithm)
|
|
90
|
+
- Process = execution flow trace from entry point to terminal
|
|
91
|
+
- Use heuristicLabel (not label) for human-readable community/process names`,
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
query: { type: 'string', description: 'Cypher query to execute' },
|
|
96
|
+
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
|
97
|
+
},
|
|
98
|
+
required: ['query'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'context',
|
|
103
|
+
description: `360-degree view of a single code symbol.
|
|
104
|
+
Shows categorized incoming/outgoing references (calls, imports, extends, implements), process participation, and file location.
|
|
105
|
+
|
|
106
|
+
WHEN TO USE: After query() to understand a specific symbol in depth. When you need to know all callers, callees, and what execution flows a symbol participates in.
|
|
107
|
+
AFTER THIS: Use impact() if planning changes, or READ code-mapper://repo/{name}/process/{processName} for full execution trace.
|
|
108
|
+
|
|
109
|
+
Handles disambiguation: if multiple symbols share the same name, returns candidates for you to pick from. Use uid param for zero-ambiguity lookup from prior results.`,
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: 'object',
|
|
112
|
+
properties: {
|
|
113
|
+
name: { type: 'string', description: 'Symbol name (e.g., "validateUser", "AuthService")' },
|
|
114
|
+
names: { type: 'array', items: { type: 'string' }, description: 'Multiple symbol names for bulk lookup (returns compact context for each)' },
|
|
115
|
+
uid: { type: 'string', description: 'Direct symbol UID from prior tool results (zero-ambiguity lookup)' },
|
|
116
|
+
file_path: { type: 'string', description: 'File path to disambiguate common names' },
|
|
117
|
+
include_content: { type: 'boolean', description: 'Include full symbol source code (default: false)', default: false },
|
|
118
|
+
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
|
119
|
+
},
|
|
120
|
+
required: [],
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
name: 'detect_changes',
|
|
125
|
+
description: `Analyze uncommitted git changes and find affected execution flows.
|
|
126
|
+
Maps git diff hunks to indexed symbols, then traces which processes are impacted.
|
|
127
|
+
|
|
128
|
+
WHEN TO USE: Before committing — to understand what your changes affect. Pre-commit review, PR preparation.
|
|
129
|
+
AFTER THIS: Review affected processes. Use context() on high-risk symbols. READ code-mapper://repo/{name}/process/{name} for full traces.
|
|
130
|
+
|
|
131
|
+
Returns: changed symbols, affected processes, and a risk summary.`,
|
|
132
|
+
inputSchema: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
properties: {
|
|
135
|
+
scope: { type: 'string', description: 'What to analyze: "unstaged" (default), "staged", "all", or "compare"', enum: ['unstaged', 'staged', 'all', 'compare'], default: 'unstaged' },
|
|
136
|
+
base_ref: { type: 'string', description: 'Branch/commit for "compare" scope (e.g., "main")' },
|
|
137
|
+
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
|
138
|
+
},
|
|
139
|
+
required: [],
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: 'rename',
|
|
144
|
+
description: `Multi-file coordinated rename using the knowledge graph + text search.
|
|
145
|
+
Finds all references via graph (high confidence) and regex text search (lower confidence). Preview by default.
|
|
146
|
+
|
|
147
|
+
WHEN TO USE: Renaming a function, class, method, or variable across the codebase. Safer than find-and-replace.
|
|
148
|
+
AFTER THIS: Run detect_changes() to verify no unexpected side effects.
|
|
149
|
+
|
|
150
|
+
Each edit is tagged with confidence:
|
|
151
|
+
- "graph": found via knowledge graph relationships (high confidence, safe to accept)
|
|
152
|
+
- "text_search": found via regex text search (lower confidence, review carefully)`,
|
|
153
|
+
inputSchema: {
|
|
154
|
+
type: 'object',
|
|
155
|
+
properties: {
|
|
156
|
+
symbol_name: { type: 'string', description: 'Current symbol name to rename' },
|
|
157
|
+
symbol_uid: { type: 'string', description: 'Direct symbol UID from prior tool results (zero-ambiguity)' },
|
|
158
|
+
new_name: { type: 'string', description: 'The new name for the symbol' },
|
|
159
|
+
file_path: { type: 'string', description: 'File path to disambiguate common names' },
|
|
160
|
+
dry_run: { type: 'boolean', description: 'Preview edits without modifying files (default: true)', default: true },
|
|
161
|
+
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
|
162
|
+
},
|
|
163
|
+
required: ['new_name'],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'impact',
|
|
168
|
+
description: `Analyze the blast radius of changing a code symbol.
|
|
169
|
+
Returns affected symbols grouped by depth, plus risk assessment, affected execution flows, and affected modules.
|
|
170
|
+
|
|
171
|
+
WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
|
|
172
|
+
AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
|
|
173
|
+
|
|
174
|
+
Output includes:
|
|
175
|
+
- risk: LOW / MEDIUM / HIGH / CRITICAL
|
|
176
|
+
- summary: direct callers, processes affected, modules affected
|
|
177
|
+
- affected_processes: which execution flows break and at which step
|
|
178
|
+
- affected_modules: which functional areas are hit (direct vs indirect)
|
|
179
|
+
- byDepth: all affected symbols grouped by traversal depth
|
|
180
|
+
|
|
181
|
+
Depth groups:
|
|
182
|
+
- d=1: WILL BREAK (direct callers/importers)
|
|
183
|
+
- d=2: LIKELY AFFECTED (indirect)
|
|
184
|
+
- d=3: MAY NEED TESTING (transitive)
|
|
185
|
+
|
|
186
|
+
EdgeType: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES
|
|
187
|
+
Confidence: 1.0 = certain, <0.8 = fuzzy match`,
|
|
188
|
+
inputSchema: {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
target: { type: 'string', description: 'Name of function, class, or file to analyze' },
|
|
192
|
+
direction: { type: 'string', description: 'upstream (what depends on this) or downstream (what this depends on)' },
|
|
193
|
+
maxDepth: { type: 'number', description: 'Max relationship depth (default: 3)', default: 3 },
|
|
194
|
+
relationTypes: { type: 'array', items: { type: 'string' }, description: 'Filter: CALLS, IMPORTS, EXTENDS, IMPLEMENTS, HAS_METHOD, OVERRIDES (default: usage-based)' },
|
|
195
|
+
includeTests: { type: 'boolean', description: 'Include test files (default: false)' },
|
|
196
|
+
minConfidence: { type: 'number', description: 'Minimum confidence 0-1 (default: 0.7)' },
|
|
197
|
+
repo: { type: 'string', description: 'Repository name or path. Omit if only one repo is indexed.' },
|
|
198
|
+
},
|
|
199
|
+
required: ['target', 'direction'],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** @file api.ts
|
|
2
|
+
* @description REST API server for browser clients to query local .code-mapper/ indexes
|
|
3
|
+
* Also hosts MCP server over StreamableHTTP for remote AI tool access
|
|
4
|
+
* Binds to 127.0.0.1 by default with CORS restricted to localhost */
|
|
5
|
+
export declare const createServer: (port: number, host?: string) => Promise<void>;
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// code-mapper/src/server/api.ts
|
|
2
|
+
/** @file api.ts
|
|
3
|
+
* @description REST API server for browser clients to query local .code-mapper/ indexes
|
|
4
|
+
* Also hosts MCP server over StreamableHTTP for remote AI tool access
|
|
5
|
+
* Binds to 127.0.0.1 by default with CORS restricted to localhost */
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import cors from 'cors';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import { loadMeta, listRegisteredRepos } from '../storage/repo-manager.js';
|
|
11
|
+
import { executeQuery, closeLbug, withLbugDb } from '../core/lbug/lbug-adapter.js';
|
|
12
|
+
import { NODE_TABLES } from '../core/lbug/schema.js';
|
|
13
|
+
import { searchFTSFromLbug } from '../core/search/bm25-index.js';
|
|
14
|
+
import { hybridSearch } from '../core/search/hybrid-search.js';
|
|
15
|
+
// Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
|
|
16
|
+
// at server startup — crashes on unsupported Node ABI versions (#89)
|
|
17
|
+
import { LocalBackend } from '../mcp/local/local-backend.js';
|
|
18
|
+
import { mountMCPEndpoints } from './mcp-http.js';
|
|
19
|
+
const buildGraph = async () => {
|
|
20
|
+
const nodes = [];
|
|
21
|
+
for (const table of NODE_TABLES) {
|
|
22
|
+
try {
|
|
23
|
+
let query = '';
|
|
24
|
+
if (table === 'File') {
|
|
25
|
+
query = `MATCH (n:File) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.content AS content`;
|
|
26
|
+
}
|
|
27
|
+
else if (table === 'Folder') {
|
|
28
|
+
query = `MATCH (n:Folder) RETURN n.id AS id, n.name AS name, n.filePath AS filePath`;
|
|
29
|
+
}
|
|
30
|
+
else if (table === 'Community') {
|
|
31
|
+
query = `MATCH (n:Community) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.cohesion AS cohesion, n.symbolCount AS symbolCount`;
|
|
32
|
+
}
|
|
33
|
+
else if (table === 'Process') {
|
|
34
|
+
query = `MATCH (n:Process) RETURN n.id AS id, n.label AS label, n.heuristicLabel AS heuristicLabel, n.processType AS processType, n.stepCount AS stepCount, n.communities AS communities, n.entryPointId AS entryPointId, n.terminalId AS terminalId`;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
query = `MATCH (n:${table}) RETURN n.id AS id, n.name AS name, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine, n.content AS content`;
|
|
38
|
+
}
|
|
39
|
+
const rows = await executeQuery(query);
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
nodes.push({
|
|
42
|
+
id: row.id ?? row[0],
|
|
43
|
+
label: table,
|
|
44
|
+
properties: {
|
|
45
|
+
name: row.name ?? row.label ?? row[1],
|
|
46
|
+
filePath: row.filePath ?? row[2],
|
|
47
|
+
startLine: row.startLine,
|
|
48
|
+
endLine: row.endLine,
|
|
49
|
+
content: row.content,
|
|
50
|
+
heuristicLabel: row.heuristicLabel,
|
|
51
|
+
cohesion: row.cohesion,
|
|
52
|
+
symbolCount: row.symbolCount,
|
|
53
|
+
processType: row.processType,
|
|
54
|
+
stepCount: row.stepCount,
|
|
55
|
+
communities: row.communities,
|
|
56
|
+
entryPointId: row.entryPointId,
|
|
57
|
+
terminalId: row.terminalId,
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// ignore empty tables
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const relationships = [];
|
|
67
|
+
const relRows = await executeQuery(`MATCH (a)-[r:CodeRelation]->(b) RETURN a.id AS sourceId, b.id AS targetId, r.type AS type, r.confidence AS confidence, r.reason AS reason, r.step AS step`);
|
|
68
|
+
for (const row of relRows) {
|
|
69
|
+
relationships.push({
|
|
70
|
+
id: `${row.sourceId}_${row.type}_${row.targetId}`,
|
|
71
|
+
type: row.type,
|
|
72
|
+
sourceId: row.sourceId,
|
|
73
|
+
targetId: row.targetId,
|
|
74
|
+
confidence: row.confidence,
|
|
75
|
+
reason: row.reason,
|
|
76
|
+
step: row.step,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return { nodes, relationships };
|
|
80
|
+
};
|
|
81
|
+
const statusFromError = (err) => {
|
|
82
|
+
const msg = String(err?.message ?? '');
|
|
83
|
+
if (msg.includes('No indexed repositories') || msg.includes('not found'))
|
|
84
|
+
return 404;
|
|
85
|
+
if (msg.includes('Multiple repositories'))
|
|
86
|
+
return 400;
|
|
87
|
+
return 500;
|
|
88
|
+
};
|
|
89
|
+
const requestedRepo = (req) => {
|
|
90
|
+
const fromQuery = typeof req.query.repo === 'string' ? req.query.repo : undefined;
|
|
91
|
+
if (fromQuery)
|
|
92
|
+
return fromQuery;
|
|
93
|
+
if (req.body && typeof req.body === 'object' && typeof req.body.repo === 'string') {
|
|
94
|
+
return req.body.repo;
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
};
|
|
98
|
+
export const createServer = async (port, host = '127.0.0.1') => {
|
|
99
|
+
const app = express();
|
|
100
|
+
// CORS: localhost origins + deployed site only
|
|
101
|
+
// Non-browser requests (curl, server-to-server) have no origin and are allowed
|
|
102
|
+
app.use(cors({
|
|
103
|
+
origin: (origin, callback) => {
|
|
104
|
+
if (!origin
|
|
105
|
+
|| origin.startsWith('http://localhost:')
|
|
106
|
+
|| origin.startsWith('http://127.0.0.1:')
|
|
107
|
+
|| origin === 'https://code-mapper.vercel.app') {
|
|
108
|
+
callback(null, true);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
callback(new Error('Not allowed by CORS'));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
app.use(express.json({ limit: '10mb' }));
|
|
116
|
+
// Initialize MCP backend (multi-repo, shared across all MCP sessions)
|
|
117
|
+
const backend = new LocalBackend();
|
|
118
|
+
await backend.init();
|
|
119
|
+
const cleanupMcp = mountMCPEndpoints(app, backend);
|
|
120
|
+
// Resolve a repo by name from the global registry, or default to first
|
|
121
|
+
const resolveRepo = async (repoName) => {
|
|
122
|
+
const repos = await listRegisteredRepos();
|
|
123
|
+
if (repos.length === 0)
|
|
124
|
+
return null;
|
|
125
|
+
if (repoName)
|
|
126
|
+
return repos.find(r => r.name === repoName) || null;
|
|
127
|
+
return repos[0]; // default to first
|
|
128
|
+
};
|
|
129
|
+
// List all registered repos
|
|
130
|
+
app.get('/api/repos', async (_req, res) => {
|
|
131
|
+
try {
|
|
132
|
+
const repos = await listRegisteredRepos();
|
|
133
|
+
res.json(repos.map(r => ({
|
|
134
|
+
name: r.name, path: r.path, indexedAt: r.indexedAt,
|
|
135
|
+
lastCommit: r.lastCommit, stats: r.stats,
|
|
136
|
+
})));
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
res.status(500).json({ error: err.message || 'Failed to list repos' });
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
// Get repo info
|
|
143
|
+
app.get('/api/repo', async (req, res) => {
|
|
144
|
+
try {
|
|
145
|
+
const entry = await resolveRepo(requestedRepo(req));
|
|
146
|
+
if (!entry) {
|
|
147
|
+
res.status(404).json({ error: 'Repository not found. Run: code-mapper analyze' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const meta = await loadMeta(entry.storagePath);
|
|
151
|
+
res.json({
|
|
152
|
+
name: entry.name,
|
|
153
|
+
repoPath: entry.path,
|
|
154
|
+
indexedAt: meta?.indexedAt ?? entry.indexedAt,
|
|
155
|
+
stats: meta?.stats ?? entry.stats ?? {},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
res.status(500).json({ error: err.message || 'Failed to get repo info' });
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
// Get full graph
|
|
163
|
+
app.get('/api/graph', async (req, res) => {
|
|
164
|
+
try {
|
|
165
|
+
const entry = await resolveRepo(requestedRepo(req));
|
|
166
|
+
if (!entry) {
|
|
167
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
171
|
+
const graph = await withLbugDb(lbugPath, async () => buildGraph());
|
|
172
|
+
res.json(graph);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
res.status(500).json({ error: err.message || 'Failed to build graph' });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// Execute Cypher query
|
|
179
|
+
app.post('/api/query', async (req, res) => {
|
|
180
|
+
try {
|
|
181
|
+
const cypher = req.body.cypher;
|
|
182
|
+
if (!cypher) {
|
|
183
|
+
res.status(400).json({ error: 'Missing "cypher" in request body' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const entry = await resolveRepo(requestedRepo(req));
|
|
187
|
+
if (!entry) {
|
|
188
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
192
|
+
const result = await withLbugDb(lbugPath, () => executeQuery(cypher));
|
|
193
|
+
res.json({ result });
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
res.status(500).json({ error: err.message || 'Query failed' });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
// Search
|
|
200
|
+
app.post('/api/search', async (req, res) => {
|
|
201
|
+
try {
|
|
202
|
+
const query = (req.body.query ?? '').trim();
|
|
203
|
+
if (!query) {
|
|
204
|
+
res.status(400).json({ error: 'Missing "query" in request body' });
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const entry = await resolveRepo(requestedRepo(req));
|
|
208
|
+
if (!entry) {
|
|
209
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const lbugPath = path.join(entry.storagePath, 'lbug');
|
|
213
|
+
const parsedLimit = Number(req.body.limit ?? 10);
|
|
214
|
+
const limit = Number.isFinite(parsedLimit)
|
|
215
|
+
? Math.max(1, Math.min(100, Math.trunc(parsedLimit)))
|
|
216
|
+
: 10;
|
|
217
|
+
const results = await withLbugDb(lbugPath, async () => {
|
|
218
|
+
const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
|
|
219
|
+
if (isEmbedderReady()) {
|
|
220
|
+
const { semanticSearch } = await import('../core/embeddings/embedding-pipeline.js');
|
|
221
|
+
return hybridSearch(query, limit, executeQuery, semanticSearch);
|
|
222
|
+
}
|
|
223
|
+
// FTS-only fallback when embeddings aren't loaded
|
|
224
|
+
return searchFTSFromLbug(query, limit);
|
|
225
|
+
});
|
|
226
|
+
res.json({ results });
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
res.status(500).json({ error: err.message || 'Search failed' });
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
// Read file (with path traversal guard)
|
|
233
|
+
app.get('/api/file', async (req, res) => {
|
|
234
|
+
try {
|
|
235
|
+
const entry = await resolveRepo(requestedRepo(req));
|
|
236
|
+
if (!entry) {
|
|
237
|
+
res.status(404).json({ error: 'Repository not found' });
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const filePath = req.query.path;
|
|
241
|
+
if (!filePath) {
|
|
242
|
+
res.status(400).json({ error: 'Missing path' });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Prevent path traversal: resolve and verify path stays within repo root
|
|
246
|
+
const repoRoot = path.resolve(entry.path);
|
|
247
|
+
const fullPath = path.resolve(repoRoot, filePath);
|
|
248
|
+
if (!fullPath.startsWith(repoRoot + path.sep) && fullPath !== repoRoot) {
|
|
249
|
+
res.status(403).json({ error: 'Path traversal denied' });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
253
|
+
res.json({ content });
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
if (err.code === 'ENOENT') {
|
|
257
|
+
res.status(404).json({ error: 'File not found' });
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
res.status(500).json({ error: err.message || 'Failed to read file' });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
// List all processes
|
|
265
|
+
app.get('/api/processes', async (req, res) => {
|
|
266
|
+
try {
|
|
267
|
+
const result = await backend.queryProcesses(requestedRepo(req));
|
|
268
|
+
res.json(result);
|
|
269
|
+
}
|
|
270
|
+
catch (err) {
|
|
271
|
+
res.status(statusFromError(err)).json({ error: err.message || 'Failed to query processes' });
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
// Process detail
|
|
275
|
+
app.get('/api/process', async (req, res) => {
|
|
276
|
+
try {
|
|
277
|
+
const name = String(req.query.name ?? '').trim();
|
|
278
|
+
if (!name) {
|
|
279
|
+
res.status(400).json({ error: 'Missing "name" query parameter' });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const result = await backend.queryProcessDetail(name, requestedRepo(req));
|
|
283
|
+
if (result?.error) {
|
|
284
|
+
res.status(404).json({ error: result.error });
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
res.json(result);
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
res.status(statusFromError(err)).json({ error: err.message || 'Failed to query process detail' });
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
// List all clusters
|
|
294
|
+
app.get('/api/clusters', async (req, res) => {
|
|
295
|
+
try {
|
|
296
|
+
const result = await backend.queryClusters(requestedRepo(req));
|
|
297
|
+
res.json(result);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
res.status(statusFromError(err)).json({ error: err.message || 'Failed to query clusters' });
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
// Cluster detail
|
|
304
|
+
app.get('/api/cluster', async (req, res) => {
|
|
305
|
+
try {
|
|
306
|
+
const name = String(req.query.name ?? '').trim();
|
|
307
|
+
if (!name) {
|
|
308
|
+
res.status(400).json({ error: 'Missing "name" query parameter' });
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const result = await backend.queryClusterDetail(name, requestedRepo(req));
|
|
312
|
+
if (result?.error) {
|
|
313
|
+
res.status(404).json({ error: result.error });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
res.json(result);
|
|
317
|
+
}
|
|
318
|
+
catch (err) {
|
|
319
|
+
res.status(statusFromError(err)).json({ error: err.message || 'Failed to query cluster detail' });
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
// Global error handler
|
|
323
|
+
app.use((err, _req, res, _next) => {
|
|
324
|
+
console.error('Unhandled error:', err);
|
|
325
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
326
|
+
});
|
|
327
|
+
const server = app.listen(port, host, () => {
|
|
328
|
+
console.log(`Code Mapper server running on http://${host}:${port}`);
|
|
329
|
+
});
|
|
330
|
+
// Graceful shutdown
|
|
331
|
+
const shutdown = async () => {
|
|
332
|
+
server.close();
|
|
333
|
+
await cleanupMcp();
|
|
334
|
+
await closeLbug();
|
|
335
|
+
await backend.disconnect();
|
|
336
|
+
process.exit(0);
|
|
337
|
+
};
|
|
338
|
+
process.once('SIGINT', shutdown);
|
|
339
|
+
process.once('SIGTERM', shutdown);
|
|
340
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** @file mcp-http.ts
|
|
2
|
+
* @description Mounts Code Mapper MCP server on Express using StreamableHTTP transport
|
|
3
|
+
* Each client gets a stateful session; LocalBackend is shared (thread-safe)
|
|
4
|
+
* Sessions are evicted on close or after idle timeout */
|
|
5
|
+
import type { Express } from 'express';
|
|
6
|
+
import type { LocalBackend } from '../mcp/local/local-backend.js';
|
|
7
|
+
export declare function mountMCPEndpoints(app: Express, backend: LocalBackend): () => Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// code-mapper/src/server/mcp-http.ts
|
|
2
|
+
/** @file mcp-http.ts
|
|
3
|
+
* @description Mounts Code Mapper MCP server on Express using StreamableHTTP transport
|
|
4
|
+
* Each client gets a stateful session; LocalBackend is shared (thread-safe)
|
|
5
|
+
* Sessions are evicted on close or after idle timeout */
|
|
6
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
|
+
import { createMCPServer } from '../mcp/server.js';
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
/** Idle sessions are evicted after 30 minutes */
|
|
10
|
+
const SESSION_TTL_MS = 30 * 60 * 1000;
|
|
11
|
+
/** Cleanup sweep runs every 5 minutes */
|
|
12
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
|
13
|
+
export function mountMCPEndpoints(app, backend) {
|
|
14
|
+
const sessions = new Map();
|
|
15
|
+
// Periodic cleanup of idle sessions
|
|
16
|
+
const cleanupTimer = setInterval(() => {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
for (const [id, session] of sessions) {
|
|
19
|
+
if (now - session.lastActivity > SESSION_TTL_MS) {
|
|
20
|
+
try {
|
|
21
|
+
session.server.close();
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
sessions.delete(id);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}, CLEANUP_INTERVAL_MS);
|
|
28
|
+
if (cleanupTimer && typeof cleanupTimer === 'object' && 'unref' in cleanupTimer) {
|
|
29
|
+
cleanupTimer.unref();
|
|
30
|
+
}
|
|
31
|
+
const handleMcpRequest = async (req, res) => {
|
|
32
|
+
const sessionId = req.headers['mcp-session-id'];
|
|
33
|
+
if (sessionId && sessions.has(sessionId)) {
|
|
34
|
+
// Existing session — delegate to its transport
|
|
35
|
+
const session = sessions.get(sessionId);
|
|
36
|
+
session.lastActivity = Date.now();
|
|
37
|
+
await session.transport.handleRequest(req, res, req.body);
|
|
38
|
+
}
|
|
39
|
+
else if (sessionId) {
|
|
40
|
+
// Unknown/expired session ID (per MCP spec, tell client to re-initialize)
|
|
41
|
+
res.status(404).json({
|
|
42
|
+
jsonrpc: '2.0',
|
|
43
|
+
error: { code: -32001, message: 'Session not found. Re-initialize.' },
|
|
44
|
+
id: null,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
else if (req.method === 'POST') {
|
|
48
|
+
// No session ID — new client initializing
|
|
49
|
+
const transport = new StreamableHTTPServerTransport({
|
|
50
|
+
sessionIdGenerator: () => randomUUID(),
|
|
51
|
+
});
|
|
52
|
+
const server = createMCPServer(backend);
|
|
53
|
+
await server.connect(transport);
|
|
54
|
+
await transport.handleRequest(req, res, req.body);
|
|
55
|
+
if (transport.sessionId) {
|
|
56
|
+
sessions.set(transport.sessionId, { server, transport, lastActivity: Date.now() });
|
|
57
|
+
transport.onclose = () => {
|
|
58
|
+
sessions.delete(transport.sessionId);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
res.status(400).json({
|
|
64
|
+
jsonrpc: '2.0',
|
|
65
|
+
error: { code: -32000, message: 'No valid session. Send a POST to initialize.' },
|
|
66
|
+
id: null,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
app.all('/api/mcp', (req, res) => {
|
|
71
|
+
void handleMcpRequest(req, res).catch((err) => {
|
|
72
|
+
console.error('MCP HTTP request failed:', err);
|
|
73
|
+
if (res.headersSent)
|
|
74
|
+
return;
|
|
75
|
+
res.status(500).json({
|
|
76
|
+
jsonrpc: '2.0',
|
|
77
|
+
error: { code: -32000, message: 'Internal MCP server error' },
|
|
78
|
+
id: null,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
const cleanup = async () => {
|
|
83
|
+
clearInterval(cleanupTimer);
|
|
84
|
+
const closers = [...sessions.values()].map(async (session) => {
|
|
85
|
+
try {
|
|
86
|
+
await Promise.resolve(session.server.close());
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
});
|
|
90
|
+
sessions.clear();
|
|
91
|
+
await Promise.allSettled(closers);
|
|
92
|
+
};
|
|
93
|
+
console.log('MCP HTTP endpoints mounted at /api/mcp');
|
|
94
|
+
return cleanup;
|
|
95
|
+
}
|