@treedy/vue-lsp-mcp 0.1.5 → 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/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # Vue LSP MCP Server
2
+
3
+ A Model Context Protocol (MCP) server that provides Vue.js code intelligence using **Volar** (`@vue/language-server`).
4
+
5
+ ## Features
6
+
7
+ * **Vue SFC Support**: Intelligent analysis of `.vue` files (Template, Script, Style).
8
+ * **Hybrid Mode**: Supports TypeScript files in Vue projects.
9
+ * **LSP Features**: Hover, Definition, Completion, etc.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install -g @treedy/vue-lsp-mcp
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ Run via command line (stdio):
20
+
21
+ ```bash
22
+ vue-lsp-mcp
23
+ ```
24
+
25
+ Or via npx:
26
+
27
+ ```bash
28
+ npx @treedy/vue-lsp-mcp
29
+ ```
30
+
31
+ ## Requirements
32
+
33
+ Requires a Vue project with `typescript` and `vue` installed in `node_modules`.
package/dist/index.js CHANGED
@@ -19382,46 +19382,71 @@ class StdioServerTransport {
19382
19382
  // src/index.ts
19383
19383
  import * as path3 from "path";
19384
19384
  import * as fs3 from "fs";
19385
- import { createRequire as createRequire2 } from "module";
19385
+ import { createRequire as createRequire3 } from "module";
19386
19386
 
19387
19387
  // src/vue-service.ts
19388
19388
  import * as path from "path";
19389
19389
  import * as fs from "fs";
19390
19390
  import { spawn } from "child_process";
19391
+ import { createRequire as createRequire2 } from "module";
19391
19392
  var documentContents = new Map;
19392
19393
  var documentVersions = new Map;
19393
19394
  var messageId = 0;
19395
+ var activeConnections = new Set;
19396
+ process.on("exit", () => {
19397
+ for (const conn of activeConnections) {
19398
+ try {
19399
+ conn.process.kill();
19400
+ if (conn.tsserver)
19401
+ conn.tsserver.kill();
19402
+ } catch (e) {}
19403
+ }
19404
+ });
19394
19405
  var connectionCache = new Map;
19395
19406
  function spawnTsServer(conn) {
19396
19407
  if (conn.tsserver)
19397
19408
  return;
19398
19409
  const projectRoot = conn.projectRoot;
19399
- const tsserverPaths = [
19400
- path.join(projectRoot, "node_modules", "typescript", "lib", "tsserver.js"),
19401
- path.join(projectRoot, "node_modules", ".bin", "tsserver")
19402
- ];
19410
+ const projectRequire = createRequire2(path.join(projectRoot, "package.json"));
19403
19411
  let tsserverPath = null;
19404
- for (const p of tsserverPaths) {
19405
- if (fs.existsSync(p)) {
19406
- tsserverPath = p;
19407
- break;
19412
+ try {
19413
+ const typescriptPath = projectRequire.resolve("typescript");
19414
+ tsserverPath = path.join(path.dirname(typescriptPath), "tsserver.js");
19415
+ } catch (e) {
19416
+ const commonPaths = [
19417
+ path.join(projectRoot, "node_modules", "typescript", "lib", "tsserver.js"),
19418
+ path.join(projectRoot, "node_modules", ".bin", "tsserver")
19419
+ ];
19420
+ for (const p of commonPaths) {
19421
+ if (fs.existsSync(p)) {
19422
+ tsserverPath = p;
19423
+ break;
19424
+ }
19408
19425
  }
19409
19426
  }
19410
- if (!tsserverPath) {
19427
+ if (!tsserverPath || !fs.existsSync(tsserverPath)) {
19411
19428
  console.error("[DEBUG] tsserver not found, Volar 3.x features may not work");
19412
19429
  return;
19413
19430
  }
19414
- const vuePluginPaths = [
19415
- path.join(projectRoot, "node_modules", "@vue", "typescript-plugin"),
19416
- path.join(projectRoot, "node_modules", "@vue", "language-core", "dist", "languagePlugin.js")
19417
- ];
19418
19431
  let vuePluginPath = null;
19419
- for (const p of vuePluginPaths) {
19420
- if (fs.existsSync(p)) {
19421
- vuePluginPath = p;
19422
- break;
19432
+ try {
19433
+ try {
19434
+ const pkgPath = projectRequire.resolve("@vue/typescript-plugin/package.json");
19435
+ vuePluginPath = path.dirname(pkgPath);
19436
+ } catch {
19437
+ const langCorePath = projectRequire.resolve("@vue/language-core");
19438
+ const commonPluginPaths = [
19439
+ path.join(projectRoot, "node_modules", "@vue", "typescript-plugin"),
19440
+ path.join(projectRoot, "node_modules", "@vue", "language-core", "dist", "languagePlugin.js")
19441
+ ];
19442
+ for (const p of commonPluginPaths) {
19443
+ if (fs.existsSync(p)) {
19444
+ vuePluginPath = p;
19445
+ break;
19446
+ }
19447
+ }
19423
19448
  }
19424
- }
19449
+ } catch (e) {}
19425
19450
  const plugins = vuePluginPath ? [{ name: vuePluginPath }] : [];
19426
19451
  conn.tsserver = spawn("node", [tsserverPath], {
19427
19452
  cwd: projectRoot,
@@ -19438,14 +19463,11 @@ function spawnTsServer(conn) {
19438
19463
  conn.tsserverBuffer += data.toString();
19439
19464
  parseTsServerMessages(conn);
19440
19465
  });
19441
- conn.tsserver.stderr?.on("data", (data) => {
19442
- console.error("[tsserver]", data.toString().trim());
19443
- });
19466
+ conn.tsserver.stderr?.on("data", (data) => {});
19444
19467
  conn.tsserver.on("error", (error2) => {
19445
19468
  console.error("[tsserver] Error:", error2);
19446
19469
  });
19447
19470
  conn.tsserver.on("exit", (code) => {
19448
- console.error(`[tsserver] Exited with code: ${code}`);
19449
19471
  conn.tsserver = undefined;
19450
19472
  });
19451
19473
  const seq = ++conn.tsserverSeq;
@@ -19567,6 +19589,61 @@ ${responseNotification}`;
19567
19589
  }
19568
19590
  }, 30000);
19569
19591
  }
19592
+ var activeWorkspace = null;
19593
+ function setActiveWorkspace(workspace) {
19594
+ activeWorkspace = path.resolve(workspace);
19595
+ return activeWorkspace;
19596
+ }
19597
+ function resolveFilePath(filePath) {
19598
+ const pathObj = path.parse(filePath);
19599
+ let absPath;
19600
+ if (!path.isAbsolute(filePath)) {
19601
+ if (activeWorkspace) {
19602
+ absPath = path.resolve(activeWorkspace, filePath);
19603
+ } else {
19604
+ absPath = path.resolve(filePath);
19605
+ }
19606
+ } else {
19607
+ absPath = filePath;
19608
+ }
19609
+ absPath = path.resolve(absPath);
19610
+ if (activeWorkspace && !absPath.startsWith(activeWorkspace)) {
19611
+ return {
19612
+ absPath: null,
19613
+ error: JSON.stringify({
19614
+ error: "Context Mismatch",
19615
+ message: `The file '${filePath}' resolves to '${absPath}', which is outside the active workspace '${activeWorkspace}'.
19616
+
19617
+ Current Logic:
19618
+ 1. I only analyze files from the active project to ensure accuracy and save resources.
19619
+ 2. You must explicitly switch the workspace if you want to work on a different project.
19620
+
19621
+ Action Required:
19622
+ Please call 'switch_workspace(path="...")' with the new project root before retrying.`,
19623
+ currentWorkspace: activeWorkspace,
19624
+ resolvedPath: absPath
19625
+ })
19626
+ };
19627
+ }
19628
+ return { absPath, error: null };
19629
+ }
19630
+ function validateFileWorkspace(filePath) {
19631
+ const { error: error2 } = resolveFilePath(filePath);
19632
+ return error2;
19633
+ }
19634
+ function clearAllConnections() {
19635
+ for (const conn of activeConnections) {
19636
+ try {
19637
+ conn.process.kill();
19638
+ if (conn.tsserver)
19639
+ conn.tsserver.kill();
19640
+ } catch (e) {}
19641
+ }
19642
+ activeConnections.clear();
19643
+ connectionCache.clear();
19644
+ documentContents.clear();
19645
+ documentVersions.clear();
19646
+ }
19570
19647
  function findProjectRoot(filePath) {
19571
19648
  let dir = path.dirname(path.resolve(filePath));
19572
19649
  while (dir !== path.dirname(dir)) {
@@ -19730,18 +19807,30 @@ async function getConnection(projectRoot) {
19730
19807
  if (connectionCache.has(projectRoot)) {
19731
19808
  return connectionCache.get(projectRoot);
19732
19809
  }
19733
- const vueServerPaths = [
19734
- path.join(projectRoot, "node_modules", "@vue", "language-server", "bin", "vue-language-server.js"),
19735
- path.join(projectRoot, "node_modules", ".bin", "vue-language-server")
19736
- ];
19810
+ const projectRequire = createRequire2(path.join(projectRoot, "package.json"));
19737
19811
  let serverPath = null;
19738
- for (const p of vueServerPaths) {
19739
- if (fs.existsSync(p)) {
19740
- serverPath = p;
19741
- break;
19812
+ let args = [];
19813
+ try {
19814
+ const pkgPath = projectRequire.resolve("@vue/language-server/package.json");
19815
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
19816
+ const binPath = pkg.bin ? typeof pkg.bin === "string" ? pkg.bin : pkg.bin["vue-language-server"] : "bin/vue-language-server.js";
19817
+ serverPath = path.join(path.dirname(pkgPath), binPath);
19818
+ args = ["node", serverPath, "--stdio"];
19819
+ } catch (e) {
19820
+ const possiblePaths = [
19821
+ path.join(projectRoot, "node_modules", "@vue", "language-server", "bin", "vue-language-server.js")
19822
+ ];
19823
+ for (const p of possiblePaths) {
19824
+ if (fs.existsSync(p)) {
19825
+ serverPath = p;
19826
+ args = ["node", serverPath, "--stdio"];
19827
+ break;
19828
+ }
19829
+ }
19830
+ if (!serverPath) {
19831
+ args = ["npx", "@vue/language-server", "--stdio"];
19742
19832
  }
19743
19833
  }
19744
- const args = serverPath ? ["node", serverPath, "--stdio"] : ["npx", "@vue/language-server", "--stdio"];
19745
19834
  const proc = spawn(args[0], args.slice(1), {
19746
19835
  cwd: projectRoot,
19747
19836
  stdio: ["pipe", "pipe", "pipe"],
@@ -19756,29 +19845,34 @@ async function getConnection(projectRoot) {
19756
19845
  openedDocuments: new Set,
19757
19846
  projectRoot
19758
19847
  };
19848
+ activeConnections.add(conn);
19759
19849
  proc.stdout?.on("data", (data) => {
19760
19850
  conn.buffer += data.toString();
19761
19851
  parseMessages(conn);
19762
19852
  });
19763
- proc.stderr?.on("data", (data) => {
19764
- console.error("[vue-language-server]", data.toString());
19765
- });
19853
+ proc.stderr?.on("data", (data) => {});
19766
19854
  proc.on("error", (error2) => {
19767
19855
  console.error("Failed to start vue-language-server:", error2);
19768
19856
  });
19769
19857
  proc.on("exit", (code) => {
19770
19858
  connectionCache.delete(projectRoot);
19859
+ activeConnections.delete(conn);
19771
19860
  });
19772
19861
  connectionCache.set(projectRoot, conn);
19773
- const tsSdkPaths = [
19774
- path.join(projectRoot, "node_modules", "typescript", "lib"),
19775
- path.join(projectRoot, "node_modules", "typescript")
19776
- ];
19777
19862
  let tsSdkPath;
19778
- for (const p of tsSdkPaths) {
19779
- if (fs.existsSync(p)) {
19780
- tsSdkPath = p;
19781
- break;
19863
+ try {
19864
+ const tsPath = projectRequire.resolve("typescript");
19865
+ tsSdkPath = path.dirname(tsPath);
19866
+ } catch {
19867
+ const tsPaths = [
19868
+ path.join(projectRoot, "node_modules", "typescript", "lib"),
19869
+ path.join(projectRoot, "node_modules", "typescript")
19870
+ ];
19871
+ for (const p of tsPaths) {
19872
+ if (fs.existsSync(p)) {
19873
+ tsSdkPath = p;
19874
+ break;
19875
+ }
19782
19876
  }
19783
19877
  }
19784
19878
  await sendMessage(conn, "initialize", {
@@ -19792,7 +19886,13 @@ async function getConnection(projectRoot) {
19792
19886
  signatureHelp: {},
19793
19887
  definition: {},
19794
19888
  references: {},
19795
- publishDiagnostics: {}
19889
+ publishDiagnostics: {},
19890
+ synchronization: {
19891
+ didOpen: true,
19892
+ didChange: true,
19893
+ didSave: true,
19894
+ dynamicRegistration: true
19895
+ }
19796
19896
  },
19797
19897
  workspace: {
19798
19898
  configuration: true,
@@ -19804,7 +19904,7 @@ async function getConnection(projectRoot) {
19804
19904
  tsdk: tsSdkPath || ""
19805
19905
  },
19806
19906
  vue: {
19807
- hybridMode: false
19907
+ hybridMode: true
19808
19908
  }
19809
19909
  },
19810
19910
  workspaceFolders: [
@@ -19836,10 +19936,26 @@ async function ensureDocumentOpen(conn, filePath) {
19836
19936
  });
19837
19937
  conn.openedDocuments.add(absPath);
19838
19938
  }
19839
- function updateDocument(filePath, content) {
19939
+ async function updateDocument(filePath, content) {
19840
19940
  const absPath = path.resolve(filePath);
19941
+ const projectRoot = findProjectRoot(filePath);
19841
19942
  documentContents.set(absPath, content);
19842
- documentVersions.set(absPath, (documentVersions.get(absPath) || 0) + 1);
19943
+ const newVersion = (documentVersions.get(absPath) || 0) + 1;
19944
+ documentVersions.set(absPath, newVersion);
19945
+ if (connectionCache.has(projectRoot)) {
19946
+ const conn = connectionCache.get(projectRoot);
19947
+ if (conn.openedDocuments.has(absPath)) {
19948
+ sendNotification(conn, "textDocument/didChange", {
19949
+ textDocument: {
19950
+ uri: toUri(absPath),
19951
+ version: newVersion
19952
+ },
19953
+ contentChanges: [
19954
+ { text: content }
19955
+ ]
19956
+ });
19957
+ }
19958
+ }
19843
19959
  }
19844
19960
  async function getQuickInfo(filePath, line, column) {
19845
19961
  const projectRoot = findProjectRoot(filePath);
@@ -19998,9 +20114,48 @@ async function getSignatureHelp(filePath, line, column) {
19998
20114
  return null;
19999
20115
  }
20000
20116
  }
20117
+ async function getInlayHints(filePath) {
20118
+ const projectRoot = findProjectRoot(filePath);
20119
+ const conn = await getConnection(projectRoot);
20120
+ await ensureDocumentOpen(conn, filePath);
20121
+ try {
20122
+ const content = getFileContent(filePath);
20123
+ const lines = content.split(`
20124
+ `);
20125
+ const result = await sendMessage(conn, "textDocument/inlayHint", {
20126
+ textDocument: { uri: toUri(filePath) },
20127
+ range: {
20128
+ start: { line: 0, character: 0 },
20129
+ end: { line: lines.length, character: lines[lines.length - 1].length }
20130
+ }
20131
+ });
20132
+ if (!result) {
20133
+ return [];
20134
+ }
20135
+ return Array.isArray(result) ? result : [];
20136
+ } catch (error2) {
20137
+ console.error("Inlay hints error:", error2);
20138
+ return [];
20139
+ }
20140
+ }
20001
20141
  async function getDiagnostics(filePath) {
20002
20142
  const projectRoot = findProjectRoot(filePath);
20003
20143
  const absPath = path.resolve(filePath);
20144
+ try {
20145
+ const conn = await getConnection(projectRoot);
20146
+ await ensureDocumentOpen(conn, absPath);
20147
+ let attempts = 0;
20148
+ while (attempts < 10) {
20149
+ if (conn.diagnosticsCache.has(absPath)) {
20150
+ return conn.diagnosticsCache.get(absPath);
20151
+ }
20152
+ await new Promise((r) => setTimeout(r, 200));
20153
+ attempts++;
20154
+ }
20155
+ return conn.diagnosticsCache.get(absPath) || [];
20156
+ } catch (e) {
20157
+ console.error("LSP diagnostics failed, falling back to vue-tsc spawn:", e);
20158
+ }
20004
20159
  const vueTscPaths = [
20005
20160
  path.join(projectRoot, "node_modules", ".bin", "vue-tsc"),
20006
20161
  path.join(projectRoot, "node_modules", "vue-tsc", "bin", "vue-tsc.js")
@@ -20236,7 +20391,7 @@ async function ensureFileOpen(conn, filePath) {
20236
20391
  await sendCommand(conn, "open", {
20237
20392
  file: absPath,
20238
20393
  fileContent: content,
20239
- scriptKindName: "TS",
20394
+ scriptKindName: "Unknown",
20240
20395
  projectRootPath: conn.projectRoot
20241
20396
  });
20242
20397
  conn.openedFiles.add(absPath);
@@ -20513,46 +20668,90 @@ async function getRenameLocations(filePath, line, column) {
20513
20668
 
20514
20669
  // src/index.ts
20515
20670
  async function getQuickInfo3(file, line, column) {
20516
- const lspResult = await getQuickInfo(file, line, column);
20671
+ const { absPath, error: error2 } = resolveFilePath(file);
20672
+ if (error2 || !absPath)
20673
+ throw new Error(error2 || "Invalid path");
20674
+ const lspResult = await getQuickInfo(absPath, line, column);
20517
20675
  if (lspResult && lspResult.contents) {
20518
20676
  return lspResult;
20519
20677
  }
20520
- return getQuickInfo2(file, line, column);
20678
+ return getQuickInfo2(absPath, line, column);
20521
20679
  }
20522
20680
  async function getDefinition3(file, line, column) {
20523
- const lspResult = await getDefinition(file, line, column);
20681
+ const { absPath, error: error2 } = resolveFilePath(file);
20682
+ if (error2 || !absPath)
20683
+ throw new Error(error2 || "Invalid path");
20684
+ const lspResult = await getDefinition(absPath, line, column);
20524
20685
  if (lspResult && lspResult.length > 0) {
20525
20686
  return lspResult;
20526
20687
  }
20527
- return getDefinition2(file, line, column);
20688
+ return getDefinition2(absPath, line, column);
20528
20689
  }
20529
20690
  async function getReferences3(file, line, column) {
20530
- const lspResult = await getReferences(file, line, column);
20691
+ const { absPath, error: error2 } = resolveFilePath(file);
20692
+ if (error2 || !absPath)
20693
+ throw new Error(error2 || "Invalid path");
20694
+ const lspResult = await getReferences(absPath, line, column);
20531
20695
  if (lspResult && lspResult.length > 0) {
20532
20696
  return lspResult;
20533
20697
  }
20534
- return getReferences2(file, line, column);
20698
+ return getReferences2(absPath, line, column);
20535
20699
  }
20536
20700
  async function getCompletions3(file, line, column, limit = 20) {
20537
- const lspResult = await getCompletions(file, line, column, limit);
20701
+ const { absPath, error: error2 } = resolveFilePath(file);
20702
+ if (error2 || !absPath)
20703
+ throw new Error(error2 || "Invalid path");
20704
+ const lspResult = await getCompletions(absPath, line, column, limit);
20538
20705
  if (lspResult && lspResult.items && lspResult.items.length > 0) {
20539
20706
  return lspResult;
20540
20707
  }
20541
- return getCompletions2(file, line, column, limit);
20708
+ return getCompletions2(absPath, line, column, limit);
20542
20709
  }
20543
20710
  async function getSignatureHelp3(file, line, column) {
20544
- const lspResult = await getSignatureHelp(file, line, column);
20711
+ const { absPath, error: error2 } = resolveFilePath(file);
20712
+ if (error2 || !absPath)
20713
+ throw new Error(error2 || "Invalid path");
20714
+ const lspResult = await getSignatureHelp(absPath, line, column);
20545
20715
  if (lspResult && lspResult.signatures && lspResult.signatures.length > 0) {
20546
20716
  return lspResult;
20547
20717
  }
20548
- return getSignatureHelp2(file, line, column);
20718
+ return getSignatureHelp2(absPath, line, column);
20549
20719
  }
20550
- var require2 = createRequire2(import.meta.url);
20720
+ var require2 = createRequire3(import.meta.url);
20551
20721
  var packageJson = require2("../package.json");
20552
20722
  var server = new McpServer({
20553
20723
  name: "vue-lsp-mcp",
20554
20724
  version: packageJson.version
20555
20725
  });
20726
+ server.tool("switch_workspace", "Switch the active workspace to a new project directory", {
20727
+ path: exports_external.string().describe("Absolute path to the new project root directory")
20728
+ }, async ({ path: inputPath }) => {
20729
+ try {
20730
+ const absPath = path3.resolve(inputPath);
20731
+ if (!fs3.existsSync(absPath) || !fs3.statSync(absPath).isDirectory()) {
20732
+ return {
20733
+ content: [{ type: "text", text: JSON.stringify({ error: "Invalid Path", message: `'${inputPath}' is not a directory.` }) }]
20734
+ };
20735
+ }
20736
+ clearAllConnections();
20737
+ const newWorkspace = setActiveWorkspace(absPath);
20738
+ return {
20739
+ content: [{
20740
+ type: "text",
20741
+ text: JSON.stringify({
20742
+ success: true,
20743
+ message: `Switched active workspace to: ${newWorkspace}`,
20744
+ workspace: newWorkspace,
20745
+ info: "All previous Vue language server connections have been closed."
20746
+ })
20747
+ }]
20748
+ };
20749
+ } catch (error2) {
20750
+ return {
20751
+ content: [{ type: "text", text: JSON.stringify({ error: String(error2) }) }]
20752
+ };
20753
+ }
20754
+ });
20556
20755
  server.tool("hover", "Get type information and documentation at a specific position in a Vue SFC file", {
20557
20756
  file: exports_external.string().describe("Absolute path to the .vue file"),
20558
20757
  line: exports_external.number().int().positive().describe("Line number (1-based)"),
@@ -20668,11 +20867,35 @@ server.tool("signature_help", "Get function signature help at a specific positio
20668
20867
  };
20669
20868
  }
20670
20869
  });
20870
+ server.tool("inlay_hints", "Get inlay hints (type annotations, parameter names) for a Vue SFC file", {
20871
+ file: exports_external.string().describe("Path to the .vue file (absolute or relative to active workspace)")
20872
+ }, async ({ file }) => {
20873
+ try {
20874
+ const { absPath, error: error2 } = resolveFilePath(file);
20875
+ if (error2 || !absPath) {
20876
+ return { content: [{ type: "text", text: error2 || "Invalid path" }] };
20877
+ }
20878
+ const hints = await getInlayHints(absPath);
20879
+ return {
20880
+ content: [{
20881
+ type: "text",
20882
+ text: JSON.stringify({ hints, count: hints.length })
20883
+ }]
20884
+ };
20885
+ } catch (error2) {
20886
+ return {
20887
+ content: [{ type: "text", text: JSON.stringify({ error: String(error2) }) }]
20888
+ };
20889
+ }
20890
+ });
20671
20891
  server.tool("diagnostics", "Get type errors and warnings for Vue SFC files", {
20672
- path: exports_external.string().describe("Path to a .vue file or directory to check")
20892
+ path: exports_external.string().describe("Path to a .vue file or directory to check (absolute or relative to active workspace)")
20673
20893
  }, async ({ path: inputPath }) => {
20674
20894
  try {
20675
- const absPath = path3.resolve(inputPath);
20895
+ const { absPath, error: error2 } = resolveFilePath(inputPath);
20896
+ if (error2 || !absPath) {
20897
+ return { content: [{ type: "text", text: error2 || "Invalid path" }] };
20898
+ }
20676
20899
  const stats = fs3.statSync(absPath);
20677
20900
  let files = [];
20678
20901
  if (stats.isDirectory()) {
@@ -20717,15 +20940,19 @@ server.tool("diagnostics", "Get type errors and warnings for Vue SFC files", {
20717
20940
  }
20718
20941
  });
20719
20942
  server.tool("update_document", "Update Vue file content for incremental analysis without writing to disk", {
20720
- file: exports_external.string().describe("Absolute path to the .vue file"),
20943
+ file: exports_external.string().describe("Path to the .vue file (absolute or relative to active workspace)"),
20721
20944
  content: exports_external.string().describe("New content for the file")
20722
20945
  }, async ({ file, content }) => {
20723
20946
  try {
20724
- updateDocument(file, content);
20947
+ const { absPath, error: error2 } = resolveFilePath(file);
20948
+ if (error2 || !absPath) {
20949
+ return { content: [{ type: "text", text: error2 || "Invalid path" }] };
20950
+ }
20951
+ await updateDocument(absPath, content);
20725
20952
  return {
20726
20953
  content: [{
20727
20954
  type: "text",
20728
- text: JSON.stringify({ success: true, file })
20955
+ text: JSON.stringify({ success: true, file: absPath })
20729
20956
  }]
20730
20957
  };
20731
20958
  } catch (error2) {
@@ -20735,7 +20962,7 @@ server.tool("update_document", "Update Vue file content for incremental analysis
20735
20962
  }
20736
20963
  });
20737
20964
  server.tool("symbols", "Extract symbols (variables, functions, components) from a Vue SFC file", {
20738
- file: exports_external.string().describe("Absolute path to the .vue file"),
20965
+ file: exports_external.string().describe("Path to the .vue file (absolute or relative to active workspace)"),
20739
20966
  query: exports_external.string().optional().describe("Optional filter query for symbol names")
20740
20967
  }, async ({ file, query }) => {
20741
20968
  try {
@@ -20770,7 +20997,11 @@ server.tool("symbols", "Extract symbols (variables, functions, components) from
20770
20997
  }
20771
20998
  }
20772
20999
  };
20773
- const tree = await getDocumentSymbols(file);
21000
+ const { absPath, error: error2 } = resolveFilePath(file);
21001
+ if (error2 || !absPath) {
21002
+ return { content: [{ type: "text", text: error2 || "Invalid path" }] };
21003
+ }
21004
+ const tree = await getDocumentSymbols(absPath);
20774
21005
  if (!tree) {
20775
21006
  return {
20776
21007
  content: [{ type: "text", text: JSON.stringify({ error: "Failed to get symbols" }) }]
@@ -20795,13 +21026,17 @@ server.tool("symbols", "Extract symbols (variables, functions, components) from
20795
21026
  }
20796
21027
  });
20797
21028
  server.tool("rename", "Preview renaming a symbol at a specific position (shows all locations that would be renamed)", {
20798
- file: exports_external.string().describe("Absolute path to the .vue file"),
21029
+ file: exports_external.string().describe("Path to the .vue file (absolute or relative to active workspace)"),
20799
21030
  line: exports_external.number().int().positive().describe("Line number (1-based)"),
20800
21031
  column: exports_external.number().int().positive().describe("Column number (1-based)"),
20801
21032
  newName: exports_external.string().describe("New name for the symbol")
20802
21033
  }, async ({ file, line, column, newName }) => {
20803
21034
  try {
20804
- const locations = await getRenameLocations(file, line, column);
21035
+ const { absPath, error: error2 } = resolveFilePath(file);
21036
+ if (error2 || !absPath) {
21037
+ return { content: [{ type: "text", text: error2 || "Invalid path" }] };
21038
+ }
21039
+ const locations = await getRenameLocations(absPath, line, column);
20805
21040
  if (!locations || locations.length === 0) {
20806
21041
  return {
20807
21042
  content: [{ type: "text", text: JSON.stringify({ error: "Cannot rename symbol at this position" }) }]
@@ -20838,12 +21073,25 @@ server.tool("rename", "Preview renaming a symbol at a specific position (shows a
20838
21073
  });
20839
21074
  server.tool("search", "Search for a pattern in Vue files using ripgrep", {
20840
21075
  pattern: exports_external.string().describe("The regex pattern to search for"),
20841
- path: exports_external.string().optional().describe("Directory or file to search in"),
21076
+ path: exports_external.string().optional().describe("Directory or file to search in (absolute or relative to active workspace)"),
20842
21077
  glob: exports_external.string().optional().describe("Glob pattern to filter files (e.g., '*.vue')"),
20843
21078
  caseSensitive: exports_external.boolean().default(true).describe("Whether the search is case sensitive"),
20844
21079
  maxResults: exports_external.number().int().positive().default(50).describe("Maximum number of results")
20845
21080
  }, async ({ pattern, path: searchPath, glob, caseSensitive, maxResults }) => {
20846
21081
  try {
21082
+ let absSearchPath;
21083
+ if (searchPath) {
21084
+ const { absPath, error: error2 } = validateFileWorkspace(searchPath) ? { absPath: null, error: validateFileWorkspace(searchPath) } : { absPath: path3.resolve(searchPath), error: null };
21085
+ const result2 = resolveFilePath(searchPath);
21086
+ if (result2.error || !result2.absPath) {
21087
+ return { content: [{ type: "text", text: result2.error || "Invalid path" }] };
21088
+ }
21089
+ absSearchPath = result2.absPath;
21090
+ } else {
21091
+ const { absPath } = resolveFilePath(".");
21092
+ if (absPath)
21093
+ absSearchPath = absPath;
21094
+ }
20847
21095
  const { execSync } = await import("child_process");
20848
21096
  const args = ["rg", "--json", "-n"];
20849
21097
  if (!caseSensitive)
@@ -20855,8 +21103,8 @@ server.tool("search", "Search for a pattern in Vue files using ripgrep", {
20855
21103
  }
20856
21104
  args.push("--max-count", maxResults.toString());
20857
21105
  args.push(pattern);
20858
- if (searchPath)
20859
- args.push(searchPath);
21106
+ if (absSearchPath)
21107
+ args.push(absSearchPath);
20860
21108
  const result = execSync(args.join(" "), {
20861
21109
  encoding: "utf-8",
20862
21110
  maxBuffer: 10485760,
@@ -20897,10 +21145,14 @@ server.tool("search", "Search for a pattern in Vue files using ripgrep", {
20897
21145
  }
20898
21146
  });
20899
21147
  server.tool("status", "Check Vue Language Server status for a project", {
20900
- file: exports_external.string().describe("A .vue file path to check the project status for")
21148
+ file: exports_external.string().describe("A .vue file path to check the project status for (absolute or relative to active workspace)")
20901
21149
  }, async ({ file }) => {
20902
21150
  try {
20903
- const status = await getProjectStatus(file);
21151
+ const { absPath, error: error2 } = resolveFilePath(file);
21152
+ if (error2 || !absPath) {
21153
+ return { content: [{ type: "text", text: error2 || "Invalid path" }] };
21154
+ }
21155
+ const status = await getProjectStatus(absPath);
20904
21156
  return {
20905
21157
  content: [{
20906
21158
  type: "text",
@@ -4,6 +4,38 @@
4
4
  * Provides programmatic access to Vue Language Server features
5
5
  * by spawning vue-language-server and communicating via LSP.
6
6
  */
7
+ /**
8
+ * Set the active workspace.
9
+ */
10
+ export declare function setActiveWorkspace(workspace: string): string;
11
+ /**
12
+ * Get the active workspace.
13
+ */
14
+ export declare function getActiveWorkspace(): string | null;
15
+ /**
16
+ * Check if a file is in the active workspace.
17
+ */
18
+ export declare function isFileInWorkspace(filePath: string): boolean;
19
+ /**
20
+ * Resolve a file path and validate it against the active workspace.
21
+ *
22
+ * Supports:
23
+ * 1. Absolute paths (must be within active workspace)
24
+ * 2. Relative paths (resolved against active workspace)
25
+ */
26
+ export declare function resolveFilePath(filePath: string): {
27
+ absPath: string | null;
28
+ error: string | null;
29
+ };
30
+ /**
31
+ * Validate that a file is within the active workspace.
32
+ * @deprecated Use resolveFilePath instead.
33
+ */
34
+ export declare function validateFileWorkspace(filePath: string): string | null;
35
+ /**
36
+ * Clear all connections and caches.
37
+ */
38
+ export declare function clearAllConnections(): void;
7
39
  /**
8
40
  * Find project root by looking for package.json or tsconfig.json
9
41
  */
@@ -21,8 +53,9 @@ export declare function offsetToPosition(content: string, offset: number): {
21
53
  };
22
54
  /**
23
55
  * Update document content for incremental analysis
56
+ * Now properly synchronizes with LSP server
24
57
  */
25
- export declare function updateDocument(filePath: string, content: string): void;
58
+ export declare function updateDocument(filePath: string, content: string): Promise<void>;
26
59
  /**
27
60
  * Create Vue language service for a project
28
61
  */
@@ -71,6 +104,10 @@ export declare function getSignatureHelp(filePath: string, line: number, column:
71
104
  activeSignature: number;
72
105
  activeParameter: number;
73
106
  } | null>;
107
+ /**
108
+ * Get inlay hints for a file
109
+ */
110
+ export declare function getInlayHints(filePath: string): Promise<any[]>;
74
111
  /**
75
112
  * Diagnostic type for internal use
76
113
  */
@@ -91,7 +128,11 @@ interface Diagnostic {
91
128
  source?: string;
92
129
  }
93
130
  /**
94
- * Get diagnostics for a file using vue-tsc
131
+ * Get diagnostics for a file
132
+ *
133
+ * Strategy:
134
+ * 1. For single files, prefer LSP diagnostics (faster, incremental)
135
+ * 2. Fallback to vue-tsc only if LSP unavailable or for project-wide checks (if needed)
95
136
  */
96
137
  export declare function getDiagnostics(filePath: string): Promise<Diagnostic[]>;
97
138
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treedy/vue-lsp-mcp",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },