@treedy/lsp-mcp 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19839,6 +19839,7 @@ class StdioServerTransport {
19839
19839
  import { createRequire as createRequire2 } from "module";
19840
19840
  import * as fs2 from "fs";
19841
19841
  import * as path2 from "path";
19842
+ import { execSync } from "child_process";
19842
19843
 
19843
19844
  // src/config.ts
19844
19845
  import * as path from "path";
@@ -21566,6 +21567,18 @@ Analyze Python and TypeScript code for structure, types, and errors.
21566
21567
 
21567
21568
  ## Tools
21568
21569
 
21570
+ ### project_structure
21571
+
21572
+ Get a visual tree structure of the project, highlighting key files.
21573
+
21574
+ \`\`\`
21575
+ project_structure(path=None)
21576
+ \`\`\`
21577
+
21578
+ **Use when:**
21579
+ - Starting a new task to understand project layout
21580
+ - Finding entry points (main.py, package.json)
21581
+
21569
21582
  ### symbols
21570
21583
 
21571
21584
  Extract all symbols (classes, functions, variables) from a file.
@@ -21592,6 +21605,18 @@ diagnostics(path)
21592
21605
  - Validating refactoring didn't break anything
21593
21606
  - Finding issues before running tests
21594
21607
 
21608
+ ### git_diagnostics
21609
+
21610
+ Check for errors ONLY in files changed in Git (working tree + staged).
21611
+
21612
+ \`\`\`
21613
+ git_diagnostics()
21614
+ \`\`\`
21615
+
21616
+ **Use when:**
21617
+ - Fast feedback loop after editing code
21618
+ - "Did I break anything I just touched?"
21619
+
21595
21620
  ### search
21596
21621
 
21597
21622
  Search for patterns across the codebase using regex. Returns positions in LSP format (file, line, column).
@@ -21809,7 +21834,7 @@ function registerPrompts(server) {
21809
21834
  text: `Please explore the project at '${path2 || "current workspace"}'.
21810
21835
 
21811
21836
  Recommended Workflow:
21812
- 1. List files to understand the directory structure (use 'ls' or 'glob').
21837
+ 1. Use 'project_structure' to visualize the directory hierarchy and identify key files.
21813
21838
  2. Identify key entry points (e.g., main.py, index.ts, App.vue, pyproject.toml, package.json).
21814
21839
  3. Use 'summarize_file' on these entry points to extract high-level symbols (classes/functions) without reading full content.
21815
21840
  4. Report back with a structural summary of the project.`
@@ -21829,7 +21854,7 @@ Recommended Workflow:
21829
21854
  text: `Please debug and analyze the file '${file}'.
21830
21855
 
21831
21856
  Recommended Workflow:
21832
- 1. Run 'diagnostics' on the file to identify syntax errors, type mismatches, or unused imports.
21857
+ 1. Run 'diagnostics' on the file (or 'git_diagnostics' to check recent changes) to identify syntax errors, type mismatches, or unused imports.
21833
21858
  2. Use 'read_file_with_hints' to read the content. This will reveal inferred types and parameter names, making it easier to spot logic errors.
21834
21859
  3. If errors are found, check specific locations with 'code_action' to see if auto-fixes (like 'Organize Imports') are available.
21835
21860
  4. Explain the findings and propose fixes.`
@@ -21851,6 +21876,8 @@ Recommended Workflow:
21851
21876
  ### 1. Smart Exploration
21852
21877
  Instead of reading raw code, use:
21853
21878
  \`\`\`
21879
+ project_structure() → See file hierarchy
21880
+ workspace_symbol("User") → Find where "User" class is
21854
21881
  summarize_file(file) → Get outline of classes/functions
21855
21882
  read_file_with_hints(file) → Read code with type/param annotations
21856
21883
  \`\`\`
@@ -21858,14 +21885,15 @@ read_file_with_hints(file) → Read code with type/param annotations
21858
21885
  ### 2. Search → LSP Tools
21859
21886
  \`\`\`
21860
21887
  search("ClassName") → get positions
21888
+ peek_definition(file, line, col) → See definition code immediately
21861
21889
  hover(file, line, column) → get type info
21862
- definition(...) → jump to definition
21863
21890
  references(...) → find usages
21864
21891
  \`\`\`
21865
21892
 
21866
21893
  ### 3. Debug & Fix
21867
21894
  \`\`\`
21868
- diagnostics(path) → Check for errors
21895
+ git_diagnostics() → Check errors in changed files
21896
+ diagnostics(path) → Check full path
21869
21897
  code_action(file, line, col) → Get quick fixes (e.g. Organize Imports)
21870
21898
  run_code_action(...) → Apply fix
21871
21899
  \`\`\`
@@ -21892,6 +21920,106 @@ var server = new McpServer({
21892
21920
  version: packageJson.version
21893
21921
  });
21894
21922
  registerPrompts(server);
21923
+ function getProjectStructure(dirPath, depth = 0, maxDepth = 3) {
21924
+ const IGNORED_DIRS = new Set([".git", "node_modules", "dist", "build", "coverage", "__pycache__", ".venv", ".idea", ".vscode", ".next", ".nuxt"]);
21925
+ const KEY_FILES = new Set(["package.json", "tsconfig.json", "pyproject.toml", "requirements.txt", "README.md", "Dockerfile", "docker-compose.yml", "cargo.toml", "go.mod", "gemfile"]);
21926
+ if (depth > maxDepth)
21927
+ return "";
21928
+ let output = "";
21929
+ let entries;
21930
+ try {
21931
+ entries = fs2.readdirSync(dirPath, { withFileTypes: true });
21932
+ } catch (e) {
21933
+ return "";
21934
+ }
21935
+ entries.sort((a, b) => {
21936
+ if (a.isDirectory() && !b.isDirectory())
21937
+ return -1;
21938
+ if (!a.isDirectory() && b.isDirectory())
21939
+ return 1;
21940
+ return a.name.localeCompare(b.name);
21941
+ });
21942
+ for (const entry of entries) {
21943
+ if (entry.name.startsWith("."))
21944
+ continue;
21945
+ if (IGNORED_DIRS.has(entry.name))
21946
+ continue;
21947
+ const isDir = entry.isDirectory();
21948
+ const indent = " ".repeat(depth);
21949
+ const marker = isDir ? "\uD83D\uDCC1 " : "\uD83D\uDCC4 ";
21950
+ const isKeyFile = KEY_FILES.has(entry.name.toLowerCase());
21951
+ const extra = isKeyFile ? " (config)" : "";
21952
+ output += `${indent}${marker}${entry.name}${extra}
21953
+ `;
21954
+ if (isDir) {
21955
+ output += getProjectStructure(path2.join(dirPath, entry.name), depth + 1, maxDepth);
21956
+ }
21957
+ }
21958
+ return output;
21959
+ }
21960
+ function getGitChangedFiles(cwd) {
21961
+ try {
21962
+ const gitRoot = execSync("git rev-parse --show-toplevel", { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
21963
+ const files = new Set;
21964
+ try {
21965
+ const stdout = execSync("git diff --name-only", { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
21966
+ stdout.split(`
21967
+ `).forEach((f) => {
21968
+ if (f.trim())
21969
+ files.add(path2.resolve(gitRoot, f.trim()));
21970
+ });
21971
+ } catch (e) {}
21972
+ try {
21973
+ const stdout = execSync("git diff --staged --name-only", { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
21974
+ stdout.split(`
21975
+ `).forEach((f) => {
21976
+ if (f.trim())
21977
+ files.add(path2.resolve(gitRoot, f.trim()));
21978
+ });
21979
+ } catch (e) {}
21980
+ return Array.from(files);
21981
+ } catch (error2) {
21982
+ return [];
21983
+ }
21984
+ }
21985
+ function validateAndFixPosition(filePath, line, column) {
21986
+ try {
21987
+ if (!fs2.existsSync(filePath))
21988
+ return { line, column };
21989
+ const stats = fs2.statSync(filePath);
21990
+ if (stats.size > 1024 * 1024)
21991
+ return { line, column };
21992
+ const content = fs2.readFileSync(filePath, "utf-8");
21993
+ const lines = content.split(`
21994
+ `);
21995
+ let newLine = line;
21996
+ let warning = "";
21997
+ if (newLine > lines.length) {
21998
+ newLine = lines.length;
21999
+ warning = `Line ${line} out of bounds (max ${lines.length}). Clamped to ${newLine}.`;
22000
+ }
22001
+ if (newLine < 1) {
22002
+ newLine = 1;
22003
+ warning = `Line ${line} must be positive. Clamped to 1.`;
22004
+ }
22005
+ const lineContent = lines[newLine - 1] || "";
22006
+ let newColumn = column;
22007
+ const maxCol = lineContent.length + 1;
22008
+ if (newColumn > maxCol) {
22009
+ newColumn = maxCol;
22010
+ const w = `Column ${column} out of bounds (max ${maxCol}). Clamped to ${newColumn}.`;
22011
+ warning = warning ? `${warning} ${w}` : w;
22012
+ }
22013
+ if (newColumn < 1) {
22014
+ newColumn = 1;
22015
+ const w = `Column ${column} must be positive. Clamped to 1.`;
22016
+ warning = warning ? `${warning} ${w}` : w;
22017
+ }
22018
+ return { line: newLine, column: newColumn, warning: warning || undefined };
22019
+ } catch (e) {
22020
+ return { line, column };
22021
+ }
22022
+ }
21895
22023
  async function startAndRegisterBackend(language) {
21896
22024
  if (startedBackends.has(language)) {
21897
22025
  const status2 = backendManager.getStatus()[language];
@@ -21953,7 +22081,11 @@ var UNIFIED_TOOLS = [
21953
22081
  { name: "summarize_file", description: "Get a high-level outline of a file (classes, functions, methods) to understand its structure without reading the full content.", schema: { file: exports_external.string() } },
21954
22082
  { name: "read_file_with_hints", description: "Read file content with inlay hints (type annotations, parameter names) inserted as comments. Useful for understanding complex code.", schema: { file: exports_external.string() } },
21955
22083
  { name: "code_action", description: "Get available code actions (refactors and quick fixes) at a specific position", schema: { file: exports_external.string(), line: exports_external.number().int().positive(), column: exports_external.number().int().positive() } },
21956
- { name: "run_code_action", description: "Apply a code action (refactor or quick fix)", schema: { file: exports_external.string(), line: exports_external.number().int().positive(), column: exports_external.number().int().positive(), kind: exports_external.enum(["refactor", "quickfix"]), name: exports_external.string(), actionName: exports_external.string().optional(), preview: exports_external.boolean().default(false).optional() } }
22084
+ { name: "run_code_action", description: "Apply a code action (refactor or quick fix)", schema: { file: exports_external.string(), line: exports_external.number().int().positive(), column: exports_external.number().int().positive(), kind: exports_external.enum(["refactor", "quickfix"]), name: exports_external.string(), actionName: exports_external.string().optional(), preview: exports_external.boolean().default(false).optional() } },
22085
+ { name: "workspace_symbol", description: "Search for a symbol (class, function, etc.) across the entire workspace. Returns locations that can be used with peek_definition.", schema: { query: exports_external.string() } },
22086
+ { name: "peek_definition", description: "Go to definition and return the surrounding code context immediately. Reduces round-trips compared to definition() + read_file().", schema: { file: exports_external.string(), line: exports_external.number().int().positive(), column: exports_external.number().int().positive() } },
22087
+ { name: "project_structure", description: "Get a visual tree structure of the project to understand hierarchy and identify key files. Ignores build artifacts.", schema: { path: exports_external.string().optional() } },
22088
+ { name: "git_diagnostics", description: "Check for errors/warnings ONLY in files changed in Git (working tree + staged). Useful for checking your changes.", schema: {} }
21957
22089
  ];
21958
22090
  function applyInlayHints(content, hints, language) {
21959
22091
  const lines = content.split(`
@@ -22071,23 +22203,116 @@ function preRegisterTools() {
22071
22203
  description: `${tool.description} (unified tool, routes automatically by file extension)`,
22072
22204
  inputSchema: tool.schema
22073
22205
  }, async (args) => {
22206
+ let paramWarning;
22207
+ if (typeof args.line === "number" && typeof args.column === "number") {
22208
+ const targetFile = args.file || args.path;
22209
+ if (targetFile) {
22210
+ let checkPath = targetFile;
22211
+ if (!path2.isAbsolute(checkPath) && activeWorkspacePath) {
22212
+ checkPath = path2.join(activeWorkspacePath, checkPath);
22213
+ } else if (!path2.isAbsolute(checkPath)) {
22214
+ checkPath = path2.resolve(checkPath);
22215
+ }
22216
+ const fixed = validateAndFixPosition(checkPath, args.line, args.column);
22217
+ if (fixed.warning) {
22218
+ console.error(`[lsp-mcp] Auto-corrected params for ${tool.name}: ${fixed.warning}`);
22219
+ args.line = fixed.line;
22220
+ args.column = fixed.column;
22221
+ paramWarning = `(Auto-corrected: ${fixed.warning})`;
22222
+ }
22223
+ }
22224
+ }
22225
+ if (tool.name === "project_structure") {
22226
+ const targetPath = args.path || activeWorkspacePath || process.cwd();
22227
+ const tree = getProjectStructure(targetPath);
22228
+ return {
22229
+ content: [{ type: "text", text: `Project Structure for ${targetPath}:
22230
+
22231
+ ${tree}` }]
22232
+ };
22233
+ }
22234
+ if (tool.name === "git_diagnostics") {
22235
+ const cwd = activeWorkspacePath || process.cwd();
22236
+ const changedFiles = getGitChangedFiles(cwd);
22237
+ if (changedFiles.length === 0) {
22238
+ return { content: [{ type: "text", text: "No changed files found in git." }] };
22239
+ }
22240
+ const results = [];
22241
+ for (const file of changedFiles) {
22242
+ const language2 = inferLanguageFromPath(file, config2);
22243
+ if (!language2)
22244
+ continue;
22245
+ if (!startedBackends.has(language2)) {
22246
+ try {
22247
+ await backendManager.getBackend(language2);
22248
+ startedBackends.add(language2);
22249
+ } catch (e) {
22250
+ results.push(`Could not check ${path2.basename(file)}: Backend failed to start`);
22251
+ continue;
22252
+ }
22253
+ }
22254
+ try {
22255
+ const relativePath = path2.relative(cwd, file);
22256
+ const res = await backendManager.callTool(language2, "diagnostics", { path: relativePath });
22257
+ const parsed = JSON.parse(res.content[0].text);
22258
+ if (parsed.error) {
22259
+ results.push(`⚠️ ${path2.basename(file)}: Backend error: ${parsed.error}`);
22260
+ continue;
22261
+ }
22262
+ let diagnostics = [];
22263
+ if (Array.isArray(parsed))
22264
+ diagnostics = parsed;
22265
+ else if (parsed.diagnostics && Array.isArray(parsed.diagnostics))
22266
+ diagnostics = parsed.diagnostics;
22267
+ else {
22268
+ console.error(`[lsp-mcp] Unexpected diagnostics format for ${file}:`, parsed);
22269
+ results.push(`⚠️ ${path2.basename(file)}: Unexpected response format`);
22270
+ continue;
22271
+ }
22272
+ if (diagnostics.length === 0) {
22273
+ results.push(`✅ ${path2.basename(file)}: No errors`);
22274
+ } else {
22275
+ const errors4 = diagnostics.map((d) => ` - [Line ${d.range?.start?.line ?? d.line ?? "?"}] ${d.message}`).join(`
22276
+ `);
22277
+ results.push(`❌ ${path2.basename(file)}:
22278
+ ${errors4}`);
22279
+ }
22280
+ } catch (e) {
22281
+ results.push(`⚠️ ${path2.basename(file)}: Check failed (${e})`);
22282
+ }
22283
+ }
22284
+ return { content: [{ type: "text", text: `Git Diagnostics Report:
22285
+
22286
+ ${results.join(`
22287
+
22288
+ `)}` }] };
22289
+ }
22074
22290
  const filePath = args.file || args.path;
22075
- if (tool.name === "search" && !filePath) {
22291
+ if ((tool.name === "search" || tool.name === "workspace_symbol") && !filePath) {
22076
22292
  const languages = Object.keys(config2.languages).filter((lang) => config2.languages[lang].enabled);
22077
22293
  const results = [];
22078
22294
  for (const lang of languages) {
22079
22295
  if (startedBackends.has(lang)) {
22080
22296
  try {
22081
- const res = await backendManager.callTool(lang, "search", args);
22082
- results.push(JSON.parse(res.content[0].text));
22297
+ const res = await backendManager.callTool(lang, tool.name, args);
22298
+ const parsed = JSON.parse(res.content[0].text);
22299
+ let items = [];
22300
+ if (Array.isArray(parsed))
22301
+ items = parsed;
22302
+ else if (parsed.matches)
22303
+ items = parsed.matches;
22304
+ else if (parsed.symbols)
22305
+ items = parsed.symbols;
22306
+ if (items.length > 0) {
22307
+ results.push(...items.map((i) => ({ ...i, language: lang })));
22308
+ }
22083
22309
  } catch (e) {}
22084
22310
  }
22085
22311
  }
22086
22312
  if (results.length === 0) {
22087
- return { content: [{ type: "text", text: JSON.stringify({ matches: [], count: 0, message: "No active backends to search in. Please specify a file path to auto-start a backend." }) }] };
22313
+ return { content: [{ type: "text", text: JSON.stringify({ matches: [], count: 0, message: "No matches found or no active backends." }) }] };
22088
22314
  }
22089
- const allMatches = results.flatMap((r) => r.matches || []);
22090
- return { content: [{ type: "text", text: JSON.stringify({ matches: allMatches, count: allMatches.length }) }] };
22315
+ return { content: [{ type: "text", text: JSON.stringify({ matches: results, count: results.length }) }] };
22091
22316
  }
22092
22317
  if (!filePath) {
22093
22318
  return {
@@ -22149,7 +22374,7 @@ function preRegisterTools() {
22149
22374
  };
22150
22375
  }
22151
22376
  }
22152
- if (tool.name !== "summarize_file" && tool.name !== "read_file_with_hints") {
22377
+ if (tool.name !== "summarize_file" && tool.name !== "read_file_with_hints" && tool.name !== "peek_definition") {
22153
22378
  const availableTools = await backendManager.getTools(language);
22154
22379
  const supportsTool = availableTools.some((t) => t.name === tool.name);
22155
22380
  if (!supportsTool) {
@@ -22190,6 +22415,59 @@ ${summary || "(No symbols found)"}`
22190
22415
  };
22191
22416
  }
22192
22417
  }
22418
+ if (tool.name === "peek_definition") {
22419
+ try {
22420
+ const result = await backendManager.callTool(language, "definition", args);
22421
+ const parsed = JSON.parse(result.content[0].text);
22422
+ if (parsed.error) {
22423
+ return { content: [{ type: "text", text: JSON.stringify(parsed) }] };
22424
+ }
22425
+ let locs = Array.isArray(parsed) ? parsed : [parsed];
22426
+ if (parsed.matches)
22427
+ locs = parsed.matches;
22428
+ if (!locs || locs.length === 0) {
22429
+ return { content: [{ type: "text", text: JSON.stringify({ message: "No definition found" }) }] };
22430
+ }
22431
+ const def = locs[0];
22432
+ const defPath = def.file || def.uri;
22433
+ if (!defPath) {
22434
+ return { content: [{ type: "text", text: JSON.stringify({ error: "Invalid definition result", raw: parsed }) }] };
22435
+ }
22436
+ let defAbsPath = defPath;
22437
+ if (!path2.isAbsolute(defPath) && activeWorkspacePath) {
22438
+ defAbsPath = path2.join(activeWorkspacePath, defPath);
22439
+ }
22440
+ if (!fs2.existsSync(defAbsPath)) {
22441
+ return { content: [{ type: "text", text: JSON.stringify({ error: `Definition file not found: ${defAbsPath}`, location: def }) }] };
22442
+ }
22443
+ const fileContent = fs2.readFileSync(defAbsPath, "utf-8");
22444
+ const lines = fileContent.split(`
22445
+ `);
22446
+ const targetLine = def.line;
22447
+ const lineIdx = targetLine - 1;
22448
+ const CONTEXT_LINES = 10;
22449
+ const startIdx = Math.max(0, lineIdx - CONTEXT_LINES);
22450
+ const endIdx = Math.min(lines.length, lineIdx + CONTEXT_LINES + 1);
22451
+ const contextSnippet = lines.slice(startIdx, endIdx).map((line, i) => {
22452
+ const currentLineNum = startIdx + i + 1;
22453
+ const marker = currentLineNum === targetLine ? " >" : " ";
22454
+ return `${marker} ${currentLineNum.toString().padEnd(4)} | ${line}`;
22455
+ }).join(`
22456
+ `);
22457
+ const responseText = `Definition found in ${defPath} at line ${targetLine}:
22458
+
22459
+ ` + "```" + (language === "python" ? "python" : "typescript") + `
22460
+ ` + contextSnippet + `
22461
+ ` + "```";
22462
+ return {
22463
+ content: [{ type: "text", text: responseText }]
22464
+ };
22465
+ } catch (error2) {
22466
+ return {
22467
+ content: [{ type: "text", text: JSON.stringify({ error: `Failed to peek definition: ${error2}` }) }]
22468
+ };
22469
+ }
22470
+ }
22193
22471
  if (tool.name === "read_file_with_hints") {
22194
22472
  try {
22195
22473
  let absPath2 = filePath;
@@ -22310,4 +22588,4 @@ main().catch((error2) => {
22310
22588
  process.exit(1);
22311
22589
  });
22312
22590
 
22313
- //# debugId=B5844EF744B1563264756E2164756E21
22591
+ //# debugId=CC41A27AB80DC0BB64756E2164756E21