lance-context 0.1.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +232 -23
  2. package/dist/__tests__/ast-chunker.test.d.ts +2 -0
  3. package/dist/__tests__/ast-chunker.test.d.ts.map +1 -0
  4. package/dist/__tests__/ast-chunker.test.js +307 -0
  5. package/dist/__tests__/ast-chunker.test.js.map +1 -0
  6. package/dist/__tests__/config.test.d.ts +2 -0
  7. package/dist/__tests__/config.test.d.ts.map +1 -0
  8. package/dist/__tests__/config.test.js +242 -0
  9. package/dist/__tests__/config.test.js.map +1 -0
  10. package/dist/__tests__/dashboard/beads.test.d.ts +2 -0
  11. package/dist/__tests__/dashboard/beads.test.d.ts.map +1 -0
  12. package/dist/__tests__/dashboard/beads.test.js +151 -0
  13. package/dist/__tests__/dashboard/beads.test.js.map +1 -0
  14. package/dist/__tests__/dashboard/index.test.d.ts +2 -0
  15. package/dist/__tests__/dashboard/index.test.d.ts.map +1 -0
  16. package/dist/__tests__/dashboard/index.test.js +116 -0
  17. package/dist/__tests__/dashboard/index.test.js.map +1 -0
  18. package/dist/__tests__/dashboard/routes.test.d.ts +2 -0
  19. package/dist/__tests__/dashboard/routes.test.d.ts.map +1 -0
  20. package/dist/__tests__/dashboard/routes.test.js +125 -0
  21. package/dist/__tests__/dashboard/routes.test.js.map +1 -0
  22. package/dist/__tests__/dashboard/server.test.d.ts +2 -0
  23. package/dist/__tests__/dashboard/server.test.d.ts.map +1 -0
  24. package/dist/__tests__/dashboard/server.test.js +75 -0
  25. package/dist/__tests__/dashboard/server.test.js.map +1 -0
  26. package/dist/__tests__/dashboard/state.test.d.ts +2 -0
  27. package/dist/__tests__/dashboard/state.test.d.ts.map +1 -0
  28. package/dist/__tests__/dashboard/state.test.js +124 -0
  29. package/dist/__tests__/dashboard/state.test.js.map +1 -0
  30. package/dist/__tests__/embeddings/factory.test.d.ts +2 -0
  31. package/dist/__tests__/embeddings/factory.test.d.ts.map +1 -0
  32. package/dist/__tests__/embeddings/factory.test.js +100 -0
  33. package/dist/__tests__/embeddings/factory.test.js.map +1 -0
  34. package/dist/__tests__/embeddings/jina.test.d.ts +2 -0
  35. package/dist/__tests__/embeddings/jina.test.d.ts.map +1 -0
  36. package/dist/__tests__/embeddings/jina.test.js +156 -0
  37. package/dist/__tests__/embeddings/jina.test.js.map +1 -0
  38. package/dist/__tests__/embeddings/ollama.test.d.ts +2 -0
  39. package/dist/__tests__/embeddings/ollama.test.d.ts.map +1 -0
  40. package/dist/__tests__/embeddings/ollama.test.js +172 -0
  41. package/dist/__tests__/embeddings/ollama.test.js.map +1 -0
  42. package/dist/__tests__/embeddings/rate-limiter.test.d.ts +2 -0
  43. package/dist/__tests__/embeddings/rate-limiter.test.d.ts.map +1 -0
  44. package/dist/__tests__/embeddings/rate-limiter.test.js +163 -0
  45. package/dist/__tests__/embeddings/rate-limiter.test.js.map +1 -0
  46. package/dist/__tests__/embeddings/retry.test.d.ts +2 -0
  47. package/dist/__tests__/embeddings/retry.test.d.ts.map +1 -0
  48. package/dist/__tests__/embeddings/retry.test.js +260 -0
  49. package/dist/__tests__/embeddings/retry.test.js.map +1 -0
  50. package/dist/__tests__/embeddings/types.test.d.ts +2 -0
  51. package/dist/__tests__/embeddings/types.test.d.ts.map +1 -0
  52. package/dist/__tests__/embeddings/types.test.js +31 -0
  53. package/dist/__tests__/embeddings/types.test.js.map +1 -0
  54. package/dist/__tests__/mocks/embedding-backend.mock.d.ts +10 -0
  55. package/dist/__tests__/mocks/embedding-backend.mock.d.ts.map +1 -0
  56. package/dist/__tests__/mocks/embedding-backend.mock.js +39 -0
  57. package/dist/__tests__/mocks/embedding-backend.mock.js.map +1 -0
  58. package/dist/__tests__/mocks/fetch.mock.d.ts +38 -0
  59. package/dist/__tests__/mocks/fetch.mock.d.ts.map +1 -0
  60. package/dist/__tests__/mocks/fetch.mock.js +74 -0
  61. package/dist/__tests__/mocks/fetch.mock.js.map +1 -0
  62. package/dist/__tests__/mocks/lancedb.mock.d.ts +38 -0
  63. package/dist/__tests__/mocks/lancedb.mock.d.ts.map +1 -0
  64. package/dist/__tests__/mocks/lancedb.mock.js +63 -0
  65. package/dist/__tests__/mocks/lancedb.mock.js.map +1 -0
  66. package/dist/__tests__/search/clustering.test.d.ts +2 -0
  67. package/dist/__tests__/search/clustering.test.d.ts.map +1 -0
  68. package/dist/__tests__/search/clustering.test.js +230 -0
  69. package/dist/__tests__/search/clustering.test.js.map +1 -0
  70. package/dist/__tests__/search/hybrid-search.test.d.ts +2 -0
  71. package/dist/__tests__/search/hybrid-search.test.d.ts.map +1 -0
  72. package/dist/__tests__/search/hybrid-search.test.js +186 -0
  73. package/dist/__tests__/search/hybrid-search.test.js.map +1 -0
  74. package/dist/__tests__/search/indexer.test.d.ts +2 -0
  75. package/dist/__tests__/search/indexer.test.d.ts.map +1 -0
  76. package/dist/__tests__/search/indexer.test.js +878 -0
  77. package/dist/__tests__/search/indexer.test.js.map +1 -0
  78. package/dist/__tests__/search/tree-sitter-chunker.test.d.ts +2 -0
  79. package/dist/__tests__/search/tree-sitter-chunker.test.d.ts.map +1 -0
  80. package/dist/__tests__/search/tree-sitter-chunker.test.js +228 -0
  81. package/dist/__tests__/search/tree-sitter-chunker.test.js.map +1 -0
  82. package/dist/__tests__/setup.d.ts +2 -0
  83. package/dist/__tests__/setup.d.ts.map +1 -0
  84. package/dist/__tests__/setup.js +11 -0
  85. package/dist/__tests__/setup.js.map +1 -0
  86. package/dist/__tests__/utils/concurrency.test.d.ts +2 -0
  87. package/dist/__tests__/utils/concurrency.test.d.ts.map +1 -0
  88. package/dist/__tests__/utils/concurrency.test.js +83 -0
  89. package/dist/__tests__/utils/concurrency.test.js.map +1 -0
  90. package/dist/__tests__/utils/errors.test.d.ts +2 -0
  91. package/dist/__tests__/utils/errors.test.d.ts.map +1 -0
  92. package/dist/__tests__/utils/errors.test.js +136 -0
  93. package/dist/__tests__/utils/errors.test.js.map +1 -0
  94. package/dist/__tests__/utils/type-guards.test.d.ts +2 -0
  95. package/dist/__tests__/utils/type-guards.test.d.ts.map +1 -0
  96. package/dist/__tests__/utils/type-guards.test.js +80 -0
  97. package/dist/__tests__/utils/type-guards.test.js.map +1 -0
  98. package/dist/__tests__/worktree/worktree-manager.test.d.ts +2 -0
  99. package/dist/__tests__/worktree/worktree-manager.test.d.ts.map +1 -0
  100. package/dist/__tests__/worktree/worktree-manager.test.js +403 -0
  101. package/dist/__tests__/worktree/worktree-manager.test.js.map +1 -0
  102. package/dist/config.d.ts +122 -0
  103. package/dist/config.d.ts.map +1 -0
  104. package/dist/config.js +508 -0
  105. package/dist/config.js.map +1 -0
  106. package/dist/dashboard/beads.d.ts +35 -0
  107. package/dist/dashboard/beads.d.ts.map +1 -0
  108. package/dist/dashboard/beads.js +102 -0
  109. package/dist/dashboard/beads.js.map +1 -0
  110. package/dist/dashboard/events.d.ts +46 -0
  111. package/dist/dashboard/events.d.ts.map +1 -0
  112. package/dist/dashboard/events.js +141 -0
  113. package/dist/dashboard/events.js.map +1 -0
  114. package/dist/dashboard/index.d.ts +67 -0
  115. package/dist/dashboard/index.d.ts.map +1 -0
  116. package/dist/dashboard/index.js +90 -0
  117. package/dist/dashboard/index.js.map +1 -0
  118. package/dist/dashboard/routes.d.ts +6 -0
  119. package/dist/dashboard/routes.d.ts.map +1 -0
  120. package/dist/dashboard/routes.js +244 -0
  121. package/dist/dashboard/routes.js.map +1 -0
  122. package/dist/dashboard/server.d.ts +27 -0
  123. package/dist/dashboard/server.d.ts.map +1 -0
  124. package/dist/dashboard/server.js +72 -0
  125. package/dist/dashboard/server.js.map +1 -0
  126. package/dist/dashboard/state.d.ts +116 -0
  127. package/dist/dashboard/state.d.ts.map +1 -0
  128. package/dist/dashboard/state.js +251 -0
  129. package/dist/dashboard/state.js.map +1 -0
  130. package/dist/dashboard/ui.d.ts +6 -0
  131. package/dist/dashboard/ui.d.ts.map +1 -0
  132. package/dist/dashboard/ui.js +1407 -0
  133. package/dist/dashboard/ui.js.map +1 -0
  134. package/dist/embeddings/index.d.ts +20 -2
  135. package/dist/embeddings/index.d.ts.map +1 -1
  136. package/dist/embeddings/index.js +49 -6
  137. package/dist/embeddings/index.js.map +1 -1
  138. package/dist/embeddings/jina.d.ts +9 -0
  139. package/dist/embeddings/jina.d.ts.map +1 -1
  140. package/dist/embeddings/jina.js +42 -2
  141. package/dist/embeddings/jina.js.map +1 -1
  142. package/dist/embeddings/ollama.d.ts +2 -0
  143. package/dist/embeddings/ollama.d.ts.map +1 -1
  144. package/dist/embeddings/ollama.js +21 -5
  145. package/dist/embeddings/ollama.js.map +1 -1
  146. package/dist/embeddings/rate-limiter.d.ts +75 -0
  147. package/dist/embeddings/rate-limiter.d.ts.map +1 -0
  148. package/dist/embeddings/rate-limiter.js +145 -0
  149. package/dist/embeddings/rate-limiter.js.map +1 -0
  150. package/dist/embeddings/retry.d.ts +14 -0
  151. package/dist/embeddings/retry.d.ts.map +1 -0
  152. package/dist/embeddings/retry.js +89 -0
  153. package/dist/embeddings/retry.js.map +1 -0
  154. package/dist/embeddings/types.d.ts +56 -2
  155. package/dist/embeddings/types.d.ts.map +1 -1
  156. package/dist/embeddings/types.js +16 -0
  157. package/dist/embeddings/types.js.map +1 -1
  158. package/dist/index.js +1870 -44
  159. package/dist/index.js.map +1 -1
  160. package/dist/memory/index.d.ts +63 -0
  161. package/dist/memory/index.d.ts.map +1 -0
  162. package/dist/memory/index.js +168 -0
  163. package/dist/memory/index.js.map +1 -0
  164. package/dist/search/ast-chunker.d.ts +34 -0
  165. package/dist/search/ast-chunker.d.ts.map +1 -0
  166. package/dist/search/ast-chunker.js +261 -0
  167. package/dist/search/ast-chunker.js.map +1 -0
  168. package/dist/search/clustering.d.ts +77 -0
  169. package/dist/search/clustering.d.ts.map +1 -0
  170. package/dist/search/clustering.js +455 -0
  171. package/dist/search/clustering.js.map +1 -0
  172. package/dist/search/indexer.d.ts +239 -3
  173. package/dist/search/indexer.d.ts.map +1 -1
  174. package/dist/search/indexer.js +941 -45
  175. package/dist/search/indexer.js.map +1 -1
  176. package/dist/search/tree-sitter-chunker.d.ts +69 -0
  177. package/dist/search/tree-sitter-chunker.d.ts.map +1 -0
  178. package/dist/search/tree-sitter-chunker.js +436 -0
  179. package/dist/search/tree-sitter-chunker.js.map +1 -0
  180. package/dist/symbols/index.d.ts +14 -0
  181. package/dist/symbols/index.d.ts.map +1 -0
  182. package/dist/symbols/index.js +19 -0
  183. package/dist/symbols/index.js.map +1 -0
  184. package/dist/symbols/name-path.d.ts +113 -0
  185. package/dist/symbols/name-path.d.ts.map +1 -0
  186. package/dist/symbols/name-path.js +194 -0
  187. package/dist/symbols/name-path.js.map +1 -0
  188. package/dist/symbols/pattern-search.d.ts +14 -0
  189. package/dist/symbols/pattern-search.d.ts.map +1 -0
  190. package/dist/symbols/pattern-search.js +224 -0
  191. package/dist/symbols/pattern-search.js.map +1 -0
  192. package/dist/symbols/reference-finder.d.ts +38 -0
  193. package/dist/symbols/reference-finder.d.ts.map +1 -0
  194. package/dist/symbols/reference-finder.js +376 -0
  195. package/dist/symbols/reference-finder.js.map +1 -0
  196. package/dist/symbols/symbol-editor.d.ts +81 -0
  197. package/dist/symbols/symbol-editor.d.ts.map +1 -0
  198. package/dist/symbols/symbol-editor.js +257 -0
  199. package/dist/symbols/symbol-editor.js.map +1 -0
  200. package/dist/symbols/symbol-extractor.d.ts +49 -0
  201. package/dist/symbols/symbol-extractor.d.ts.map +1 -0
  202. package/dist/symbols/symbol-extractor.js +593 -0
  203. package/dist/symbols/symbol-extractor.js.map +1 -0
  204. package/dist/symbols/symbol-renamer.d.ts +81 -0
  205. package/dist/symbols/symbol-renamer.d.ts.map +1 -0
  206. package/dist/symbols/symbol-renamer.js +204 -0
  207. package/dist/symbols/symbol-renamer.js.map +1 -0
  208. package/dist/symbols/types.d.ts +234 -0
  209. package/dist/symbols/types.d.ts.map +1 -0
  210. package/dist/symbols/types.js +106 -0
  211. package/dist/symbols/types.js.map +1 -0
  212. package/dist/utils/concurrency.d.ts +32 -0
  213. package/dist/utils/concurrency.d.ts.map +1 -0
  214. package/dist/utils/concurrency.js +57 -0
  215. package/dist/utils/concurrency.js.map +1 -0
  216. package/dist/utils/errors.d.ts +36 -0
  217. package/dist/utils/errors.d.ts.map +1 -0
  218. package/dist/utils/errors.js +91 -0
  219. package/dist/utils/errors.js.map +1 -0
  220. package/dist/utils/type-guards.d.ts +17 -0
  221. package/dist/utils/type-guards.d.ts.map +1 -0
  222. package/dist/utils/type-guards.js +25 -0
  223. package/dist/utils/type-guards.js.map +1 -0
  224. package/dist/worktree/index.d.ts +6 -0
  225. package/dist/worktree/index.d.ts.map +1 -0
  226. package/dist/worktree/index.js +6 -0
  227. package/dist/worktree/index.js.map +1 -0
  228. package/dist/worktree/types.d.ts +101 -0
  229. package/dist/worktree/types.d.ts.map +1 -0
  230. package/dist/worktree/types.js +6 -0
  231. package/dist/worktree/types.js.map +1 -0
  232. package/dist/worktree/worktree-manager.d.ts +80 -0
  233. package/dist/worktree/worktree-manager.d.ts.map +1 -0
  234. package/dist/worktree/worktree-manager.js +407 -0
  235. package/dist/worktree/worktree-manager.js.map +1 -0
  236. package/package.json +39 -5
  237. package/scripts/postinstall.js +48 -0
package/dist/index.js CHANGED
@@ -1,82 +1,860 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ const execAsync = promisify(exec);
10
+ import { createRequire } from 'module';
11
+ const require = createRequire(import.meta.url);
12
+ const packageJson = require('../package.json');
5
13
  import { createEmbeddingBackend } from './embeddings/index.js';
6
14
  import { CodeIndexer } from './search/indexer.js';
15
+ import { isStringArray, isString, isNumber, isBoolean } from './utils/type-guards.js';
16
+ import { logError, formatErrorResponse, wrapError, LanceContextError } from './utils/errors.js';
17
+ import { loadConfig, loadSecrets, getInstructions, getDashboardConfig } from './config.js';
18
+ import { startDashboard, stopDashboard, dashboardState, isPortAvailable, } from './dashboard/index.js';
19
+ // Symbolic analysis imports
20
+ import { SymbolExtractor, searchForPattern, formatPatternSearchResults, ReferenceFinder, formatReferencesResult, SymbolEditor, SymbolRenamer, formatRenameResult, SymbolKindNames, parseNamePath, matchNamePath, formatNamePath, } from './symbols/index.js';
21
+ import { MemoryManager, formatMemoryList } from './memory/index.js';
22
+ import { WorktreeManager, formatWorktreeInfo, formatWorktreeList } from './worktree/index.js';
23
+ /**
24
+ * Check if browser was recently opened (within the last hour)
25
+ */
26
+ function wasBrowserRecentlyOpened(projectPath) {
27
+ const flagFile = path.join(projectPath, '.lance-context', 'browser-opened');
28
+ try {
29
+ if (fs.existsSync(flagFile)) {
30
+ const timestamp = parseInt(fs.readFileSync(flagFile, 'utf-8'), 10);
31
+ const oneHourAgo = Date.now() - 60 * 60 * 1000;
32
+ return timestamp > oneHourAgo;
33
+ }
34
+ }
35
+ catch {
36
+ // Ignore errors
37
+ }
38
+ return false;
39
+ }
40
+ /**
41
+ * Record that browser was opened
42
+ */
43
+ function recordBrowserOpened(projectPath) {
44
+ const flagFile = path.join(projectPath, '.lance-context', 'browser-opened');
45
+ try {
46
+ fs.writeFileSync(flagFile, Date.now().toString());
47
+ }
48
+ catch {
49
+ // Ignore errors
50
+ }
51
+ }
52
+ /**
53
+ * Open a URL in the user's default browser (cross-platform)
54
+ */
55
+ function openBrowser(url, projectPath) {
56
+ // Don't open if already opened recently
57
+ if (wasBrowserRecentlyOpened(projectPath)) {
58
+ console.error('[lance-context] Dashboard was recently opened, skipping');
59
+ return;
60
+ }
61
+ const platform = process.platform;
62
+ let command;
63
+ switch (platform) {
64
+ case 'darwin':
65
+ command = `open "${url}"`;
66
+ break;
67
+ case 'win32':
68
+ command = `start "" "${url}"`;
69
+ break;
70
+ default:
71
+ // Linux and others
72
+ command = `xdg-open "${url}"`;
73
+ }
74
+ exec(command, (error) => {
75
+ if (error) {
76
+ console.error('[lance-context] Failed to open browser:', error.message);
77
+ }
78
+ else {
79
+ recordBrowserOpened(projectPath);
80
+ }
81
+ });
82
+ }
7
83
  const PROJECT_PATH = process.env.LANCE_CONTEXT_PROJECT || process.cwd();
8
- let indexer = null;
84
+ /**
85
+ * Brief guidance appended to tool responses to reinforce tool selection preferences.
86
+ */
87
+ const TOOL_GUIDANCE = `
88
+
89
+ ---
90
+ **Tip:** Prefer lance-context's \`search_code\` over pattern-based tools (grep, find_symbol) when exploring code or unsure of exact names.`;
91
+ /**
92
+ * Server instructions provided at MCP initialization.
93
+ * These guide Claude on when to use lance-context tools vs alternatives.
94
+ */
95
+ const SERVER_INSTRUCTIONS = `# lance-context - Semantic Code Search & Symbol Analysis
96
+
97
+ ## When to Use lance-context Tools
98
+
99
+ **PREFER lance-context tools** over pattern-based alternatives (grep, find) for code exploration:
100
+
101
+ | Task | Use lance-context | Instead of |
102
+ |------|-------------------|------------|
103
+ | Find code by concept | \`search_code\` | grep with regex |
104
+ | Unsure of exact names | \`search_code\` | wildcards, substring matching |
105
+ | Explore unfamiliar code | \`search_code\` | multiple grep/find attempts |
106
+ | Find similar patterns | \`search_similar\` | manual comparison |
107
+ | Commit changes | \`commit\` | git commit |
108
+ | Understand file structure | \`get_symbols_overview\` | reading entire file |
109
+ | Find specific function/class | \`find_symbol\` | grep for definition |
110
+ | Find symbol usages | \`find_referencing_symbols\` | grep for name |
111
+ | Search with regex | \`search_for_pattern\` | grep/rg |
112
+
113
+ ## Tool Categories
114
+
115
+ ### Semantic Search
116
+ - **search_code**: Natural language code search. One call replaces multiple pattern searches.
117
+ - **search_similar**: Find duplicate/related code patterns.
118
+
119
+ ### Symbol Navigation
120
+ - **get_symbols_overview**: List all symbols in a file grouped by kind (Class, Function, etc.)
121
+ - **find_symbol**: Search by name path pattern (e.g., "MyClass/myMethod", "get*")
122
+ - **find_referencing_symbols**: Find all usages of a symbol across codebase
123
+ - **search_for_pattern**: Regex search with context lines
124
+
125
+ ### Symbol Editing
126
+ - **replace_symbol_body**: Replace entire symbol definition
127
+ - **insert_before_symbol**: Add code before a symbol
128
+ - **insert_after_symbol**: Add code after a symbol
129
+ - **rename_symbol**: Rename symbol across entire codebase
130
+
131
+ ### Memory (Project Context)
132
+ - **write_memory**: Save project-specific notes/decisions
133
+ - **read_memory**: Retrieve saved context
134
+ - **list_memories**: See available memories
135
+ - **edit_memory**: Find/replace in memory
136
+ - **delete_memory**: Remove a memory
137
+
138
+ ### Git & Index
139
+ - **commit**: Git commit with validation (feature branch, message format)
140
+ - **index_codebase**: Build/update the search index
141
+ - **get_index_status**: Check if index is ready
142
+
143
+ ## CRITICAL: Always Use the \`commit\` Tool
144
+
145
+ **NEVER use raw \`git commit\`** - ALWAYS use the \`commit\` MCP tool.
146
+
147
+ The \`commit\` tool:
148
+ - Validates you're on a feature branch (not main)
149
+ - Checks message format (≤72 chars, imperative mood)
150
+ - Enforces single responsibility per commit
151
+ - Prevents common mistakes
152
+
153
+ A post-commit hook will warn if commits bypass this tool.
154
+
155
+ ## Signs You Should Have Used search_code
156
+
157
+ - You used wildcards or regex alternation
158
+ - Multiple search attempts to find something
159
+ - Pattern-based search returned nothing
160
+ - Searching for a concept, not an exact identifier`;
161
+ /**
162
+ * Priority instructions returned by get_project_instructions
163
+ */
164
+ const PRIORITY_INSTRUCTIONS = `
165
+ ## Tool Selection Priority
166
+
167
+ **ALWAYS prefer lance-context tools** over other code exploration tools (Serena, grep, find):
168
+
169
+ 1. **search_code** - Use for ANY code exploration:
170
+ - Finding code by concept ("error handling", "authentication")
171
+ - When unsure of exact names or locations
172
+ - When you would otherwise use wildcards or regex
173
+ - If your first search attempt fails
174
+
175
+ 2. **search_similar** - Finding related/duplicate code patterns
176
+
177
+ 3. **get_index_status** - Check if index is ready before searching
178
+
179
+ 4. **commit** - **ALWAYS use instead of \`git commit\`** (MANDATORY):
180
+ - NEVER run raw \`git commit\` - always use this tool
181
+ - Validates you're on a feature branch (not main)
182
+ - Checks message format (≤72 chars, imperative mood)
183
+ - Enforces single responsibility per commit
184
+ - Auto-appends Co-Authored-By trailer
185
+
186
+ **Signs you should have used search_code:**
187
+ - You used wildcards or regex alternation (e.g., \`foo|bar\`)
188
+ - You made multiple search calls to find something
189
+ - You searched for a partial name with substring matching
190
+ - Your pattern-based search returned nothing
191
+
192
+ `;
193
+ // Package version - read from package.json
194
+ const PACKAGE_VERSION = packageJson.version;
195
+ /**
196
+ * Check for updates from npm registry (non-blocking).
197
+ * Logs a warning if a newer version is available.
198
+ */
199
+ async function checkForUpdates() {
200
+ try {
201
+ // Use npm view command to get latest version
202
+ const { stdout } = await execAsync('npm view lance-context version 2>/dev/null', {
203
+ timeout: 5000, // 5 second timeout
204
+ });
205
+ const latestVersion = stdout.trim();
206
+ if (latestVersion && latestVersion !== PACKAGE_VERSION) {
207
+ // Simple semver comparison: split and compare major.minor.patch
208
+ const current = PACKAGE_VERSION.split('.').map(Number);
209
+ const latest = latestVersion.split('.').map(Number);
210
+ const isOutdated = latest[0] > current[0] ||
211
+ (latest[0] === current[0] && latest[1] > current[1]) ||
212
+ (latest[0] === current[0] && latest[1] === current[1] && latest[2] > current[2]);
213
+ if (isOutdated) {
214
+ console.error(`[lance-context] Update available: ${PACKAGE_VERSION} → ${latestVersion}`);
215
+ console.error('[lance-context] Run: npm install -g lance-context@latest');
216
+ }
217
+ }
218
+ }
219
+ catch {
220
+ // Silently ignore update check failures (network issues, npm not available, etc.)
221
+ }
222
+ }
223
+ let indexerPromise = null;
224
+ let configPromise = null;
225
+ async function getConfig() {
226
+ if (!configPromise) {
227
+ configPromise = loadConfig(PROJECT_PATH);
228
+ }
229
+ return configPromise;
230
+ }
9
231
  async function getIndexer() {
10
- if (!indexer) {
11
- const backend = await createEmbeddingBackend();
12
- indexer = new CodeIndexer(PROJECT_PATH, backend);
13
- await indexer.initialize();
232
+ if (!indexerPromise) {
233
+ indexerPromise = (async () => {
234
+ // Load config and secrets to configure embedding backend
235
+ const config = await getConfig();
236
+ const secrets = await loadSecrets(PROJECT_PATH);
237
+ const backend = await createEmbeddingBackend({
238
+ backend: config.embedding?.backend,
239
+ apiKey: secrets.jinaApiKey,
240
+ });
241
+ const idx = new CodeIndexer(PROJECT_PATH, backend);
242
+ await idx.initialize();
243
+ // Share indexer and config with dashboard state
244
+ dashboardState.setIndexer(idx);
245
+ dashboardState.setConfig(config);
246
+ dashboardState.setProjectPath(PROJECT_PATH);
247
+ return idx;
248
+ })();
14
249
  }
15
- return indexer;
250
+ return indexerPromise;
16
251
  }
17
252
  const server = new Server({
18
253
  name: 'lance-context',
19
- version: '0.1.0',
254
+ version: PACKAGE_VERSION,
20
255
  }, {
21
256
  capabilities: {
22
257
  tools: {},
23
258
  },
259
+ instructions: SERVER_INSTRUCTIONS,
24
260
  });
25
261
  // List available tools
26
262
  server.setRequestHandler(ListToolsRequestSchema, async () => {
27
263
  return {
28
264
  tools: [
29
265
  {
30
- name: 'index_codebase',
31
- description: 'Index the codebase for semantic search. Creates vector embeddings of all code files.',
266
+ name: 'index_codebase',
267
+ description: 'Index the codebase for semantic search. Creates vector embeddings of all code files. Supports incremental indexing - only changed files are re-indexed unless forceReindex is true.',
268
+ inputSchema: {
269
+ type: 'object',
270
+ properties: {
271
+ patterns: {
272
+ type: 'array',
273
+ items: { type: 'string' },
274
+ description: 'Glob patterns for files to index (default: common code files)',
275
+ },
276
+ excludePatterns: {
277
+ type: 'array',
278
+ items: { type: 'string' },
279
+ description: 'Glob patterns for files to exclude (default: node_modules, dist, .git)',
280
+ },
281
+ forceReindex: {
282
+ type: 'boolean',
283
+ description: 'Force a full reindex, ignoring cached file modification times (default: false)',
284
+ },
285
+ autoRepair: {
286
+ type: 'boolean',
287
+ description: 'Automatically repair a corrupted index by forcing a full reindex (default: false)',
288
+ },
289
+ },
290
+ },
291
+ },
292
+ {
293
+ name: 'search_code',
294
+ description: 'Search the codebase using natural language. Returns relevant code snippets. PREFER THIS TOOL when: (1) you are unsure of exact symbol/function names, (2) you would need wildcards or regex to find something, (3) you are exploring code by concept rather than exact identifier, (4) your first search attempt failed or returned nothing. Semantic search handles name uncertainty naturally - one call here replaces multiple pattern-based searches.',
295
+ inputSchema: {
296
+ type: 'object',
297
+ properties: {
298
+ query: {
299
+ type: 'string',
300
+ description: 'Natural language query to search for',
301
+ },
302
+ limit: {
303
+ type: 'number',
304
+ description: 'Maximum number of results (default: 10)',
305
+ },
306
+ pathPattern: {
307
+ type: 'string',
308
+ description: "Glob pattern to filter results by file path (e.g., 'src/**/*.ts', '!**/*.test.ts')",
309
+ },
310
+ languages: {
311
+ type: 'array',
312
+ items: { type: 'string' },
313
+ description: "Filter results to specific languages (e.g., ['typescript', 'javascript'])",
314
+ },
315
+ },
316
+ required: ['query'],
317
+ },
318
+ },
319
+ {
320
+ name: 'get_index_status',
321
+ description: 'Get the current status of the code index.',
322
+ inputSchema: {
323
+ type: 'object',
324
+ properties: {},
325
+ },
326
+ },
327
+ {
328
+ name: 'clear_index',
329
+ description: 'Clear the code index.',
330
+ inputSchema: {
331
+ type: 'object',
332
+ properties: {},
333
+ },
334
+ },
335
+ {
336
+ name: 'get_project_instructions',
337
+ description: 'Get project-specific instructions from the .lance-context.json config file. Returns instructions for how to work with this codebase.',
338
+ inputSchema: {
339
+ type: 'object',
340
+ properties: {},
341
+ },
342
+ },
343
+ {
344
+ name: 'search_similar',
345
+ description: 'Find code semantically similar to a given code snippet or file location. Useful for finding duplicate logic, similar implementations, or related code patterns.',
346
+ inputSchema: {
347
+ type: 'object',
348
+ properties: {
349
+ filepath: {
350
+ type: 'string',
351
+ description: 'File path (relative to project root) to find similar code for. Use with startLine/endLine to specify a range.',
352
+ },
353
+ startLine: {
354
+ type: 'number',
355
+ description: 'Starting line number (1-indexed). Requires filepath.',
356
+ },
357
+ endLine: {
358
+ type: 'number',
359
+ description: 'Ending line number (1-indexed). Requires filepath.',
360
+ },
361
+ code: {
362
+ type: 'string',
363
+ description: 'Code snippet to find similar code for. Alternative to filepath - provide either code or filepath, not both.',
364
+ },
365
+ limit: {
366
+ type: 'number',
367
+ description: 'Maximum number of results (default: 10)',
368
+ },
369
+ threshold: {
370
+ type: 'number',
371
+ description: 'Minimum similarity score 0-1 (default: 0)',
372
+ },
373
+ excludeSelf: {
374
+ type: 'boolean',
375
+ description: 'Exclude the source chunk from results (default: true)',
376
+ },
377
+ },
378
+ },
379
+ },
380
+ {
381
+ name: 'summarize_codebase',
382
+ description: 'Generate a comprehensive summary of the codebase including file statistics, language distribution, and discovered concept areas. Uses k-means clustering on embeddings to identify related code groups.',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ numClusters: {
387
+ type: 'number',
388
+ description: 'Target number of concept clusters (default: auto-determined based on codebase size)',
389
+ },
390
+ },
391
+ },
392
+ },
393
+ {
394
+ name: 'list_concepts',
395
+ description: 'List all discovered concept clusters in the codebase. Each cluster represents a semantic grouping of related code (e.g., authentication, database, API handlers). Returns cluster labels, sizes, and representative code chunks.',
396
+ inputSchema: {
397
+ type: 'object',
398
+ properties: {
399
+ forceRecluster: {
400
+ type: 'boolean',
401
+ description: 'Force reclustering even if cached results exist (default: false)',
402
+ },
403
+ },
404
+ },
405
+ },
406
+ {
407
+ name: 'search_by_concept',
408
+ description: 'Search for code within a specific concept cluster. Use list_concepts first to discover available clusters and their IDs. Can optionally combine with a semantic query to search within the cluster.',
409
+ inputSchema: {
410
+ type: 'object',
411
+ properties: {
412
+ conceptId: {
413
+ type: 'number',
414
+ description: 'The cluster ID to search within (from list_concepts)',
415
+ },
416
+ query: {
417
+ type: 'string',
418
+ description: 'Optional semantic query to search within the cluster. If not provided, returns representative chunks.',
419
+ },
420
+ limit: {
421
+ type: 'number',
422
+ description: 'Maximum number of results (default: 10)',
423
+ },
424
+ },
425
+ required: ['conceptId'],
426
+ },
427
+ },
428
+ {
429
+ name: 'commit',
430
+ description: 'Create a git commit with validation. USE THIS TOOL instead of running git commit directly. This tool enforces project commit rules: (1) validates you are on a feature branch (not main), (2) checks commit message format (<=72 chars, imperative mood, single responsibility), (3) returns commit rules as a reminder. Prevents common mistakes like committing to main or multi-responsibility commits.',
431
+ inputSchema: {
432
+ type: 'object',
433
+ properties: {
434
+ message: {
435
+ type: 'string',
436
+ description: 'The commit message. Must be <=72 characters, imperative mood, single responsibility.',
437
+ },
438
+ files: {
439
+ type: 'array',
440
+ items: { type: 'string' },
441
+ description: 'Files to stage before committing. If not provided, commits already-staged files.',
442
+ },
443
+ },
444
+ required: ['message'],
445
+ },
446
+ },
447
+ // --- Symbolic Analysis Tools ---
448
+ {
449
+ name: 'get_symbols_overview',
450
+ description: "Get a high-level overview of code symbols in a file. Returns symbols grouped by kind (Class, Function, Method, etc.) in a compact format. Use this to understand a file's structure before diving into specific symbols.",
451
+ inputSchema: {
452
+ type: 'object',
453
+ properties: {
454
+ relative_path: {
455
+ type: 'string',
456
+ description: 'The relative path to the file to analyze.',
457
+ },
458
+ depth: {
459
+ type: 'number',
460
+ description: 'Depth of descendants to retrieve (0 = top-level only). Default: 0.',
461
+ },
462
+ max_answer_chars: {
463
+ type: 'number',
464
+ description: 'Maximum response size in characters. Default: 50000.',
465
+ },
466
+ },
467
+ required: ['relative_path'],
468
+ },
469
+ },
470
+ {
471
+ name: 'find_symbol',
472
+ description: 'Find symbols by name path pattern. Supports: (1) simple name "myFunction", (2) relative path "MyClass/myMethod", (3) absolute path "/MyClass/myMethod", (4) glob pattern "get*" with substring_matching. Returns symbol locations and optionally their source code body.',
473
+ inputSchema: {
474
+ type: 'object',
475
+ properties: {
476
+ name_path_pattern: {
477
+ type: 'string',
478
+ description: 'The name path pattern to search for (e.g., "MyClass/myMethod", "get*").',
479
+ },
480
+ relative_path: {
481
+ type: 'string',
482
+ description: 'Optional. Restrict search to this file or directory.',
483
+ },
484
+ depth: {
485
+ type: 'number',
486
+ description: 'Depth of descendants to retrieve (e.g., 1 for class methods). Default: 0.',
487
+ },
488
+ include_body: {
489
+ type: 'boolean',
490
+ description: "Whether to include the symbol's source code. Default: false.",
491
+ },
492
+ include_info: {
493
+ type: 'boolean',
494
+ description: 'Whether to include additional info (docstring, signature). Default: false.',
495
+ },
496
+ substring_matching: {
497
+ type: 'boolean',
498
+ description: 'If true, use substring matching for the last element of the pattern. Default: false.',
499
+ },
500
+ include_kinds: {
501
+ type: 'array',
502
+ items: { type: 'number' },
503
+ description: 'LSP symbol kind integers to include. If not provided, all kinds are included.',
504
+ },
505
+ exclude_kinds: {
506
+ type: 'array',
507
+ items: { type: 'number' },
508
+ description: 'LSP symbol kind integers to exclude. Takes precedence over include_kinds.',
509
+ },
510
+ max_answer_chars: {
511
+ type: 'number',
512
+ description: 'Maximum response size in characters. Default: 50000.',
513
+ },
514
+ },
515
+ required: ['name_path_pattern'],
516
+ },
517
+ },
518
+ {
519
+ name: 'find_referencing_symbols',
520
+ description: 'Find all references to a symbol across the codebase. Returns code snippets showing where the symbol is used.',
521
+ inputSchema: {
522
+ type: 'object',
523
+ properties: {
524
+ name_path: {
525
+ type: 'string',
526
+ description: 'Name path of the symbol to find references for (e.g., "MyClass/myMethod").',
527
+ },
528
+ relative_path: {
529
+ type: 'string',
530
+ description: 'The relative path to the file containing the symbol.',
531
+ },
532
+ include_info: {
533
+ type: 'boolean',
534
+ description: 'Whether to include additional info about referencing symbols. Default: false.',
535
+ },
536
+ include_kinds: {
537
+ type: 'array',
538
+ items: { type: 'number' },
539
+ description: 'LSP symbol kind integers to include.',
540
+ },
541
+ exclude_kinds: {
542
+ type: 'array',
543
+ items: { type: 'number' },
544
+ description: 'LSP symbol kind integers to exclude.',
545
+ },
546
+ max_answer_chars: {
547
+ type: 'number',
548
+ description: 'Maximum response size in characters. Default: 50000.',
549
+ },
550
+ },
551
+ required: ['name_path', 'relative_path'],
552
+ },
553
+ },
554
+ {
555
+ name: 'search_for_pattern',
556
+ description: 'Search for a regex pattern in the codebase. Returns matched lines with optional context. Useful for finding code patterns, TODO comments, specific strings, etc.',
557
+ inputSchema: {
558
+ type: 'object',
559
+ properties: {
560
+ substring_pattern: {
561
+ type: 'string',
562
+ description: 'Regular expression pattern to search for.',
563
+ },
564
+ relative_path: {
565
+ type: 'string',
566
+ description: 'Restrict search to this file or directory. Default: entire project.',
567
+ },
568
+ restrict_search_to_code_files: {
569
+ type: 'boolean',
570
+ description: 'Only search in code files (not config, docs). Default: false.',
571
+ },
572
+ paths_include_glob: {
573
+ type: 'string',
574
+ description: 'Glob pattern for files to include (e.g., "*.py", "src/**/*.ts").',
575
+ },
576
+ paths_exclude_glob: {
577
+ type: 'string',
578
+ description: 'Glob pattern for files to exclude (e.g., "*test*", "**/*_generated.py").',
579
+ },
580
+ context_lines_before: {
581
+ type: 'number',
582
+ description: 'Number of context lines before each match. Default: 0.',
583
+ },
584
+ context_lines_after: {
585
+ type: 'number',
586
+ description: 'Number of context lines after each match. Default: 0.',
587
+ },
588
+ max_answer_chars: {
589
+ type: 'number',
590
+ description: 'Maximum response size in characters. Default: 50000.',
591
+ },
592
+ },
593
+ required: ['substring_pattern'],
594
+ },
595
+ },
596
+ // --- Memory Tools ---
597
+ {
598
+ name: 'write_memory',
599
+ description: 'Write information about this project to a named memory file. Memories persist across sessions and can be read later. Useful for storing architectural decisions, patterns, or project-specific context.',
600
+ inputSchema: {
601
+ type: 'object',
602
+ properties: {
603
+ memory_file_name: {
604
+ type: 'string',
605
+ description: 'The name of the memory (will be saved as .md file).',
606
+ },
607
+ content: {
608
+ type: 'string',
609
+ description: 'The markdown content to write to the memory.',
610
+ },
611
+ max_answer_chars: {
612
+ type: 'number',
613
+ description: 'Maximum response size in characters. Default: 50000.',
614
+ },
615
+ },
616
+ required: ['memory_file_name', 'content'],
617
+ },
618
+ },
619
+ {
620
+ name: 'read_memory',
621
+ description: 'Read the content of a memory file. Only read memories that are relevant to the current task.',
622
+ inputSchema: {
623
+ type: 'object',
624
+ properties: {
625
+ memory_file_name: {
626
+ type: 'string',
627
+ description: 'The name of the memory to read.',
628
+ },
629
+ max_answer_chars: {
630
+ type: 'number',
631
+ description: 'Maximum response size in characters. Default: 50000.',
632
+ },
633
+ },
634
+ required: ['memory_file_name'],
635
+ },
636
+ },
637
+ {
638
+ name: 'list_memories',
639
+ description: 'List all available memory files for this project. Use this to discover what project context has been saved.',
640
+ inputSchema: {
641
+ type: 'object',
642
+ properties: {},
643
+ },
644
+ },
645
+ {
646
+ name: 'delete_memory',
647
+ description: 'Delete a memory file. Only delete memories when explicitly requested by the user.',
648
+ inputSchema: {
649
+ type: 'object',
650
+ properties: {
651
+ memory_file_name: {
652
+ type: 'string',
653
+ description: 'The name of the memory to delete.',
654
+ },
655
+ },
656
+ required: ['memory_file_name'],
657
+ },
658
+ },
659
+ {
660
+ name: 'edit_memory',
661
+ description: 'Edit a memory file using find/replace. Supports both literal string and regex replacement.',
662
+ inputSchema: {
663
+ type: 'object',
664
+ properties: {
665
+ memory_file_name: {
666
+ type: 'string',
667
+ description: 'The name of the memory to edit.',
668
+ },
669
+ needle: {
670
+ type: 'string',
671
+ description: 'The string or regex pattern to search for.',
672
+ },
673
+ repl: {
674
+ type: 'string',
675
+ description: 'The replacement string.',
676
+ },
677
+ mode: {
678
+ type: 'string',
679
+ enum: ['literal', 'regex'],
680
+ description: 'How to interpret the needle: "literal" for exact match, "regex" for regex pattern.',
681
+ },
682
+ },
683
+ required: ['memory_file_name', 'needle', 'repl', 'mode'],
684
+ },
685
+ },
686
+ // --- Symbol Editing Tools ---
687
+ {
688
+ name: 'replace_symbol_body',
689
+ description: 'Replace the entire body of a symbol (function, class, method, etc.) with new content. Use this for significant rewrites of symbol definitions.',
690
+ inputSchema: {
691
+ type: 'object',
692
+ properties: {
693
+ name_path: {
694
+ type: 'string',
695
+ description: 'Name path of the symbol to replace (e.g., "MyClass/myMethod").',
696
+ },
697
+ relative_path: {
698
+ type: 'string',
699
+ description: 'The relative path to the file containing the symbol.',
700
+ },
701
+ body: {
702
+ type: 'string',
703
+ description: 'The new body content for the symbol (including signature line for functions).',
704
+ },
705
+ },
706
+ required: ['name_path', 'relative_path', 'body'],
707
+ },
708
+ },
709
+ {
710
+ name: 'insert_before_symbol',
711
+ description: 'Insert code before a symbol definition. Useful for adding new functions, classes, or imports before an existing symbol.',
712
+ inputSchema: {
713
+ type: 'object',
714
+ properties: {
715
+ name_path: {
716
+ type: 'string',
717
+ description: 'Name path of the symbol to insert before.',
718
+ },
719
+ relative_path: {
720
+ type: 'string',
721
+ description: 'The relative path to the file containing the symbol.',
722
+ },
723
+ body: {
724
+ type: 'string',
725
+ description: 'The content to insert before the symbol.',
726
+ },
727
+ },
728
+ required: ['name_path', 'relative_path', 'body'],
729
+ },
730
+ },
731
+ {
732
+ name: 'insert_after_symbol',
733
+ description: 'Insert code after a symbol definition. Useful for adding new functions, classes, or code after an existing symbol.',
734
+ inputSchema: {
735
+ type: 'object',
736
+ properties: {
737
+ name_path: {
738
+ type: 'string',
739
+ description: 'Name path of the symbol to insert after.',
740
+ },
741
+ relative_path: {
742
+ type: 'string',
743
+ description: 'The relative path to the file containing the symbol.',
744
+ },
745
+ body: {
746
+ type: 'string',
747
+ description: 'The content to insert after the symbol.',
748
+ },
749
+ },
750
+ required: ['name_path', 'relative_path', 'body'],
751
+ },
752
+ },
753
+ {
754
+ name: 'rename_symbol',
755
+ description: 'Rename a symbol throughout the entire codebase. Updates the symbol definition and all references.',
32
756
  inputSchema: {
33
757
  type: 'object',
34
758
  properties: {
35
- patterns: {
36
- type: 'array',
37
- items: { type: 'string' },
38
- description: 'Glob patterns for files to index (default: common code files)',
759
+ name_path: {
760
+ type: 'string',
761
+ description: 'Name path of the symbol to rename.',
39
762
  },
40
- excludePatterns: {
41
- type: 'array',
42
- items: { type: 'string' },
43
- description: 'Glob patterns for files to exclude (default: node_modules, dist, .git)',
763
+ relative_path: {
764
+ type: 'string',
765
+ description: 'The relative path to the file containing the symbol definition.',
766
+ },
767
+ new_name: {
768
+ type: 'string',
769
+ description: 'The new name for the symbol.',
770
+ },
771
+ dry_run: {
772
+ type: 'boolean',
773
+ description: 'If true, preview changes without making them. Default: false.',
44
774
  },
45
775
  },
776
+ required: ['name_path', 'relative_path', 'new_name'],
46
777
  },
47
778
  },
779
+ // --- Worktree Tools ---
48
780
  {
49
- name: 'search_code',
50
- description: 'Search the codebase using natural language. Returns relevant code snippets.',
781
+ name: 'create_worktree',
782
+ description: 'Create an isolated git worktree for parallel agent work. Prevents file conflicts when multiple agents work simultaneously. Creates a new branch and optionally installs dependencies.',
51
783
  inputSchema: {
52
784
  type: 'object',
53
785
  properties: {
54
- query: {
786
+ short_name: {
55
787
  type: 'string',
56
- description: 'Natural language query to search for',
788
+ description: 'Short descriptive name for the worktree (e.g., "add-auth", "fix-login"). Used in branch name.',
57
789
  },
58
- limit: {
59
- type: 'number',
60
- description: 'Maximum number of results (default: 10)',
790
+ issue_id: {
791
+ type: 'string',
792
+ description: 'Optional issue ID (e.g., "bd-123"). Combined with short_name for naming.',
793
+ },
794
+ prefix: {
795
+ type: 'string',
796
+ enum: ['feature', 'fix', 'refactor', 'docs', 'test'],
797
+ description: 'Branch prefix (default: "feature").',
798
+ },
799
+ base_branch: {
800
+ type: 'string',
801
+ description: 'Base branch to create from (default: main or current branch).',
802
+ },
803
+ install_deps: {
804
+ type: 'boolean',
805
+ description: 'Whether to install dependencies after creation (default: true).',
806
+ },
807
+ package_manager: {
808
+ type: 'string',
809
+ enum: ['npm', 'yarn', 'pnpm', 'bun'],
810
+ description: 'Package manager to use (default: auto-detect from lock file).',
61
811
  },
62
812
  },
63
- required: ['query'],
813
+ required: ['short_name'],
64
814
  },
65
815
  },
66
816
  {
67
- name: 'get_index_status',
68
- description: 'Get the current status of the code index.',
817
+ name: 'list_worktrees',
818
+ description: 'List all agent worktrees and their status. Shows branch, commit, dirty state, and ahead/behind counts.',
69
819
  inputSchema: {
70
820
  type: 'object',
71
821
  properties: {},
72
822
  },
73
823
  },
74
824
  {
75
- name: 'clear_index',
76
- description: 'Clear the code index.',
825
+ name: 'remove_worktree',
826
+ description: 'Remove an agent worktree. Optionally deletes the associated branch. Fails if worktree has uncommitted changes unless force is true.',
77
827
  inputSchema: {
78
828
  type: 'object',
79
- properties: {},
829
+ properties: {
830
+ name: {
831
+ type: 'string',
832
+ description: 'Name of the worktree to remove.',
833
+ },
834
+ delete_branch: {
835
+ type: 'boolean',
836
+ description: 'Whether to also delete the branch (default: false).',
837
+ },
838
+ force: {
839
+ type: 'boolean',
840
+ description: 'Force removal even if worktree has uncommitted changes (default: false).',
841
+ },
842
+ },
843
+ required: ['name'],
844
+ },
845
+ },
846
+ {
847
+ name: 'worktree_status',
848
+ description: 'Get detailed status of a specific worktree including branch, commit, and dirty state.',
849
+ inputSchema: {
850
+ type: 'object',
851
+ properties: {
852
+ name: {
853
+ type: 'string',
854
+ description: 'Name of the worktree to get status for.',
855
+ },
856
+ },
857
+ required: ['name'],
80
858
  },
81
859
  },
82
860
  ],
@@ -85,45 +863,124 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
85
863
  // Handle tool calls
86
864
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
87
865
  const { name, arguments: args } = request.params;
866
+ // Record command usage for dashboard
867
+ const validCommands = [
868
+ 'index_codebase',
869
+ 'search_code',
870
+ 'search_similar',
871
+ 'get_index_status',
872
+ 'clear_index',
873
+ 'get_project_instructions',
874
+ 'commit',
875
+ // Symbolic analysis
876
+ 'get_symbols_overview',
877
+ 'find_symbol',
878
+ 'find_referencing_symbols',
879
+ 'search_for_pattern',
880
+ 'replace_symbol_body',
881
+ 'insert_before_symbol',
882
+ 'insert_after_symbol',
883
+ 'rename_symbol',
884
+ // Memory
885
+ 'write_memory',
886
+ 'read_memory',
887
+ 'list_memories',
888
+ 'delete_memory',
889
+ 'edit_memory',
890
+ // Worktree
891
+ 'create_worktree',
892
+ 'list_worktrees',
893
+ 'remove_worktree',
894
+ 'worktree_status',
895
+ // Clustering
896
+ 'list_concepts',
897
+ 'search_by_concept',
898
+ 'summarize_codebase',
899
+ ];
900
+ if (validCommands.includes(name)) {
901
+ dashboardState.recordCommandUsage(name);
902
+ }
88
903
  try {
89
904
  const idx = await getIndexer();
90
905
  switch (name) {
91
906
  case 'index_codebase': {
92
- const patterns = args?.patterns || undefined;
93
- const excludePatterns = args?.excludePatterns || undefined;
94
- const result = await idx.indexCodebase(patterns, excludePatterns);
907
+ const patterns = isStringArray(args?.patterns) ? args.patterns : undefined;
908
+ const excludePatterns = isStringArray(args?.excludePatterns)
909
+ ? args.excludePatterns
910
+ : undefined;
911
+ const forceReindex = isBoolean(args?.forceReindex) ? args.forceReindex : false;
912
+ const autoRepair = isBoolean(args?.autoRepair) ? args.autoRepair : false;
913
+ // Notify dashboard of indexing start
914
+ dashboardState.onIndexingStart();
915
+ const result = await idx.indexCodebase(patterns, excludePatterns, forceReindex, (progress) => {
916
+ // Emit progress events to dashboard
917
+ dashboardState.onProgress(progress);
918
+ }, autoRepair);
919
+ // Notify dashboard of indexing completion
920
+ dashboardState.onIndexingComplete(result);
921
+ const mode = result.repaired
922
+ ? 'Repaired (corruption detected)'
923
+ : result.incremental
924
+ ? 'Incremental update'
925
+ : 'Full reindex';
95
926
  return {
96
927
  content: [
97
928
  {
98
929
  type: 'text',
99
- text: `Indexed ${result.filesIndexed} files, created ${result.chunksCreated} chunks.`,
930
+ text: `${mode}: Indexed ${result.filesIndexed} files, total ${result.chunksCreated} chunks.${TOOL_GUIDANCE}`,
100
931
  },
101
932
  ],
102
933
  };
103
934
  }
104
935
  case 'search_code': {
105
- const query = args?.query;
106
- const limit = args?.limit || 10;
107
- const results = await idx.search(query, limit);
936
+ const query = isString(args?.query) ? args.query : '';
937
+ if (!query) {
938
+ throw new LanceContextError('query is required', 'validation', { tool: 'search_code' });
939
+ }
940
+ const results = await idx.search({
941
+ query,
942
+ limit: isNumber(args?.limit) ? args.limit : 10,
943
+ pathPattern: isString(args?.pathPattern) ? args.pathPattern : undefined,
944
+ languages: isStringArray(args?.languages) ? args.languages : undefined,
945
+ });
108
946
  const formatted = results
109
- .map((r, i) => `## Result ${i + 1}: ${r.filePath}:${r.startLine}-${r.endLine}\n\`\`\`${r.language}\n${r.content}\n\`\`\``)
947
+ .map((r, i) => {
948
+ // Build header with optional symbol context
949
+ let header = `## Result ${i + 1}: ${r.filepath}:${r.startLine}-${r.endLine}`;
950
+ if (r.symbolName) {
951
+ const typeLabel = r.symbolType ? ` (${r.symbolType})` : '';
952
+ header += `\n**Symbol:** \`${r.symbolName}\`${typeLabel}`;
953
+ }
954
+ return `${header}\n\`\`\`${r.language}\n${r.content}\n\`\`\``;
955
+ })
110
956
  .join('\n\n');
111
957
  return {
112
958
  content: [
113
959
  {
114
960
  type: 'text',
115
- text: formatted || 'No results found.',
961
+ text: (formatted || 'No results found.') + TOOL_GUIDANCE,
116
962
  },
117
963
  ],
118
964
  };
119
965
  }
120
966
  case 'get_index_status': {
121
967
  const status = await idx.getStatus();
968
+ let statusText = JSON.stringify(status, null, 2);
969
+ // Add corruption warning if detected
970
+ if (status.corrupted) {
971
+ statusText =
972
+ `**WARNING: Index corruption detected!**\n` +
973
+ `Reason: ${status.corruptionReason}\n` +
974
+ `\nTo repair, either:\n` +
975
+ `1. Run \`index_codebase\` with \`autoRepair: true\`\n` +
976
+ `2. Run \`clear_index\` followed by \`index_codebase\`\n\n` +
977
+ statusText;
978
+ }
122
979
  return {
123
980
  content: [
124
981
  {
125
982
  type: 'text',
126
- text: JSON.stringify(status, null, 2),
983
+ text: statusText + TOOL_GUIDANCE,
127
984
  },
128
985
  ],
129
986
  };
@@ -134,21 +991,896 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
134
991
  content: [
135
992
  {
136
993
  type: 'text',
137
- text: 'Index cleared.',
994
+ text: 'Index cleared.' + TOOL_GUIDANCE,
995
+ },
996
+ ],
997
+ };
998
+ }
999
+ case 'get_project_instructions': {
1000
+ const config = await loadConfig(PROJECT_PATH);
1001
+ const projectInstructions = getInstructions(config);
1002
+ const fullInstructions = PRIORITY_INSTRUCTIONS + (projectInstructions || '');
1003
+ return {
1004
+ content: [
1005
+ {
1006
+ type: 'text',
1007
+ text: fullInstructions ||
1008
+ 'No project instructions configured. Add an "instructions" field to .lance-context.json.',
1009
+ },
1010
+ ],
1011
+ };
1012
+ }
1013
+ case 'search_similar': {
1014
+ const results = await idx.searchSimilar({
1015
+ filepath: isString(args?.filepath) ? args.filepath : undefined,
1016
+ startLine: isNumber(args?.startLine) ? args.startLine : undefined,
1017
+ endLine: isNumber(args?.endLine) ? args.endLine : undefined,
1018
+ code: isString(args?.code) ? args.code : undefined,
1019
+ limit: isNumber(args?.limit) ? args.limit : 10,
1020
+ threshold: isNumber(args?.threshold) ? args.threshold : 0,
1021
+ excludeSelf: isBoolean(args?.excludeSelf) ? args.excludeSelf : true,
1022
+ });
1023
+ if (results.length === 0) {
1024
+ return {
1025
+ content: [
1026
+ {
1027
+ type: 'text',
1028
+ text: 'No similar code found.' + TOOL_GUIDANCE,
1029
+ },
1030
+ ],
1031
+ };
1032
+ }
1033
+ const formatted = results
1034
+ .map((r, i) => {
1035
+ let header = `## Similar ${i + 1}: ${r.filepath}:${r.startLine}-${r.endLine} (${(r.similarity * 100).toFixed(1)}% similar)`;
1036
+ if (r.symbolName) {
1037
+ const typeLabel = r.symbolType ? ` (${r.symbolType})` : '';
1038
+ header += `\n**Symbol:** \`${r.symbolName}\`${typeLabel}`;
1039
+ }
1040
+ return `${header}\n\`\`\`${r.language}\n${r.content}\n\`\`\``;
1041
+ })
1042
+ .join('\n\n');
1043
+ return {
1044
+ content: [
1045
+ {
1046
+ type: 'text',
1047
+ text: formatted + TOOL_GUIDANCE,
1048
+ },
1049
+ ],
1050
+ };
1051
+ }
1052
+ case 'summarize_codebase': {
1053
+ const numClusters = isNumber(args?.numClusters) ? args.numClusters : undefined;
1054
+ const summary = await idx.summarizeCodebase(numClusters ? { numClusters } : undefined);
1055
+ const languageList = summary.languages
1056
+ .map((l) => `- **${l.language}**: ${l.fileCount} files, ${l.chunkCount} chunks`)
1057
+ .join('\n');
1058
+ const conceptList = summary.concepts
1059
+ .map((c) => {
1060
+ const keywords = c.keywords.slice(0, 5).join(', ');
1061
+ return `- **Cluster ${c.id}: ${c.label}** (${c.size} chunks)\n Keywords: ${keywords}`;
1062
+ })
1063
+ .join('\n');
1064
+ const formatted = `# Codebase Summary
1065
+
1066
+ ## Overview
1067
+ - **Total Files**: ${summary.totalFiles}
1068
+ - **Total Chunks**: ${summary.totalChunks}
1069
+ - **Concept Clusters**: ${summary.concepts.length}
1070
+ - **Clustering Quality**: ${(summary.clusteringQuality * 100).toFixed(1)}% (silhouette score)
1071
+ - **Generated At**: ${summary.generatedAt}
1072
+
1073
+ ## Languages
1074
+ ${languageList}
1075
+
1076
+ ## Concept Areas
1077
+ ${conceptList}`;
1078
+ return {
1079
+ content: [
1080
+ {
1081
+ type: 'text',
1082
+ text: formatted + TOOL_GUIDANCE,
1083
+ },
1084
+ ],
1085
+ };
1086
+ }
1087
+ case 'list_concepts': {
1088
+ const forceRecluster = isBoolean(args?.forceRecluster) ? args.forceRecluster : false;
1089
+ const concepts = await idx.listConcepts(forceRecluster);
1090
+ if (concepts.length === 0) {
1091
+ return {
1092
+ content: [
1093
+ {
1094
+ type: 'text',
1095
+ text: 'No concept clusters found. Make sure the codebase is indexed first.' +
1096
+ TOOL_GUIDANCE,
1097
+ },
1098
+ ],
1099
+ };
1100
+ }
1101
+ const formatted = concepts
1102
+ .map((c) => {
1103
+ const keywords = c.keywords.slice(0, 5).join(', ');
1104
+ return `## Cluster ${c.id}: ${c.label}
1105
+ - **Size**: ${c.size} code chunks
1106
+ - **Keywords**: ${keywords}
1107
+ - **Representatives**: ${c.representativeChunks.slice(0, 3).join(', ')}`;
1108
+ })
1109
+ .join('\n\n');
1110
+ return {
1111
+ content: [
1112
+ {
1113
+ type: 'text',
1114
+ text: `# Concept Clusters\n\n${formatted}` + TOOL_GUIDANCE,
1115
+ },
1116
+ ],
1117
+ };
1118
+ }
1119
+ case 'search_by_concept': {
1120
+ const conceptId = isNumber(args?.conceptId) ? args.conceptId : -1;
1121
+ if (conceptId < 0) {
1122
+ throw new LanceContextError('conceptId is required and must be a non-negative number', 'validation', { tool: 'search_by_concept' });
1123
+ }
1124
+ const query = isString(args?.query) ? args.query : undefined;
1125
+ const limit = isNumber(args?.limit) ? args.limit : 10;
1126
+ const results = await idx.searchByConcept(conceptId, query, limit);
1127
+ if (results.length === 0) {
1128
+ return {
1129
+ content: [
1130
+ {
1131
+ type: 'text',
1132
+ text: `No code found in concept cluster ${conceptId}. Try list_concepts to see available clusters.` +
1133
+ TOOL_GUIDANCE,
1134
+ },
1135
+ ],
1136
+ };
1137
+ }
1138
+ const formatted = results
1139
+ .map((r, i) => {
1140
+ let header = `## Result ${i + 1}: ${r.filepath}:${r.startLine}-${r.endLine}`;
1141
+ if (r.symbolName) {
1142
+ const typeLabel = r.symbolType ? ` (${r.symbolType})` : '';
1143
+ header += `\n**Symbol:** \`${r.symbolName}\`${typeLabel}`;
1144
+ }
1145
+ return `${header}\n\`\`\`${r.language}\n${r.content}\n\`\`\``;
1146
+ })
1147
+ .join('\n\n');
1148
+ return {
1149
+ content: [
1150
+ {
1151
+ type: 'text',
1152
+ text: formatted + TOOL_GUIDANCE,
1153
+ },
1154
+ ],
1155
+ };
1156
+ }
1157
+ case 'commit': {
1158
+ const message = isString(args?.message) ? args.message : '';
1159
+ const files = isStringArray(args?.files) ? args.files : [];
1160
+ if (!message) {
1161
+ throw new LanceContextError('message is required', 'validation', { tool: 'commit' });
1162
+ }
1163
+ // Commit rules to return with every response
1164
+ const COMMIT_RULES = `
1165
+ ## Commit Rules Reminder
1166
+
1167
+ 1. **Branch**: Must be on a feature branch, not main/master
1168
+ 2. **Message length**: Subject line must be ≤72 characters
1169
+ 3. **Imperative mood**: "Add feature" not "Added feature"
1170
+ 4. **Single responsibility**: One logical change per commit
1171
+ 5. **Body format**: Only "Co-Authored-By: Claude <noreply@anthropic.com>"
1172
+
1173
+ **Signs of multi-responsibility** (split into separate commits):
1174
+ - Message contains "and" connecting actions
1175
+ - Message lists multiple changes with commas
1176
+ - Changes span unrelated files/features
1177
+ `;
1178
+ const errors = [];
1179
+ const warnings = [];
1180
+ // Check 1: Not on main/master branch
1181
+ let currentBranch = '';
1182
+ try {
1183
+ const { stdout } = await execAsync('git branch --show-current', { cwd: PROJECT_PATH });
1184
+ currentBranch = stdout.trim();
1185
+ if (currentBranch === 'main' || currentBranch === 'master') {
1186
+ errors.push(`Cannot commit directly to ${currentBranch}. Create a feature branch first:\n git checkout -b feature/your-feature-name`);
1187
+ }
1188
+ }
1189
+ catch {
1190
+ errors.push('Failed to determine current branch. Are you in a git repository?');
1191
+ }
1192
+ // Check 2: Message length
1193
+ const subjectLine = message.split('\n')[0];
1194
+ if (subjectLine.length > 72) {
1195
+ errors.push(`Subject line is ${subjectLine.length} characters (max 72). Shorten it.`);
1196
+ }
1197
+ // Check 3: Imperative mood (heuristic - check for common past tense patterns)
1198
+ const pastTensePatterns = /^(Added|Fixed|Updated|Changed|Removed|Implemented|Created|Deleted|Modified|Refactored|Merged)\b/i;
1199
+ if (pastTensePatterns.test(subjectLine)) {
1200
+ warnings.push(`Subject may not be imperative mood. Use "Add" not "Added", "Fix" not "Fixed", etc.`);
1201
+ }
1202
+ // Check 4: Single responsibility (heuristic - check for "and" or multiple verbs)
1203
+ const multiResponsibilityPatterns = /\b(and|,)\s+(add|fix|update|change|remove|implement|create|delete|modify|refactor)\b/i;
1204
+ if (multiResponsibilityPatterns.test(subjectLine)) {
1205
+ errors.push(`Message suggests multiple responsibilities. Split into separate commits.`);
1206
+ }
1207
+ // If there are blocking errors, return them without committing
1208
+ if (errors.length > 0) {
1209
+ return {
1210
+ content: [
1211
+ {
1212
+ type: 'text',
1213
+ text: `## Commit Blocked\n\n**Errors:**\n${errors.map((e) => `- ${e}`).join('\n')}\n${warnings.length > 0 ? `\n**Warnings:**\n${warnings.map((w) => `- ${w}`).join('\n')}` : ''}\n${COMMIT_RULES}`,
1214
+ },
1215
+ ],
1216
+ isError: true,
1217
+ };
1218
+ }
1219
+ // Stage files if provided
1220
+ if (files.length > 0) {
1221
+ try {
1222
+ const fileArgs = files.map((f) => `"${f}"`).join(' ');
1223
+ await execAsync(`git add ${fileArgs}`, { cwd: PROJECT_PATH });
1224
+ }
1225
+ catch (e) {
1226
+ throw wrapError('Failed to stage files', 'git', e, { files });
1227
+ }
1228
+ }
1229
+ // Check if there are staged changes
1230
+ try {
1231
+ const { stdout } = await execAsync('git diff --cached --name-only', {
1232
+ cwd: PROJECT_PATH,
1233
+ });
1234
+ if (!stdout.trim()) {
1235
+ throw new LanceContextError('No staged changes to commit. Stage files first or pass files parameter.', 'git');
1236
+ }
1237
+ }
1238
+ catch (e) {
1239
+ if (e instanceof LanceContextError) {
1240
+ throw e;
1241
+ }
1242
+ throw wrapError('Failed to check staged changes', 'git', e);
1243
+ }
1244
+ // Build commit message with Co-Authored-By
1245
+ const fullMessage = `${message}\n\nCo-Authored-By: Claude <noreply@anthropic.com>`;
1246
+ // Write marker file to indicate commit is via MCP tool (for post-commit hook)
1247
+ const markerPath = path.join(PROJECT_PATH, '.git', 'MCP_COMMIT_MARKER');
1248
+ try {
1249
+ fs.writeFileSync(markerPath, Date.now().toString());
1250
+ }
1251
+ catch {
1252
+ // Ignore marker write failures - non-critical
1253
+ }
1254
+ // Execute commit
1255
+ try {
1256
+ const { stdout } = await execAsync(`git commit -m "${fullMessage.replace(/"/g, '\\"')}"`, { cwd: PROJECT_PATH });
1257
+ let response = `## Commit Successful\n\n${stdout.trim()}`;
1258
+ if (warnings.length > 0) {
1259
+ response += `\n\n**Warnings:**\n${warnings.map((w) => `- ${w}`).join('\n')}`;
1260
+ }
1261
+ response += `\n${COMMIT_RULES}`;
1262
+ return {
1263
+ content: [
1264
+ {
1265
+ type: 'text',
1266
+ text: response,
1267
+ },
1268
+ ],
1269
+ };
1270
+ }
1271
+ catch (e) {
1272
+ throw wrapError('Git commit failed', 'git', e, { message });
1273
+ }
1274
+ }
1275
+ // --- Symbolic Analysis Tools ---
1276
+ case 'get_symbols_overview': {
1277
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1278
+ if (!relativePath) {
1279
+ throw new LanceContextError('relative_path is required', 'validation', {
1280
+ tool: 'get_symbols_overview',
1281
+ });
1282
+ }
1283
+ const depth = isNumber(args?.depth) ? args.depth : 0;
1284
+ const extractor = new SymbolExtractor(PROJECT_PATH);
1285
+ const overview = await extractor.getSymbolsOverview(relativePath, depth);
1286
+ // Format the output
1287
+ const parts = [];
1288
+ parts.push(`## Symbols in ${overview.filepath}\n`);
1289
+ parts.push(`Total: ${overview.totalSymbols} symbols\n`);
1290
+ for (const [kindName, entries] of Object.entries(overview.byKind)) {
1291
+ parts.push(`\n### ${kindName} (${entries.length})\n`);
1292
+ for (const entry of entries) {
1293
+ const childInfo = entry.children ? ` [${entry.children} children]` : '';
1294
+ parts.push(`- **${entry.name}** (${entry.lines})${childInfo}`);
1295
+ }
1296
+ }
1297
+ return {
1298
+ content: [
1299
+ {
1300
+ type: 'text',
1301
+ text: parts.join('\n') + TOOL_GUIDANCE,
1302
+ },
1303
+ ],
1304
+ };
1305
+ }
1306
+ case 'find_symbol': {
1307
+ const namePathPattern = isString(args?.name_path_pattern) ? args.name_path_pattern : '';
1308
+ if (!namePathPattern) {
1309
+ throw new LanceContextError('name_path_pattern is required', 'validation', {
1310
+ tool: 'find_symbol',
1311
+ });
1312
+ }
1313
+ const relativePath = isString(args?.relative_path) ? args.relative_path : undefined;
1314
+ const depth = isNumber(args?.depth) ? args.depth : 0;
1315
+ const includeBody = isBoolean(args?.include_body) ? args.include_body : false;
1316
+ const substringMatching = isBoolean(args?.substring_matching)
1317
+ ? args.substring_matching
1318
+ : false;
1319
+ const includeKinds = Array.isArray(args?.include_kinds)
1320
+ ? args.include_kinds
1321
+ : undefined;
1322
+ const excludeKinds = Array.isArray(args?.exclude_kinds)
1323
+ ? args.exclude_kinds
1324
+ : undefined;
1325
+ const extractor = new SymbolExtractor(PROJECT_PATH);
1326
+ // If relativePath is provided, search in that file/directory
1327
+ // Otherwise, we need to search the whole codebase (more expensive)
1328
+ const files = [];
1329
+ if (relativePath) {
1330
+ const fullPath = path.join(PROJECT_PATH, relativePath);
1331
+ try {
1332
+ const fsStat = fs.statSync(fullPath);
1333
+ if (fsStat.isFile()) {
1334
+ files.push(relativePath);
1335
+ }
1336
+ else {
1337
+ // Directory - find all analyzable files
1338
+ const { glob: globFn } = await import('glob');
1339
+ const codeExtensions = [
1340
+ '*.ts',
1341
+ '*.tsx',
1342
+ '*.js',
1343
+ '*.jsx',
1344
+ '*.py',
1345
+ '*.go',
1346
+ '*.rs',
1347
+ '*.java',
1348
+ '*.rb',
1349
+ ];
1350
+ for (const ext of codeExtensions) {
1351
+ const matches = await globFn(`**/${ext}`, {
1352
+ cwd: fullPath,
1353
+ ignore: ['node_modules/**', 'dist/**', '.git/**'],
1354
+ });
1355
+ files.push(...matches.map((f) => path.join(relativePath, f)));
1356
+ }
1357
+ }
1358
+ }
1359
+ catch {
1360
+ throw new LanceContextError(`Path not found: ${relativePath}`, 'validation', {
1361
+ tool: 'find_symbol',
1362
+ });
1363
+ }
1364
+ }
1365
+ else {
1366
+ // Search whole codebase - expensive, limit to reasonable set
1367
+ const { glob: globFn } = await import('glob');
1368
+ const codeExtensions = [
1369
+ '*.ts',
1370
+ '*.tsx',
1371
+ '*.js',
1372
+ '*.jsx',
1373
+ '*.py',
1374
+ '*.go',
1375
+ '*.rs',
1376
+ '*.java',
1377
+ '*.rb',
1378
+ ];
1379
+ for (const ext of codeExtensions) {
1380
+ const matches = await globFn(`**/${ext}`, {
1381
+ cwd: PROJECT_PATH,
1382
+ ignore: ['node_modules/**', 'dist/**', '.git/**'],
1383
+ });
1384
+ files.push(...matches);
1385
+ }
1386
+ }
1387
+ // Parse the pattern
1388
+ const pattern = parseNamePath(namePathPattern);
1389
+ // Find matching symbols
1390
+ const matchedSymbols = [];
1391
+ for (const file of files.slice(0, 100)) {
1392
+ // Limit to prevent timeout
1393
+ try {
1394
+ const symbols = await extractor.extractSymbols(file, includeBody);
1395
+ const findMatches = (syms, currentDepth) => {
1396
+ for (const sym of syms) {
1397
+ // Apply kind filters
1398
+ if (excludeKinds && excludeKinds.includes(sym.kind))
1399
+ continue;
1400
+ if (includeKinds && !includeKinds.includes(sym.kind))
1401
+ continue;
1402
+ if (matchNamePath(sym.namePath, pattern, substringMatching)) {
1403
+ matchedSymbols.push({ symbol: sym, file });
1404
+ }
1405
+ // Search children up to requested depth
1406
+ if (currentDepth < depth && sym.children) {
1407
+ findMatches(sym.children, currentDepth + 1);
1408
+ }
1409
+ }
1410
+ };
1411
+ findMatches(symbols, 0);
1412
+ }
1413
+ catch {
1414
+ // Skip files that can't be analyzed
1415
+ }
1416
+ }
1417
+ if (matchedSymbols.length === 0) {
1418
+ return {
1419
+ content: [
1420
+ {
1421
+ type: 'text',
1422
+ text: `No symbols found matching pattern: ${namePathPattern}` + TOOL_GUIDANCE,
1423
+ },
1424
+ ],
1425
+ };
1426
+ }
1427
+ // Format results
1428
+ const parts = [];
1429
+ parts.push(`Found ${matchedSymbols.length} matching symbol(s):\n`);
1430
+ for (const { symbol } of matchedSymbols) {
1431
+ const kindName = SymbolKindNames[symbol.kind];
1432
+ parts.push(`\n## ${formatNamePath(symbol.namePath)} (${kindName})`);
1433
+ parts.push(`**Location:** ${symbol.location.filepath}:${symbol.location.startLine}-${symbol.location.endLine}`);
1434
+ if (symbol.body) {
1435
+ parts.push('\n```');
1436
+ parts.push(symbol.body);
1437
+ parts.push('```');
1438
+ }
1439
+ if (symbol.children && symbol.children.length > 0) {
1440
+ parts.push(`\n**Children:** ${symbol.children.length}`);
1441
+ for (const child of symbol.children) {
1442
+ const childKind = SymbolKindNames[child.kind];
1443
+ parts.push(` - ${child.name} (${childKind}, lines ${child.location.startLine}-${child.location.endLine})`);
1444
+ }
1445
+ }
1446
+ }
1447
+ return {
1448
+ content: [
1449
+ {
1450
+ type: 'text',
1451
+ text: parts.join('\n') + TOOL_GUIDANCE,
1452
+ },
1453
+ ],
1454
+ };
1455
+ }
1456
+ case 'find_referencing_symbols': {
1457
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1458
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1459
+ if (!namePath || !relativePath) {
1460
+ throw new LanceContextError('name_path and relative_path are required', 'validation', {
1461
+ tool: 'find_referencing_symbols',
1462
+ });
1463
+ }
1464
+ const includeInfo = isBoolean(args?.include_info) ? args.include_info : false;
1465
+ const includeKinds = Array.isArray(args?.include_kinds)
1466
+ ? args.include_kinds
1467
+ : undefined;
1468
+ const excludeKinds = Array.isArray(args?.exclude_kinds)
1469
+ ? args.exclude_kinds
1470
+ : undefined;
1471
+ const finder = new ReferenceFinder(PROJECT_PATH);
1472
+ const references = await finder.findReferences({
1473
+ namePath,
1474
+ relativePath,
1475
+ includeInfo,
1476
+ includeKinds,
1477
+ excludeKinds,
1478
+ });
1479
+ return {
1480
+ content: [
1481
+ {
1482
+ type: 'text',
1483
+ text: formatReferencesResult(references) + TOOL_GUIDANCE,
1484
+ },
1485
+ ],
1486
+ };
1487
+ }
1488
+ case 'search_for_pattern': {
1489
+ const substringPattern = isString(args?.substring_pattern) ? args.substring_pattern : '';
1490
+ if (!substringPattern) {
1491
+ throw new LanceContextError('substring_pattern is required', 'validation', {
1492
+ tool: 'search_for_pattern',
1493
+ });
1494
+ }
1495
+ const result = await searchForPattern(PROJECT_PATH, {
1496
+ substringPattern,
1497
+ relativePath: isString(args?.relative_path) ? args.relative_path : undefined,
1498
+ restrictSearchToCodeFiles: isBoolean(args?.restrict_search_to_code_files)
1499
+ ? args.restrict_search_to_code_files
1500
+ : false,
1501
+ pathsIncludeGlob: isString(args?.paths_include_glob)
1502
+ ? args.paths_include_glob
1503
+ : undefined,
1504
+ pathsExcludeGlob: isString(args?.paths_exclude_glob)
1505
+ ? args.paths_exclude_glob
1506
+ : undefined,
1507
+ contextLinesBefore: isNumber(args?.context_lines_before) ? args.context_lines_before : 0,
1508
+ contextLinesAfter: isNumber(args?.context_lines_after) ? args.context_lines_after : 0,
1509
+ maxAnswerChars: isNumber(args?.max_answer_chars) ? args.max_answer_chars : 50000,
1510
+ });
1511
+ return {
1512
+ content: [
1513
+ {
1514
+ type: 'text',
1515
+ text: formatPatternSearchResults(result) + TOOL_GUIDANCE,
1516
+ },
1517
+ ],
1518
+ };
1519
+ }
1520
+ // --- Memory Tools ---
1521
+ case 'write_memory': {
1522
+ const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
1523
+ const content = isString(args?.content) ? args.content : '';
1524
+ if (!memoryFileName || !content) {
1525
+ throw new LanceContextError('memory_file_name and content are required', 'validation', {
1526
+ tool: 'write_memory',
1527
+ });
1528
+ }
1529
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1530
+ await memoryManager.writeMemory(memoryFileName, content);
1531
+ return {
1532
+ content: [
1533
+ {
1534
+ type: 'text',
1535
+ text: `Memory "${memoryFileName}" saved successfully.` + TOOL_GUIDANCE,
1536
+ },
1537
+ ],
1538
+ };
1539
+ }
1540
+ case 'read_memory': {
1541
+ const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
1542
+ if (!memoryFileName) {
1543
+ throw new LanceContextError('memory_file_name is required', 'validation', {
1544
+ tool: 'read_memory',
1545
+ });
1546
+ }
1547
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1548
+ const content = await memoryManager.readMemory(memoryFileName);
1549
+ return {
1550
+ content: [
1551
+ {
1552
+ type: 'text',
1553
+ text: `## Memory: ${memoryFileName}\n\n${content}` + TOOL_GUIDANCE,
1554
+ },
1555
+ ],
1556
+ };
1557
+ }
1558
+ case 'list_memories': {
1559
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1560
+ const memories = await memoryManager.listMemories();
1561
+ return {
1562
+ content: [
1563
+ {
1564
+ type: 'text',
1565
+ text: formatMemoryList(memories) + TOOL_GUIDANCE,
1566
+ },
1567
+ ],
1568
+ };
1569
+ }
1570
+ case 'delete_memory': {
1571
+ const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
1572
+ if (!memoryFileName) {
1573
+ throw new LanceContextError('memory_file_name is required', 'validation', {
1574
+ tool: 'delete_memory',
1575
+ });
1576
+ }
1577
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1578
+ await memoryManager.deleteMemory(memoryFileName);
1579
+ return {
1580
+ content: [
1581
+ {
1582
+ type: 'text',
1583
+ text: `Memory "${memoryFileName}" deleted successfully.` + TOOL_GUIDANCE,
1584
+ },
1585
+ ],
1586
+ };
1587
+ }
1588
+ case 'edit_memory': {
1589
+ const memoryFileName = isString(args?.memory_file_name) ? args.memory_file_name : '';
1590
+ const needle = isString(args?.needle) ? args.needle : '';
1591
+ const repl = isString(args?.repl) ? args.repl : '';
1592
+ const mode = isString(args?.mode) ? args.mode : '';
1593
+ if (!memoryFileName || !needle || mode === '') {
1594
+ throw new LanceContextError('memory_file_name, needle, repl, and mode are required', 'validation', { tool: 'edit_memory' });
1595
+ }
1596
+ if (mode !== 'literal' && mode !== 'regex') {
1597
+ throw new LanceContextError('mode must be "literal" or "regex"', 'validation', {
1598
+ tool: 'edit_memory',
1599
+ });
1600
+ }
1601
+ const memoryManager = new MemoryManager(PROJECT_PATH);
1602
+ const result = await memoryManager.editMemory(memoryFileName, needle, repl, mode);
1603
+ return {
1604
+ content: [
1605
+ {
1606
+ type: 'text',
1607
+ text: `Memory "${memoryFileName}" edited. ${result.matchCount} replacement(s) made.` +
1608
+ TOOL_GUIDANCE,
1609
+ },
1610
+ ],
1611
+ };
1612
+ }
1613
+ // --- Symbol Editing Tools ---
1614
+ case 'replace_symbol_body': {
1615
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1616
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1617
+ const body = isString(args?.body) ? args.body : '';
1618
+ if (!namePath || !relativePath || !body) {
1619
+ throw new LanceContextError('name_path, relative_path, and body are required', 'validation', { tool: 'replace_symbol_body' });
1620
+ }
1621
+ const editor = new SymbolEditor(PROJECT_PATH);
1622
+ const result = await editor.replaceSymbolBody({ namePath, relativePath, body });
1623
+ if (!result.success) {
1624
+ return {
1625
+ content: [
1626
+ {
1627
+ type: 'text',
1628
+ text: `Failed to replace symbol: ${result.error}` + TOOL_GUIDANCE,
1629
+ },
1630
+ ],
1631
+ isError: true,
1632
+ };
1633
+ }
1634
+ return {
1635
+ content: [
1636
+ {
1637
+ type: 'text',
1638
+ text: `Symbol "${result.symbolName}" replaced in ${result.filepath}.\n` +
1639
+ `New location: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
1640
+ TOOL_GUIDANCE,
1641
+ },
1642
+ ],
1643
+ };
1644
+ }
1645
+ case 'insert_before_symbol': {
1646
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1647
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1648
+ const body = isString(args?.body) ? args.body : '';
1649
+ if (!namePath || !relativePath || !body) {
1650
+ throw new LanceContextError('name_path, relative_path, and body are required', 'validation', { tool: 'insert_before_symbol' });
1651
+ }
1652
+ const editor = new SymbolEditor(PROJECT_PATH);
1653
+ const result = await editor.insertBeforeSymbol({ namePath, relativePath, body });
1654
+ if (!result.success) {
1655
+ return {
1656
+ content: [
1657
+ {
1658
+ type: 'text',
1659
+ text: `Failed to insert before symbol: ${result.error}` + TOOL_GUIDANCE,
1660
+ },
1661
+ ],
1662
+ isError: true,
1663
+ };
1664
+ }
1665
+ return {
1666
+ content: [
1667
+ {
1668
+ type: 'text',
1669
+ text: `Content inserted before "${result.symbolName}" in ${result.filepath}.\n` +
1670
+ `Inserted at: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
1671
+ TOOL_GUIDANCE,
1672
+ },
1673
+ ],
1674
+ };
1675
+ }
1676
+ case 'insert_after_symbol': {
1677
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1678
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1679
+ const body = isString(args?.body) ? args.body : '';
1680
+ if (!namePath || !relativePath || !body) {
1681
+ throw new LanceContextError('name_path, relative_path, and body are required', 'validation', { tool: 'insert_after_symbol' });
1682
+ }
1683
+ const editor = new SymbolEditor(PROJECT_PATH);
1684
+ const result = await editor.insertAfterSymbol({ namePath, relativePath, body });
1685
+ if (!result.success) {
1686
+ return {
1687
+ content: [
1688
+ {
1689
+ type: 'text',
1690
+ text: `Failed to insert after symbol: ${result.error}` + TOOL_GUIDANCE,
1691
+ },
1692
+ ],
1693
+ isError: true,
1694
+ };
1695
+ }
1696
+ return {
1697
+ content: [
1698
+ {
1699
+ type: 'text',
1700
+ text: `Content inserted after "${result.symbolName}" in ${result.filepath}.\n` +
1701
+ `Inserted at: lines ${result.newRange?.startLine}-${result.newRange?.endLine}` +
1702
+ TOOL_GUIDANCE,
1703
+ },
1704
+ ],
1705
+ };
1706
+ }
1707
+ case 'rename_symbol': {
1708
+ const namePath = isString(args?.name_path) ? args.name_path : '';
1709
+ const relativePath = isString(args?.relative_path) ? args.relative_path : '';
1710
+ const newName = isString(args?.new_name) ? args.new_name : '';
1711
+ const dryRun = isBoolean(args?.dry_run) ? args.dry_run : false;
1712
+ if (!namePath || !relativePath || !newName) {
1713
+ throw new LanceContextError('name_path, relative_path, and new_name are required', 'validation', { tool: 'rename_symbol' });
1714
+ }
1715
+ const renamer = new SymbolRenamer(PROJECT_PATH);
1716
+ const result = await renamer.renameSymbol({ namePath, relativePath, newName, dryRun });
1717
+ if (!result.success) {
1718
+ return {
1719
+ content: [
1720
+ {
1721
+ type: 'text',
1722
+ text: `Failed to rename symbol: ${result.error}` + TOOL_GUIDANCE,
1723
+ },
1724
+ ],
1725
+ isError: true,
1726
+ };
1727
+ }
1728
+ const modeLabel = dryRun ? ' (dry run)' : '';
1729
+ return {
1730
+ content: [
1731
+ {
1732
+ type: 'text',
1733
+ text: formatRenameResult(result) + modeLabel + TOOL_GUIDANCE,
1734
+ },
1735
+ ],
1736
+ };
1737
+ }
1738
+ // --- Worktree Tools ---
1739
+ case 'create_worktree': {
1740
+ const shortName = isString(args?.short_name) ? args.short_name : '';
1741
+ if (!shortName) {
1742
+ throw new LanceContextError('short_name is required', 'validation', {
1743
+ tool: 'create_worktree',
1744
+ });
1745
+ }
1746
+ const worktreeManager = new WorktreeManager(PROJECT_PATH);
1747
+ const result = await worktreeManager.createWorktree({
1748
+ shortName,
1749
+ issueId: isString(args?.issue_id) ? args.issue_id : undefined,
1750
+ prefix: isString(args?.prefix)
1751
+ ? args.prefix
1752
+ : undefined,
1753
+ baseBranch: isString(args?.base_branch) ? args.base_branch : undefined,
1754
+ installDeps: isBoolean(args?.install_deps) ? args.install_deps : true,
1755
+ packageManager: isString(args?.package_manager)
1756
+ ? args.package_manager
1757
+ : undefined,
1758
+ });
1759
+ if (!result.success) {
1760
+ return {
1761
+ content: [
1762
+ {
1763
+ type: 'text',
1764
+ text: `Failed to create worktree: ${result.error}` + TOOL_GUIDANCE,
1765
+ },
1766
+ ],
1767
+ isError: true,
1768
+ };
1769
+ }
1770
+ const worktree = result.worktree;
1771
+ const parts = [];
1772
+ parts.push('## Worktree Created\n');
1773
+ parts.push(`**Name:** ${worktree?.name}`);
1774
+ parts.push(`**Path:** ${worktree?.path}`);
1775
+ parts.push(`**Branch:** ${worktree?.branch}`);
1776
+ if (result.depsInstalled !== undefined) {
1777
+ const depsStatus = result.depsInstalled ? 'installed' : 'skipped/failed';
1778
+ const timeInfo = result.depsInstallTime ? ` (${result.depsInstallTime}ms)` : '';
1779
+ parts.push(`**Dependencies:** ${depsStatus}${timeInfo}`);
1780
+ }
1781
+ parts.push('\n**Usage:** Spawn agent with `cwd: "' + (worktree?.path ?? '') + '"`');
1782
+ return {
1783
+ content: [
1784
+ {
1785
+ type: 'text',
1786
+ text: parts.join('\n') + TOOL_GUIDANCE,
1787
+ },
1788
+ ],
1789
+ };
1790
+ }
1791
+ case 'list_worktrees': {
1792
+ const worktreeManager = new WorktreeManager(PROJECT_PATH);
1793
+ const result = await worktreeManager.listWorktrees();
1794
+ return {
1795
+ content: [
1796
+ {
1797
+ type: 'text',
1798
+ text: formatWorktreeList(result) + TOOL_GUIDANCE,
1799
+ },
1800
+ ],
1801
+ };
1802
+ }
1803
+ case 'remove_worktree': {
1804
+ const worktreeName = isString(args?.name) ? args.name : '';
1805
+ if (!worktreeName) {
1806
+ throw new LanceContextError('name is required', 'validation', {
1807
+ tool: 'remove_worktree',
1808
+ });
1809
+ }
1810
+ const worktreeManager = new WorktreeManager(PROJECT_PATH);
1811
+ const result = await worktreeManager.removeWorktree({
1812
+ name: worktreeName,
1813
+ deleteBranch: isBoolean(args?.delete_branch) ? args.delete_branch : false,
1814
+ force: isBoolean(args?.force) ? args.force : false,
1815
+ });
1816
+ if (!result.success) {
1817
+ return {
1818
+ content: [
1819
+ {
1820
+ type: 'text',
1821
+ text: `Failed to remove worktree: ${result.error}` + TOOL_GUIDANCE,
1822
+ },
1823
+ ],
1824
+ isError: true,
1825
+ };
1826
+ }
1827
+ const parts = [];
1828
+ parts.push('## Worktree Removed\n');
1829
+ parts.push(`**Name:** ${worktreeName}`);
1830
+ if (result.branch) {
1831
+ parts.push(`**Branch:** ${result.branch}`);
1832
+ parts.push(`**Branch deleted:** ${result.branchDeleted ? 'yes' : 'no'}`);
1833
+ }
1834
+ return {
1835
+ content: [
1836
+ {
1837
+ type: 'text',
1838
+ text: parts.join('\n') + TOOL_GUIDANCE,
1839
+ },
1840
+ ],
1841
+ };
1842
+ }
1843
+ case 'worktree_status': {
1844
+ const worktreeName = isString(args?.name) ? args.name : '';
1845
+ if (!worktreeName) {
1846
+ throw new LanceContextError('name is required', 'validation', {
1847
+ tool: 'worktree_status',
1848
+ });
1849
+ }
1850
+ const worktreeManager = new WorktreeManager(PROJECT_PATH);
1851
+ const info = await worktreeManager.getWorktreeInfo(worktreeName);
1852
+ if (!info) {
1853
+ return {
1854
+ content: [
1855
+ {
1856
+ type: 'text',
1857
+ text: `Worktree "${worktreeName}" not found.` + TOOL_GUIDANCE,
1858
+ },
1859
+ ],
1860
+ isError: true,
1861
+ };
1862
+ }
1863
+ return {
1864
+ content: [
1865
+ {
1866
+ type: 'text',
1867
+ text: formatWorktreeInfo(info) + TOOL_GUIDANCE,
138
1868
  },
139
1869
  ],
140
1870
  };
141
1871
  }
142
1872
  default:
143
- throw new Error(`Unknown tool: ${name}`);
1873
+ throw new LanceContextError(`Unknown tool: ${name}`, 'validation', { tool: name });
144
1874
  }
145
1875
  }
146
1876
  catch (error) {
1877
+ // Log full error details server-side for debugging
1878
+ logError(error, name);
147
1879
  return {
148
1880
  content: [
149
1881
  {
150
1882
  type: 'text',
151
- text: `Error: ${error instanceof Error ? error.message : String(error)}`,
1883
+ text: formatErrorResponse(error),
152
1884
  },
153
1885
  ],
154
1886
  isError: true,
@@ -157,10 +1889,104 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
157
1889
  });
158
1890
  // Start server
159
1891
  async function main() {
1892
+ // Check for updates in background (non-blocking)
1893
+ checkForUpdates();
1894
+ // Load config to check if dashboard is enabled
1895
+ const config = await getConfig();
1896
+ const dashboardConfig = getDashboardConfig(config);
1897
+ // Initialize the indexer eagerly so dashboard has data
1898
+ let indexer = null;
1899
+ try {
1900
+ indexer = await getIndexer();
1901
+ }
1902
+ catch (error) {
1903
+ console.error('[lance-context] Failed to initialize indexer:', error);
1904
+ }
1905
+ // Auto-index if project is not yet indexed or backend has changed
1906
+ if (indexer) {
1907
+ const status = await indexer.getStatus();
1908
+ const needsIndex = !status.indexed;
1909
+ const needsReindex = status.indexed && status.backendMismatch;
1910
+ if (needsIndex) {
1911
+ console.error('[lance-context] Project not indexed, starting auto-index...');
1912
+ }
1913
+ else if (needsReindex) {
1914
+ console.error(`[lance-context] ${status.backendMismatchReason}`);
1915
+ console.error('[lance-context] Starting automatic reindex with new backend...');
1916
+ }
1917
+ if (needsIndex || needsReindex) {
1918
+ dashboardState.onIndexingStart();
1919
+ // Run indexing in background so server can start immediately
1920
+ // Force reindex if backend changed to rebuild all vectors
1921
+ indexer
1922
+ .indexCodebase(undefined, undefined, needsReindex, (progress) => {
1923
+ dashboardState.onProgress(progress);
1924
+ })
1925
+ .then((result) => {
1926
+ dashboardState.onIndexingComplete(result);
1927
+ console.error(`[lance-context] Auto-index complete: ${result.filesIndexed} files, ${result.chunksCreated} chunks`);
1928
+ })
1929
+ .catch((error) => {
1930
+ console.error('[lance-context] Auto-index failed:', error);
1931
+ });
1932
+ }
1933
+ }
1934
+ // Start dashboard if enabled
1935
+ if (dashboardConfig.enabled) {
1936
+ const dashboardPort = dashboardConfig.port || 24300;
1937
+ const portAvailable = await isPortAvailable(dashboardPort);
1938
+ if (!portAvailable) {
1939
+ // Another process is already running the dashboard
1940
+ console.error(`[lance-context] Dashboard already running on port ${dashboardPort}`);
1941
+ }
1942
+ else {
1943
+ try {
1944
+ const dashboard = await startDashboard({
1945
+ port: dashboardPort,
1946
+ config,
1947
+ projectPath: PROJECT_PATH,
1948
+ });
1949
+ console.error(`[lance-context] Dashboard started at ${dashboard.url}`);
1950
+ // Open dashboard in user's default browser if configured
1951
+ if (dashboardConfig.openBrowser) {
1952
+ openBrowser(dashboard.url, PROJECT_PATH);
1953
+ }
1954
+ }
1955
+ catch (error) {
1956
+ console.error('[lance-context] Failed to start dashboard:', error);
1957
+ }
1958
+ }
1959
+ }
160
1960
  const transport = new StdioServerTransport();
161
1961
  await server.connect(transport);
162
1962
  console.error('[lance-context] MCP server started');
163
1963
  }
1964
+ /**
1965
+ * Gracefully shutdown the server and cleanup resources
1966
+ */
1967
+ async function shutdown(signal) {
1968
+ console.error(`[lance-context] Received ${signal}, shutting down gracefully...`);
1969
+ try {
1970
+ // Stop the dashboard server
1971
+ await stopDashboard();
1972
+ console.error('[lance-context] Dashboard stopped');
1973
+ }
1974
+ catch (error) {
1975
+ console.error('[lance-context] Error stopping dashboard:', error);
1976
+ }
1977
+ // Close the MCP server connection
1978
+ try {
1979
+ await server.close();
1980
+ console.error('[lance-context] MCP server closed');
1981
+ }
1982
+ catch (error) {
1983
+ console.error('[lance-context] Error closing MCP server:', error);
1984
+ }
1985
+ process.exit(0);
1986
+ }
1987
+ // Register signal handlers for graceful shutdown
1988
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
1989
+ process.on('SIGINT', () => shutdown('SIGINT'));
164
1990
  main().catch((error) => {
165
1991
  console.error('[lance-context] Fatal error:', error);
166
1992
  process.exit(1);