@lightpath-dev/mcp 0.0.0-stage → 0.0.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/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/format.d.ts +9 -0
- package/dist/format.js +63 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +90 -0
- package/package.json +44 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lightpath contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lightpath-dev/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The MCP server of [Lightpath](https://github.com/Unknown-333/Lightpath#readme), so coding agents can ask which code ran for an action you just did in your app.
|
|
4
|
+
|
|
5
|
+
Run it with `npx -y @lightpath-dev/cli mcp`. Setup for Claude Code, Cursor, VS Code, and Kiro: [Ask your coding agent](https://github.com/Unknown-333/Lightpath#ask-your-coding-agent).
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LineHit, Trace, TraceSummary } from '@lightpath-dev/protocol';
|
|
2
|
+
/** Compresses sorted line numbers into ranges: [1,2,3,7,9,10] → "1-3, 7, 9-10". */
|
|
3
|
+
export declare function lineRanges(lines: readonly LineHit[]): string;
|
|
4
|
+
/** A compact, agent-readable description of one trace. */
|
|
5
|
+
export declare function describeTrace(trace: Trace): string;
|
|
6
|
+
export declare function describeSummaries(traces: readonly TraceSummary[]): string;
|
|
7
|
+
/** Lines that ran in only one of two traces, per file. */
|
|
8
|
+
export declare function diffTraces(a: Trace, b: Trace): string;
|
|
9
|
+
//# sourceMappingURL=format.d.ts.map
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/** Compresses sorted line numbers into ranges: [1,2,3,7,9,10] → "1-3, 7, 9-10". */
|
|
2
|
+
export function lineRanges(lines) {
|
|
3
|
+
const parts = [];
|
|
4
|
+
let start;
|
|
5
|
+
let prev;
|
|
6
|
+
for (const { line } of lines) {
|
|
7
|
+
if (start === undefined)
|
|
8
|
+
start = line;
|
|
9
|
+
else if (prev !== undefined && line !== prev + 1) {
|
|
10
|
+
parts.push(start === prev ? `${start}` : `${start}-${prev}`);
|
|
11
|
+
start = line;
|
|
12
|
+
}
|
|
13
|
+
prev = line;
|
|
14
|
+
}
|
|
15
|
+
if (start !== undefined && prev !== undefined)
|
|
16
|
+
parts.push(start === prev ? `${start}` : `${start}-${prev}`);
|
|
17
|
+
return parts.join(', ');
|
|
18
|
+
}
|
|
19
|
+
/** A compact, agent-readable description of one trace. */
|
|
20
|
+
export function describeTrace(trace) {
|
|
21
|
+
const out = [`Trace ${trace.id}: ${trace.label}`];
|
|
22
|
+
for (const r of trace.requests) {
|
|
23
|
+
out.push(` request: ${r.method} ${r.path}${r.status ? ` → ${r.status}` : ''} (${r.collector}, ${Math.round(r.durationMs)} ms${r.overlapped ? ', overlapped with another request' : ''})`);
|
|
24
|
+
}
|
|
25
|
+
if (trace.files.length === 0)
|
|
26
|
+
out.push(' no workspace code ran');
|
|
27
|
+
for (const f of trace.files) {
|
|
28
|
+
const hottest = Math.max(...f.lines.map((l) => l.count));
|
|
29
|
+
const bg = f.background ? ' (background: only code that also runs while idle, likely unrelated)' : '';
|
|
30
|
+
out.push(` ${f.path} [${f.collector}] lines ${lineRanges(f.lines)}${hottest > 1 ? ` (max ${hottest} hits)` : ''}${bg}`);
|
|
31
|
+
}
|
|
32
|
+
const dropped = trace.dropped.filter((d) => d.count > 0);
|
|
33
|
+
if (dropped.length > 0)
|
|
34
|
+
out.push(` not shown: ${dropped.map((d) => `${d.count} ${d.reason}`).join(', ')}`);
|
|
35
|
+
return out.join('\n');
|
|
36
|
+
}
|
|
37
|
+
export function describeSummaries(traces) {
|
|
38
|
+
if (traces.length === 0)
|
|
39
|
+
return 'No traces yet. Click something in the app opened by `lightpath dev`.';
|
|
40
|
+
return traces
|
|
41
|
+
.map((t) => `${t.id} ${new Date(t.startedAt).toISOString()} ${t.label} (${t.fileCount} files)`)
|
|
42
|
+
.join('\n');
|
|
43
|
+
}
|
|
44
|
+
/** Lines that ran in only one of two traces, per file. */
|
|
45
|
+
export function diffTraces(a, b) {
|
|
46
|
+
const index = (t) => new Map(t.files.map((f) => [f.path, new Set(f.lines.map((l) => l.line))]));
|
|
47
|
+
const ia = index(a);
|
|
48
|
+
const ib = index(b);
|
|
49
|
+
const out = [`Lines that ran in only one of ${a.id} (${a.label}) and ${b.id} (${b.label}):`];
|
|
50
|
+
for (const path of [...new Set([...ia.keys(), ...ib.keys()])].sort()) {
|
|
51
|
+
const la = ia.get(path) ?? new Set();
|
|
52
|
+
const lb = ib.get(path) ?? new Set();
|
|
53
|
+
const onlyA = [...la].filter((l) => !lb.has(l)).sort((x, y) => x - y);
|
|
54
|
+
const onlyB = [...lb].filter((l) => !la.has(l)).sort((x, y) => x - y);
|
|
55
|
+
const fmt = (ls) => lineRanges(ls.map((line) => ({ line, count: 1 })));
|
|
56
|
+
if (onlyA.length)
|
|
57
|
+
out.push(` ${path}: only in A: ${fmt(onlyA)}`);
|
|
58
|
+
if (onlyB.length)
|
|
59
|
+
out.push(` ${path}: only in B: ${fmt(onlyB)}`);
|
|
60
|
+
}
|
|
61
|
+
return out.length === 1 ? `${out[0]}\n none, both traces ran the same lines` : out.join('\n');
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=format.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
export interface McpOptions {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Builds the MCP server (docs/ARCHITECTURE.md section 9.3). Every call reads the discovery file
|
|
7
|
+
* and connects fresh, so the server works whether `lightpath dev` started before or after it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createMcpServer(options: McpOptions): McpServer;
|
|
10
|
+
/** Runs the MCP server over stdio. stdout belongs to the protocol, so nothing else may print to it. */
|
|
11
|
+
export declare function runMcpServer(options: McpOptions): Promise<void>;
|
|
12
|
+
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { DaemonClient, DaemonDiscovery, DISCOVERY_FILE } from '@lightpath-dev/protocol';
|
|
5
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
6
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { describeSummaries, describeTrace, diffTraces } from './format.js';
|
|
9
|
+
/** This package's version, read from package.json next to dist/ so it can't drift. */
|
|
10
|
+
const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
11
|
+
const text = (t) => ({ content: [{ type: 'text', text: t }] });
|
|
12
|
+
const traceIdSchema = z
|
|
13
|
+
.string()
|
|
14
|
+
.regex(/^t_[a-z0-9]{12}$/)
|
|
15
|
+
.describe('Trace id, like t_k3x9q0m2a7bz');
|
|
16
|
+
/**
|
|
17
|
+
* Builds the MCP server (docs/ARCHITECTURE.md section 9.3). Every call reads the discovery file
|
|
18
|
+
* and connects fresh, so the server works whether `lightpath dev` started before or after it.
|
|
19
|
+
*/
|
|
20
|
+
export function createMcpServer(options) {
|
|
21
|
+
const server = new McpServer({ name: 'lightpath', version: VERSION });
|
|
22
|
+
const withClient = async (fn) => {
|
|
23
|
+
const client = await connect(options.workspaceRoot);
|
|
24
|
+
try {
|
|
25
|
+
return await fn(client);
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
client.close();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
server.registerTool('list_traces', {
|
|
32
|
+
description: 'List recent user actions (clicks and requests) recorded by Lightpath, newest first.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
label: z.string().optional().describe('Only actions whose label contains this text, e.g. "Checkout"'),
|
|
35
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
36
|
+
},
|
|
37
|
+
}, async ({ label, limit }) => text(describeSummaries(await withClient((c) => c.listTraces({ limit: limit ?? 20, ...(label ? { label } : {}) })))));
|
|
38
|
+
server.registerTool('get_trace', {
|
|
39
|
+
description: 'Show every source file and line that ran for one recorded action, frontend and backend.',
|
|
40
|
+
inputSchema: { traceId: traceIdSchema },
|
|
41
|
+
}, async ({ traceId }) => {
|
|
42
|
+
const trace = await withClient((c) => c.getTrace(traceId));
|
|
43
|
+
return text(trace ? describeTrace(trace) : `No trace ${traceId}; it may have been evicted.`);
|
|
44
|
+
});
|
|
45
|
+
server.registerTool('find_code_for_action', {
|
|
46
|
+
description: 'Find the code that runs when the user does something in the app, e.g. "Checkout" or "Add to cart". ' +
|
|
47
|
+
'Uses real runtime data from the most recent matching action, not static analysis.',
|
|
48
|
+
inputSchema: { action: z.string().min(1).describe('Words from the button or link label') },
|
|
49
|
+
}, async ({ action }) => {
|
|
50
|
+
const trace = await withClient(async (c) => {
|
|
51
|
+
const [match] = await c.listTraces({ label: action, limit: 1 });
|
|
52
|
+
return match ? c.getTrace(match.id) : null;
|
|
53
|
+
});
|
|
54
|
+
return text(trace
|
|
55
|
+
? describeTrace(trace)
|
|
56
|
+
: `No recorded action matches "${action}". Ask the user to click it in the app opened by \`lightpath dev\`, then try again.`);
|
|
57
|
+
});
|
|
58
|
+
server.registerTool('diff_traces', {
|
|
59
|
+
description: 'Compare two recorded actions and list the lines that ran in only one of them.',
|
|
60
|
+
inputSchema: { a: traceIdSchema, b: traceIdSchema },
|
|
61
|
+
}, async ({ a, b }) => {
|
|
62
|
+
const [ta, tb] = await withClient((c) => Promise.all([c.getTrace(a), c.getTrace(b)]));
|
|
63
|
+
if (!ta || !tb)
|
|
64
|
+
return text(`Trace not found: ${[!ta && a, !tb && b].filter(Boolean).join(', ')}`);
|
|
65
|
+
return text(diffTraces(ta, tb));
|
|
66
|
+
});
|
|
67
|
+
return server;
|
|
68
|
+
}
|
|
69
|
+
/** Runs the MCP server over stdio. stdout belongs to the protocol, so nothing else may print to it. */
|
|
70
|
+
export async function runMcpServer(options) {
|
|
71
|
+
await createMcpServer(options).connect(new StdioServerTransport());
|
|
72
|
+
}
|
|
73
|
+
async function connect(workspaceRoot) {
|
|
74
|
+
// The nearest folder at or above the root with a discovery file: editors don't always start
|
|
75
|
+
// MCP servers in the project folder itself.
|
|
76
|
+
let raw;
|
|
77
|
+
for (let dir = resolve(workspaceRoot);;) {
|
|
78
|
+
raw = await readFile(join(dir, DISCOVERY_FILE), 'utf8').catch(() => undefined);
|
|
79
|
+
const parent = dirname(dir);
|
|
80
|
+
if (raw !== undefined || parent === dir)
|
|
81
|
+
break;
|
|
82
|
+
dir = parent;
|
|
83
|
+
}
|
|
84
|
+
if (raw === undefined) {
|
|
85
|
+
throw new Error(`Lightpath isn't running in ${workspaceRoot}. Start it with: lightpath dev -- <your dev command>`);
|
|
86
|
+
}
|
|
87
|
+
const info = DaemonDiscovery.parse(JSON.parse(raw));
|
|
88
|
+
return DaemonClient.connect(info.port, info.token);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=server.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightpath-dev/mcp",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "MCP server that lets coding agents ask which code ran for an action in your app",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"coverage",
|
|
9
|
+
"trace",
|
|
10
|
+
"agent"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"homepage": "https://github.com/Unknown-333/Lightpath#readme",
|
|
14
|
+
"bugs": "https://github.com/Unknown-333/Lightpath/issues",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/Unknown-333/Lightpath.git",
|
|
18
|
+
"directory": "packages/mcp"
|
|
19
|
+
},
|
|
20
|
+
"type": "module",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"!dist/.tsbuildinfo",
|
|
30
|
+
"!dist/**/*.map"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.12"
|
|
34
|
+
},
|
|
35
|
+
"publishConfig": {
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@lightpath-dev/protocol": "0.0.1",
|
|
40
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
41
|
+
"zod": "4.6.7"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "vitest run",
|
|
45
|
+
"typecheck": "tsc -p test"
|
|
46
|
+
}
|
|
6
47
|
}
|