mcp-optimizer 0.0.2-alpha.1 → 0.0.2
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 +20 -4
- package/dist/mcpServer.js +70 -7
- package/dist/runner/lighthouseRunner.js +3 -1
- package/package.json +1 -1
- package/src/mcpServer.ts +67 -7
- package/src/runner/lighthouseRunner.ts +3 -1
- package/dist/server.js +0 -79
package/README.md
CHANGED
|
@@ -2,7 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
A minimal scaffold that runs Lighthouse to produce performance reports. The project includes a placeholder fixer that can be extended to integrate an LLM for automatic code fixes.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Using with an MCP Host
|
|
6
|
+
This section provides an example of launching via an MCP host (stdio).
|
|
7
|
+
|
|
8
|
+
If you want to configure an MCP host to spawn the optimizer via `npx`, add a server entry like the following to your host config:
|
|
9
|
+
|
|
10
|
+
```json
|
|
11
|
+
{
|
|
12
|
+
"mcpServers": {
|
|
13
|
+
"mcp-optimizer": {
|
|
14
|
+
"command": "npx",
|
|
15
|
+
"args": ["-y", "mcp-optimizer@latest", "--","--port", "5000"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This will instruct the host to spawn `npx -y mcp-optimizer@latest -- --port 5000` and communicate with the child over stdio.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
6
24
|
|
|
7
25
|
1. Install runtime dependencies:
|
|
8
26
|
|
|
@@ -25,7 +43,5 @@ npm run dev
|
|
|
25
43
|
curl -X POST http://localhost:3000/audit -H "Content-Type: application/json" -d '{"url":"https://example.com"}'
|
|
26
44
|
```
|
|
27
45
|
|
|
28
|
-
|
|
29
|
-
- `src/runner/lighthouseRunner.ts` — runs Lighthouse via `chrome-launcher` and returns the LHR.
|
|
30
|
-
- `src/fix/fixer.ts` — placeholder to convert LHR into actionable fixes; integrate LLM (e.g., via `@modelcontextprotocol/sdk`) here.
|
|
46
|
+
|
|
31
47
|
|
package/dist/mcpServer.js
CHANGED
|
@@ -39,6 +39,7 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
39
39
|
const zod_1 = require("zod");
|
|
40
40
|
const lighthouseRunner_1 = require("./runner/lighthouseRunner");
|
|
41
41
|
const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
|
|
42
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
42
43
|
const http = __importStar(require("http"));
|
|
43
44
|
const fixer_1 = require("./fix/fixer");
|
|
44
45
|
class LighthouseMcpServer {
|
|
@@ -105,12 +106,11 @@ class LighthouseMcpServer {
|
|
|
105
106
|
performance: perf !== null ? Math.round(perf * 100) : undefined,
|
|
106
107
|
accessibility: accessibility !== null ? Math.round(accessibility * 100) : undefined
|
|
107
108
|
};
|
|
108
|
-
//
|
|
109
|
+
// 返回精简信息:只返回 summary 与 reportId,完整报告保存在服务器以便按需获取
|
|
109
110
|
return {
|
|
110
111
|
content: [
|
|
111
112
|
{ type: "text", text: JSON.stringify(summary, null, 2) },
|
|
112
|
-
{ type: "text", text:
|
|
113
|
-
{ type: "text", text: JSON.stringify(reportObj, null, 2) }
|
|
113
|
+
{ type: "text", text: `reportId:${id}` }
|
|
114
114
|
]
|
|
115
115
|
};
|
|
116
116
|
}
|
|
@@ -165,12 +165,12 @@ class LighthouseMcpServer {
|
|
|
165
165
|
performance: perf !== null ? Math.round(perf * 100) : undefined,
|
|
166
166
|
accessibility: accessibility !== null ? Math.round(accessibility * 100) : undefined
|
|
167
167
|
};
|
|
168
|
+
const fixAvailable = !!fix;
|
|
168
169
|
return {
|
|
169
170
|
content: [
|
|
170
171
|
{ type: "text", text: JSON.stringify(summary, null, 2) },
|
|
171
|
-
{ type: "text", text:
|
|
172
|
-
{ type: "text", text: JSON.stringify(
|
|
173
|
-
{ type: "text", text: JSON.stringify({ fix }, null, 2) }
|
|
172
|
+
{ type: "text", text: `reportId:${result.id}` },
|
|
173
|
+
{ type: "text", text: JSON.stringify({ fixAvailable }, null, 2) }
|
|
174
174
|
]
|
|
175
175
|
};
|
|
176
176
|
}
|
|
@@ -205,9 +205,72 @@ exports.LighthouseMcpServer = LighthouseMcpServer;
|
|
|
205
205
|
exports.default = LighthouseMcpServer;
|
|
206
206
|
async function startMcpServer() {
|
|
207
207
|
// Run HTTP/SSE server
|
|
208
|
-
|
|
208
|
+
// Prefer explicit environment variables, but also allow parsing CLI args
|
|
209
|
+
// (e.g. when launched directly without the lightweight `bin` wrapper).
|
|
210
|
+
let portEnv = process.env.PORT || process.env.AUDIT_PORT;
|
|
211
|
+
if (!portEnv) {
|
|
212
|
+
const args = process.argv.slice(2);
|
|
213
|
+
for (let i = 0; i < args.length; i++) {
|
|
214
|
+
const a = args[i];
|
|
215
|
+
if (a.includes('=') && !a.startsWith('--')) {
|
|
216
|
+
const [k, v] = a.split('=', 2);
|
|
217
|
+
if ((k === 'PORT' || k === 'AUDIT_PORT') && v !== undefined) {
|
|
218
|
+
portEnv = v;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (a.startsWith('--port=')) {
|
|
223
|
+
portEnv = a.split('=')[1];
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
else if (a === '--port' && args[i + 1]) {
|
|
227
|
+
portEnv = args[i + 1];
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
else if (a.startsWith('--audit-port=')) {
|
|
231
|
+
portEnv = a.split('=')[1];
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
else if (a === '--audit-port' && args[i + 1]) {
|
|
235
|
+
portEnv = args[i + 1];
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const port = Number(portEnv || 5000);
|
|
209
241
|
const mcp = new LighthouseMcpServer();
|
|
210
242
|
let sseTransport = null;
|
|
243
|
+
let stdioTransport = null;
|
|
244
|
+
// If process stdin/stdout appear to be non-TTY pipes, assume we're being
|
|
245
|
+
// launched as a stdio child by an MCP host and use the stdio transport.
|
|
246
|
+
const shouldUseStdio = (process.stdin && !process.stdin.isTTY) && (process.stdout && !process.stdout.isTTY);
|
|
247
|
+
if (shouldUseStdio) {
|
|
248
|
+
// Redirect console output to stderr to avoid corrupting the JSON stdio protocol
|
|
249
|
+
const _orig = { log: console.log, info: console.info, warn: console.warn };
|
|
250
|
+
console.log = (...args) => { process.stderr.write(args.map(String).join(' ') + '\n'); };
|
|
251
|
+
console.info = console.log;
|
|
252
|
+
console.warn = console.log;
|
|
253
|
+
try {
|
|
254
|
+
stdioTransport = new stdio_js_1.StdioServerTransport(process.stdin, process.stdout);
|
|
255
|
+
await mcp.connect(stdioTransport);
|
|
256
|
+
console.error('Stdio: connected to parent process over stdio');
|
|
257
|
+
// When running over stdio we do not start the HTTP server; stay alive
|
|
258
|
+
// and let the parent coordinate messages. Return a promise that
|
|
259
|
+
// resolves when the transport closes.
|
|
260
|
+
return new Promise((resolve, reject) => {
|
|
261
|
+
stdioTransport.onclose = () => resolve();
|
|
262
|
+
stdioTransport.onerror = (err) => reject(err);
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
console.error('Stdio: failed to start transport, falling back to HTTP:', err);
|
|
267
|
+
stdioTransport = null;
|
|
268
|
+
// restore console in case we fall back to HTTP server mode
|
|
269
|
+
console.log = _orig.log;
|
|
270
|
+
console.info = _orig.info;
|
|
271
|
+
console.warn = _orig.warn;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
211
274
|
const pendingPosts = [];
|
|
212
275
|
const { Readable, Writable } = await (async () => {
|
|
213
276
|
const mod = await Promise.resolve().then(() => __importStar(require('stream')));
|
|
@@ -30,7 +30,9 @@ async function runLighthouseAudit(url, opts) {
|
|
|
30
30
|
// Run via npx to ensure local package is used
|
|
31
31
|
const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
32
32
|
const reportJson = await new Promise((resolve, reject) => {
|
|
33
|
-
|
|
33
|
+
// Use a shell on Windows to ensure .cmd/.bat wrappers are executed
|
|
34
|
+
// correctly (avoids spawn EINVAL in some environments).
|
|
35
|
+
execFile(cmd, args, { maxBuffer: 10 * 1024 * 1024, shell: true, env: process.env }, (err, stdout, stderr) => {
|
|
34
36
|
if (err) {
|
|
35
37
|
const message = stderr || (err && err.message) || String(err);
|
|
36
38
|
return reject(new Error(message));
|
package/package.json
CHANGED
package/src/mcpServer.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { runLighthouseAudit } from "./runner/lighthouseRunner";
|
|
4
4
|
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
|
5
5
|
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
7
|
import * as http from 'http';
|
|
7
8
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
8
9
|
import { autoFixFromReport } from './fix/fixer';
|
|
@@ -79,12 +80,11 @@ export class LighthouseMcpServer {
|
|
|
79
80
|
performance: perf !== null ? Math.round(perf * 100) : undefined,
|
|
80
81
|
accessibility: accessibility !== null ? Math.round(accessibility * 100) : undefined
|
|
81
82
|
};
|
|
82
|
-
//
|
|
83
|
+
// 返回精简信息:只返回 summary 与 reportId,完整报告保存在服务器以便按需获取
|
|
83
84
|
return {
|
|
84
85
|
content: [
|
|
85
86
|
{ type: "text", text: JSON.stringify(summary, null, 2) },
|
|
86
|
-
{ type: "text", text:
|
|
87
|
-
{ type: "text", text: JSON.stringify(reportObj, null, 2) }
|
|
87
|
+
{ type: "text", text: `reportId:${id}` }
|
|
88
88
|
]
|
|
89
89
|
};
|
|
90
90
|
} catch (error) {
|
|
@@ -149,12 +149,12 @@ export class LighthouseMcpServer {
|
|
|
149
149
|
performance: perf !== null ? Math.round(perf * 100) : undefined,
|
|
150
150
|
accessibility: accessibility !== null ? Math.round(accessibility * 100) : undefined
|
|
151
151
|
};
|
|
152
|
+
const fixAvailable = !!fix;
|
|
152
153
|
return {
|
|
153
154
|
content: [
|
|
154
155
|
{ type: "text", text: JSON.stringify(summary, null, 2) },
|
|
155
|
-
{ type: "text", text:
|
|
156
|
-
{ type: "text", text: JSON.stringify(
|
|
157
|
-
{ type: "text", text: JSON.stringify({ fix }, null, 2) }
|
|
156
|
+
{ type: "text", text: `reportId:${result.id}` },
|
|
157
|
+
{ type: "text", text: JSON.stringify({ fixAvailable }, null, 2) }
|
|
158
158
|
]
|
|
159
159
|
};
|
|
160
160
|
} catch (err: any) {
|
|
@@ -192,9 +192,69 @@ export default LighthouseMcpServer;
|
|
|
192
192
|
|
|
193
193
|
export async function startMcpServer(): Promise<void> {
|
|
194
194
|
// Run HTTP/SSE server
|
|
195
|
-
|
|
195
|
+
// Prefer explicit environment variables, but also allow parsing CLI args
|
|
196
|
+
// (e.g. when launched directly without the lightweight `bin` wrapper).
|
|
197
|
+
let portEnv = process.env.PORT || process.env.AUDIT_PORT;
|
|
198
|
+
if (!portEnv) {
|
|
199
|
+
const args = process.argv.slice(2);
|
|
200
|
+
for (let i = 0; i < args.length; i++) {
|
|
201
|
+
const a = args[i];
|
|
202
|
+
if (a.includes('=') && !a.startsWith('--')) {
|
|
203
|
+
const [k, v] = a.split('=', 2);
|
|
204
|
+
if ((k === 'PORT' || k === 'AUDIT_PORT') && v !== undefined) {
|
|
205
|
+
portEnv = v;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (a.startsWith('--port=')) {
|
|
210
|
+
portEnv = a.split('=')[1];
|
|
211
|
+
break;
|
|
212
|
+
} else if (a === '--port' && args[i + 1]) {
|
|
213
|
+
portEnv = args[i + 1];
|
|
214
|
+
break;
|
|
215
|
+
} else if (a.startsWith('--audit-port=')) {
|
|
216
|
+
portEnv = a.split('=')[1];
|
|
217
|
+
break;
|
|
218
|
+
} else if (a === '--audit-port' && args[i + 1]) {
|
|
219
|
+
portEnv = args[i + 1];
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const port = Number(portEnv || 5000);
|
|
196
226
|
const mcp = new LighthouseMcpServer();
|
|
197
227
|
let sseTransport: SSEServerTransport | null = null;
|
|
228
|
+
let stdioTransport: StdioServerTransport | null = null;
|
|
229
|
+
// If process stdin/stdout appear to be non-TTY pipes, assume we're being
|
|
230
|
+
// launched as a stdio child by an MCP host and use the stdio transport.
|
|
231
|
+
const shouldUseStdio = (process.stdin && !process.stdin.isTTY) && (process.stdout && !process.stdout.isTTY);
|
|
232
|
+
if (shouldUseStdio) {
|
|
233
|
+
// Redirect console output to stderr to avoid corrupting the JSON stdio protocol
|
|
234
|
+
const _orig = { log: console.log, info: console.info, warn: console.warn };
|
|
235
|
+
console.log = (...args: any[]) => { process.stderr.write(args.map(String).join(' ') + '\n'); };
|
|
236
|
+
console.info = console.log;
|
|
237
|
+
console.warn = console.log;
|
|
238
|
+
try {
|
|
239
|
+
stdioTransport = new StdioServerTransport(process.stdin, process.stdout);
|
|
240
|
+
await mcp.connect(stdioTransport as unknown as Transport);
|
|
241
|
+
console.error('Stdio: connected to parent process over stdio');
|
|
242
|
+
// When running over stdio we do not start the HTTP server; stay alive
|
|
243
|
+
// and let the parent coordinate messages. Return a promise that
|
|
244
|
+
// resolves when the transport closes.
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
stdioTransport!.onclose = () => resolve();
|
|
247
|
+
stdioTransport!.onerror = (err: any) => reject(err);
|
|
248
|
+
});
|
|
249
|
+
} catch (err) {
|
|
250
|
+
console.error('Stdio: failed to start transport, falling back to HTTP:', err);
|
|
251
|
+
stdioTransport = null;
|
|
252
|
+
// restore console in case we fall back to HTTP server mode
|
|
253
|
+
console.log = _orig.log;
|
|
254
|
+
console.info = _orig.info;
|
|
255
|
+
console.warn = _orig.warn;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
198
258
|
const pendingPosts: Array<{ body: string; url: string | undefined; headers: any }> = [];
|
|
199
259
|
|
|
200
260
|
const { Readable, Writable } = await (async () => {
|
|
@@ -31,7 +31,9 @@ export async function runLighthouseAudit(
|
|
|
31
31
|
const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
32
32
|
|
|
33
33
|
const reportJson: string = await new Promise((resolve, reject) => {
|
|
34
|
-
|
|
34
|
+
// Use a shell on Windows to ensure .cmd/.bat wrappers are executed
|
|
35
|
+
// correctly (avoids spawn EINVAL in some environments).
|
|
36
|
+
execFile(cmd, args, { maxBuffer: 10 * 1024 * 1024, shell: true, env: process.env }, (err: any, stdout: string, stderr: string) => {
|
|
35
37
|
if (err) {
|
|
36
38
|
const message = stderr || (err && err.message) || String(err);
|
|
37
39
|
return reject(new Error(message));
|
package/dist/server.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.startHttpServer = startHttpServer;
|
|
37
|
-
const http = __importStar(require("http"));
|
|
38
|
-
const lighthouseRunner_1 = require("./runner/lighthouseRunner");
|
|
39
|
-
const fixer_1 = require("./fix/fixer");
|
|
40
|
-
async function startHttpServer(port) {
|
|
41
|
-
const p = Number(port ?? process.env.PORT ?? process.env.AUDIT_PORT ?? 5000);
|
|
42
|
-
const requestHandler = async (req, res) => {
|
|
43
|
-
if (req.method === 'POST' && req.url === '/audit') {
|
|
44
|
-
let body = '';
|
|
45
|
-
for await (const chunk of req) {
|
|
46
|
-
body += chunk;
|
|
47
|
-
}
|
|
48
|
-
const parsed = JSON.parse(body || '{}');
|
|
49
|
-
const url = parsed.url;
|
|
50
|
-
if (!url) {
|
|
51
|
-
res.writeHead(400);
|
|
52
|
-
res.end('missing url');
|
|
53
|
-
return;
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
const report = await (0, lighthouseRunner_1.runLighthouseAudit)(url);
|
|
57
|
-
const fix = await (0, fixer_1.autoFixFromReport)(report);
|
|
58
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
59
|
-
res.end(JSON.stringify({ summary: report.lhr?.categories || null, fix }));
|
|
60
|
-
}
|
|
61
|
-
catch (err) {
|
|
62
|
-
res.writeHead(500);
|
|
63
|
-
res.end(String(err));
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
68
|
-
res.end('MCP Optimizer running — POST /audit { "url": "https://..." }');
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
const server = http.createServer(requestHandler);
|
|
72
|
-
return new Promise((resolve, reject) => {
|
|
73
|
-
server.listen(p, () => {
|
|
74
|
-
console.log(`Server listening on ${p}`);
|
|
75
|
-
resolve(server);
|
|
76
|
-
});
|
|
77
|
-
server.on('error', reject);
|
|
78
|
-
});
|
|
79
|
-
}
|