@jiangxiaoxu/lm-tools-bridge-proxy 0.1.18 → 0.1.20
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 +3 -4
- package/dist/index.js +202 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,15 +15,14 @@ The proxy uses the current working directory (`cwd`) to resolve the VS Code inst
|
|
|
15
15
|
|
|
16
16
|
Codex does not pass `cwd` into `npx` MCP services, so the proxy requires an explicit workspace handshake before it forwards MCP requests.
|
|
17
17
|
|
|
18
|
-
1. Call `
|
|
18
|
+
1. Call `lmToolsBridgeProxy.requestWorkspaceMCPServer` with `params.cwd`.
|
|
19
19
|
2. Wait for `ok: true` and a resolved target.
|
|
20
|
-
3. Optionally call `lmTools/status` to confirm the target is still online.
|
|
21
20
|
|
|
22
|
-
Until `
|
|
21
|
+
Until `lmToolsBridgeProxy.requestWorkspaceMCPServer` succeeds, the proxy rejects all MCP requests (including `roots/list`) with a workspace-not-ready error.
|
|
23
22
|
|
|
24
23
|
If the target MCP goes offline, the proxy marks itself disconnected and attempts auto-reconnect every second. `lmTools/status` returns `offlineDurationSec` to show how long it has been offline.
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
The proxy always exposes a minimal MCP resource (`lm-tools-bridge-proxy://handshake`) via `resources/list`, and it is pinned to the top of the list. It also returns `lmToolsBridgeProxy.requestWorkspaceMCPServer` from `tools/list` before handshake so clients can discover the exact method name.
|
|
27
26
|
|
|
28
27
|
## Logging
|
|
29
28
|
|
package/dist/index.js
CHANGED
|
@@ -41,8 +41,7 @@ var ERROR_NO_MATCH = -32004;
|
|
|
41
41
|
var ERROR_WORKSPACE_NOT_SET = -32005;
|
|
42
42
|
var ERROR_MCP_OFFLINE = -32006;
|
|
43
43
|
var STARTUP_TIME = Date.now();
|
|
44
|
-
var REQUEST_WORKSPACE_METHOD = "
|
|
45
|
-
var STATUS_METHOD = "lmTools/status";
|
|
44
|
+
var REQUEST_WORKSPACE_METHOD = "lmToolsBridgeProxy.requestWorkspaceMCPServer";
|
|
46
45
|
var TOOLS_LIST_METHOD = "tools/list";
|
|
47
46
|
var TOOLS_CALL_METHOD = "tools/call";
|
|
48
47
|
var INITIALIZE_METHOD = "initialize";
|
|
@@ -259,60 +258,224 @@ function getProxyVersion() {
|
|
|
259
258
|
}
|
|
260
259
|
return "unknown";
|
|
261
260
|
}
|
|
261
|
+
async function requestTargetJson(target, payload) {
|
|
262
|
+
return new Promise((resolve) => {
|
|
263
|
+
const body = JSON.stringify(payload);
|
|
264
|
+
const request = import_node_http.default.request(
|
|
265
|
+
{
|
|
266
|
+
hostname: target.host,
|
|
267
|
+
port: Number(target.port),
|
|
268
|
+
path: "/mcp",
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: {
|
|
271
|
+
Accept: "application/json",
|
|
272
|
+
"Content-Type": "application/json",
|
|
273
|
+
"Content-Length": Buffer.byteLength(body)
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
(response) => {
|
|
277
|
+
const chunks = [];
|
|
278
|
+
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
|
279
|
+
response.on("end", () => {
|
|
280
|
+
try {
|
|
281
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
282
|
+
const parsed = JSON.parse(text);
|
|
283
|
+
resolve({ ok: true, data: parsed });
|
|
284
|
+
} catch {
|
|
285
|
+
resolve({ ok: false });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
request.on("error", () => {
|
|
291
|
+
resolve({ ok: false });
|
|
292
|
+
});
|
|
293
|
+
request.write(body);
|
|
294
|
+
request.end();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
262
297
|
function createStdioMessageHandler(targetGetter, targetRefresher, stateGetter) {
|
|
263
298
|
return async (message) => {
|
|
264
299
|
const state = stateGetter();
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
300
|
+
const getLiveTarget = async () => {
|
|
301
|
+
let target2 = await getLiveTarget();
|
|
302
|
+
if (target2) {
|
|
303
|
+
const health = await checkTargetHealth(target2);
|
|
304
|
+
if (!isHealthOk(health)) {
|
|
305
|
+
workspaceMatched = false;
|
|
306
|
+
currentTarget = void 0;
|
|
307
|
+
if (!offlineSince) {
|
|
308
|
+
offlineSince = Date.now();
|
|
309
|
+
}
|
|
310
|
+
target2 = void 0;
|
|
311
|
+
} else {
|
|
312
|
+
const refreshed3 = await targetRefresher(Date.now() + RESOLVE_RETRY_DELAY_MS);
|
|
313
|
+
return refreshed3?.target ?? target2;
|
|
270
314
|
}
|
|
271
|
-
|
|
315
|
+
}
|
|
316
|
+
const refreshed2 = await targetRefresher();
|
|
317
|
+
return refreshed2?.target;
|
|
318
|
+
};
|
|
319
|
+
if (message?.method === "resources/read" && message?.params?.uri === HANDSHAKE_RESOURCE_URI) {
|
|
320
|
+
logDebug("resources.read.handshake", { id: message.id ?? null });
|
|
321
|
+
if (message.id === void 0 || message.id === null) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
const statusResult = await buildStatusPayload();
|
|
325
|
+
const content = [
|
|
326
|
+
"This MCP proxy requires an explicit workspace handshake.",
|
|
327
|
+
"Call lmToolsBridgeProxy.requestWorkspaceMCPServer with params.cwd, wait for ok:true.",
|
|
328
|
+
"",
|
|
329
|
+
"Status snapshot:",
|
|
330
|
+
JSON.stringify(statusResult.payload, null, 2)
|
|
331
|
+
].join("\n");
|
|
332
|
+
const resultPayload = {
|
|
333
|
+
jsonrpc: "2.0",
|
|
334
|
+
id: message.id,
|
|
335
|
+
result: {
|
|
336
|
+
contents: [
|
|
337
|
+
{
|
|
338
|
+
uri: HANDSHAKE_RESOURCE_URI,
|
|
339
|
+
mimeType: "text/plain",
|
|
340
|
+
text: content
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload)}
|
|
346
|
+
`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (message?.method === "resources/list") {
|
|
350
|
+
logDebug("resources.list.handshake", { id: message.id ?? null });
|
|
351
|
+
if (message.id === void 0 || message.id === null) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const proxyResource = {
|
|
355
|
+
uri: HANDSHAKE_RESOURCE_URI,
|
|
356
|
+
name: "MCP proxy handshake",
|
|
357
|
+
description: "Call lmToolsBridgeProxy.requestWorkspaceMCPServer with params.cwd before using MCP.",
|
|
358
|
+
mimeType: "text/plain"
|
|
359
|
+
};
|
|
360
|
+
if (!state.workspaceMatched) {
|
|
361
|
+
const resultPayload2 = {
|
|
272
362
|
jsonrpc: "2.0",
|
|
273
363
|
id: message.id,
|
|
274
364
|
result: {
|
|
275
|
-
resources: [
|
|
276
|
-
{
|
|
277
|
-
uri: HANDSHAKE_RESOURCE_URI,
|
|
278
|
-
name: "MCP proxy handshake",
|
|
279
|
-
description: "Call lmTools/requestWorkspaceMCPServer with params.cwd before using MCP.",
|
|
280
|
-
mimeType: "text/plain"
|
|
281
|
-
}
|
|
282
|
-
]
|
|
365
|
+
resources: [proxyResource]
|
|
283
366
|
}
|
|
284
367
|
};
|
|
285
|
-
import_node_process.default.stdout.write(`${JSON.stringify(
|
|
368
|
+
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload2)}
|
|
286
369
|
`);
|
|
287
370
|
return;
|
|
288
371
|
}
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
372
|
+
const target2 = await getLiveTarget();
|
|
373
|
+
if (!target2) {
|
|
374
|
+
const resultPayload2 = {
|
|
375
|
+
jsonrpc: "2.0",
|
|
376
|
+
id: message.id,
|
|
377
|
+
result: {
|
|
378
|
+
resources: [proxyResource]
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload2)}
|
|
382
|
+
`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const remote = await requestTargetJson(target2, {
|
|
386
|
+
jsonrpc: "2.0",
|
|
387
|
+
id: message.id,
|
|
388
|
+
method: "resources/list",
|
|
389
|
+
params: message?.params ?? {}
|
|
390
|
+
});
|
|
391
|
+
if (!remote.ok) {
|
|
392
|
+
const retryHealth = await checkTargetHealth(target2);
|
|
393
|
+
if (!isHealthOk(retryHealth)) {
|
|
394
|
+
workspaceMatched = false;
|
|
395
|
+
currentTarget = void 0;
|
|
396
|
+
if (!offlineSince) {
|
|
397
|
+
offlineSince = Date.now();
|
|
398
|
+
}
|
|
293
399
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
400
|
+
}
|
|
401
|
+
const remoteResources = remote.ok && remote.data?.result?.resources ? remote.data.result.resources : [];
|
|
402
|
+
const merged = [
|
|
403
|
+
proxyResource,
|
|
404
|
+
...remoteResources.filter((entry) => {
|
|
405
|
+
const uri = entry?.uri;
|
|
406
|
+
return uri !== HANDSHAKE_RESOURCE_URI;
|
|
407
|
+
})
|
|
408
|
+
];
|
|
409
|
+
const resultPayload = {
|
|
410
|
+
jsonrpc: "2.0",
|
|
411
|
+
id: message.id,
|
|
412
|
+
result: {
|
|
413
|
+
resources: merged
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload)}
|
|
417
|
+
`);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (message?.method === "resources/templates/list") {
|
|
421
|
+
logDebug("resources.templates.list", { id: message.id ?? null });
|
|
422
|
+
if (message.id === void 0 || message.id === null) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (!state.workspaceMatched) {
|
|
426
|
+
const resultPayload2 = {
|
|
300
427
|
jsonrpc: "2.0",
|
|
301
428
|
id: message.id,
|
|
302
429
|
result: {
|
|
303
|
-
|
|
304
|
-
{
|
|
305
|
-
uri: HANDSHAKE_RESOURCE_URI,
|
|
306
|
-
mimeType: "text/plain",
|
|
307
|
-
text: content
|
|
308
|
-
}
|
|
309
|
-
]
|
|
430
|
+
resourceTemplates: []
|
|
310
431
|
}
|
|
311
432
|
};
|
|
312
|
-
import_node_process.default.stdout.write(`${JSON.stringify(
|
|
433
|
+
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload2)}
|
|
434
|
+
`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const target2 = await getLiveTarget();
|
|
438
|
+
if (!target2) {
|
|
439
|
+
const resultPayload2 = {
|
|
440
|
+
jsonrpc: "2.0",
|
|
441
|
+
id: message.id,
|
|
442
|
+
result: {
|
|
443
|
+
resourceTemplates: []
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload2)}
|
|
313
447
|
`);
|
|
314
448
|
return;
|
|
315
449
|
}
|
|
450
|
+
const remote = await requestTargetJson(target2, {
|
|
451
|
+
jsonrpc: "2.0",
|
|
452
|
+
id: message.id,
|
|
453
|
+
method: "resources/templates/list",
|
|
454
|
+
params: message?.params ?? {}
|
|
455
|
+
});
|
|
456
|
+
if (!remote.ok) {
|
|
457
|
+
const retryHealth = await checkTargetHealth(target2);
|
|
458
|
+
if (!isHealthOk(retryHealth)) {
|
|
459
|
+
workspaceMatched = false;
|
|
460
|
+
currentTarget = void 0;
|
|
461
|
+
if (!offlineSince) {
|
|
462
|
+
offlineSince = Date.now();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const remoteTemplates = remote.ok && remote.data?.result?.resourceTemplates ? remote.data.result.resourceTemplates : [];
|
|
467
|
+
const resultPayload = {
|
|
468
|
+
jsonrpc: "2.0",
|
|
469
|
+
id: message.id,
|
|
470
|
+
result: {
|
|
471
|
+
resourceTemplates: remoteTemplates
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload)}
|
|
475
|
+
`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
if (!state.workspaceMatched) {
|
|
316
479
|
if (message.id === void 0 || message.id === null) {
|
|
317
480
|
return;
|
|
318
481
|
}
|
|
@@ -321,7 +484,7 @@ function createStdioMessageHandler(targetGetter, targetRefresher, stateGetter) {
|
|
|
321
484
|
id: message.id,
|
|
322
485
|
error: {
|
|
323
486
|
code: state.workspaceSetExplicitly ? ERROR_NO_MATCH : ERROR_WORKSPACE_NOT_SET,
|
|
324
|
-
message: state.workspaceSetExplicitly ? "Workspace not matched. Call
|
|
487
|
+
message: state.workspaceSetExplicitly ? "Workspace not matched. Call lmToolsBridgeProxy.requestWorkspaceMCPServer with params.cwd and wait for success." : "Workspace not set. Call lmToolsBridgeProxy.requestWorkspaceMCPServer with params.cwd before using MCP."
|
|
325
488
|
}
|
|
326
489
|
};
|
|
327
490
|
if (message?.method === "roots/list") {
|
|
@@ -333,7 +496,7 @@ function createStdioMessageHandler(targetGetter, targetRefresher, stateGetter) {
|
|
|
333
496
|
}
|
|
334
497
|
if (message?.method === "roots/list") {
|
|
335
498
|
logDebug("roots.list.request", { id: message.id ?? null });
|
|
336
|
-
let target2 =
|
|
499
|
+
let target2 = await getLiveTarget();
|
|
337
500
|
if (!target2) {
|
|
338
501
|
const now2 = Date.now();
|
|
339
502
|
const graceDeadline2 = STARTUP_TIME + STARTUP_GRACE_MS;
|
|
@@ -399,7 +562,7 @@ function createStdioMessageHandler(targetGetter, targetRefresher, stateGetter) {
|
|
|
399
562
|
return;
|
|
400
563
|
}
|
|
401
564
|
const payload = JSON.stringify(message);
|
|
402
|
-
let target =
|
|
565
|
+
let target = await getLiveTarget();
|
|
403
566
|
if (!target) {
|
|
404
567
|
const now2 = Date.now();
|
|
405
568
|
const graceDeadline2 = STARTUP_TIME + STARTUP_GRACE_MS;
|
|
@@ -669,14 +832,6 @@ async function main() {
|
|
|
669
832
|
},
|
|
670
833
|
required: ["cwd"]
|
|
671
834
|
}
|
|
672
|
-
},
|
|
673
|
-
{
|
|
674
|
-
name: STATUS_METHOD,
|
|
675
|
-
description: "Get proxy status and online state for the current target.",
|
|
676
|
-
inputSchema: {
|
|
677
|
-
type: "object",
|
|
678
|
-
properties: {}
|
|
679
|
-
}
|
|
680
835
|
}
|
|
681
836
|
];
|
|
682
837
|
const respondToolCall = (id, payload) => {
|
|
@@ -714,7 +869,7 @@ async function main() {
|
|
|
714
869
|
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload)}
|
|
715
870
|
`);
|
|
716
871
|
};
|
|
717
|
-
const
|
|
872
|
+
const buildStatusPayload2 = async () => {
|
|
718
873
|
let resolveResult;
|
|
719
874
|
if (workspaceSetExplicitly && !workspaceMatched2) {
|
|
720
875
|
const deadline = Date.now() + RESOLVE_RETRY_DELAY_MS;
|
|
@@ -908,31 +1063,11 @@ async function main() {
|
|
|
908
1063
|
respondToolCall(message.id, result.payload);
|
|
909
1064
|
return;
|
|
910
1065
|
}
|
|
911
|
-
if (name === STATUS_METHOD) {
|
|
912
|
-
const statusResult = await buildStatusPayload();
|
|
913
|
-
respondToolCall(message.id, statusResult.payload);
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
1066
|
if (!workspaceMatched2) {
|
|
917
1067
|
respondToolError(message.id, -32602, `Unknown tool: ${String(name)}`);
|
|
918
1068
|
return;
|
|
919
1069
|
}
|
|
920
1070
|
}
|
|
921
|
-
if (message?.method === STATUS_METHOD) {
|
|
922
|
-
logDebug("status.request", { id: message.id ?? null });
|
|
923
|
-
const statusResult = await buildStatusPayload();
|
|
924
|
-
logDebug("status.state", statusResult.debug);
|
|
925
|
-
if (message.id !== void 0 && message.id !== null) {
|
|
926
|
-
const resultPayload = {
|
|
927
|
-
jsonrpc: "2.0",
|
|
928
|
-
id: message.id,
|
|
929
|
-
result: statusResult.payload
|
|
930
|
-
};
|
|
931
|
-
import_node_process.default.stdout.write(`${JSON.stringify(resultPayload)}
|
|
932
|
-
`);
|
|
933
|
-
}
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
1071
|
if (message?.method === REQUEST_WORKSPACE_METHOD) {
|
|
937
1072
|
logDebug("requestWorkspaceMCPServer.request", { id: message.id ?? null, cwd: message?.params?.cwd ?? null });
|
|
938
1073
|
const result = await handleRequestWorkspace(message?.params?.cwd);
|