@akiojin/unity-mcp-server 2.41.7 → 2.41.8
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/package.json +1 -1
- package/src/handlers/script/CodeIndexBuildToolHandler.js +2 -2
- package/src/handlers/script/CodeIndexUpdateToolHandler.js +2 -2
- package/src/handlers/script/ScriptEditSnippetToolHandler.js +2 -2
- package/src/handlers/script/ScriptEditStructuredToolHandler.js +2 -2
- package/src/handlers/script/ScriptRefactorRenameToolHandler.js +2 -2
- package/src/handlers/script/ScriptRefsFindToolHandler.js +2 -2
- package/src/handlers/script/ScriptRemoveSymbolToolHandler.js +2 -2
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +2 -2
- package/src/handlers/script/ScriptSymbolsGetToolHandler.js +2 -2
- package/src/lsp/LspProcessManager.js +9 -0
- package/src/lsp/LspRpcClientSingleton.js +90 -0
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@ import { CodeIndex } from '../../core/codeIndex.js';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
6
|
-
import {
|
|
6
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
7
7
|
import { logger } from '../../core/config.js';
|
|
8
8
|
import { JobManager } from '../../core/jobManager.js';
|
|
9
9
|
|
|
@@ -99,7 +99,7 @@ export class CodeIndexBuildToolHandler extends BaseToolHandler {
|
|
|
99
99
|
// Initialize LSP with error handling
|
|
100
100
|
if (!this.lsp) {
|
|
101
101
|
try {
|
|
102
|
-
this.lsp =
|
|
102
|
+
this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
103
103
|
logger.info(`[index][${job.id}] LSP initialized for project: ${info.projectRoot}`);
|
|
104
104
|
} catch (lspError) {
|
|
105
105
|
logger.error(`[index][${job.id}] LSP initialization failed: ${lspError.message}`);
|
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
4
4
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
5
5
|
import { CodeIndex } from '../../core/codeIndex.js';
|
|
6
|
-
import {
|
|
6
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Incrementally refresh the persistent code index after local edits.
|
|
@@ -60,7 +60,7 @@ export class CodeIndexUpdateToolHandler extends BaseToolHandler {
|
|
|
60
60
|
const projectRoot = info.projectRoot;
|
|
61
61
|
|
|
62
62
|
if (!this.lsp) {
|
|
63
|
-
this.lsp =
|
|
63
|
+
this.lsp = await LspRpcClientSingleton.getInstance(projectRoot);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
const normalized = requested.map(p => this._normalizePath(p, projectRoot));
|
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
5
5
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
6
|
-
import {
|
|
6
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
7
7
|
|
|
8
8
|
const MAX_INSTRUCTIONS = 10;
|
|
9
9
|
const MAX_DIFF_CHARS = 80;
|
|
@@ -258,7 +258,7 @@ export class ScriptEditSnippetToolHandler extends BaseToolHandler {
|
|
|
258
258
|
|
|
259
259
|
async #validateWithLsp(info, relative, updatedText) {
|
|
260
260
|
if (!this.lsp) {
|
|
261
|
-
this.lsp =
|
|
261
|
+
this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
262
262
|
}
|
|
263
263
|
return await this.lsp.validateText(relative, updatedText);
|
|
264
264
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
-
import {
|
|
2
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
3
3
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
4
4
|
|
|
5
5
|
export class ScriptEditStructuredToolHandler extends BaseToolHandler {
|
|
@@ -91,7 +91,7 @@ export class ScriptEditStructuredToolHandler extends BaseToolHandler {
|
|
|
91
91
|
|
|
92
92
|
// Map operations to LSP extensions
|
|
93
93
|
const info = await this.projectInfo.get();
|
|
94
|
-
if (!this.lsp) this.lsp =
|
|
94
|
+
if (!this.lsp) this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
95
95
|
|
|
96
96
|
if (operation === 'replace_body') {
|
|
97
97
|
const resp = await this.lsp.request('mcp/replaceSymbolBody', {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
-
import {
|
|
2
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
3
3
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
4
4
|
|
|
5
5
|
export class ScriptRefactorRenameToolHandler extends BaseToolHandler {
|
|
@@ -38,7 +38,7 @@ export class ScriptRefactorRenameToolHandler extends BaseToolHandler {
|
|
|
38
38
|
async execute(params) {
|
|
39
39
|
const { relative, namePath, newName, preview = true } = params;
|
|
40
40
|
const info = await this.projectInfo.get();
|
|
41
|
-
if (!this.lsp) this.lsp =
|
|
41
|
+
if (!this.lsp) this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
42
42
|
const resp = await this.lsp.request('mcp/renameByNamePath', {
|
|
43
43
|
relative: String(relative).replace(/\\\\/g, '/'),
|
|
44
44
|
namePath: String(namePath),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
2
|
import { CodeIndex } from '../../core/codeIndex.js';
|
|
3
|
-
import {
|
|
3
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
4
4
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
5
5
|
|
|
6
6
|
export class ScriptRefsFindToolHandler extends BaseToolHandler {
|
|
@@ -84,7 +84,7 @@ export class ScriptRefsFindToolHandler extends BaseToolHandler {
|
|
|
84
84
|
|
|
85
85
|
// LSP拡張へ委譲(mcp/referencesByName)
|
|
86
86
|
const info = await this.projectInfo.get();
|
|
87
|
-
if (!this.lsp) this.lsp =
|
|
87
|
+
if (!this.lsp) this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
88
88
|
const resp = await this.lsp.request('mcp/referencesByName', { name: String(name) });
|
|
89
89
|
let raw = Array.isArray(resp?.result) ? resp.result : [];
|
|
90
90
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
|
-
import {
|
|
2
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
3
3
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
4
4
|
|
|
5
5
|
// script_*系に名称統一: シンボル削除(型/メンバー)
|
|
@@ -39,7 +39,7 @@ export class ScriptRemoveSymbolToolHandler extends BaseToolHandler {
|
|
|
39
39
|
removeEmptyFile = false
|
|
40
40
|
} = params;
|
|
41
41
|
const info = await this.projectInfo.get();
|
|
42
|
-
if (!this.lsp) this.lsp =
|
|
42
|
+
if (!this.lsp) this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
43
43
|
const resp = await this.lsp.request('mcp/removeSymbol', {
|
|
44
44
|
relative: String(path).replace(/\\\\/g, '/'),
|
|
45
45
|
namePath: String(namePath),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
2
2
|
import { CodeIndex } from '../../core/codeIndex.js';
|
|
3
|
-
import {
|
|
3
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
4
4
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
5
5
|
|
|
6
6
|
export class ScriptSymbolFindToolHandler extends BaseToolHandler {
|
|
@@ -72,7 +72,7 @@ export class ScriptSymbolFindToolHandler extends BaseToolHandler {
|
|
|
72
72
|
}));
|
|
73
73
|
} else {
|
|
74
74
|
const info = await this.projectInfo.get();
|
|
75
|
-
if (!this.lsp) this.lsp =
|
|
75
|
+
if (!this.lsp) this.lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
76
76
|
const resp = await this.lsp.request('workspace/symbol', { query: String(name) });
|
|
77
77
|
const arr = resp?.result || [];
|
|
78
78
|
const root = String(info.projectRoot || '').replace(/\\\\/g, '/');
|
|
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { BaseToolHandler } from '../base/BaseToolHandler.js';
|
|
4
4
|
import { ProjectInfoProvider } from '../../core/projectInfo.js';
|
|
5
|
+
import { LspRpcClientSingleton } from '../../lsp/LspRpcClientSingleton.js';
|
|
5
6
|
|
|
6
7
|
export class ScriptSymbolsGetToolHandler extends BaseToolHandler {
|
|
7
8
|
constructor(unityConnection) {
|
|
@@ -55,8 +56,7 @@ export class ScriptSymbolsGetToolHandler extends BaseToolHandler {
|
|
|
55
56
|
const abs = path.join(info.projectRoot, relPath);
|
|
56
57
|
const st = await fs.stat(abs).catch(() => null);
|
|
57
58
|
if (!st || !st.isFile()) return { error: 'File not found', path: relPath };
|
|
58
|
-
const
|
|
59
|
-
const lsp = new LspRpcClient(info.projectRoot);
|
|
59
|
+
const lsp = await LspRpcClientSingleton.getInstance(info.projectRoot);
|
|
60
60
|
const uri = 'file://' + abs.replace(/\\\\/g, '/');
|
|
61
61
|
const res = await lsp.request('textDocument/documentSymbol', { textDocument: { uri } });
|
|
62
62
|
const docSymbols = res?.result ?? res ?? [];
|
|
@@ -51,6 +51,15 @@ export class LspProcessManager {
|
|
|
51
51
|
if (!this.state.proc || this.state.proc.killed) return;
|
|
52
52
|
const p = this.state.proc;
|
|
53
53
|
this.state.proc = null;
|
|
54
|
+
|
|
55
|
+
// Remove all listeners to prevent memory leaks
|
|
56
|
+
try {
|
|
57
|
+
if (p.stdout) p.stdout.removeAllListeners();
|
|
58
|
+
if (p.stderr) p.stderr.removeAllListeners();
|
|
59
|
+
p.removeAllListeners('error');
|
|
60
|
+
p.removeAllListeners('close');
|
|
61
|
+
} catch {}
|
|
62
|
+
|
|
54
63
|
try {
|
|
55
64
|
// Send LSP shutdown/exit if possible
|
|
56
65
|
const shutdown = obj => {
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { LspRpcClient } from './LspRpcClient.js';
|
|
2
|
+
import { logger } from '../core/config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Singleton manager for LspRpcClient instances.
|
|
6
|
+
* Maintains one client per projectRoot to enable process reuse and improve performance.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let instance = null;
|
|
10
|
+
let currentProjectRoot = null;
|
|
11
|
+
let heartbeatTimer = null;
|
|
12
|
+
|
|
13
|
+
export class LspRpcClientSingleton {
|
|
14
|
+
/**
|
|
15
|
+
* Get or create a shared LspRpcClient instance.
|
|
16
|
+
* @param {string} projectRoot - Project root path
|
|
17
|
+
* @returns {Promise<LspRpcClient>} Shared client instance
|
|
18
|
+
*/
|
|
19
|
+
static async getInstance(projectRoot) {
|
|
20
|
+
// If projectRoot changed, reset the instance
|
|
21
|
+
if (instance && currentProjectRoot !== projectRoot) {
|
|
22
|
+
logger.info('[LspRpcClientSingleton] projectRoot changed, resetting instance');
|
|
23
|
+
await LspRpcClientSingleton.reset();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!instance) {
|
|
27
|
+
instance = new LspRpcClient(projectRoot);
|
|
28
|
+
currentProjectRoot = projectRoot;
|
|
29
|
+
logger.info(`[LspRpcClientSingleton] created new instance for ${projectRoot}`);
|
|
30
|
+
LspRpcClientSingleton.#startHeartbeat();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return instance;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Reset the singleton instance and stop the process.
|
|
38
|
+
*/
|
|
39
|
+
static async reset() {
|
|
40
|
+
LspRpcClientSingleton.#stopHeartbeat();
|
|
41
|
+
if (instance) {
|
|
42
|
+
try {
|
|
43
|
+
await instance.mgr.stop();
|
|
44
|
+
} catch (e) {
|
|
45
|
+
logger.warn(`[LspRpcClientSingleton] error stopping: ${e.message}`);
|
|
46
|
+
}
|
|
47
|
+
instance = null;
|
|
48
|
+
currentProjectRoot = null;
|
|
49
|
+
logger.info('[LspRpcClientSingleton] instance reset');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start heartbeat monitoring to detect dead processes.
|
|
55
|
+
*/
|
|
56
|
+
static #startHeartbeat() {
|
|
57
|
+
if (heartbeatTimer) return;
|
|
58
|
+
heartbeatTimer = setInterval(async () => {
|
|
59
|
+
if (!instance) return;
|
|
60
|
+
try {
|
|
61
|
+
// Use workspace/symbol with empty query as a lightweight ping
|
|
62
|
+
await instance.request('workspace/symbol', { query: '' });
|
|
63
|
+
} catch (e) {
|
|
64
|
+
logger.warn(`[LspRpcClientSingleton] heartbeat failed: ${e.message}, resetting...`);
|
|
65
|
+
// Process is dead, reset instance for next request
|
|
66
|
+
instance = null;
|
|
67
|
+
currentProjectRoot = null;
|
|
68
|
+
LspRpcClientSingleton.#stopHeartbeat();
|
|
69
|
+
}
|
|
70
|
+
}, 30000); // 30 seconds
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Stop heartbeat monitoring.
|
|
75
|
+
*/
|
|
76
|
+
static #stopHeartbeat() {
|
|
77
|
+
if (heartbeatTimer) {
|
|
78
|
+
clearInterval(heartbeatTimer);
|
|
79
|
+
heartbeatTimer = null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if an instance exists.
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
static hasInstance() {
|
|
88
|
+
return instance !== null;
|
|
89
|
+
}
|
|
90
|
+
}
|