@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.
Files changed (3) hide show
  1. package/README.md +3 -4
  2. package/dist/index.js +202 -67
  3. 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 `lmTools/requestWorkspaceMCPServer` with `params.cwd`.
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 `lmTools/requestWorkspaceMCPServer` succeeds, the proxy rejects all MCP requests (including `roots/list`) with a workspace-not-ready error.
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
- When the proxy is not ready, it exposes a minimal MCP resource (`lm-tools-bridge-proxy://handshake`) via `resources/list` and also returns `lmTools/requestWorkspaceMCPServer` and `lmTools/status` from `tools/list` so clients can discover the exact method names.
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 = "lmTools/requestWorkspaceMCPServer";
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
- if (!state.workspaceMatched) {
266
- if (message?.method === "resources/list") {
267
- logDebug("resources.list.handshake", { id: message.id ?? null });
268
- if (message.id === void 0 || message.id === null) {
269
- return;
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
- const resultPayload = {
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(resultPayload)}
368
+ import_node_process.default.stdout.write(`${JSON.stringify(resultPayload2)}
286
369
  `);
287
370
  return;
288
371
  }
289
- if (message?.method === "resources/read" && message?.params?.uri === HANDSHAKE_RESOURCE_URI) {
290
- logDebug("resources.read.handshake", { id: message.id ?? null });
291
- if (message.id === void 0 || message.id === null) {
292
- return;
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
- const content = [
295
- "This MCP proxy requires an explicit workspace handshake.",
296
- "Call lmTools/requestWorkspaceMCPServer with params.cwd, wait for ok:true.",
297
- "Then call lmTools/status to confirm online status."
298
- ].join("\n");
299
- const resultPayload = {
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
- contents: [
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(resultPayload)}
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 lmTools/requestWorkspaceMCPServer with params.cwd and wait for success." : "Workspace not set. Call lmTools/requestWorkspaceMCPServer with params.cwd before using MCP."
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 = targetGetter();
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 = targetGetter();
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 buildStatusPayload = async () => {
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiangxiaoxu/lm-tools-bridge-proxy",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Stdio MCP proxy for LM Tools Bridge (Windows Named Pipe resolve).",
5
5
  "license": "MIT",
6
6
  "bin": {