lensmcp 1.8.0 → 1.10.0
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/.claude-plugin/marketplace.json +17 -0
- package/bundled/main.js +138 -0
- package/lib/cli.d.ts.map +1 -1
- package/lib/cli.js +133 -4
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +23 -0
- package/plugin/.mcp.json +13 -0
- package/plugin/README.md +49 -0
- package/plugin/scripts/dashboard-open.sh +23 -0
- package/plugin/scripts/gateway-start.sh +17 -0
- package/plugin/scripts/gateway-status.sh +15 -0
- package/plugin/scripts/gateway-stop.sh +13 -0
- package/plugin/skills/dashboard/SKILL.md +19 -0
- package/plugin/skills/start/SKILL.md +35 -0
- package/plugin/skills/status/SKILL.md +18 -0
- package/plugin/skills/stop/SKILL.md +17 -0
- package/plugin/skills/using-lensmcp/SKILL.md +51 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lensmcp-marketplace",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "David Antoon",
|
|
5
|
+
"email": "davidmantoon@gmail.com"
|
|
6
|
+
},
|
|
7
|
+
"metadata": {
|
|
8
|
+
"description": "The LensMCP observability lens, packaged as a Claude Code plugin."
|
|
9
|
+
},
|
|
10
|
+
"plugins": [
|
|
11
|
+
{
|
|
12
|
+
"name": "lensmcp",
|
|
13
|
+
"source": "./plugin",
|
|
14
|
+
"description": "Run the LensMCP dev cluster gateway + per-project dashboard + MCP server from inside Claude Code."
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
package/bundled/main.js
CHANGED
|
@@ -11,6 +11,9 @@ function __decorate(decorators, target, key, desc) {
|
|
|
11
11
|
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
12
12
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
13
13
|
}
|
|
14
|
+
function __metadata(metadataKey, metadataValue) {
|
|
15
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
|
|
16
|
+
}
|
|
14
17
|
|
|
15
18
|
// servers/lensmcp-mcp/dist/main.js
|
|
16
19
|
import "reflect-metadata";
|
|
@@ -20232,10 +20235,145 @@ RenderApp = __decorate([
|
|
|
20232
20235
|
})
|
|
20233
20236
|
], RenderApp);
|
|
20234
20237
|
|
|
20238
|
+
// servers/lensmcp-mcp/dist/setup-gate/setup.app.js
|
|
20239
|
+
import { App as App21 } from "@frontmcp/sdk";
|
|
20240
|
+
|
|
20241
|
+
// servers/lensmcp-mcp/dist/setup-gate/setup-status.tool.js
|
|
20242
|
+
import { Tool as Tool24, ToolContext as ToolContext24 } from "@frontmcp/sdk";
|
|
20243
|
+
|
|
20244
|
+
// servers/lensmcp-mcp/dist/setup-gate/state.js
|
|
20245
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "node:fs";
|
|
20246
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
20247
|
+
import { fileURLToPath } from "node:url";
|
|
20248
|
+
function lensmcpDir() {
|
|
20249
|
+
const eventFile = process.env["LENSMCP_EVENT_FILE"];
|
|
20250
|
+
if (eventFile)
|
|
20251
|
+
return dirname3(eventFile);
|
|
20252
|
+
return join3(process.cwd(), ".lensmcp");
|
|
20253
|
+
}
|
|
20254
|
+
function readSetupState() {
|
|
20255
|
+
try {
|
|
20256
|
+
const raw = readFileSync3(join3(lensmcpDir(), "setup-state.json"), "utf8");
|
|
20257
|
+
const parsed = JSON.parse(raw);
|
|
20258
|
+
return typeof parsed?.version === "string" ? parsed : void 0;
|
|
20259
|
+
} catch {
|
|
20260
|
+
return void 0;
|
|
20261
|
+
}
|
|
20262
|
+
}
|
|
20263
|
+
function installedVersion() {
|
|
20264
|
+
let dir = dirname3(fileURLToPath(import.meta.url));
|
|
20265
|
+
for (let i = 0; i < 6; i++) {
|
|
20266
|
+
const pkg = join3(dir, "package.json");
|
|
20267
|
+
if (existsSync5(pkg)) {
|
|
20268
|
+
try {
|
|
20269
|
+
const version2 = JSON.parse(readFileSync3(pkg, "utf8")).version;
|
|
20270
|
+
if (version2)
|
|
20271
|
+
return version2;
|
|
20272
|
+
} catch {
|
|
20273
|
+
}
|
|
20274
|
+
}
|
|
20275
|
+
const parent = dirname3(dir);
|
|
20276
|
+
if (parent === dir)
|
|
20277
|
+
break;
|
|
20278
|
+
dir = parent;
|
|
20279
|
+
}
|
|
20280
|
+
return "0.0.0";
|
|
20281
|
+
}
|
|
20282
|
+
function checkReadiness() {
|
|
20283
|
+
const installed = installedVersion();
|
|
20284
|
+
const state2 = readSetupState();
|
|
20285
|
+
if (!state2)
|
|
20286
|
+
return { ready: false, installedVersion: installed, reason: "never-setup" };
|
|
20287
|
+
if (state2.version !== installed) {
|
|
20288
|
+
return { ready: false, installedVersion: installed, setupVersion: state2.version, reason: "version-mismatch" };
|
|
20289
|
+
}
|
|
20290
|
+
return { ready: true, installedVersion: installed, setupVersion: state2.version };
|
|
20291
|
+
}
|
|
20292
|
+
function gateDisabled() {
|
|
20293
|
+
const flag = process.env["LENSMCP_NO_SETUP_GATE"];
|
|
20294
|
+
return flag === "1" || flag === "true";
|
|
20295
|
+
}
|
|
20296
|
+
function setupRequiredMessage(r) {
|
|
20297
|
+
const last = r.reason === "version-mismatch" && r.setupVersion ? `last set up for v${r.setupVersion}` : "not set up on this machine yet";
|
|
20298
|
+
return `LensMCP is not ready: ${last}, but v${r.installedVersion} is installed. Ask the user to run \`lensmcp setup\` once in their terminal \u2014 it installs the plugin wiring and trusts the dev CA + /etc/hosts (sudo prompts inline; idempotent) \u2014 then retry. Call the \`setup.status\` tool any time to re-check.`;
|
|
20299
|
+
}
|
|
20300
|
+
|
|
20301
|
+
// servers/lensmcp-mcp/dist/setup-gate/setup-status.tool.js
|
|
20302
|
+
var SetupStatusTool = class SetupStatusTool2 extends ToolContext24 {
|
|
20303
|
+
async execute() {
|
|
20304
|
+
const readiness = checkReadiness();
|
|
20305
|
+
return {
|
|
20306
|
+
ready: readiness.ready,
|
|
20307
|
+
installedVersion: readiness.installedVersion,
|
|
20308
|
+
setupVersion: readiness.setupVersion ?? null,
|
|
20309
|
+
reason: readiness.reason ?? null,
|
|
20310
|
+
gateDisabled: gateDisabled(),
|
|
20311
|
+
message: readiness.ready ? `LensMCP is set up for v${readiness.installedVersion}. The lens is open.` : setupRequiredMessage(readiness),
|
|
20312
|
+
fix: readiness.ready ? null : "lensmcp setup"
|
|
20313
|
+
};
|
|
20314
|
+
}
|
|
20315
|
+
};
|
|
20316
|
+
SetupStatusTool = __decorate([
|
|
20317
|
+
Tool24({
|
|
20318
|
+
name: "setup.status",
|
|
20319
|
+
description: "Report whether LensMCP is set up for the installed version on this machine. Always available (exempt from the setup-gate) so the agent can diagnose. If `ready` is false, ask the user to run `lensmcp setup`.",
|
|
20320
|
+
inputSchema: {}
|
|
20321
|
+
})
|
|
20322
|
+
], SetupStatusTool);
|
|
20323
|
+
var setup_status_tool_default = SetupStatusTool;
|
|
20324
|
+
|
|
20325
|
+
// servers/lensmcp-mcp/dist/setup-gate/gate.plugin.js
|
|
20326
|
+
import { Plugin, ToolHook } from "@frontmcp/sdk";
|
|
20327
|
+
var { Will } = ToolHook;
|
|
20328
|
+
var EXEMPT_TOOLS = /* @__PURE__ */ new Set(["setup.status", "lensmcp.session"]);
|
|
20329
|
+
var SetupGatePlugin = class SetupGatePlugin2 {
|
|
20330
|
+
enforceSetup(ctx) {
|
|
20331
|
+
if (gateDisabled())
|
|
20332
|
+
return;
|
|
20333
|
+
let readiness;
|
|
20334
|
+
try {
|
|
20335
|
+
readiness = checkReadiness();
|
|
20336
|
+
} catch {
|
|
20337
|
+
return;
|
|
20338
|
+
}
|
|
20339
|
+
if (!readiness.ready)
|
|
20340
|
+
ctx.fail(new Error(setupRequiredMessage(readiness)));
|
|
20341
|
+
}
|
|
20342
|
+
};
|
|
20343
|
+
__decorate([
|
|
20344
|
+
Will("execute", {
|
|
20345
|
+
priority: 1e4,
|
|
20346
|
+
filter: (ctx) => !EXEMPT_TOOLS.has(ctx.toolName ?? "")
|
|
20347
|
+
}),
|
|
20348
|
+
__metadata("design:type", Function),
|
|
20349
|
+
__metadata("design:paramtypes", [Object]),
|
|
20350
|
+
__metadata("design:returntype", void 0)
|
|
20351
|
+
], SetupGatePlugin.prototype, "enforceSetup", null);
|
|
20352
|
+
SetupGatePlugin = __decorate([
|
|
20353
|
+
Plugin({
|
|
20354
|
+
name: "lensmcp-setup-gate",
|
|
20355
|
+
description: "Blocks lens tools until the workspace has run `lensmcp setup` for the installed lensmcp version (after install or upgrade). Fails open on error."
|
|
20356
|
+
})
|
|
20357
|
+
], SetupGatePlugin);
|
|
20358
|
+
|
|
20359
|
+
// servers/lensmcp-mcp/dist/setup-gate/setup.app.js
|
|
20360
|
+
var SetupApp = class SetupApp2 {
|
|
20361
|
+
};
|
|
20362
|
+
SetupApp = __decorate([
|
|
20363
|
+
App21({
|
|
20364
|
+
id: "lensmcp-setup",
|
|
20365
|
+
name: "LensMCP Setup Gate",
|
|
20366
|
+
description: "Readiness gate: blocks lens tools until the workspace has run `lensmcp setup`, plus the always-open `setup.status` diagnostic.",
|
|
20367
|
+
tools: [setup_status_tool_default],
|
|
20368
|
+
plugins: [SetupGatePlugin]
|
|
20369
|
+
})
|
|
20370
|
+
], SetupApp);
|
|
20371
|
+
|
|
20235
20372
|
// servers/lensmcp-mcp/dist/main.js
|
|
20236
20373
|
var { session, providers } = createLensSession();
|
|
20237
20374
|
var transport = process.env["LENSMCP_TRANSPORT"] ?? "stdio";
|
|
20238
20375
|
var apps = [
|
|
20376
|
+
SetupApp,
|
|
20239
20377
|
MetaApp,
|
|
20240
20378
|
AgentApp,
|
|
20241
20379
|
EventsApp,
|
package/lib/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,gEAAgE;IAChE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../../src/lib/cli.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,SAAS;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,UAAU,UAAU;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,qDAAqD;IACrD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,gEAAgE;IAChE,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAiED,wBAAsB,MAAM,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAwChE"}
|
package/lib/cli.js
CHANGED
|
@@ -9,6 +9,13 @@ Usage:
|
|
|
9
9
|
lensmcp <command> [options]
|
|
10
10
|
|
|
11
11
|
Commands:
|
|
12
|
+
setup [--cwd <dir>] [--skip-format] [--register-host-config <mode>]
|
|
13
|
+
First-time bootstrap — runs \`install\` then \`trust\` in
|
|
14
|
+
one idempotent pass: plugin + .mcp.json + nx config +
|
|
15
|
+
vite/nest wiring, then the dev CA (System keychain),
|
|
16
|
+
/etc/hosts (both families) + DNS flush. sudo self-prompts
|
|
17
|
+
only for the steps that need it — run in a real terminal.
|
|
18
|
+
Then \`lensmcp gateway start\`.
|
|
12
19
|
mcp [--cwd <dir>] [--transport stdio|http] [--port <n>]
|
|
13
20
|
Launch the LensMCP MCP server. Default transport is
|
|
14
21
|
stdio (set LENSMCP_TRANSPORT=http or pass --transport
|
|
@@ -38,6 +45,12 @@ Commands:
|
|
|
38
45
|
Install LensMCP into the host Nx workspace at --cwd
|
|
39
46
|
(current directory by default). Delegates to
|
|
40
47
|
\`nx g @lensmcp/nx-plugin:init\`. Idempotent.
|
|
48
|
+
trust [--cwd <dir>] [--project <p>] [--target <t>]
|
|
49
|
+
Make the HTTPS dev gateway trusted: mint + trust the local
|
|
50
|
+
dev CA, write /etc/hosts (127.0.0.1 + ::1) for every
|
|
51
|
+
cluster host (+ lensmcp.local), flush DNS. Wraps the
|
|
52
|
+
auto-discovered \`@lensmcp/cluster:trust\` target.
|
|
53
|
+
Idempotent; sudo prompts inline — run in a terminal.
|
|
41
54
|
doctor [--cwd <dir>] [--json]
|
|
42
55
|
Diagnose the install: Node, workspace + plugin,
|
|
43
56
|
.mcp.json server entry, MCP bundle, per-project
|
|
@@ -65,6 +78,8 @@ export async function runCli(ctx) {
|
|
|
65
78
|
return { exitCode: 0 };
|
|
66
79
|
}
|
|
67
80
|
switch (command) {
|
|
81
|
+
case 'setup':
|
|
82
|
+
return runSetup(ctx, rest, out, err);
|
|
68
83
|
case 'mcp':
|
|
69
84
|
return runMcp(ctx, rest, out, err);
|
|
70
85
|
case 'dashboard':
|
|
@@ -75,6 +90,8 @@ export async function runCli(ctx) {
|
|
|
75
90
|
return runBridge(ctx, rest, out, err);
|
|
76
91
|
case 'install':
|
|
77
92
|
return runInstall(ctx, rest, out, err);
|
|
93
|
+
case 'trust':
|
|
94
|
+
return runTrust(ctx, rest, out, err);
|
|
78
95
|
case 'doctor':
|
|
79
96
|
return runDoctor(ctx, rest, out);
|
|
80
97
|
case 'rollout':
|
|
@@ -261,9 +278,10 @@ function runGateway(ctx, args, out, err) {
|
|
|
261
278
|
return { exitCode: 2 };
|
|
262
279
|
}
|
|
263
280
|
}
|
|
264
|
-
/**
|
|
265
|
-
* `project
|
|
266
|
-
|
|
281
|
+
/** Scan every project.json for the first project carrying a target whose executor
|
|
282
|
+
* is `executor`. An explicit project+target short-circuits the scan; a partial
|
|
283
|
+
* override fills the missing half from the match. */
|
|
284
|
+
function scanExecutorTarget(cwd, executor, project, target) {
|
|
267
285
|
if (project && target)
|
|
268
286
|
return { project, target };
|
|
269
287
|
const files = walkProjectFiles(cwd, (n) => n === 'project.json');
|
|
@@ -277,14 +295,30 @@ function findGatewayTarget(cwd, project, target) {
|
|
|
277
295
|
}
|
|
278
296
|
const name = pj.name ?? basename(dirname(f));
|
|
279
297
|
for (const [t, def] of Object.entries(pj.targets ?? {})) {
|
|
280
|
-
if (def.executor ===
|
|
298
|
+
if (def.executor === executor) {
|
|
281
299
|
return { project: project ?? name, target: target ?? t };
|
|
282
300
|
}
|
|
283
301
|
}
|
|
284
302
|
}
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
/** Find a project + target whose executor is `@lensmcp/cluster:gateway`. */
|
|
306
|
+
function findGatewayTarget(cwd, project, target) {
|
|
307
|
+
const found = scanExecutorTarget(cwd, '@lensmcp/cluster:gateway', project, target);
|
|
308
|
+
if (found)
|
|
309
|
+
return found;
|
|
285
310
|
// Sensible fallback used by most workspaces (tetros: tools/gateway → `gateway:serve`).
|
|
286
311
|
return project || target ? { project: project ?? 'gateway', target: target ?? 'serve' } : undefined;
|
|
287
312
|
}
|
|
313
|
+
/** Find a project + target whose executor is `@lensmcp/cluster:trust`. Falls back
|
|
314
|
+
* unconditionally to `gateway:trust` — the conventional home the init generator
|
|
315
|
+
* scaffolds — since trust is idempotent and safe to attempt anywhere. */
|
|
316
|
+
function findTrustTarget(cwd, project, target) {
|
|
317
|
+
return (scanExecutorTarget(cwd, '@lensmcp/cluster:trust', project, target) ?? {
|
|
318
|
+
project: project ?? 'gateway',
|
|
319
|
+
target: target ?? 'trust',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
288
322
|
function readPid(pidFile) {
|
|
289
323
|
try {
|
|
290
324
|
const n = Number(readFileSync(pidFile, 'utf8').trim());
|
|
@@ -384,6 +418,96 @@ function runInstall(ctx, args, out, err) {
|
|
|
384
418
|
});
|
|
385
419
|
return { exitCode: result.status ?? 1 };
|
|
386
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* `lensmcp trust` — make the HTTPS dev gateway trusted in real browsers: mint +
|
|
423
|
+
* trust the local dev CA, write `/etc/hosts` (both `127.0.0.1` and `::1`) for
|
|
424
|
+
* every cluster host plus `lensmcp.local`, and flush DNS. Delegates to the
|
|
425
|
+
* auto-discovered `@lensmcp/cluster:trust` target. The executor self-escalates
|
|
426
|
+
* with `sudo` per privileged step and skips each when already satisfied, so this
|
|
427
|
+
* is idempotent — but it needs a real terminal so sudo can prompt.
|
|
428
|
+
*/
|
|
429
|
+
function runTrust(ctx, args, out, err) {
|
|
430
|
+
const opts = parseFlags(args, { string: ['--cwd', '--project', '--target'] });
|
|
431
|
+
const cwd = resolve(stringFlag(opts.flags['--cwd']) ?? ctx.cwd);
|
|
432
|
+
if (!existsSync(join(cwd, 'nx.json'))) {
|
|
433
|
+
err(`No nx.json found at ${cwd}.`);
|
|
434
|
+
err('Run `lensmcp trust` from inside a Nx workspace (after `lensmcp install`).');
|
|
435
|
+
return { exitCode: 1 };
|
|
436
|
+
}
|
|
437
|
+
const nxBin = findNxBinary(cwd);
|
|
438
|
+
if (!nxBin) {
|
|
439
|
+
err('Could not find the `nx` binary in the workspace. Install Nx first: `yarn add -D nx`.');
|
|
440
|
+
return { exitCode: 1 };
|
|
441
|
+
}
|
|
442
|
+
const target = findTrustTarget(cwd, stringFlag(opts.flags['--project']), stringFlag(opts.flags['--target']));
|
|
443
|
+
if (!isInteractive()) {
|
|
444
|
+
out(' note: trust runs `sudo` for the CA + /etc/hosts — run it in a real terminal if a step is needed.');
|
|
445
|
+
}
|
|
446
|
+
out(`> ${nxBin} run ${target.project}:${target.target} (sudo prompts inline for the CA + /etc/hosts)`);
|
|
447
|
+
const result = spawnSync(nxBin, ['run', `${target.project}:${target.target}`], {
|
|
448
|
+
cwd,
|
|
449
|
+
stdio: 'inherit',
|
|
450
|
+
env: { ...process.env, ...(ctx.env ?? {}) },
|
|
451
|
+
});
|
|
452
|
+
return { exitCode: result.status ?? 1 };
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Write the setup-state marker the MCP setup-gate reads to decide readiness.
|
|
456
|
+
* The shape MUST match `servers/lensmcp-mcp/src/setup-gate/state.ts`
|
|
457
|
+
* (`SetupState`). Machine-local (`.lensmcp/` is gitignored): it records the
|
|
458
|
+
* lensmcp version setup completed for, so an upgrade re-arms the gate until
|
|
459
|
+
* `setup` runs again.
|
|
460
|
+
*/
|
|
461
|
+
function writeSetupState(cwd) {
|
|
462
|
+
ensureLensmcpDir(cwd);
|
|
463
|
+
const state = {
|
|
464
|
+
version: readCliVersion(),
|
|
465
|
+
completedAt: new Date().toISOString(),
|
|
466
|
+
steps: { install: true, trust: true },
|
|
467
|
+
};
|
|
468
|
+
try {
|
|
469
|
+
writeFileSync(join(cwd, '.lensmcp', 'setup-state.json'), `${JSON.stringify(state, null, 2)}\n`);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
/* non-fatal: the gate just treats the workspace as not-yet-set-up */
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* `lensmcp setup` — first-time bootstrap. Runs `install` (plugin + .mcp.json +
|
|
477
|
+
* nx config + vite/nest wiring, no sudo) then `trust` (dev CA → keychain,
|
|
478
|
+
* /etc/hosts, DNS flush, sudo). Both steps are idempotent, so re-running is safe.
|
|
479
|
+
* Flags are forwarded to whichever sub-command understands them (`--skip-format`,
|
|
480
|
+
* `--register-host-config` → install; `--project`/`--target` → trust).
|
|
481
|
+
*/
|
|
482
|
+
function runSetup(ctx, args, out, err) {
|
|
483
|
+
const opts = parseFlags(args, { string: ['--cwd'] });
|
|
484
|
+
const cwd = resolve(stringFlag(opts.flags['--cwd']) ?? ctx.cwd);
|
|
485
|
+
const subCtx = { ...ctx, cwd };
|
|
486
|
+
out('lensmcp setup — first-time bootstrap (install → trust)');
|
|
487
|
+
out('');
|
|
488
|
+
out('[1/2] install — plugin, .mcp.json, nx config, vite/nest wiring');
|
|
489
|
+
const installRes = runInstall(subCtx, args, out, err);
|
|
490
|
+
if (installRes.exitCode !== 0) {
|
|
491
|
+
err('');
|
|
492
|
+
err('setup: the install step failed — fix the above and re-run `lensmcp setup`.');
|
|
493
|
+
return installRes;
|
|
494
|
+
}
|
|
495
|
+
out('');
|
|
496
|
+
out('[2/2] trust — dev CA → System keychain, /etc/hosts, DNS flush (sudo)');
|
|
497
|
+
const trustRes = runTrust(subCtx, args, out, err);
|
|
498
|
+
if (trustRes.exitCode !== 0) {
|
|
499
|
+
err('');
|
|
500
|
+
err('setup: the trust step failed — run `lensmcp setup` directly in a terminal so sudo can prompt.');
|
|
501
|
+
return trustRes;
|
|
502
|
+
}
|
|
503
|
+
// Both steps succeeded → mark the workspace set up for this version (opens the MCP gate).
|
|
504
|
+
writeSetupState(cwd);
|
|
505
|
+
out('');
|
|
506
|
+
out('✓ setup complete.');
|
|
507
|
+
out(' next: lensmcp gateway start (the :443 front door — needs sudo)');
|
|
508
|
+
out(' verify: lensmcp doctor');
|
|
509
|
+
return { exitCode: 0 };
|
|
510
|
+
}
|
|
387
511
|
function runDoctor(ctx, args, out) {
|
|
388
512
|
const opts = parseFlags(args, { string: ['--cwd'], boolean: ['--json'] });
|
|
389
513
|
const cwd = resolve(stringFlag(opts.flags['--cwd']) ?? ctx.cwd);
|
|
@@ -697,6 +821,11 @@ function ensureLensmcpDir(cwd) {
|
|
|
697
821
|
if (!existsSync(dir))
|
|
698
822
|
mkdirSync(dir, { recursive: true });
|
|
699
823
|
}
|
|
824
|
+
/** `sudo` can only prompt for a password when both stdin and stdout are TTYs.
|
|
825
|
+
* Used to warn (not block) before the trust step's privileged sub-commands. */
|
|
826
|
+
function isInteractive() {
|
|
827
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
828
|
+
}
|
|
700
829
|
/** Narrow a flag value to string-or-undefined, treating booleans as absent. */
|
|
701
830
|
function stringFlag(v) {
|
|
702
831
|
if (v === undefined)
|
package/package.json
CHANGED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "lensmcp",
|
|
3
|
+
"displayName": "LensMCP",
|
|
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.10.0",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "David Antoon",
|
|
8
|
+
"email": "davidmantoon@gmail.com"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/kiwiapps-ltd/lensmcp",
|
|
11
|
+
"repository": "https://github.com/kiwiapps-ltd/lensmcp",
|
|
12
|
+
"license": "Apache-2.0",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"lens",
|
|
16
|
+
"observability",
|
|
17
|
+
"nx",
|
|
18
|
+
"vite",
|
|
19
|
+
"nestjs",
|
|
20
|
+
"gateway"
|
|
21
|
+
],
|
|
22
|
+
"mcpServers": "./.mcp.json"
|
|
23
|
+
}
|
package/plugin/.mcp.json
ADDED
package/plugin/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# LensMCP — Claude Code plugin
|
|
2
|
+
|
|
3
|
+
Run the LensMCP observability lens for **any** project, from inside Claude Code: the dev
|
|
4
|
+
cluster **gateway**, the per-project **dashboard**, and the **MCP server** your agent
|
|
5
|
+
connects to — all scoped automatically to the project you opened Claude Code in.
|
|
6
|
+
|
|
7
|
+
## What it bundles
|
|
8
|
+
|
|
9
|
+
- **MCP server** (`.mcp.json`) — launched as `lensmcp mcp --cwd ${CLAUDE_PROJECT_DIR}`, so it
|
|
10
|
+
always reads *that project's* `.lensmcp/events.jsonl`. Open Claude Code in `tetros` → you
|
|
11
|
+
get tetros's flows; open it elsewhere → that project's flows. No cross-talk.
|
|
12
|
+
- **Commands** (skills):
|
|
13
|
+
- `/lensmcp:start` — start the gateway (every `project.json` `cluster` decl → its host on
|
|
14
|
+
:443) + the lens dashboard at `https://lensmcp.local/<key>/`.
|
|
15
|
+
- `/lensmcp:stop` — stop the gateway.
|
|
16
|
+
- `/lensmcp:status` — gateway liveness + `lensmcp doctor`.
|
|
17
|
+
- `/lensmcp:dashboard` — open the per-project dashboard in the browser.
|
|
18
|
+
- `/lensmcp:using-lensmcp` — how to drive the lens (which tool answers which question).
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
The target project must have **`lensmcp` installed** as a dev dependency
|
|
23
|
+
(`yarn add -D lensmcp`) — the plugin shells out to its project-local CLI so versions stay in
|
|
24
|
+
lock-step with the project. `lensmcp` is a **dev-only** tool; never a prod/CI dependency.
|
|
25
|
+
|
|
26
|
+
Binding **:443** needs privilege — run `nx run gateway:trust` once per machine (sets up the
|
|
27
|
+
`*.local` hosts + a TLS CA), and start the gateway with the elevation your OS requires.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
**Dev (fastest, what we use while iterating):**
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
claude --plugin-dir /Users/davidantoon/git/lensmcp/plugin
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**Via marketplace (once published):**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
/plugin marketplace add kiwiapps-ltd/lensmcp
|
|
41
|
+
/plugin install lensmcp@lensmcp-marketplace
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Multi-project
|
|
45
|
+
|
|
46
|
+
Each project derives its own `key`, ports, and dashboard mount path
|
|
47
|
+
(`.lensmcp/config.json`), so the lens runs cleanly across projects — switch projects and the
|
|
48
|
+
dashboard moves to `lensmcp.local/<that-project>/` with its own ports. One gateway owns :443
|
|
49
|
+
at a time.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Open the per-project LensMCP dashboard (history-routed) in the default browser.
|
|
3
|
+
# The mount path comes from .lensmcp/config.json (e.g. /tetros), served by the gateway
|
|
4
|
+
# at https://lensmcp.local/<key>/.
|
|
5
|
+
set -uo pipefail
|
|
6
|
+
|
|
7
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
8
|
+
CFG="$PROJECT_DIR/.lensmcp/config.json"
|
|
9
|
+
|
|
10
|
+
BASE=""
|
|
11
|
+
if [ -f "$CFG" ]; then
|
|
12
|
+
BASE="$(node -e "try{process.stdout.write(require('$CFG').dashboardBasePath||'')}catch(e){}" 2>/dev/null || echo '')"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
URL="https://lensmcp.local${BASE}/"
|
|
16
|
+
echo "LensMCP dashboard → $URL"
|
|
17
|
+
echo "(needs the gateway running: /lensmcp:start)"
|
|
18
|
+
|
|
19
|
+
case "$(uname -s)" in
|
|
20
|
+
Darwin) open "$URL" 2>/dev/null || true ;;
|
|
21
|
+
Linux) xdg-open "$URL" 2>/dev/null || true ;;
|
|
22
|
+
*) echo "Open it manually: $URL" ;;
|
|
23
|
+
esac
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Start the LensMCP dev cluster gateway for the project Claude Code is open in.
|
|
3
|
+
# The gateway reads every project.json `cluster` decl, launches each app/service under
|
|
4
|
+
# its host on :443, and serves the per-project lens dashboard at lensmcp.local/<key>/.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
8
|
+
BIN="$PROJECT_DIR/node_modules/lensmcp/bin.js"
|
|
9
|
+
|
|
10
|
+
if [ ! -f "$BIN" ]; then
|
|
11
|
+
echo "✗ lensmcp is not installed in $PROJECT_DIR (no node_modules/lensmcp/bin.js)." >&2
|
|
12
|
+
echo " Install it as a dev dependency: yarn add -D lensmcp" >&2
|
|
13
|
+
echo " (the gateway is a dev-only tool — never a prod/CI dependency)." >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
exec node "$BIN" gateway start --cwd "$PROJECT_DIR"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Report LensMCP gateway status for the current project, then run the install doctor.
|
|
3
|
+
set -uo pipefail
|
|
4
|
+
|
|
5
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
6
|
+
BIN="$PROJECT_DIR/node_modules/lensmcp/bin.js"
|
|
7
|
+
|
|
8
|
+
if [ ! -f "$BIN" ]; then
|
|
9
|
+
echo "✗ lensmcp is not installed in $PROJECT_DIR." >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
node "$BIN" gateway status --cwd "$PROJECT_DIR" || true
|
|
14
|
+
echo
|
|
15
|
+
node "$BIN" doctor --cwd "$PROJECT_DIR" || true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop the LensMCP dev cluster gateway for the current project.
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
6
|
+
BIN="$PROJECT_DIR/node_modules/lensmcp/bin.js"
|
|
7
|
+
|
|
8
|
+
if [ ! -f "$BIN" ]; then
|
|
9
|
+
echo "✗ lensmcp is not installed in $PROJECT_DIR." >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
exec node "$BIN" gateway stop --cwd "$PROJECT_DIR"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Open the per-project LensMCP dashboard (Flows / Cluster / Logs / Resources) in the browser at https://lensmcp.local/<key>/. Use when the user asks to open the lens dashboard, see the flows/cluster UI, or watch the app live.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Open the LensMCP dashboard
|
|
6
|
+
|
|
7
|
+
Run the open script with the Bash tool:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"${CLAUDE_PLUGIN_ROOT}/scripts/dashboard-open.sh"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It reads the project's mount path from `.lensmcp/config.json` and opens
|
|
14
|
+
`https://lensmcp.local/<key>/` — the human view of the lens (the same event stream the
|
|
15
|
+
MCP server reads): **Flows**, **Cluster**, **Logs**, **Resources**, history-routed so deep
|
|
16
|
+
links + refresh work.
|
|
17
|
+
|
|
18
|
+
The dashboard is served by the gateway, so it needs the gateway running first
|
|
19
|
+
(`/lensmcp:start`). If the page doesn't load, check `/lensmcp:status`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Start the LensMCP dev cluster gateway for the current project — brings up every app/service/site under its host on :443 and the per-project lens dashboard at https://lensmcp.local/<key>/. Use when the user asks to start the gateway/cluster, "start lensmcp", bring the dev cluster up, or start listening.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Start the LensMCP gateway
|
|
6
|
+
|
|
7
|
+
Run the startup script with the Bash tool:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"${CLAUDE_PLUGIN_ROOT}/scripts/gateway-start.sh"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The script calls `lensmcp gateway start` for the project Claude Code is open in
|
|
14
|
+
(`${CLAUDE_PROJECT_DIR}`). That single command:
|
|
15
|
+
|
|
16
|
+
- reads every `project.json` `cluster` declaration and launches each app / service /
|
|
17
|
+
website under its own host (e.g. `app.tetros.ai.local`, `auth.tetros.ai.local`,
|
|
18
|
+
`api.tetros.ai.local/crm`), each on its own port/socket via the one HTTPS front door
|
|
19
|
+
on **:443**;
|
|
20
|
+
- serves the project-scoped lens **dashboard** at `https://lensmcp.local/<key>/`;
|
|
21
|
+
- writes a pid + log under `.lensmcp/` so it can be stopped later.
|
|
22
|
+
|
|
23
|
+
After running it:
|
|
24
|
+
|
|
25
|
+
1. Report the dashboard URL the script printed.
|
|
26
|
+
2. **Binding :443 needs privilege.** If the log shows `EACCES`/permission denied, tell
|
|
27
|
+
the user to either run the gateway target with `sudo`, or that ports <1024 require
|
|
28
|
+
elevation on macOS — and that `lensmcp setup` (install + trust) or `lensmcp trust`
|
|
29
|
+
must be run once (sudo) to set up the `*.local` hosts + TLS CA. Check
|
|
30
|
+
`.lensmcp/gateway.log` for the real error.
|
|
31
|
+
3. If the user has not run `lensmcp setup` / `lensmcp trust` yet, mention it (one-time
|
|
32
|
+
per machine).
|
|
33
|
+
|
|
34
|
+
Do **not** run a service's `serve`/`serve-hmr` manually — the gateway owns each service's
|
|
35
|
+
lifecycle. This one command is the entry point.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Report LensMCP status for the current project — gateway liveness, the dashboard URL, and the install doctor (Node, plugin wiring, MCP bundle, per-project vite/nest wiring). Use when the user asks if lensmcp is running, to check the gateway, or to diagnose the lens setup.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# LensMCP status + doctor
|
|
6
|
+
|
|
7
|
+
Run the status script with the Bash tool:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"${CLAUDE_PLUGIN_ROOT}/scripts/gateway-status.sh"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It runs `lensmcp gateway status` (is the gateway up? the project key + dashboard URL) and
|
|
14
|
+
then `lensmcp doctor` (Node version, plugin registration, `.mcp.json`, the MCP server
|
|
15
|
+
bundle, per-project Vite/Nest wiring, agent-dev targets, Chrome for browser capture).
|
|
16
|
+
|
|
17
|
+
Summarize the result for the user: whether the gateway is running, the dashboard URL, and
|
|
18
|
+
any `✗`/`!` items doctor flagged with their suggested fix.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Stop the LensMCP dev cluster gateway for the current project. Use when the user asks to stop the gateway/cluster, "stop lensmcp", or shut the dev cluster down.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Stop the LensMCP gateway
|
|
6
|
+
|
|
7
|
+
Run the stop script with the Bash tool:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
"${CLAUDE_PLUGIN_ROOT}/scripts/gateway-stop.sh"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It calls `lensmcp gateway stop` for `${CLAUDE_PROJECT_DIR}`, which reads the pid recorded
|
|
14
|
+
in `.lensmcp/gateway.pid` and terminates the gateway process group (the gateway plus the
|
|
15
|
+
app/service pods + the lens dashboard it spawned).
|
|
16
|
+
|
|
17
|
+
Report whether it stopped, or that it was already not running.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: How to drive the LensMCP lens to observe a running app — the start→interact→read loop, and which MCP tool answers which question (what happened on a click, why a component re-rendered, what's slow, memory leaks). Use when the user asks how to use lensmcp, what it can do, or to explain/trace something the running app just did.
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Using LensMCP
|
|
6
|
+
|
|
7
|
+
LensMCP gives you (and the user) eyes on a running application. Producers (the gateway,
|
|
8
|
+
agent-dev, instrumented apps) write events to `<project>/.lensmcp/events.jsonl`; the MCP
|
|
9
|
+
server and the dashboard both read that file. **Project isolation is automatic** — the MCP
|
|
10
|
+
server is launched with `--cwd ${CLAUDE_PROJECT_DIR}`, so it always reads the flows of the
|
|
11
|
+
project Claude Code is open in. Different project → different event file → different flows.
|
|
12
|
+
|
|
13
|
+
## First: is the lens set up?
|
|
14
|
+
|
|
15
|
+
LensMCP gates its tools until the workspace has been set up **once per machine** (and again
|
|
16
|
+
after a lensmcp upgrade). If a tool fails with a **"LensMCP is not ready — run `lensmcp setup`"**
|
|
17
|
+
message — or if `setup.status` reports `ready: false` — do **not** keep retrying. Tell the user
|
|
18
|
+
to run **`lensmcp setup`** in their terminal (it wires the plugin + trusts the dev CA/hosts;
|
|
19
|
+
sudo prompts inline; idempotent), then continue. The `setup.status` tool and the session
|
|
20
|
+
bootstrap stay available even while the gate is closed.
|
|
21
|
+
|
|
22
|
+
## The loop
|
|
23
|
+
|
|
24
|
+
1. **Start** the cluster + lens: `/lensmcp:start` (gateway up, all hosts routed, dashboard
|
|
25
|
+
at `https://lensmcp.local/<key>/`). For a single app instead of the whole cluster, the
|
|
26
|
+
workspace's `nx run <project>:agent-dev` also produces events.
|
|
27
|
+
2. **Interact** with the running app (click around in the browser, or let an e2e drive it).
|
|
28
|
+
3. **Read** the lens — ask in plain language and call the right MCP tool:
|
|
29
|
+
|
|
30
|
+
| The user asks… | Tool |
|
|
31
|
+
|---|---|
|
|
32
|
+
| "what's the current state / is the lens live?" | `observe_current_status` |
|
|
33
|
+
| "show me recent flows" / "what just happened?" | `flow_list_recent`, then `flow_get` |
|
|
34
|
+
| "what happened when I clicked X?" | `graph_find_backend_work_caused_by_click`, `graph_trace_from_origin` |
|
|
35
|
+
| "why did this component re-render?" | `graph_explain_rerender` |
|
|
36
|
+
| "what's slow?" / "is there a DB call in a loop?" | `graph_find_slow_path`, `graph_find_db_in_loop` |
|
|
37
|
+
| "what state updates did this cause?" | `graph_find_state_updates_caused_by` |
|
|
38
|
+
| "did anything leak memory?" | `memory_explain_leak`, `memory_find_growth_by_flow` |
|
|
39
|
+
| "tell the story of this flow" | `story_compile`, `story_get` |
|
|
40
|
+
| visual / render checks | `visual_capture_frame`, `visual_inspect_node`, `visual_compare_frames` |
|
|
41
|
+
|
|
42
|
+
4. Watch it as a human anytime with `/lensmcp:dashboard`.
|
|
43
|
+
|
|
44
|
+
## Tips
|
|
45
|
+
|
|
46
|
+
- If a tool returns nothing, no events have been produced yet — start the gateway (or an
|
|
47
|
+
`agent-dev`) and interact with the app first.
|
|
48
|
+
- `/lensmcp:status` runs the doctor if something seems off (Node, plugin wiring, MCP bundle,
|
|
49
|
+
per-project Vite/Nest instrumentation, Chrome for browser capture).
|
|
50
|
+
- Don't run a service's `serve`/`serve-hmr` by hand — the gateway owns each service's
|
|
51
|
+
lifecycle; `/lensmcp:start` is the one entry point.
|