lensmcp 1.17.4 → 1.18.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/bundled/dashboard.js +6 -0
- package/bundled/main.js +111 -1
- package/bundled/shim.js +270 -109
- package/lib/mcp-mode.d.ts +13 -2
- package/lib/mcp-mode.d.ts.map +1 -1
- package/lib/mcp-mode.js +13 -2
- package/lib/workspace-scope.d.ts +22 -0
- package/lib/workspace-scope.d.ts.map +1 -1
- package/lib/workspace-scope.js +10 -0
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
package/bundled/dashboard.js
CHANGED
|
@@ -17435,9 +17435,14 @@ var MAX_ERRORS = 50;
|
|
|
17435
17435
|
var STATE_BY_KIND = {
|
|
17436
17436
|
"service-starting": "starting",
|
|
17437
17437
|
"service-respawn": "starting",
|
|
17438
|
+
// A lens frontend's vite has been SPAWNED — started, not yet proven to serve.
|
|
17439
|
+
// The gateway's liveness probe emits `service-up` once its port accepts, so
|
|
17440
|
+
// this must claim `starting` and never `up`.
|
|
17441
|
+
"lens-app-up": "starting",
|
|
17438
17442
|
"service-up": "up",
|
|
17439
17443
|
"cold-start": "up",
|
|
17440
17444
|
"service-down": "down",
|
|
17445
|
+
"idle-kill": "idle",
|
|
17441
17446
|
"service-recycle": "wedged",
|
|
17442
17447
|
"pod-recycle": "wedged",
|
|
17443
17448
|
"service-crash-loop": "crash-looping"
|
|
@@ -17494,6 +17499,7 @@ var ClusterHealth = class {
|
|
|
17494
17499
|
down: byState("down"),
|
|
17495
17500
|
wedged: byState("wedged"),
|
|
17496
17501
|
crashLooping: byState("crash-looping"),
|
|
17502
|
+
idle: byState("idle"),
|
|
17497
17503
|
buildFailed: services.filter((s) => s.buildFailed).map((s) => s.project),
|
|
17498
17504
|
restarts: services.reduce((n, s) => n + s.restarts, 0),
|
|
17499
17505
|
errors: this.errors.slice()
|
package/bundled/main.js
CHANGED
|
@@ -17437,9 +17437,14 @@ var MAX_ERRORS = 50;
|
|
|
17437
17437
|
var STATE_BY_KIND = {
|
|
17438
17438
|
"service-starting": "starting",
|
|
17439
17439
|
"service-respawn": "starting",
|
|
17440
|
+
// A lens frontend's vite has been SPAWNED — started, not yet proven to serve.
|
|
17441
|
+
// The gateway's liveness probe emits `service-up` once its port accepts, so
|
|
17442
|
+
// this must claim `starting` and never `up`.
|
|
17443
|
+
"lens-app-up": "starting",
|
|
17440
17444
|
"service-up": "up",
|
|
17441
17445
|
"cold-start": "up",
|
|
17442
17446
|
"service-down": "down",
|
|
17447
|
+
"idle-kill": "idle",
|
|
17443
17448
|
"service-recycle": "wedged",
|
|
17444
17449
|
"pod-recycle": "wedged",
|
|
17445
17450
|
"service-crash-loop": "crash-looping"
|
|
@@ -17496,6 +17501,7 @@ var ClusterHealth = class {
|
|
|
17496
17501
|
down: byState("down"),
|
|
17497
17502
|
wedged: byState("wedged"),
|
|
17498
17503
|
crashLooping: byState("crash-looping"),
|
|
17504
|
+
idle: byState("idle"),
|
|
17499
17505
|
buildFailed: services.filter((s) => s.buildFailed).map((s) => s.project),
|
|
17500
17506
|
restarts: services.reduce((n, s) => n + s.restarts, 0),
|
|
17501
17507
|
errors: this.errors.slice()
|
|
@@ -21901,6 +21907,92 @@ function watchParentGateway(label) {
|
|
|
21901
21907
|
timer.unref();
|
|
21902
21908
|
}
|
|
21903
21909
|
|
|
21910
|
+
// servers/lensmcp-mcp/dist/shim/workspace.js
|
|
21911
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, realpathSync } from "node:fs";
|
|
21912
|
+
import { basename, dirname as dirname5, join as join5 } from "node:path";
|
|
21913
|
+
var BASE_PORTS = { dashboard: 4321, mcpHttp: 4500, bridge: 5747 };
|
|
21914
|
+
function readJson(path) {
|
|
21915
|
+
try {
|
|
21916
|
+
return JSON.parse(readFileSync5(path, "utf8"));
|
|
21917
|
+
} catch {
|
|
21918
|
+
return void 0;
|
|
21919
|
+
}
|
|
21920
|
+
}
|
|
21921
|
+
function hashToRange(s, range) {
|
|
21922
|
+
let h = 2166136261;
|
|
21923
|
+
for (let i = 0; i < s.length; i++) {
|
|
21924
|
+
h ^= s.charCodeAt(i);
|
|
21925
|
+
h = Math.imul(h, 16777619);
|
|
21926
|
+
}
|
|
21927
|
+
return Math.abs(h | 0) % range;
|
|
21928
|
+
}
|
|
21929
|
+
function slugify2(input) {
|
|
21930
|
+
return input.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "workspace";
|
|
21931
|
+
}
|
|
21932
|
+
function resolveWorkspaceKey(root) {
|
|
21933
|
+
const nx = readJson(join5(root, "nx.json"));
|
|
21934
|
+
if (nx?.lensmcp?.key)
|
|
21935
|
+
return slugify2(nx.lensmcp.key);
|
|
21936
|
+
const pkg = readJson(join5(root, "package.json"));
|
|
21937
|
+
if (pkg?.name)
|
|
21938
|
+
return slugify2(pkg.name);
|
|
21939
|
+
if (nx?.name)
|
|
21940
|
+
return slugify2(nx.name);
|
|
21941
|
+
return slugify2(basename(root));
|
|
21942
|
+
}
|
|
21943
|
+
function deriveMcpHttpPort(key) {
|
|
21944
|
+
return BASE_PORTS.mcpHttp + hashToRange(key, 200);
|
|
21945
|
+
}
|
|
21946
|
+
function findWorkspaceRoot(start) {
|
|
21947
|
+
let nxFallback;
|
|
21948
|
+
let dir = start;
|
|
21949
|
+
for (; ; ) {
|
|
21950
|
+
if (existsSync7(join5(dir, ".lensmcp", "config.json")))
|
|
21951
|
+
return dir;
|
|
21952
|
+
if (nxFallback === void 0 && existsSync7(join5(dir, "nx.json")))
|
|
21953
|
+
nxFallback = dir;
|
|
21954
|
+
const parent = dirname5(dir);
|
|
21955
|
+
if (parent === dir)
|
|
21956
|
+
break;
|
|
21957
|
+
dir = parent;
|
|
21958
|
+
}
|
|
21959
|
+
return nxFallback ?? start;
|
|
21960
|
+
}
|
|
21961
|
+
function resolveWorkspaceScope(start, env = process.env) {
|
|
21962
|
+
const root = findWorkspaceRoot(start);
|
|
21963
|
+
const file2 = readJson(join5(root, ".lensmcp", "config.json"));
|
|
21964
|
+
const key = file2?.key ? slugify2(file2.key) : resolveWorkspaceKey(root);
|
|
21965
|
+
const mcpHttpPort = typeof file2?.ports?.mcpHttp === "number" ? file2.ports.mcpHttp : deriveMcpHttpPort(key);
|
|
21966
|
+
return {
|
|
21967
|
+
root,
|
|
21968
|
+
key,
|
|
21969
|
+
mcpHttpPort,
|
|
21970
|
+
eventFile: env["LENSMCP_EVENT_FILE"] ?? join5(root, ".lensmcp", "events.jsonl")
|
|
21971
|
+
};
|
|
21972
|
+
}
|
|
21973
|
+
var IDENTITY_PATH = "/lensmcp/identity";
|
|
21974
|
+
var IDENTITY_KIND = "lensmcp-mcp-identity";
|
|
21975
|
+
function canonicalRoot(path) {
|
|
21976
|
+
try {
|
|
21977
|
+
return realpathSync(path);
|
|
21978
|
+
} catch {
|
|
21979
|
+
return path;
|
|
21980
|
+
}
|
|
21981
|
+
}
|
|
21982
|
+
function resolveServerIdentity(env = process.env, cwd = process.cwd()) {
|
|
21983
|
+
const scope = resolveWorkspaceScope(cwd, env);
|
|
21984
|
+
const root = canonicalRoot(env["LENSMCP_WORKSPACE_ROOT"] ?? scope.root);
|
|
21985
|
+
const key = env["LENSMCP_WORKSPACE_KEY"] ? slugify2(env["LENSMCP_WORKSPACE_KEY"]) : env["LENSMCP_WORKSPACE_ROOT"] ? resolveWorkspaceScope(root, env).key : scope.key;
|
|
21986
|
+
return {
|
|
21987
|
+
kind: IDENTITY_KIND,
|
|
21988
|
+
key,
|
|
21989
|
+
root,
|
|
21990
|
+
eventFile: scope.eventFile,
|
|
21991
|
+
pid: process.pid,
|
|
21992
|
+
port: env["LENSMCP_PORT"] ? Number(env["LENSMCP_PORT"]) : void 0
|
|
21993
|
+
};
|
|
21994
|
+
}
|
|
21995
|
+
|
|
21904
21996
|
// servers/lensmcp-mcp/dist/main.js
|
|
21905
21997
|
watchParentGateway("mcp");
|
|
21906
21998
|
var { session, providers } = createLensSession();
|
|
@@ -21950,7 +22042,25 @@ if (transport === "stdio") {
|
|
|
21950
22042
|
});
|
|
21951
22043
|
} else {
|
|
21952
22044
|
const entryPath = process.env["LENSMCP_HTTP_PATH"] ?? "/mcp";
|
|
21953
|
-
const
|
|
22045
|
+
const identity = resolveServerIdentity();
|
|
22046
|
+
const identityRoute = {
|
|
22047
|
+
method: "GET",
|
|
22048
|
+
path: IDENTITY_PATH,
|
|
22049
|
+
handler: (_req, res) => {
|
|
22050
|
+
res.setHeader("content-type", "application/json");
|
|
22051
|
+
res.setHeader("cache-control", "no-store");
|
|
22052
|
+
res.end(JSON.stringify(identity));
|
|
22053
|
+
}
|
|
22054
|
+
};
|
|
22055
|
+
const http = transport === "socket" ? {
|
|
22056
|
+
socketPath: process.env["LENSMCP_SOCKET_PATH"] ?? "/tmp/lensmcp-mcp.sock",
|
|
22057
|
+
entryPath,
|
|
22058
|
+
routes: [identityRoute]
|
|
22059
|
+
} : {
|
|
22060
|
+
port: Number(process.env["LENSMCP_PORT"] ?? 3e3),
|
|
22061
|
+
entryPath,
|
|
22062
|
+
routes: [identityRoute]
|
|
22063
|
+
};
|
|
21954
22064
|
void FrontMcpInstance.bootstrap({
|
|
21955
22065
|
...serverConfig,
|
|
21956
22066
|
logging: { level: LogLevel.Info },
|
package/bundled/shim.js
CHANGED
|
@@ -8485,8 +8485,17 @@ async function ensureSharedServer(deps) {
|
|
|
8485
8485
|
const poll = deps.pollMs ?? READY_POLL_MS;
|
|
8486
8486
|
const ttl = deps.lockTtlMs ?? SPAWN_LOCK_TTL_MS;
|
|
8487
8487
|
const elapsed = () => deps.now() - started;
|
|
8488
|
+
const live = (outcome) => {
|
|
8489
|
+
const foreign = deps.verifyIdentity?.();
|
|
8490
|
+
if (foreign)
|
|
8491
|
+
return { outcome: "unavailable", reason: foreign, waitedMs: elapsed() };
|
|
8492
|
+
return { outcome, waitedMs: elapsed() };
|
|
8493
|
+
};
|
|
8488
8494
|
if (await deps.probe())
|
|
8489
|
-
return
|
|
8495
|
+
return live("already-running");
|
|
8496
|
+
const taken = await deps.foreignPortOwner?.();
|
|
8497
|
+
if (taken)
|
|
8498
|
+
return { outcome: "unavailable", reason: taken, waitedMs: elapsed() };
|
|
8490
8499
|
let holdsLock = deps.tryCreateLock(JSON.stringify({ pid: process.pid, at: deps.now() }));
|
|
8491
8500
|
if (!holdsLock) {
|
|
8492
8501
|
const existing = parseSpawnLock(deps.readLock());
|
|
@@ -8507,7 +8516,7 @@ async function ensureSharedServer(deps) {
|
|
|
8507
8516
|
while (elapsed() < timeout) {
|
|
8508
8517
|
await deps.sleep(poll);
|
|
8509
8518
|
if (await deps.probe())
|
|
8510
|
-
return
|
|
8519
|
+
return live("started");
|
|
8511
8520
|
if (deps.serverExited?.()) {
|
|
8512
8521
|
return {
|
|
8513
8522
|
outcome: "unavailable",
|
|
@@ -8528,7 +8537,7 @@ async function ensureSharedServer(deps) {
|
|
|
8528
8537
|
while (elapsed() < timeout) {
|
|
8529
8538
|
await deps.sleep(poll);
|
|
8530
8539
|
if (await deps.probe())
|
|
8531
|
-
return
|
|
8540
|
+
return live("waited");
|
|
8532
8541
|
const lock = parseSpawnLock(deps.readLock());
|
|
8533
8542
|
if (!isSpawnLockHeld(lock, deps.now(), deps.isAlive, ttl) && !await deps.probe()) {
|
|
8534
8543
|
return {
|
|
@@ -8590,7 +8599,9 @@ function startSharedServerWatched(opts) {
|
|
|
8590
8599
|
LENSMCP_TRANSPORT: "http",
|
|
8591
8600
|
LENSMCP_PORT: String(opts.port),
|
|
8592
8601
|
LENSMCP_EVENT_FILE: opts.eventFile,
|
|
8593
|
-
LENSMCP_SHARED_OWNER: "shim"
|
|
8602
|
+
LENSMCP_SHARED_OWNER: "shim",
|
|
8603
|
+
...opts.workspaceKey ? { LENSMCP_WORKSPACE_KEY: opts.workspaceKey } : {},
|
|
8604
|
+
...opts.workspaceRoot ? { LENSMCP_WORKSPACE_ROOT: opts.workspaceRoot } : {}
|
|
8594
8605
|
};
|
|
8595
8606
|
delete env["LENSMCP_PARENT_PID"];
|
|
8596
8607
|
const child = spawn(process.execPath, [opts.serverEntry], {
|
|
@@ -8616,6 +8627,191 @@ function startSharedServerWatched(opts) {
|
|
|
8616
8627
|
}
|
|
8617
8628
|
}
|
|
8618
8629
|
|
|
8630
|
+
// servers/lensmcp-mcp/dist/shim/probe.js
|
|
8631
|
+
import { connect } from "node:net";
|
|
8632
|
+
|
|
8633
|
+
// servers/lensmcp-mcp/dist/shim/workspace.js
|
|
8634
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, realpathSync } from "node:fs";
|
|
8635
|
+
import { basename, dirname, join as join2 } from "node:path";
|
|
8636
|
+
var BASE_PORTS = { dashboard: 4321, mcpHttp: 4500, bridge: 5747 };
|
|
8637
|
+
function readJson(path) {
|
|
8638
|
+
try {
|
|
8639
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
8640
|
+
} catch {
|
|
8641
|
+
return void 0;
|
|
8642
|
+
}
|
|
8643
|
+
}
|
|
8644
|
+
function hashToRange(s, range) {
|
|
8645
|
+
let h = 2166136261;
|
|
8646
|
+
for (let i = 0; i < s.length; i++) {
|
|
8647
|
+
h ^= s.charCodeAt(i);
|
|
8648
|
+
h = Math.imul(h, 16777619);
|
|
8649
|
+
}
|
|
8650
|
+
return Math.abs(h | 0) % range;
|
|
8651
|
+
}
|
|
8652
|
+
function slugify2(input) {
|
|
8653
|
+
return input.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "workspace";
|
|
8654
|
+
}
|
|
8655
|
+
function resolveWorkspaceKey(root) {
|
|
8656
|
+
const nx = readJson(join2(root, "nx.json"));
|
|
8657
|
+
if (nx?.lensmcp?.key)
|
|
8658
|
+
return slugify2(nx.lensmcp.key);
|
|
8659
|
+
const pkg = readJson(join2(root, "package.json"));
|
|
8660
|
+
if (pkg?.name)
|
|
8661
|
+
return slugify2(pkg.name);
|
|
8662
|
+
if (nx?.name)
|
|
8663
|
+
return slugify2(nx.name);
|
|
8664
|
+
return slugify2(basename(root));
|
|
8665
|
+
}
|
|
8666
|
+
function deriveMcpHttpPort(key) {
|
|
8667
|
+
return BASE_PORTS.mcpHttp + hashToRange(key, 200);
|
|
8668
|
+
}
|
|
8669
|
+
function findWorkspaceRoot(start) {
|
|
8670
|
+
let nxFallback;
|
|
8671
|
+
let dir = start;
|
|
8672
|
+
for (; ; ) {
|
|
8673
|
+
if (existsSync2(join2(dir, ".lensmcp", "config.json")))
|
|
8674
|
+
return dir;
|
|
8675
|
+
if (nxFallback === void 0 && existsSync2(join2(dir, "nx.json")))
|
|
8676
|
+
nxFallback = dir;
|
|
8677
|
+
const parent = dirname(dir);
|
|
8678
|
+
if (parent === dir)
|
|
8679
|
+
break;
|
|
8680
|
+
dir = parent;
|
|
8681
|
+
}
|
|
8682
|
+
return nxFallback ?? start;
|
|
8683
|
+
}
|
|
8684
|
+
function resolveWorkspaceScope(start, env = process.env) {
|
|
8685
|
+
const root = findWorkspaceRoot(start);
|
|
8686
|
+
const file = readJson(join2(root, ".lensmcp", "config.json"));
|
|
8687
|
+
const key = file?.key ? slugify2(file.key) : resolveWorkspaceKey(root);
|
|
8688
|
+
const mcpHttpPort = typeof file?.ports?.mcpHttp === "number" ? file.ports.mcpHttp : deriveMcpHttpPort(key);
|
|
8689
|
+
return {
|
|
8690
|
+
root,
|
|
8691
|
+
key,
|
|
8692
|
+
mcpHttpPort,
|
|
8693
|
+
eventFile: env["LENSMCP_EVENT_FILE"] ?? join2(root, ".lensmcp", "events.jsonl")
|
|
8694
|
+
};
|
|
8695
|
+
}
|
|
8696
|
+
function sharedMcpUrl(port) {
|
|
8697
|
+
return `http://127.0.0.1:${port}/mcp`;
|
|
8698
|
+
}
|
|
8699
|
+
var IDENTITY_PATH = "/lensmcp/identity";
|
|
8700
|
+
var IDENTITY_KIND = "lensmcp-mcp-identity";
|
|
8701
|
+
function identityUrl(port) {
|
|
8702
|
+
return `http://127.0.0.1:${port}${IDENTITY_PATH}`;
|
|
8703
|
+
}
|
|
8704
|
+
function canonicalRoot(path) {
|
|
8705
|
+
try {
|
|
8706
|
+
return realpathSync(path);
|
|
8707
|
+
} catch {
|
|
8708
|
+
return path;
|
|
8709
|
+
}
|
|
8710
|
+
}
|
|
8711
|
+
function parseServerIdentity(value) {
|
|
8712
|
+
if (!value || typeof value !== "object")
|
|
8713
|
+
return void 0;
|
|
8714
|
+
const o = value;
|
|
8715
|
+
if (o["kind"] !== IDENTITY_KIND)
|
|
8716
|
+
return void 0;
|
|
8717
|
+
if (typeof o["key"] !== "string" || typeof o["root"] !== "string")
|
|
8718
|
+
return void 0;
|
|
8719
|
+
return {
|
|
8720
|
+
kind: IDENTITY_KIND,
|
|
8721
|
+
key: o["key"],
|
|
8722
|
+
root: o["root"],
|
|
8723
|
+
...typeof o["eventFile"] === "string" ? { eventFile: o["eventFile"] } : {},
|
|
8724
|
+
...typeof o["pid"] === "number" ? { pid: o["pid"] } : {},
|
|
8725
|
+
...typeof o["port"] === "number" ? { port: o["port"] } : {}
|
|
8726
|
+
};
|
|
8727
|
+
}
|
|
8728
|
+
function describeIdentityMismatch(expected, actual) {
|
|
8729
|
+
if (!actual)
|
|
8730
|
+
return void 0;
|
|
8731
|
+
if (actual.key !== expected.key) {
|
|
8732
|
+
return `it serves workspace "${actual.key}", not "${expected.key}"`;
|
|
8733
|
+
}
|
|
8734
|
+
const ours = canonicalRoot(expected.root);
|
|
8735
|
+
const theirs = canonicalRoot(actual.root);
|
|
8736
|
+
if (ours !== theirs) {
|
|
8737
|
+
return `it serves another checkout of "${actual.key}" (${theirs}), not ${ours}`;
|
|
8738
|
+
}
|
|
8739
|
+
return void 0;
|
|
8740
|
+
}
|
|
8741
|
+
|
|
8742
|
+
// servers/lensmcp-mcp/dist/shim/probe.js
|
|
8743
|
+
async function probeMcp(url2, timeoutMs) {
|
|
8744
|
+
const ctrl = new AbortController();
|
|
8745
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
8746
|
+
try {
|
|
8747
|
+
const res = await fetch(url2, {
|
|
8748
|
+
method: "POST",
|
|
8749
|
+
signal: ctrl.signal,
|
|
8750
|
+
headers: {
|
|
8751
|
+
"content-type": "application/json",
|
|
8752
|
+
accept: "application/json, text/event-stream"
|
|
8753
|
+
},
|
|
8754
|
+
body: JSON.stringify({
|
|
8755
|
+
jsonrpc: "2.0",
|
|
8756
|
+
id: 1,
|
|
8757
|
+
method: "initialize",
|
|
8758
|
+
params: {
|
|
8759
|
+
protocolVersion: "2025-06-18",
|
|
8760
|
+
capabilities: {},
|
|
8761
|
+
clientInfo: { name: "lensmcp-shim-probe", version: "1" }
|
|
8762
|
+
}
|
|
8763
|
+
})
|
|
8764
|
+
});
|
|
8765
|
+
if (!res.ok)
|
|
8766
|
+
return false;
|
|
8767
|
+
const text = await res.text();
|
|
8768
|
+
return text.includes('"result"');
|
|
8769
|
+
} catch {
|
|
8770
|
+
return false;
|
|
8771
|
+
} finally {
|
|
8772
|
+
clearTimeout(timer);
|
|
8773
|
+
}
|
|
8774
|
+
}
|
|
8775
|
+
async function probeShared(port, timeoutMs = 2e3) {
|
|
8776
|
+
const ctrl = new AbortController();
|
|
8777
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
8778
|
+
let answered = false;
|
|
8779
|
+
try {
|
|
8780
|
+
const res = await fetch(identityUrl(port), {
|
|
8781
|
+
signal: ctrl.signal,
|
|
8782
|
+
headers: { accept: "application/json" }
|
|
8783
|
+
});
|
|
8784
|
+
answered = true;
|
|
8785
|
+
if (res.ok) {
|
|
8786
|
+
const identity = parseServerIdentity(await res.json().catch(() => void 0));
|
|
8787
|
+
if (identity)
|
|
8788
|
+
return { alive: true, identity, owner: "lensmcp" };
|
|
8789
|
+
} else {
|
|
8790
|
+
await res.text().catch(() => void 0);
|
|
8791
|
+
}
|
|
8792
|
+
} catch {
|
|
8793
|
+
} finally {
|
|
8794
|
+
clearTimeout(timer);
|
|
8795
|
+
}
|
|
8796
|
+
if (!answered)
|
|
8797
|
+
return { alive: false, owner: "none" };
|
|
8798
|
+
const legacy = await probeMcp(sharedMcpUrl(port), timeoutMs);
|
|
8799
|
+
return { alive: legacy, owner: legacy ? "lensmcp" : "foreign" };
|
|
8800
|
+
}
|
|
8801
|
+
function tcpAccepts(port, timeoutMs = 750) {
|
|
8802
|
+
return new Promise((resolve2) => {
|
|
8803
|
+
const socket = connect({ port, host: "127.0.0.1" });
|
|
8804
|
+
const settle = (accepted) => {
|
|
8805
|
+
socket.destroy();
|
|
8806
|
+
resolve2(accepted);
|
|
8807
|
+
};
|
|
8808
|
+
socket.setTimeout(timeoutMs);
|
|
8809
|
+
socket.once("connect", () => settle(true));
|
|
8810
|
+
socket.once("timeout", () => settle(false));
|
|
8811
|
+
socket.once("error", () => settle(false));
|
|
8812
|
+
});
|
|
8813
|
+
}
|
|
8814
|
+
|
|
8619
8815
|
// servers/lensmcp-mcp/dist/shim/proxy.js
|
|
8620
8816
|
function isRequest(m) {
|
|
8621
8817
|
return typeof m.method === "string" && m.id !== void 0;
|
|
@@ -8769,73 +8965,6 @@ function startProxy(opts) {
|
|
|
8769
8965
|
};
|
|
8770
8966
|
}
|
|
8771
8967
|
|
|
8772
|
-
// servers/lensmcp-mcp/dist/shim/workspace.js
|
|
8773
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
8774
|
-
import { basename, dirname, join as join2 } from "node:path";
|
|
8775
|
-
var BASE_PORTS = { dashboard: 4321, mcpHttp: 4500, bridge: 5747 };
|
|
8776
|
-
function readJson(path) {
|
|
8777
|
-
try {
|
|
8778
|
-
return JSON.parse(readFileSync2(path, "utf8"));
|
|
8779
|
-
} catch {
|
|
8780
|
-
return void 0;
|
|
8781
|
-
}
|
|
8782
|
-
}
|
|
8783
|
-
function hashToRange(s, range) {
|
|
8784
|
-
let h = 2166136261;
|
|
8785
|
-
for (let i = 0; i < s.length; i++) {
|
|
8786
|
-
h ^= s.charCodeAt(i);
|
|
8787
|
-
h = Math.imul(h, 16777619);
|
|
8788
|
-
}
|
|
8789
|
-
return Math.abs(h | 0) % range;
|
|
8790
|
-
}
|
|
8791
|
-
function slugify2(input) {
|
|
8792
|
-
return input.toLowerCase().replace(/^@[^/]+\//, "").replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "") || "workspace";
|
|
8793
|
-
}
|
|
8794
|
-
function resolveWorkspaceKey(root) {
|
|
8795
|
-
const nx = readJson(join2(root, "nx.json"));
|
|
8796
|
-
if (nx?.lensmcp?.key)
|
|
8797
|
-
return slugify2(nx.lensmcp.key);
|
|
8798
|
-
const pkg = readJson(join2(root, "package.json"));
|
|
8799
|
-
if (pkg?.name)
|
|
8800
|
-
return slugify2(pkg.name);
|
|
8801
|
-
if (nx?.name)
|
|
8802
|
-
return slugify2(nx.name);
|
|
8803
|
-
return slugify2(basename(root));
|
|
8804
|
-
}
|
|
8805
|
-
function deriveMcpHttpPort(key) {
|
|
8806
|
-
return BASE_PORTS.mcpHttp + hashToRange(key, 200);
|
|
8807
|
-
}
|
|
8808
|
-
function findWorkspaceRoot(start) {
|
|
8809
|
-
let nxFallback;
|
|
8810
|
-
let dir = start;
|
|
8811
|
-
for (; ; ) {
|
|
8812
|
-
if (existsSync2(join2(dir, ".lensmcp", "config.json")))
|
|
8813
|
-
return dir;
|
|
8814
|
-
if (nxFallback === void 0 && existsSync2(join2(dir, "nx.json")))
|
|
8815
|
-
nxFallback = dir;
|
|
8816
|
-
const parent = dirname(dir);
|
|
8817
|
-
if (parent === dir)
|
|
8818
|
-
break;
|
|
8819
|
-
dir = parent;
|
|
8820
|
-
}
|
|
8821
|
-
return nxFallback ?? start;
|
|
8822
|
-
}
|
|
8823
|
-
function resolveWorkspaceScope(start, env = process.env) {
|
|
8824
|
-
const root = findWorkspaceRoot(start);
|
|
8825
|
-
const file = readJson(join2(root, ".lensmcp", "config.json"));
|
|
8826
|
-
const key = file?.key ? slugify2(file.key) : resolveWorkspaceKey(root);
|
|
8827
|
-
const mcpHttpPort = typeof file?.ports?.mcpHttp === "number" ? file.ports.mcpHttp : deriveMcpHttpPort(key);
|
|
8828
|
-
return {
|
|
8829
|
-
root,
|
|
8830
|
-
key,
|
|
8831
|
-
mcpHttpPort,
|
|
8832
|
-
eventFile: env["LENSMCP_EVENT_FILE"] ?? join2(root, ".lensmcp", "events.jsonl")
|
|
8833
|
-
};
|
|
8834
|
-
}
|
|
8835
|
-
function sharedMcpUrl(port) {
|
|
8836
|
-
return `http://127.0.0.1:${port}/mcp`;
|
|
8837
|
-
}
|
|
8838
|
-
|
|
8839
8968
|
// servers/lensmcp-mcp/dist/shim.js
|
|
8840
8969
|
function log(line) {
|
|
8841
8970
|
process.stderr.write(`${line}
|
|
@@ -8848,43 +8977,12 @@ function argValue(flag) {
|
|
|
8848
8977
|
function serverEntry() {
|
|
8849
8978
|
return join3(dirname2(fileURLToPath(import.meta.url)), "main.js");
|
|
8850
8979
|
}
|
|
8851
|
-
|
|
8852
|
-
const ctrl = new AbortController();
|
|
8853
|
-
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
8854
|
-
try {
|
|
8855
|
-
const res = await fetch(url2, {
|
|
8856
|
-
method: "POST",
|
|
8857
|
-
signal: ctrl.signal,
|
|
8858
|
-
headers: {
|
|
8859
|
-
"content-type": "application/json",
|
|
8860
|
-
accept: "application/json, text/event-stream"
|
|
8861
|
-
},
|
|
8862
|
-
body: JSON.stringify({
|
|
8863
|
-
jsonrpc: "2.0",
|
|
8864
|
-
id: 1,
|
|
8865
|
-
method: "initialize",
|
|
8866
|
-
params: {
|
|
8867
|
-
protocolVersion: "2025-06-18",
|
|
8868
|
-
capabilities: {},
|
|
8869
|
-
clientInfo: { name: "lensmcp-shim-probe", version: "1" }
|
|
8870
|
-
}
|
|
8871
|
-
})
|
|
8872
|
-
});
|
|
8873
|
-
if (!res.ok)
|
|
8874
|
-
return false;
|
|
8875
|
-
const text = await res.text();
|
|
8876
|
-
return text.includes('"result"');
|
|
8877
|
-
} catch {
|
|
8878
|
-
return false;
|
|
8879
|
-
} finally {
|
|
8880
|
-
clearTimeout(timer);
|
|
8881
|
-
}
|
|
8882
|
-
}
|
|
8883
|
-
function runEmbedded(cwd, why) {
|
|
8980
|
+
function runEmbedded(cwd, why, advise = true) {
|
|
8884
8981
|
log(`[lensmcp] shared MCP server unavailable (${why}) \u2014 falling back to an embedded server.`);
|
|
8885
8982
|
log("[lensmcp] the lens works for this session, but it is pinned to the bundle it");
|
|
8886
8983
|
log("[lensmcp] starts with and will not pick up an upgrade until the session ends.");
|
|
8887
|
-
|
|
8984
|
+
if (advise)
|
|
8985
|
+
log("[lensmcp] to get the shared server: `lensmcp gateway start`");
|
|
8888
8986
|
const env = { ...process.env };
|
|
8889
8987
|
delete env["LENSMCP_MCP_MODE"];
|
|
8890
8988
|
env["LENSMCP_TRANSPORT"] = "stdio";
|
|
@@ -8898,16 +8996,70 @@ async function main() {
|
|
|
8898
8996
|
const scope = resolveWorkspaceScope(cwd);
|
|
8899
8997
|
const url2 = sharedMcpUrl(scope.mcpHttpPort);
|
|
8900
8998
|
log(`[lensmcp] shim \u2192 workspace "${scope.key}" (${scope.root}) \u2192 ${url2}`);
|
|
8999
|
+
let seen = { alive: false, owner: "none" };
|
|
9000
|
+
let warnedUnknown = false;
|
|
9001
|
+
let explained = false;
|
|
9002
|
+
const verifyIdentity = () => {
|
|
9003
|
+
const mismatch = describeIdentityMismatch(scope, seen.identity);
|
|
9004
|
+
if (!mismatch) {
|
|
9005
|
+
if (seen.alive && !seen.identity && !warnedUnknown) {
|
|
9006
|
+
warnedUnknown = true;
|
|
9007
|
+
log(`[lensmcp] note: the server on :${scope.mcpHttpPort} does not report a workspace (it predates this check), so it cannot be verified as "${scope.key}". Restart it \u2014 \`lensmcp gateway restart\` \u2014 to get the check.`);
|
|
9008
|
+
}
|
|
9009
|
+
return void 0;
|
|
9010
|
+
}
|
|
9011
|
+
const theirs = seen.identity;
|
|
9012
|
+
explained = true;
|
|
9013
|
+
log("");
|
|
9014
|
+
log(`[lensmcp] REFUSING the shared MCP server on 127.0.0.1:${scope.mcpHttpPort} \u2014 ${mismatch}.`);
|
|
9015
|
+
log(`[lensmcp] this session's workspace : "${scope.key}" ${scope.root}`);
|
|
9016
|
+
log(`[lensmcp] the server's workspace : "${theirs?.key}" ${theirs?.root}` + (theirs?.pid ? ` (pid ${theirs.pid})` : ""));
|
|
9017
|
+
log("[lensmcp] Two workspaces derive the same MCP port. Attaching would show you the");
|
|
9018
|
+
log("[lensmcp] OTHER project's events, builds and flows, so this session will not.");
|
|
9019
|
+
log("[lensmcp] Fix: pin a distinct port in this workspace's .lensmcp/config.json \u2014");
|
|
9020
|
+
log(`[lensmcp] { "ports": { "mcpHttp": ${scope.mcpHttpPort + 1} } }`);
|
|
9021
|
+
log("[lensmcp] then restart this session. Any free 4500-4699 port works.");
|
|
9022
|
+
log("");
|
|
9023
|
+
return `the shared MCP port is served by workspace "${theirs?.key}", not "${scope.key}"`;
|
|
9024
|
+
};
|
|
9025
|
+
const foreignPortOwner = async () => {
|
|
9026
|
+
const port = scope.mcpHttpPort;
|
|
9027
|
+
const nonHttp = seen.owner === "foreign" ? false : await tcpAccepts(port);
|
|
9028
|
+
if (seen.owner !== "foreign" && !nonHttp)
|
|
9029
|
+
return void 0;
|
|
9030
|
+
explained = true;
|
|
9031
|
+
log("");
|
|
9032
|
+
log(`[lensmcp] the shared MCP port 127.0.0.1:${port} is TAKEN by another process.`);
|
|
9033
|
+
log(`[lensmcp] It ${nonHttp ? "accepts connections but does not speak HTTP" : "answers HTTP but is not a lensmcp MCP server"}, so this session cannot attach to it.`);
|
|
9034
|
+
log("[lensmcp] Not starting a server on it: on macOS a second bind SUCCEEDS on the");
|
|
9035
|
+
log("[lensmcp] wildcard address while the kernel keeps routing 127.0.0.1 to the");
|
|
9036
|
+
log("[lensmcp] process already there \u2014 the new server would be unreachable, and");
|
|
9037
|
+
log("[lensmcp] would keep running after this session ends.");
|
|
9038
|
+
log("[lensmcp] Fix EITHER by freeing the port:");
|
|
9039
|
+
log(`[lensmcp] lsof -nP -iTCP:${port} -sTCP:LISTEN`);
|
|
9040
|
+
log(`[lensmcp] OR by pinning a different one in ${join3(scope.root, ".lensmcp", "config.json")}:`);
|
|
9041
|
+
log(`[lensmcp] { "ports": { "mcpHttp": ${port + 1} } }`);
|
|
9042
|
+
log("[lensmcp] then restart this session. Any free 4500-4699 port works.");
|
|
9043
|
+
log("");
|
|
9044
|
+
return `the shared MCP port ${port} is held by another process that is not a lensmcp server`;
|
|
9045
|
+
};
|
|
8901
9046
|
const ensure = () => {
|
|
8902
9047
|
let watch;
|
|
8903
9048
|
return ensureSharedServer({
|
|
8904
|
-
probe: () =>
|
|
9049
|
+
probe: async () => {
|
|
9050
|
+
seen = await probeShared(scope.mcpHttpPort);
|
|
9051
|
+
return seen.alive;
|
|
9052
|
+
},
|
|
9053
|
+
verifyIdentity,
|
|
9054
|
+
foreignPortOwner,
|
|
8905
9055
|
startServer: () => {
|
|
8906
9056
|
const started = startSharedServerWatched({
|
|
8907
9057
|
serverEntry: serverEntry(),
|
|
8908
9058
|
port: scope.mcpHttpPort,
|
|
8909
9059
|
eventFile: scope.eventFile,
|
|
8910
9060
|
cwd: scope.root,
|
|
9061
|
+
workspaceKey: scope.key,
|
|
9062
|
+
workspaceRoot: scope.root,
|
|
8911
9063
|
onLog: log
|
|
8912
9064
|
});
|
|
8913
9065
|
watch = started.exited;
|
|
@@ -8922,7 +9074,7 @@ async function main() {
|
|
|
8922
9074
|
};
|
|
8923
9075
|
const ensured = await ensure();
|
|
8924
9076
|
if (ensured.outcome === "unavailable") {
|
|
8925
|
-
runEmbedded(cwd, ensured.reason ?? "unknown");
|
|
9077
|
+
runEmbedded(cwd, ensured.reason ?? "unknown", !explained);
|
|
8926
9078
|
return;
|
|
8927
9079
|
}
|
|
8928
9080
|
log(`[lensmcp] shim: shared server ${ensured.outcome} (${ensured.waitedMs}ms)`);
|
|
@@ -8936,8 +9088,17 @@ async function main() {
|
|
|
8936
9088
|
}
|
|
8937
9089
|
return new StreamableHTTPClientTransport(new URL(url2));
|
|
8938
9090
|
},
|
|
8939
|
-
//
|
|
8940
|
-
|
|
9091
|
+
// Filters blips so a transient error does not cost a live session — but a
|
|
9092
|
+
// server that has become a STRANGER is not a blip. Reporting it unhealthy
|
|
9093
|
+
// sends the pump into `connect()`, which re-ensures, refuses, and ends the
|
|
9094
|
+
// session cleanly; calling it healthy would instead leave the pump erroring
|
|
9095
|
+
// forever against a server that has never heard of our session id.
|
|
9096
|
+
isUpstreamHealthy: async () => {
|
|
9097
|
+
const now = await probeShared(scope.mcpHttpPort, 1e3);
|
|
9098
|
+
if (!now.alive)
|
|
9099
|
+
return false;
|
|
9100
|
+
return describeIdentityMismatch(scope, now.identity) === void 0;
|
|
9101
|
+
},
|
|
8941
9102
|
log,
|
|
8942
9103
|
onFatal: (reason) => log(`[lensmcp] shim: ${reason}`)
|
|
8943
9104
|
});
|
package/lib/mcp-mode.d.ts
CHANGED
|
@@ -36,14 +36,25 @@
|
|
|
36
36
|
* spawns the server, waits, and if it still cannot get one it falls back to
|
|
37
37
|
* today's embedded server so a session is NEVER left without the lens.
|
|
38
38
|
*
|
|
39
|
-
* The isolation boundary is the PORT
|
|
40
|
-
*
|
|
39
|
+
* The isolation boundary is the PORT — and the port is VERIFIED
|
|
40
|
+
* -------------------------------------------------------------
|
|
41
41
|
* One daemon hosts many workspaces (foodguard + tetros), and their observability
|
|
42
42
|
* data must never mix. Each workspace already reserves its own `ports.mcpHttp`
|
|
43
43
|
* in `.lensmcp/config.json` (deterministic per workspace key). The shim resolves
|
|
44
44
|
* that port from the workspace it was pointed at — NEVER a shared default. Two
|
|
45
45
|
* workspaces landing on one port would cross-feed each other's events.
|
|
46
46
|
*
|
|
47
|
+
* But the port is DERIVED (`4500 + fnv1a(key) % 200`), so two workspaces CAN
|
|
48
|
+
* hash onto one — and unlike the dashboard port it must not step up on
|
|
49
|
+
* collision, because the port IS the shim's contract. Two checkouts of one repo
|
|
50
|
+
* are worse: same key, same port, different event files. So the port alone is
|
|
51
|
+
* not proof of ownership. The shared server STATES which workspace it serves, on
|
|
52
|
+
* a `GET /lensmcp/identity` route mounted beside the MCP endpoint, and the shim
|
|
53
|
+
* REFUSES a server whose workspace is not its own — degrading to the embedded
|
|
54
|
+
* server rather than silently reading another project's events. It is checked
|
|
55
|
+
* before a single JSON-RPC frame is proxied, and costs nothing: that GET
|
|
56
|
+
* REPLACES the `initialize` probe the shim already had to make.
|
|
57
|
+
*
|
|
47
58
|
* This module is pure (values in, values out) so every rule below is
|
|
48
59
|
* unit-testable without a filesystem, a daemon, or an agent client.
|
|
49
60
|
*/
|
package/lib/mcp-mode.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-mode.d.ts","sourceRoot":"","sources":["../../src/lib/mcp-mode.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"mcp-mode.d.ts","sourceRoot":"","sources":["../../src/lib/mcp-mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2DG;AAEH,sDAAsD;AACtD,MAAM,MAAM,OAAO;AACjB,yFAAyF;AACvF,UAAU;AACZ,sFAAsF;GACpF,QAAQ,CAAC;AAEb,sFAAsF;AACtF,eAAO,MAAM,gBAAgB,EAAE,OAAoB,CAAC;AAEpD,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,SAAS,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAEnD,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAE1D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,MAAgC,GAAG,MAAM,CAEzF;AAED,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,2EAA2E;IAC3E,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACzB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CAKlE;AAED,kFAAkF;AAClF,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB;AAcD,kFAAkF;AAClF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,OAAO,GAAG,QAAQ,CASnF"}
|
package/lib/mcp-mode.js
CHANGED
|
@@ -36,14 +36,25 @@
|
|
|
36
36
|
* spawns the server, waits, and if it still cannot get one it falls back to
|
|
37
37
|
* today's embedded server so a session is NEVER left without the lens.
|
|
38
38
|
*
|
|
39
|
-
* The isolation boundary is the PORT
|
|
40
|
-
*
|
|
39
|
+
* The isolation boundary is the PORT — and the port is VERIFIED
|
|
40
|
+
* -------------------------------------------------------------
|
|
41
41
|
* One daemon hosts many workspaces (foodguard + tetros), and their observability
|
|
42
42
|
* data must never mix. Each workspace already reserves its own `ports.mcpHttp`
|
|
43
43
|
* in `.lensmcp/config.json` (deterministic per workspace key). The shim resolves
|
|
44
44
|
* that port from the workspace it was pointed at — NEVER a shared default. Two
|
|
45
45
|
* workspaces landing on one port would cross-feed each other's events.
|
|
46
46
|
*
|
|
47
|
+
* But the port is DERIVED (`4500 + fnv1a(key) % 200`), so two workspaces CAN
|
|
48
|
+
* hash onto one — and unlike the dashboard port it must not step up on
|
|
49
|
+
* collision, because the port IS the shim's contract. Two checkouts of one repo
|
|
50
|
+
* are worse: same key, same port, different event files. So the port alone is
|
|
51
|
+
* not proof of ownership. The shared server STATES which workspace it serves, on
|
|
52
|
+
* a `GET /lensmcp/identity` route mounted beside the MCP endpoint, and the shim
|
|
53
|
+
* REFUSES a server whose workspace is not its own — degrading to the embedded
|
|
54
|
+
* server rather than silently reading another project's events. It is checked
|
|
55
|
+
* before a single JSON-RPC frame is proxied, and costs nothing: that GET
|
|
56
|
+
* REPLACES the `initialize` probe the shim already had to make.
|
|
57
|
+
*
|
|
47
58
|
* This module is pure (values in, values out) so every rule below is
|
|
48
59
|
* unit-testable without a filesystem, a daemon, or an agent client.
|
|
49
60
|
*/
|
package/lib/workspace-scope.d.ts
CHANGED
|
@@ -44,6 +44,20 @@ export interface LensConfig {
|
|
|
44
44
|
* just because this field was added.
|
|
45
45
|
*/
|
|
46
46
|
mcp?: LensMcpConfig;
|
|
47
|
+
/**
|
|
48
|
+
* Optional. Dirs removed from the source-set staleness signature + pod scope
|
|
49
|
+
* (e.g. `["docs"]` so a docs write does not recycle every vite pod). READ by
|
|
50
|
+
* `@lensmcp/cluster` (`runtime/scope.ts` `readSourceSetExclude`); declared here
|
|
51
|
+
* so this writer round-trips it instead of dropping it.
|
|
52
|
+
*/
|
|
53
|
+
sourceSet?: {
|
|
54
|
+
exclude?: string[];
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Any key this version does not know about. Preserved verbatim on write —
|
|
58
|
+
* see the round-trip contract on `readLensConfig`.
|
|
59
|
+
*/
|
|
60
|
+
[key: string]: unknown;
|
|
47
61
|
}
|
|
48
62
|
/** Lowercase kebab slug; empty input → `workspace`. */
|
|
49
63
|
export declare function slugify(input: string): string;
|
|
@@ -62,6 +76,14 @@ export declare function deriveConfig(root: string): LensConfig;
|
|
|
62
76
|
* Read `<root>/.lensmcp/config.json` if present, else the derived defaults. A present
|
|
63
77
|
* file OVERRIDES derived values field-by-field (so a user can pin a key/port without
|
|
64
78
|
* losing the rest), and is the seam the gateway + CLI both read so they agree.
|
|
79
|
+
*
|
|
80
|
+
* ROUND-TRIP CONTRACT: every key on disk survives, including ones this version does
|
|
81
|
+
* not model. `ensureLensConfig` writes whatever this returns, so anything dropped
|
|
82
|
+
* here is silently DELETED from the user's file. That was a real bug: the result was
|
|
83
|
+
* built field-by-field from the four known keys, so a hand-set `sourceSet.exclude`
|
|
84
|
+
* (honored by `@lensmcp/cluster`, but not declared here) was stripped on every
|
|
85
|
+
* `lensmcp setup` — the docs-exclusion never took effect, and `docs/**` writes kept
|
|
86
|
+
* recycling every vite pod. Spread the file FIRST, then normalize; never enumerate.
|
|
65
87
|
*/
|
|
66
88
|
export declare function readLensConfig(root: string): LensConfig;
|
|
67
89
|
/** Persist the (derived/merged) config so it is visible + overridable. Idempotent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-scope.d.ts","sourceRoot":"","sources":["../../src/lib/workspace-scope.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,SAAS;IACxB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,CAAC,CAAC;IACjB,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,+DAA+D;IAC/D,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,GAAG,CAAC,EAAE,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"workspace-scope.d.ts","sourceRoot":"","sources":["../../src/lib/workspace-scope.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,SAAS;IACxB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CAC3B;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,CAAC,CAAC;IACjB,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,CAAC;IACjB,+DAA+D;IAC/D,iBAAiB,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACnC;;;OAGG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAaD,uDAAuD;AACvD,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQ7C;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAOxD;AAOD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAOlD;AAED,8EAA8E;AAC9E,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAGrD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAiBvD;AAED;yDACyD;AACzD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAQzD"}
|
package/lib/workspace-scope.js
CHANGED
|
@@ -57,6 +57,14 @@ export function deriveConfig(root) {
|
|
|
57
57
|
* Read `<root>/.lensmcp/config.json` if present, else the derived defaults. A present
|
|
58
58
|
* file OVERRIDES derived values field-by-field (so a user can pin a key/port without
|
|
59
59
|
* losing the rest), and is the seam the gateway + CLI both read so they agree.
|
|
60
|
+
*
|
|
61
|
+
* ROUND-TRIP CONTRACT: every key on disk survives, including ones this version does
|
|
62
|
+
* not model. `ensureLensConfig` writes whatever this returns, so anything dropped
|
|
63
|
+
* here is silently DELETED from the user's file. That was a real bug: the result was
|
|
64
|
+
* built field-by-field from the four known keys, so a hand-set `sourceSet.exclude`
|
|
65
|
+
* (honored by `@lensmcp/cluster`, but not declared here) was stripped on every
|
|
66
|
+
* `lensmcp setup` — the docs-exclusion never took effect, and `docs/**` writes kept
|
|
67
|
+
* recycling every vite pod. Spread the file FIRST, then normalize; never enumerate.
|
|
60
68
|
*/
|
|
61
69
|
export function readLensConfig(root) {
|
|
62
70
|
const derived = deriveConfig(root);
|
|
@@ -65,6 +73,8 @@ export function readLensConfig(root) {
|
|
|
65
73
|
return derived;
|
|
66
74
|
const key = file.key ? slugify(file.key) : derived.key;
|
|
67
75
|
return {
|
|
76
|
+
// Unknown//future keys ride through untouched — the normalized fields below win.
|
|
77
|
+
...file,
|
|
68
78
|
schemaVersion: 1,
|
|
69
79
|
key,
|
|
70
80
|
ports: { ...derived.ports, ...(file.ports ?? {}) },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lensmcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.18.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@frontmcp/sdk": "^1.5.
|
|
20
|
+
"@frontmcp/sdk": "^1.5.7",
|
|
21
21
|
"reflect-metadata": "^0.2.2",
|
|
22
22
|
"tslib": "^2.3.0",
|
|
23
23
|
"vectoriadb": "^2.2.0"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "lensmcp",
|
|
3
3
|
"displayName": "LensMCP",
|
|
4
4
|
"description": "The observability lens for coding agents. One command brings up the dev cluster gateway (every project.json `cluster` decl → its host on :443), the per-project lens dashboard at https://lensmcp.local/<project>/, and the MCP server your agent connects to — scoped automatically to whatever project you opened Claude Code in.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.18.1",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "David Antoon",
|
|
8
8
|
"email": "davidmantoon@gmail.com"
|