@jiangxiaoxu/lm-tools-bridge-proxy 0.1.0 → 0.1.2
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 +11 -1
- package/dist/index.js +174 -53
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,9 +11,19 @@ args = ["-y", "@jiangxiaoxu/lm-tools-bridge-proxy"]
|
|
|
11
11
|
|
|
12
12
|
The proxy uses the current working directory (`cwd`) to resolve the VS Code instance.
|
|
13
13
|
|
|
14
|
+
## Logging
|
|
15
|
+
|
|
16
|
+
Set `LM_TOOLS_BRIDGE_PROXY_LOG` to a file path to enable log output. If unset, the proxy emits no logs.
|
|
17
|
+
|
|
18
|
+
## Resolve test
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
node ./scripts/resolve-test.mjs --cwd <workspace-path>
|
|
22
|
+
```
|
|
23
|
+
|
|
14
24
|
## Development
|
|
15
25
|
|
|
16
26
|
```
|
|
17
27
|
npm install
|
|
18
28
|
npm run build
|
|
19
|
-
```
|
|
29
|
+
```
|
package/dist/index.js
CHANGED
|
@@ -27,10 +27,15 @@ var import_node_http = __toESM(require("node:http"));
|
|
|
27
27
|
var import_node_process = __toESM(require("node:process"));
|
|
28
28
|
var import_node_crypto = __toESM(require("node:crypto"));
|
|
29
29
|
var import_node_os = __toESM(require("node:os"));
|
|
30
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
30
31
|
var MANAGER_TIMEOUT_MS = 1500;
|
|
31
|
-
var RESOLVE_RETRIES =
|
|
32
|
-
var RESOLVE_RETRY_DELAY_MS =
|
|
33
|
-
var
|
|
32
|
+
var RESOLVE_RETRIES = 10;
|
|
33
|
+
var RESOLVE_RETRY_DELAY_MS = 500;
|
|
34
|
+
var STARTUP_GRACE_MS = 5e3;
|
|
35
|
+
var LOG_ENV = "LM_TOOLS_BRIDGE_PROXY_LOG";
|
|
36
|
+
var ERROR_MANAGER_UNREACHABLE = -32003;
|
|
37
|
+
var ERROR_NO_MATCH = -32004;
|
|
38
|
+
var STARTUP_TIME = Date.now();
|
|
34
39
|
function getUserSeed() {
|
|
35
40
|
return import_node_process.default.env.USERNAME ?? import_node_process.default.env.USERPROFILE ?? import_node_os.default.userInfo().username ?? "default-user";
|
|
36
41
|
}
|
|
@@ -43,15 +48,42 @@ async function delay(ms) {
|
|
|
43
48
|
setTimeout(resolve, ms);
|
|
44
49
|
});
|
|
45
50
|
}
|
|
46
|
-
async function
|
|
47
|
-
|
|
51
|
+
async function resolveTargetWithDeadline(cwd, deadlineMs) {
|
|
52
|
+
let sawNoMatch = false;
|
|
53
|
+
let sawUnreachable = false;
|
|
54
|
+
while (Date.now() < deadlineMs) {
|
|
48
55
|
const result = await managerRequest("POST", "/resolve", { cwd });
|
|
49
56
|
if (result.ok && result.data && result.data.match) {
|
|
50
|
-
return result.data.match;
|
|
57
|
+
return { target: result.data.match, errorKind: void 0 };
|
|
58
|
+
}
|
|
59
|
+
if (result.errorKind === "no_match") {
|
|
60
|
+
sawNoMatch = true;
|
|
61
|
+
} else if (result.errorKind === "unreachable") {
|
|
62
|
+
sawUnreachable = true;
|
|
51
63
|
}
|
|
52
|
-
|
|
64
|
+
const remaining = deadlineMs - Date.now();
|
|
65
|
+
if (remaining <= 0) {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
await delay(Math.min(RESOLVE_RETRY_DELAY_MS, remaining));
|
|
69
|
+
}
|
|
70
|
+
if (sawNoMatch) {
|
|
71
|
+
return { target: void 0, errorKind: "no_match" };
|
|
53
72
|
}
|
|
54
|
-
|
|
73
|
+
if (sawUnreachable) {
|
|
74
|
+
return { target: void 0, errorKind: "unreachable" };
|
|
75
|
+
}
|
|
76
|
+
return { target: void 0, errorKind: "unreachable" };
|
|
77
|
+
}
|
|
78
|
+
async function resolveTarget(cwd) {
|
|
79
|
+
const deadline = Date.now() + RESOLVE_RETRIES * RESOLVE_RETRY_DELAY_MS;
|
|
80
|
+
return resolveTargetWithDeadline(cwd, deadline);
|
|
81
|
+
}
|
|
82
|
+
function isSameTarget(left, right) {
|
|
83
|
+
if (!left || !right) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return left.host === right.host && left.port === right.port;
|
|
55
87
|
}
|
|
56
88
|
async function managerRequest(method, requestPath, body) {
|
|
57
89
|
return new Promise((resolve) => {
|
|
@@ -71,6 +103,10 @@ async function managerRequest(method, requestPath, body) {
|
|
|
71
103
|
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
72
104
|
response.on("end", () => {
|
|
73
105
|
const status = response.statusCode ?? 500;
|
|
106
|
+
if (status === 404) {
|
|
107
|
+
resolve({ ok: false, status, errorKind: "no_match" });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
74
110
|
if (chunks.length === 0) {
|
|
75
111
|
resolve({ ok: status >= 200 && status < 300, status });
|
|
76
112
|
return;
|
|
@@ -90,7 +126,7 @@ async function managerRequest(method, requestPath, body) {
|
|
|
90
126
|
}, MANAGER_TIMEOUT_MS);
|
|
91
127
|
request.on("error", () => {
|
|
92
128
|
clearTimeout(timeout);
|
|
93
|
-
resolve({ ok: false });
|
|
129
|
+
resolve({ ok: false, errorKind: "unreachable" });
|
|
94
130
|
});
|
|
95
131
|
request.on("close", () => {
|
|
96
132
|
clearTimeout(timeout);
|
|
@@ -101,68 +137,151 @@ async function managerRequest(method, requestPath, body) {
|
|
|
101
137
|
request.end();
|
|
102
138
|
});
|
|
103
139
|
}
|
|
104
|
-
function
|
|
105
|
-
|
|
106
|
-
|
|
140
|
+
function getLogPath() {
|
|
141
|
+
return import_node_process.default.env[LOG_ENV];
|
|
142
|
+
}
|
|
143
|
+
function appendLog(message) {
|
|
144
|
+
const logPath = getLogPath();
|
|
145
|
+
if (!logPath) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
import_node_fs.default.appendFileSync(logPath, `${message}
|
|
150
|
+
`, { encoding: "utf8" });
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function createStdioMessageHandler(targetGetter, targetRefresher) {
|
|
155
|
+
return async (message) => {
|
|
107
156
|
const payload = JSON.stringify(message);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
let target = targetGetter();
|
|
158
|
+
if (!target) {
|
|
159
|
+
const now2 = Date.now();
|
|
160
|
+
const graceDeadline2 = STARTUP_TIME + STARTUP_GRACE_MS;
|
|
161
|
+
const refreshResult2 = await targetRefresher(now2 < graceDeadline2 ? graceDeadline2 : void 0);
|
|
162
|
+
target = refreshResult2?.target;
|
|
163
|
+
if (!target) {
|
|
164
|
+
appendLog("No target resolved for MCP proxy.");
|
|
165
|
+
if (message.id === void 0 || message.id === null) {
|
|
166
|
+
return;
|
|
118
167
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
const errorKind = refreshResult2?.errorKind ?? "unreachable";
|
|
169
|
+
const errorPayload2 = {
|
|
170
|
+
jsonrpc: "2.0",
|
|
171
|
+
id: message.id,
|
|
172
|
+
error: {
|
|
173
|
+
code: errorKind === "no_match" ? ERROR_NO_MATCH : ERROR_MANAGER_UNREACHABLE,
|
|
174
|
+
message: errorKind === "no_match" ? "No matching VS Code instance for current workspace." : "Manager unreachable."
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
import_node_process.default.stdout.write(`${JSON.stringify(errorPayload2)}
|
|
127
178
|
`);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const attemptSend = async (sendTarget) => {
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
const targetUrl = new URL(`http://${sendTarget.host}:${sendTarget.port}/mcp`);
|
|
185
|
+
const request = import_node_http.default.request(
|
|
186
|
+
{
|
|
187
|
+
hostname: targetUrl.hostname,
|
|
188
|
+
port: Number(targetUrl.port),
|
|
189
|
+
path: targetUrl.pathname,
|
|
190
|
+
method: "POST",
|
|
191
|
+
headers: {
|
|
192
|
+
"Content-Type": "application/json",
|
|
193
|
+
"Content-Length": Buffer.byteLength(payload)
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
(response) => {
|
|
197
|
+
const chunks = [];
|
|
198
|
+
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
199
|
+
response.on("end", () => {
|
|
200
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
201
|
+
if (text.length > 0) {
|
|
202
|
+
import_node_process.default.stdout.write(`${text}
|
|
203
|
+
`);
|
|
204
|
+
}
|
|
205
|
+
resolve({ ok: true });
|
|
206
|
+
});
|
|
128
207
|
}
|
|
208
|
+
);
|
|
209
|
+
request.on("error", (error) => {
|
|
210
|
+
resolve({ ok: false, error });
|
|
129
211
|
});
|
|
212
|
+
request.write(payload);
|
|
213
|
+
request.end();
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
const firstAttempt = await attemptSend(target);
|
|
217
|
+
if (firstAttempt.ok) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
appendLog(`MCP proxy request failed: ${String(firstAttempt.error)}`);
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
const graceDeadline = STARTUP_TIME + STARTUP_GRACE_MS;
|
|
223
|
+
const refreshResult = await targetRefresher(now < graceDeadline ? graceDeadline : void 0);
|
|
224
|
+
const refreshed = refreshResult?.target;
|
|
225
|
+
if (!refreshed || isSameTarget(target, refreshed)) {
|
|
226
|
+
const errorKind = refreshResult?.errorKind ?? "unreachable";
|
|
227
|
+
if (message.id === void 0 || message.id === null) {
|
|
228
|
+
return;
|
|
130
229
|
}
|
|
131
|
-
|
|
132
|
-
request.on("error", (error) => {
|
|
133
|
-
const errorPayload = {
|
|
230
|
+
const errorPayload2 = {
|
|
134
231
|
jsonrpc: "2.0",
|
|
135
|
-
id: message.id
|
|
232
|
+
id: message.id,
|
|
136
233
|
error: {
|
|
137
|
-
code:
|
|
138
|
-
message:
|
|
234
|
+
code: errorKind === "no_match" ? ERROR_NO_MATCH : ERROR_MANAGER_UNREACHABLE,
|
|
235
|
+
message: errorKind === "no_match" ? "No matching VS Code instance for current workspace." : "Manager unreachable."
|
|
139
236
|
}
|
|
140
237
|
};
|
|
141
|
-
import_node_process.default.stdout.write(`${JSON.stringify(
|
|
238
|
+
import_node_process.default.stdout.write(`${JSON.stringify(errorPayload2)}
|
|
142
239
|
`);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const retryAttempt = await attemptSend(refreshed);
|
|
243
|
+
if (retryAttempt.ok) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
appendLog(`MCP proxy retry failed: ${String(retryAttempt.error)}`);
|
|
247
|
+
if (message.id === void 0 || message.id === null) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
152
250
|
const errorPayload = {
|
|
153
251
|
jsonrpc: "2.0",
|
|
154
|
-
id:
|
|
252
|
+
id: message.id,
|
|
155
253
|
error: {
|
|
156
|
-
code:
|
|
157
|
-
message:
|
|
254
|
+
code: ERROR_MANAGER_UNREACHABLE,
|
|
255
|
+
message: "Manager unreachable."
|
|
158
256
|
}
|
|
159
257
|
};
|
|
160
258
|
import_node_process.default.stdout.write(`${JSON.stringify(errorPayload)}
|
|
161
259
|
`);
|
|
162
|
-
|
|
163
|
-
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async function main() {
|
|
263
|
+
const cwd = import_node_process.default.cwd();
|
|
264
|
+
let currentTargetResult = await resolveTarget(cwd);
|
|
265
|
+
let currentTarget = currentTargetResult.target;
|
|
266
|
+
if (!currentTarget) {
|
|
267
|
+
appendLog(`No VS Code instance registered for cwd: ${cwd}`);
|
|
164
268
|
}
|
|
165
|
-
|
|
269
|
+
let resolveInFlight;
|
|
270
|
+
const refreshTarget = async (deadlineMs) => {
|
|
271
|
+
if (resolveInFlight) {
|
|
272
|
+
return resolveInFlight;
|
|
273
|
+
}
|
|
274
|
+
const resolver = deadlineMs ? resolveTargetWithDeadline(cwd, deadlineMs) : resolveTarget(cwd);
|
|
275
|
+
resolveInFlight = resolver.finally(() => {
|
|
276
|
+
resolveInFlight = void 0;
|
|
277
|
+
});
|
|
278
|
+
const resolved = await resolveInFlight;
|
|
279
|
+
if (resolved?.target) {
|
|
280
|
+
currentTarget = resolved.target;
|
|
281
|
+
}
|
|
282
|
+
return resolved;
|
|
283
|
+
};
|
|
284
|
+
const handler = createStdioMessageHandler(() => currentTarget, refreshTarget);
|
|
166
285
|
let buffer = "";
|
|
167
286
|
import_node_process.default.stdin.setEncoding("utf8");
|
|
168
287
|
import_node_process.default.stdin.on("data", (chunk) => {
|
|
@@ -174,8 +293,9 @@ async function main() {
|
|
|
174
293
|
if (line.length > 0) {
|
|
175
294
|
try {
|
|
176
295
|
const message = JSON.parse(line);
|
|
177
|
-
handler(message);
|
|
296
|
+
void handler(message);
|
|
178
297
|
} catch {
|
|
298
|
+
appendLog("Invalid JSON received by MCP proxy.");
|
|
179
299
|
const errorPayload = {
|
|
180
300
|
jsonrpc: "2.0",
|
|
181
301
|
id: null,
|
|
@@ -196,6 +316,7 @@ async function main() {
|
|
|
196
316
|
});
|
|
197
317
|
}
|
|
198
318
|
main().catch((error) => {
|
|
319
|
+
appendLog(`MCP proxy startup failed: ${String(error)}`);
|
|
199
320
|
const errorPayload = {
|
|
200
321
|
jsonrpc: "2.0",
|
|
201
322
|
id: null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jiangxiaoxu/lm-tools-bridge-proxy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Stdio MCP proxy for LM Tools Bridge (Windows Named Pipe resolve).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -23,4 +23,4 @@
|
|
|
23
23
|
"esbuild": "^0.27.2",
|
|
24
24
|
"typescript": "^5.4.5"
|
|
25
25
|
}
|
|
26
|
-
}
|
|
26
|
+
}
|