lighthouse-mcp 0.1.15 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/build/index.js +44 -121
- package/build/network.d.ts +11 -0
- package/build/network.js +107 -0
- package/package.json +12 -15
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ Run a comprehensive Lighthouse audit on a URL.
|
|
|
101
101
|
**Parameters:**
|
|
102
102
|
- `url` (required): The URL to audit
|
|
103
103
|
- `categories` (optional): Array of categories to audit (defaults to all)
|
|
104
|
-
- Options: "performance", "accessibility", "best-practices", "seo"
|
|
104
|
+
- Options: "performance", "accessibility", "best-practices", "seo"
|
|
105
105
|
- `device` (optional): Device to emulate (defaults to "mobile")
|
|
106
106
|
- Options: "mobile", "desktop"
|
|
107
107
|
- `throttling` (optional): Whether to apply network throttling (defaults to true)
|
|
@@ -145,10 +145,16 @@ Claude will use the `get_performance_score` tool to analyze the website and retu
|
|
|
145
145
|
|
|
146
146
|
## Requirements
|
|
147
147
|
|
|
148
|
-
- Node.js
|
|
148
|
+
- Node.js 22.19+
|
|
149
149
|
- Chrome/Chromium browser (for Lighthouse)
|
|
150
150
|
|
|
151
151
|
## Endorsements
|
|
152
152
|
<a href="https://glama.ai/mcp/servers/@priyankark/lighthouse-mcp">
|
|
153
153
|
<img width="380" height="200" src="https://glama.ai/mcp/servers/@priyankark/lighthouse-mcp/badge" />
|
|
154
154
|
</a>
|
|
155
|
+
|
|
156
|
+
## Security
|
|
157
|
+
|
|
158
|
+
Chrome runs with its sandbox enabled. Localhost, IPv4 loopback, and IPv6 loopback remain supported by default. Private, link-local, cloud metadata, and other non-public destinations are blocked, including requests made by redirects and page resources. Connections use validated IP addresses to prevent DNS rebinding. Because loopback access is intentional, audit only pages you trust to access services on your own machine. For hostile sites, use a separate environment with network isolation.
|
|
159
|
+
|
|
160
|
+
Audits are limited to one at a time and time out after 120 seconds. Chrome and the audit proxy are cleaned up after success or failure.
|
package/build/index.js
CHANGED
|
@@ -7,123 +7,15 @@ import * as chromeLauncher from 'chrome-launcher';
|
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import fs from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
|
-
import
|
|
11
|
-
import net from 'net';
|
|
12
|
-
// Workaround for modelcontextprotocol/typescript-sdk#1380
|
|
13
|
-
// In some Zod runtimes, the method literal is stored under `_def.values[0]`
|
|
14
|
-
// instead of `_def.value` / `.value`, causing "Schema method literal must be a string"
|
|
15
|
-
// during Server initialization. This patches setRequestHandler to handle both cases.
|
|
16
|
-
const originalSetRequestHandler = Server.prototype.setRequestHandler;
|
|
17
|
-
Server.prototype.setRequestHandler = function patchedSetRequestHandler(requestSchema, handler) {
|
|
18
|
-
try {
|
|
19
|
-
return originalSetRequestHandler.call(this, requestSchema, handler);
|
|
20
|
-
}
|
|
21
|
-
catch (err) {
|
|
22
|
-
if (err?.message !== 'Schema method literal must be a string')
|
|
23
|
-
throw err;
|
|
24
|
-
// Attempt to fix the schema by copying values[0] to value
|
|
25
|
-
try {
|
|
26
|
-
const shape = requestSchema?.shape ?? requestSchema?._def?.shape?.();
|
|
27
|
-
const methodSchema = shape?.method;
|
|
28
|
-
const def = methodSchema?._def;
|
|
29
|
-
const maybeValue = Array.isArray(def?.values) ? def.values[0] : undefined;
|
|
30
|
-
if (typeof maybeValue === 'string') {
|
|
31
|
-
if (def && def.value === undefined)
|
|
32
|
-
def.value = maybeValue;
|
|
33
|
-
if (methodSchema && methodSchema.value === undefined)
|
|
34
|
-
methodSchema.value = maybeValue;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// If patching fails, rethrow the original error
|
|
39
|
-
}
|
|
40
|
-
return originalSetRequestHandler.call(this, requestSchema, handler);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
// SSRF protection — validate URLs before passing to Lighthouse
|
|
45
|
-
// Allows localhost/loopback (needed for local dev servers) but blocks
|
|
46
|
-
// cloud metadata endpoints, RFC 1918 private ranges, and link-local IPs.
|
|
47
|
-
// ---------------------------------------------------------------------------
|
|
48
|
-
const BLOCKED_IP_RANGES = [
|
|
49
|
-
// RFC 1918 private networks
|
|
50
|
-
{ prefix: '10.', mask: null },
|
|
51
|
-
{ prefix: '172.', mask: (ip) => { const b = parseInt(ip.split('.')[1], 10); return b >= 16 && b <= 31; } },
|
|
52
|
-
{ prefix: '192.168.', mask: null },
|
|
53
|
-
// Link-local (includes AWS metadata 169.254.169.254)
|
|
54
|
-
{ prefix: '169.254.', mask: null },
|
|
55
|
-
];
|
|
56
|
-
const BLOCKED_HOSTNAMES = [
|
|
57
|
-
'metadata.google.internal',
|
|
58
|
-
'metadata.goog',
|
|
59
|
-
];
|
|
60
|
-
function isBlockedIP(ip) {
|
|
61
|
-
for (const range of BLOCKED_IP_RANGES) {
|
|
62
|
-
if (ip.startsWith(range.prefix)) {
|
|
63
|
-
if (range.mask === null || range.mask(ip))
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
function isLoopback(ip) {
|
|
70
|
-
if (ip === '::1')
|
|
71
|
-
return true;
|
|
72
|
-
if (ip.startsWith('127.'))
|
|
73
|
-
return true;
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
|
-
async function validateUrl(url) {
|
|
77
|
-
let parsed;
|
|
78
|
-
try {
|
|
79
|
-
parsed = new URL(url);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
throw new McpError(ErrorCode.InvalidParams, `Invalid URL: ${url}`);
|
|
83
|
-
}
|
|
84
|
-
// Only allow http and https schemes
|
|
85
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
86
|
-
throw new McpError(ErrorCode.InvalidParams, `Unsupported URL scheme "${parsed.protocol}" — only http: and https: are allowed`);
|
|
87
|
-
}
|
|
88
|
-
const hostname = parsed.hostname;
|
|
89
|
-
// Block known cloud metadata hostnames
|
|
90
|
-
if (BLOCKED_HOSTNAMES.includes(hostname.toLowerCase())) {
|
|
91
|
-
throw new McpError(ErrorCode.InvalidParams, `URL hostname "${hostname}" is blocked (cloud metadata endpoint)`);
|
|
92
|
-
}
|
|
93
|
-
// If the hostname is an IP literal, validate it directly
|
|
94
|
-
if (net.isIP(hostname)) {
|
|
95
|
-
if (isLoopback(hostname))
|
|
96
|
-
return; // allow localhost
|
|
97
|
-
if (isBlockedIP(hostname)) {
|
|
98
|
-
throw new McpError(ErrorCode.InvalidParams, `URL resolves to a blocked internal IP address (${hostname})`);
|
|
99
|
-
}
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
// Resolve the hostname and check every returned address
|
|
103
|
-
let addresses;
|
|
104
|
-
try {
|
|
105
|
-
const results = await dns.resolve4(hostname);
|
|
106
|
-
addresses = results;
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
// If DNS resolution fails, let Lighthouse handle the error naturally
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
for (const ip of addresses) {
|
|
113
|
-
if (isLoopback(ip))
|
|
114
|
-
continue; // allow localhost
|
|
115
|
-
if (isBlockedIP(ip)) {
|
|
116
|
-
throw new McpError(ErrorCode.InvalidParams, `URL hostname "${hostname}" resolves to blocked internal IP address (${ip})`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
10
|
+
import { createAuditProxy, parseUrl, resolveTarget } from './network.js';
|
|
120
11
|
const isValidAuditArgs = (args) => {
|
|
121
12
|
return (typeof args === 'object' &&
|
|
122
13
|
args !== null &&
|
|
123
14
|
typeof args.url === 'string' &&
|
|
124
15
|
(args.categories === undefined ||
|
|
125
16
|
(Array.isArray(args.categories) &&
|
|
126
|
-
args.categories.
|
|
17
|
+
args.categories.length > 0 && args.categories.length <= 4 &&
|
|
18
|
+
args.categories.every((cat) => ['performance', 'accessibility', 'best-practices', 'seo'].includes(cat)))) &&
|
|
127
19
|
(args.device === undefined ||
|
|
128
20
|
args.device === 'mobile' ||
|
|
129
21
|
args.device === 'desktop') &&
|
|
@@ -131,9 +23,10 @@ const isValidAuditArgs = (args) => {
|
|
|
131
23
|
};
|
|
132
24
|
class LighthouseServer {
|
|
133
25
|
constructor() {
|
|
26
|
+
this.auditRunning = false;
|
|
134
27
|
this.server = new Server({
|
|
135
28
|
name: 'lighthouse-mcp',
|
|
136
|
-
version: '0.1.
|
|
29
|
+
version: '0.1.16',
|
|
137
30
|
}, {
|
|
138
31
|
capabilities: {
|
|
139
32
|
tools: {},
|
|
@@ -169,7 +62,6 @@ class LighthouseServer {
|
|
|
169
62
|
'accessibility',
|
|
170
63
|
'best-practices',
|
|
171
64
|
'seo',
|
|
172
|
-
'pwa',
|
|
173
65
|
],
|
|
174
66
|
},
|
|
175
67
|
description: 'Categories to audit (defaults to all)',
|
|
@@ -223,9 +115,17 @@ class LighthouseServer {
|
|
|
223
115
|
if (!isValidAuditArgs(args)) {
|
|
224
116
|
throw new McpError(ErrorCode.InvalidParams, 'Invalid audit arguments');
|
|
225
117
|
}
|
|
226
|
-
|
|
227
|
-
|
|
118
|
+
if (this.auditRunning)
|
|
119
|
+
throw new McpError(ErrorCode.InvalidRequest, 'An audit is already running');
|
|
120
|
+
this.auditRunning = true;
|
|
121
|
+
let chrome;
|
|
122
|
+
let proxy;
|
|
123
|
+
let timer;
|
|
228
124
|
try {
|
|
125
|
+
const allowLoopback = true;
|
|
126
|
+
const url = parseUrl(args.url);
|
|
127
|
+
await resolveTarget(url.hostname, allowLoopback);
|
|
128
|
+
proxy = await createAuditProxy(allowLoopback);
|
|
229
129
|
// Ensure temp directory exists and is writable (fixes #19 - Windows EPERM)
|
|
230
130
|
// On Windows, os.tmpdir() reads TEMP -> TMP -> USERPROFILE, so we verify
|
|
231
131
|
// the resolved path is usable before launching Chrome.
|
|
@@ -237,7 +137,7 @@ class LighthouseServer {
|
|
|
237
137
|
// If the default temp dir isn't writable, create a fallback in the user's home
|
|
238
138
|
const fallbackTmp = path.join(os.homedir(), '.lighthouse-tmp');
|
|
239
139
|
if (!fs.existsSync(fallbackTmp)) {
|
|
240
|
-
fs.mkdirSync(fallbackTmp, { recursive: true });
|
|
140
|
+
fs.mkdirSync(fallbackTmp, { recursive: true, mode: 0o700 });
|
|
241
141
|
}
|
|
242
142
|
process.env.TEMP = fallbackTmp;
|
|
243
143
|
process.env.TMP = fallbackTmp;
|
|
@@ -245,12 +145,14 @@ class LighthouseServer {
|
|
|
245
145
|
}
|
|
246
146
|
// Explicitly pass process.env so MCP-configured env vars (TEMP, TMP, TMPDIR)
|
|
247
147
|
// propagate to the Chrome child process on all platforms.
|
|
248
|
-
|
|
249
|
-
chromeFlags: ['--headless',
|
|
148
|
+
chrome = await chromeLauncher.launch({
|
|
149
|
+
chromeFlags: ['--headless', `--proxy-server=http://127.0.0.1:${proxy.port}`,
|
|
150
|
+
'--proxy-bypass-list=<-loopback>', '--disable-quic',
|
|
151
|
+
'--force-webrtc-ip-handling-policy=disable_non_proxied_udp'],
|
|
250
152
|
envVars: process.env,
|
|
251
153
|
});
|
|
252
154
|
const options = {
|
|
253
|
-
logLevel: '
|
|
155
|
+
logLevel: 'error',
|
|
254
156
|
output: 'json',
|
|
255
157
|
onlyCategories: args.categories,
|
|
256
158
|
port: chrome.port,
|
|
@@ -272,8 +174,12 @@ class LighthouseServer {
|
|
|
272
174
|
cpuSlowdownMultiplier: 1,
|
|
273
175
|
},
|
|
274
176
|
};
|
|
275
|
-
const runnerResult = await
|
|
276
|
-
|
|
177
|
+
const runnerResult = await Promise.race([
|
|
178
|
+
lighthouse(args.url, options),
|
|
179
|
+
new Promise((_resolve, reject) => {
|
|
180
|
+
timer = setTimeout(() => reject(new Error('Audit timed out after 120 seconds')), 120000);
|
|
181
|
+
}),
|
|
182
|
+
]);
|
|
277
183
|
if (!runnerResult) {
|
|
278
184
|
throw new McpError(ErrorCode.InternalError, 'Failed to run Lighthouse audit');
|
|
279
185
|
}
|
|
@@ -342,6 +248,21 @@ class LighthouseServer {
|
|
|
342
248
|
isError: true,
|
|
343
249
|
};
|
|
344
250
|
}
|
|
251
|
+
finally {
|
|
252
|
+
if (timer)
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
try {
|
|
255
|
+
await chrome?.kill();
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
try {
|
|
259
|
+
await proxy?.close();
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
this.auditRunning = false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
345
266
|
}
|
|
346
267
|
async handleGetPerformanceScore(args) {
|
|
347
268
|
if (!isValidAuditArgs(args)) {
|
|
@@ -356,6 +277,8 @@ class LighthouseServer {
|
|
|
356
277
|
throttling: true,
|
|
357
278
|
};
|
|
358
279
|
const result = await this.handleRunAudit(auditArgs);
|
|
280
|
+
if (result.isError)
|
|
281
|
+
return result;
|
|
359
282
|
// Extract just the performance data
|
|
360
283
|
const resultData = JSON.parse(result.content[0].text);
|
|
361
284
|
const performanceData = {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import dns from 'node:dns/promises';
|
|
2
|
+
export declare function isAllowedAddress(address: string, allowLoopback?: boolean): boolean;
|
|
3
|
+
export declare function parseUrl(input: string): URL;
|
|
4
|
+
export declare function resolveTarget(host: string, allowLoopback?: boolean, lookup?: typeof dns.lookup): Promise<{
|
|
5
|
+
address: string;
|
|
6
|
+
family: number;
|
|
7
|
+
}>;
|
|
8
|
+
export declare function createAuditProxy(allowLoopback?: boolean): Promise<{
|
|
9
|
+
port: number;
|
|
10
|
+
close: () => Promise<void>;
|
|
11
|
+
}>;
|
package/build/network.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import dns from 'node:dns/promises';
|
|
2
|
+
import http from 'node:http';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import ipaddr from 'ipaddr.js';
|
|
5
|
+
export function isAllowedAddress(address, allowLoopback = true) {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = ipaddr.process(address);
|
|
8
|
+
return parsed.range() === 'unicast' || (allowLoopback && parsed.range() === 'loopback');
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function parseUrl(input) {
|
|
15
|
+
const url = new URL(input);
|
|
16
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password) {
|
|
17
|
+
throw new Error('Only HTTP(S) URLs without embedded credentials are allowed');
|
|
18
|
+
}
|
|
19
|
+
return url;
|
|
20
|
+
}
|
|
21
|
+
export async function resolveTarget(host, allowLoopback = true, lookup = dns.lookup) {
|
|
22
|
+
const hostname = host.replace(/^\[|\]$/g, '').replace(/\.$/, '').toLowerCase();
|
|
23
|
+
if (['metadata.google.internal', 'metadata.goog'].includes(hostname)) {
|
|
24
|
+
throw new Error('Cloud metadata endpoints are blocked');
|
|
25
|
+
}
|
|
26
|
+
const addresses = net.isIP(hostname)
|
|
27
|
+
? [{ address: hostname, family: net.isIP(hostname) }]
|
|
28
|
+
: await lookup(hostname, { all: true, verbatim: true });
|
|
29
|
+
if (!addresses.length || addresses.some(a => !isAllowedAddress(a.address, allowLoopback))) {
|
|
30
|
+
throw new Error('Non-public network addresses are blocked');
|
|
31
|
+
}
|
|
32
|
+
return addresses[0];
|
|
33
|
+
}
|
|
34
|
+
// Resolve and validate each connection, then connect to that exact IP. Chrome
|
|
35
|
+
// must use this proxy for redirects and subresources too (no loopback bypass).
|
|
36
|
+
export async function createAuditProxy(allowLoopback = true) {
|
|
37
|
+
const sockets = new Set();
|
|
38
|
+
const track = (socket) => {
|
|
39
|
+
sockets.add(socket);
|
|
40
|
+
socket.setTimeout(120000, () => socket.destroy());
|
|
41
|
+
socket.once('close', () => sockets.delete(socket));
|
|
42
|
+
return socket;
|
|
43
|
+
};
|
|
44
|
+
const server = http.createServer(async (req, res) => {
|
|
45
|
+
try {
|
|
46
|
+
const url = parseUrl(req.url || '');
|
|
47
|
+
if (url.protocol !== 'http:')
|
|
48
|
+
throw new Error('Use CONNECT for HTTPS');
|
|
49
|
+
const target = await resolveTarget(url.hostname, allowLoopback);
|
|
50
|
+
if (res.destroyed)
|
|
51
|
+
return;
|
|
52
|
+
const headers = { ...req.headers, host: url.host };
|
|
53
|
+
delete headers['proxy-authorization'];
|
|
54
|
+
delete headers['proxy-connection'];
|
|
55
|
+
const upstream = http.request({
|
|
56
|
+
hostname: target.address, family: target.family,
|
|
57
|
+
port: url.port || 80, path: url.pathname + url.search,
|
|
58
|
+
method: req.method, headers, agent: false,
|
|
59
|
+
}, response => { res.writeHead(response.statusCode || 502, response.headers); response.pipe(res); });
|
|
60
|
+
upstream.on('socket', track);
|
|
61
|
+
upstream.on('error', () => { if (!res.headersSent)
|
|
62
|
+
res.writeHead(502); res.end(); });
|
|
63
|
+
res.on('close', () => upstream.destroy());
|
|
64
|
+
req.pipe(upstream);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
res.writeHead(403);
|
|
68
|
+
res.end('Destination blocked');
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
server.on('connection', track);
|
|
72
|
+
server.on('connect', async (req, client, head) => {
|
|
73
|
+
try {
|
|
74
|
+
const url = parseUrl(`https://${req.url}`);
|
|
75
|
+
const target = await resolveTarget(url.hostname, allowLoopback);
|
|
76
|
+
if (client.destroyed)
|
|
77
|
+
return;
|
|
78
|
+
const upstream = track(net.connect({ host: target.address, family: target.family, port: Number(url.port || 443) }));
|
|
79
|
+
upstream.on('error', () => client.destroy());
|
|
80
|
+
client.on('error', () => upstream.destroy());
|
|
81
|
+
client.on('close', () => upstream.destroy());
|
|
82
|
+
upstream.once('connect', () => {
|
|
83
|
+
client.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
|
84
|
+
upstream.write(head);
|
|
85
|
+
client.pipe(upstream);
|
|
86
|
+
upstream.pipe(client);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
client.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
server.on('clientError', (_error, socket) => socket.destroy());
|
|
94
|
+
await new Promise((resolve, reject) => {
|
|
95
|
+
server.once('error', reject);
|
|
96
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
97
|
+
});
|
|
98
|
+
const address = server.address();
|
|
99
|
+
return {
|
|
100
|
+
port: address.port,
|
|
101
|
+
close: async () => {
|
|
102
|
+
for (const socket of sockets)
|
|
103
|
+
socket.destroy();
|
|
104
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lighthouse-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "MCP server for Google Lighthouse performance metrics",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
"build": "tsc && chmod +x build/index.js",
|
|
16
16
|
"start": "node build/index.js",
|
|
17
17
|
"dev": "tsc -w",
|
|
18
|
-
"prepublishOnly": "npm
|
|
19
|
-
"update-server-json": "node -e \"const fs=require('fs');const pkg=JSON.parse(fs.readFileSync('package.json'));const server=JSON.parse(fs.readFileSync('server.json'));server.version=pkg.version;server.packages[0].version=pkg.version;fs.writeFileSync('server.json',JSON.stringify(server,null,2));\""
|
|
18
|
+
"prepublishOnly": "npm test",
|
|
19
|
+
"update-server-json": "node -e \"const fs=require('fs');const pkg=JSON.parse(fs.readFileSync('package.json'));const server=JSON.parse(fs.readFileSync('server.json'));server.version=pkg.version;server.packages[0].version=pkg.version;fs.writeFileSync('server.json',JSON.stringify(server,null,2));\"",
|
|
20
|
+
"test": "npm run build && node --test test/*.test.mjs"
|
|
20
21
|
},
|
|
21
22
|
"keywords": [
|
|
22
23
|
"lighthouse",
|
|
@@ -32,23 +33,19 @@
|
|
|
32
33
|
"license": "MIT",
|
|
33
34
|
"repository": {
|
|
34
35
|
"type": "git",
|
|
35
|
-
"url": ""
|
|
36
|
+
"url": "https://github.com/priyankark/lighthouse-mcp.git"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
39
|
-
"chrome-launcher": "^
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"overrides": {
|
|
43
|
-
"basic-ftp": ">=5.3.0",
|
|
44
|
-
"lodash-es": ">=4.18.1",
|
|
45
|
-
"path-to-regexp": ">=8.4.0",
|
|
46
|
-
"brace-expansion": ">=5.0.5",
|
|
47
|
-
"hono": ">=4.12.14",
|
|
48
|
-
"@hono/node-server": ">=1.19.13"
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
40
|
+
"chrome-launcher": "^1.2.1",
|
|
41
|
+
"ipaddr.js": "^2.5.0",
|
|
42
|
+
"lighthouse": "^13.4.1"
|
|
49
43
|
},
|
|
50
44
|
"devDependencies": {
|
|
51
45
|
"@types/node": "^20.4.5",
|
|
52
46
|
"typescript": "^5.1.6"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=22.19.0"
|
|
53
50
|
}
|
|
54
51
|
}
|