@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 +33 -0
- package/dist/index.js +325 -73
- package/dist/vue-service.d.ts +43 -2
- package/package.json +1 -1
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
|
|
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
|
|
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
|
-
|
|
19405
|
-
|
|
19406
|
-
|
|
19407
|
-
|
|
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
|
-
|
|
19420
|
-
|
|
19421
|
-
|
|
19422
|
-
|
|
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
|
|
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
|
-
|
|
19739
|
-
|
|
19740
|
-
|
|
19741
|
-
|
|
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
|
-
|
|
19779
|
-
|
|
19780
|
-
|
|
19781
|
-
|
|
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:
|
|
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
|
-
|
|
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: "
|
|
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
|
|
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(
|
|
20678
|
+
return getQuickInfo2(absPath, line, column);
|
|
20521
20679
|
}
|
|
20522
20680
|
async function getDefinition3(file, line, column) {
|
|
20523
|
-
const
|
|
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(
|
|
20688
|
+
return getDefinition2(absPath, line, column);
|
|
20528
20689
|
}
|
|
20529
20690
|
async function getReferences3(file, line, column) {
|
|
20530
|
-
const
|
|
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(
|
|
20698
|
+
return getReferences2(absPath, line, column);
|
|
20535
20699
|
}
|
|
20536
20700
|
async function getCompletions3(file, line, column, limit = 20) {
|
|
20537
|
-
const
|
|
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(
|
|
20708
|
+
return getCompletions2(absPath, line, column, limit);
|
|
20542
20709
|
}
|
|
20543
20710
|
async function getSignatureHelp3(file, line, column) {
|
|
20544
|
-
const
|
|
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(
|
|
20718
|
+
return getSignatureHelp2(absPath, line, column);
|
|
20549
20719
|
}
|
|
20550
|
-
var require2 =
|
|
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 =
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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
|
|
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("
|
|
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
|
|
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 (
|
|
20859
|
-
args.push(
|
|
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
|
|
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",
|
package/dist/vue-service.d.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|