mcp-optimizer 0.0.1-alpha.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.
@@ -0,0 +1,34 @@
1
+ name: Publish package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+ workflow_dispatch: {}
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '20'
20
+ registry-url: 'https://registry.npmjs.org'
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Build
26
+ run: npm run build
27
+
28
+ - name: Publish to npm
29
+ env:
30
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
31
+ run: |
32
+ if [ -z "$NPM_TOKEN" ]; then echo "NPM_TOKEN is not set"; exit 1; fi
33
+ echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
34
+ npm publish --access public --tag beta
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # MCP Optimizer
2
+
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
+
5
+ Quick start
6
+
7
+ 1. Install runtime dependencies:
8
+
9
+ ```bash
10
+ npm install lighthouse chrome-launcher
11
+ ```
12
+
13
+ 2. Start the server (after building or in dev):
14
+
15
+ ```bash
16
+ npm run build
17
+ npm start
18
+ # or for development:
19
+ npm run dev
20
+ ```
21
+
22
+ 3. Run an audit:
23
+
24
+ ```bash
25
+ curl -X POST http://localhost:3000/audit -H "Content-Type: application/json" -d '{"url":"https://example.com"}'
26
+ ```
27
+
28
+ Notes
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.
31
+
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Lightweight CLI wrapper for the MCP Optimizer package.
4
+ // This file is intentionally small so `npx mcp-optimizer` can run it.
5
+
6
+ try {
7
+ // Require built output. When installed via npm the package root
8
+ // will contain `dist/index.js` after `npm pack` / `npm publish`.
9
+ require('../dist/index.js');
10
+ } catch (err) {
11
+ // Provide a helpful error for local development if the package
12
+ // hasn't been built yet.
13
+ console.error('Failed to start MCP Optimizer. Have you run `npm run build`?');
14
+ console.error(err && err.stack ? err.stack : String(err));
15
+ process.exit(1);
16
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.autoFixFromReport = autoFixFromReport;
4
+ async function autoFixFromReport(report, opts) {
5
+ const performance = report.lhr?.categories?.performance || null;
6
+ const audits = report.lhr?.audits || {};
7
+ let failures = null;
8
+ if (opts?.onlyFailures) {
9
+ failures = Object.fromEntries(Object.entries(audits).filter(([, v]) => {
10
+ const score = v && (v.score ?? v.scoreDisplayMode === 'notApplicable' ? 1 : v.score);
11
+ return typeof score === 'number' ? score < 1 : false;
12
+ }));
13
+ }
14
+ return {
15
+ suggestion: 'Review performance opportunities and apply targeted fixes',
16
+ performance,
17
+ failures,
18
+ };
19
+ }
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const mcpServer_1 = require("./mcpServer");
4
+ (async () => {
5
+ try {
6
+ await (0, mcpServer_1.startMcpServer)();
7
+ }
8
+ catch (err) {
9
+ console.error('Failed to start services:', err);
10
+ process.exit(1);
11
+ }
12
+ })();
@@ -0,0 +1,351 @@
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.LighthouseMcpServer = void 0;
37
+ exports.startMcpServer = startMcpServer;
38
+ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
39
+ const zod_1 = require("zod");
40
+ const lighthouseRunner_1 = require("./runner/lighthouseRunner");
41
+ const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js");
42
+ const http = __importStar(require("http"));
43
+ const fixer_1 = require("./fix/fixer");
44
+ class LighthouseMcpServer {
45
+ constructor() {
46
+ this.reports = new Map();
47
+ this.server = new mcp_js_1.McpServer({ name: "Lighthouse MCP Server", version: "0.1.0" });
48
+ this.registerTools();
49
+ }
50
+ /**
51
+ * Run an audit and store the report. Returns stored record.
52
+ */
53
+ async runAudit(options) {
54
+ const { url, categories, formFactor } = options;
55
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
56
+ const opts = {};
57
+ if (categories && categories.length > 0)
58
+ opts.categories = categories;
59
+ if (formFactor === 'mobile')
60
+ opts.emulateMobile = true;
61
+ const runnerResult = await (0, lighthouseRunner_1.runLighthouseAudit)(url, opts);
62
+ const reportJson = runnerResult.report;
63
+ const lhr = runnerResult.lhr;
64
+ const reportObj = typeof reportJson === 'string' ? JSON.parse(reportJson) : reportJson;
65
+ const record = {
66
+ id,
67
+ url,
68
+ fetchedAt: new Date().toISOString(),
69
+ lhr,
70
+ report: reportObj
71
+ };
72
+ this.reports.set(id, record);
73
+ return record;
74
+ }
75
+ registerTools() {
76
+ this.server.tool("lighthouse_run_audit", "Run a Lighthouse audit against a URL and store the report", {
77
+ url: zod_1.z.string().describe("The URL to audit, including protocol (http:// or https://)"),
78
+ categories: zod_1.z.array(zod_1.z.string()).optional().describe("Optional Lighthouse categories to run, e.g. ['performance','accessibility']"),
79
+ formFactor: zod_1.z.enum(["mobile", "desktop"]).optional().describe("Emulated form factor")
80
+ }, async ({ url, categories, formFactor }) => {
81
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
82
+ try {
83
+ const opts = {};
84
+ if (categories && categories.length > 0)
85
+ opts.categories = categories;
86
+ if (formFactor === 'mobile')
87
+ opts.emulateMobile = true;
88
+ const runnerResult = await (0, lighthouseRunner_1.runLighthouseAudit)(url, opts);
89
+ const reportJson = runnerResult.report;
90
+ const lhr = runnerResult.lhr;
91
+ const reportObj = typeof reportJson === 'string' ? JSON.parse(reportJson) : reportJson;
92
+ this.reports.set(id, {
93
+ id,
94
+ url,
95
+ fetchedAt: new Date().toISOString(),
96
+ lhr,
97
+ report: reportObj
98
+ });
99
+ const perf = lhr.categories?.performance?.score ?? null;
100
+ const accessibility = lhr.categories?.accessibility?.score ?? null;
101
+ const summary = {
102
+ reportId: id,
103
+ url,
104
+ fetchedAt: new Date().toISOString(),
105
+ performance: perf !== null ? Math.round(perf * 100) : undefined,
106
+ accessibility: accessibility !== null ? Math.round(accessibility * 100) : undefined
107
+ };
108
+ // 返回所有信息,便于 HTTP 路由复用
109
+ return {
110
+ content: [
111
+ { type: "text", text: JSON.stringify(summary, null, 2) },
112
+ { type: "text", text: JSON.stringify(lhr, null, 2) },
113
+ { type: "text", text: JSON.stringify(reportObj, null, 2) }
114
+ ]
115
+ };
116
+ }
117
+ catch (error) {
118
+ return {
119
+ content: [
120
+ { type: "text", text: `Lighthouse audit failed: ${error}` }
121
+ ]
122
+ };
123
+ }
124
+ });
125
+ this.server.tool("lighthouse_get_report", "Retrieve a previously-run Lighthouse report by reportId", {
126
+ reportId: zod_1.z.string().describe("The report id returned by `lighthouse_run_audit`")
127
+ }, async ({ reportId }) => {
128
+ const record = this.reports.get(reportId);
129
+ if (!record) {
130
+ return {
131
+ content: [
132
+ { type: "text", text: `Report not found: ${reportId}` }
133
+ ]
134
+ };
135
+ }
136
+ return {
137
+ content: [
138
+ { type: "text", text: JSON.stringify(record.report, null, 2) }
139
+ ]
140
+ };
141
+ });
142
+ // 新增:从用户 Prompt 中识别 URL 并自动运行 Lighthouse 审计
143
+ this.server.tool("lighthouse_analyze_prompt", "Scan a text prompt for a URL, run Lighthouse, and return a summary", {
144
+ prompt: zod_1.z.string().describe("A text prompt that may contain a URL to analyze")
145
+ }, async ({ prompt }) => {
146
+ try {
147
+ const urlMatch = prompt.match(/https?:\/\/[^\s"'<>]+/i);
148
+ if (!urlMatch) {
149
+ return {
150
+ content: [{ type: "text", text: "No URL found in prompt." }]
151
+ };
152
+ }
153
+ const url = urlMatch[0];
154
+ const result = await this.runAuditViaTool({ url });
155
+ let fix = null;
156
+ if (result && result.lhr) {
157
+ fix = await (0, fixer_1.autoFixFromReport)({ lhr: result.lhr, report: JSON.stringify(result.report) });
158
+ }
159
+ const perf = result.lhr?.categories?.performance?.score ?? null;
160
+ const accessibility = result.lhr?.categories?.accessibility?.score ?? null;
161
+ const summary = {
162
+ reportId: result.id,
163
+ url: result.url,
164
+ fetchedAt: result.fetchedAt,
165
+ performance: perf !== null ? Math.round(perf * 100) : undefined,
166
+ accessibility: accessibility !== null ? Math.round(accessibility * 100) : undefined
167
+ };
168
+ return {
169
+ content: [
170
+ { type: "text", text: JSON.stringify(summary, null, 2) },
171
+ { type: "text", text: JSON.stringify(result.lhr || {}, null, 2) },
172
+ { type: "text", text: JSON.stringify(result.report || {}, null, 2) },
173
+ { type: "text", text: JSON.stringify({ fix }, null, 2) }
174
+ ]
175
+ };
176
+ }
177
+ catch (err) {
178
+ return { content: [{ type: "text", text: `Error analyzing prompt: ${String(err)}` }] };
179
+ }
180
+ });
181
+ }
182
+ // 新增:暴露一个直接调用 MCP 工具的接口
183
+ async runAuditViaTool(params) {
184
+ // Some versions of the MCP SDK don't expose an `invokeTool` helper.
185
+ // Call the internal runner directly and return a shape similar to the
186
+ // tool's output so HTTP callers can use `result.summary` / `result.lhr`.
187
+ const record = await this.runAudit(params);
188
+ const perf = record.lhr?.categories?.performance?.score ?? null;
189
+ const accessibility = record.lhr?.categories?.accessibility?.score ?? null;
190
+ const summary = {
191
+ reportId: record.id,
192
+ url: record.url,
193
+ fetchedAt: record.fetchedAt,
194
+ performance: perf !== null ? Math.round(perf * 100) : undefined,
195
+ accessibility: accessibility !== null ? Math.round(accessibility * 100) : undefined
196
+ };
197
+ // return record plus a `summary` to match existing HTTP handler expectations
198
+ return { ...record, summary };
199
+ }
200
+ async connect(transport) {
201
+ await this.server.connect(transport);
202
+ }
203
+ }
204
+ exports.LighthouseMcpServer = LighthouseMcpServer;
205
+ exports.default = LighthouseMcpServer;
206
+ async function startMcpServer() {
207
+ // Run HTTP/SSE server
208
+ const port = Number(process.env.PORT || process.env.AUDIT_PORT || 5000);
209
+ const mcp = new LighthouseMcpServer();
210
+ let sseTransport = null;
211
+ const pendingPosts = [];
212
+ const { Readable, Writable } = await (async () => {
213
+ const mod = await Promise.resolve().then(() => __importStar(require('stream')));
214
+ return { Readable: mod.Readable, Writable: mod.Writable };
215
+ })();
216
+ function makeMockReq(body, url, headers) {
217
+ const r = new Readable({ read() { this.push(body); this.push(null); } });
218
+ r.method = 'POST';
219
+ r.url = url || '/sse';
220
+ r.headers = headers || { 'content-type': 'application/json' };
221
+ return r;
222
+ }
223
+ function makeMockRes() {
224
+ const w = new Writable({ write(chunk, _enc, cb) { cb(); } });
225
+ w.writeHead = (status, headers) => { w.statusCode = status; w._headers = headers; };
226
+ w.end = (data) => { if (data) {
227
+ try { /* consume */ }
228
+ catch (_) { }
229
+ } };
230
+ return w;
231
+ }
232
+ const server = http.createServer(async (req, res) => {
233
+ try {
234
+ if (req.method === 'GET' && req.url && req.url.startsWith('/sse')) {
235
+ // SSE handshake: set headers and create transport
236
+ // Ensure we send proper SSE headers so clients don't fallback to polling.
237
+ // Register the transport using '/sse' as the message POST path
238
+ sseTransport = new sse_js_1.SSEServerTransport('/sse', res);
239
+ try {
240
+ await mcp.connect(sseTransport);
241
+ console.info('SSE: new connection established');
242
+ }
243
+ catch (err) {
244
+ console.error('SSE: failed to start transport:', err);
245
+ // Ensure client receives an error
246
+ try {
247
+ res.writeHead(500, { 'Content-Type': 'application/json' });
248
+ res.end(JSON.stringify({ error: 'failed to start sse transport' }));
249
+ }
250
+ catch (_) { }
251
+ return;
252
+ }
253
+ // replay any pending POSTs that arrived before the GET
254
+ if (pendingPosts.length > 0) {
255
+ console.info(`SSE: replaying ${pendingPosts.length} pending POST(s)`);
256
+ for (const p of pendingPosts.splice(0)) {
257
+ try {
258
+ const mockReq = makeMockReq(p.body, p.url, p.headers);
259
+ const mockRes = makeMockRes();
260
+ // don't await to avoid blocking the handshake
261
+ sseTransport.handlePostMessage(mockReq, mockRes).catch((err) => {
262
+ console.error('SSE: replay handlePostMessage failed:', err);
263
+ });
264
+ }
265
+ catch (err) {
266
+ console.error('SSE: failed to replay pending POST:', err);
267
+ }
268
+ }
269
+ }
270
+ return;
271
+ }
272
+ if (req.method === 'POST' && req.url && (req.url === '/messages' || req.url.startsWith('/sse'))) {
273
+ console.info(`SSE: received POST to ${req.url}`);
274
+ if (!sseTransport) {
275
+ // Buffer the POST body so it can be processed once the GET arrives.
276
+ try {
277
+ let body = '';
278
+ for await (const chunk of req) {
279
+ body += chunk;
280
+ }
281
+ console.info('SSE: POST received before GET; buffering');
282
+ pendingPosts.push({ body, url: req.url, headers: req.headers });
283
+ res.writeHead(200, { 'Content-Type': 'application/json' });
284
+ res.end(JSON.stringify({ ok: true }));
285
+ return;
286
+ }
287
+ catch (e) {
288
+ console.error('SSE: error reading POST body before GET:', e);
289
+ res.writeHead(500, { 'Content-Type': 'application/json' });
290
+ res.end(JSON.stringify({ error: String(e) }));
291
+ return;
292
+ }
293
+ }
294
+ try {
295
+ await sseTransport.handlePostMessage(req, res);
296
+ return;
297
+ }
298
+ catch (err) {
299
+ console.error('SSE: handlePostMessage failed:', err);
300
+ res.writeHead(500, { 'Content-Type': 'application/json' });
301
+ res.end(JSON.stringify({ error: String(err) }));
302
+ return;
303
+ }
304
+ }
305
+ if (req.method === 'POST' && req.url === '/audit') {
306
+ let body = '';
307
+ for await (const chunk of req) {
308
+ body += chunk;
309
+ }
310
+ const parsed = JSON.parse(body || '{}');
311
+ const url = parsed.url;
312
+ if (!url) {
313
+ res.writeHead(400, { 'Content-Type': 'application/json' });
314
+ res.end(JSON.stringify({ error: 'missing url' }));
315
+ return;
316
+ }
317
+ try {
318
+ const result = await mcp.runAuditViaTool({ url, categories: parsed.categories, formFactor: parsed.formFactor });
319
+ let fix = null;
320
+ if (result && result.lhr) {
321
+ fix = await (0, fixer_1.autoFixFromReport)({ lhr: result.lhr, report: JSON.stringify(result.report) });
322
+ }
323
+ res.writeHead(200, { 'Content-Type': 'application/json' });
324
+ res.end(JSON.stringify({ summary: result.summary, fix, error: result.error }));
325
+ }
326
+ catch (err) {
327
+ res.writeHead(500, { 'Content-Type': 'application/json' });
328
+ res.end(JSON.stringify({ error: String(err) }));
329
+ }
330
+ return;
331
+ }
332
+ // fallback
333
+ res.writeHead(200, { 'Content-Type': 'application/json' });
334
+ res.end(JSON.stringify({ message: 'MCP Optimizer running — POST /audit { "url": "https://..." }' }));
335
+ }
336
+ catch (outerErr) {
337
+ // ensure no plain-text stdout noise
338
+ try {
339
+ res.writeHead(500, { 'Content-Type': 'application/json' });
340
+ res.end(JSON.stringify({ error: String(outerErr) }));
341
+ }
342
+ catch (_) {
343
+ // ignore
344
+ }
345
+ }
346
+ });
347
+ return new Promise((resolve, reject) => {
348
+ server.listen(port, () => resolve());
349
+ server.on('error', reject);
350
+ });
351
+ }
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runLighthouseAudit = runLighthouseAudit;
4
+ async function runLighthouseAudit(url, opts) {
5
+ // Use the Lighthouse CLI via `npx` to avoid importing the ESM-only
6
+ // Lighthouse package into this CommonJS runtime.
7
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
8
+ const chromeLauncherModule = await dynamicImport('chrome-launcher');
9
+ const chromeLauncher = (chromeLauncherModule && (chromeLauncherModule.default ?? chromeLauncherModule));
10
+ const { execFile } = await dynamicImport('node:child_process');
11
+ const os = await dynamicImport('node:os');
12
+ const pathMod = await dynamicImport('node:path');
13
+ const tmpDir = pathMod.join(os.tmpdir(), `lighthouse-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`);
14
+ const fs = await dynamicImport('node:fs');
15
+ fs.mkdirSync(tmpDir, { recursive: true });
16
+ const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'], userDataDir: tmpDir });
17
+ try {
18
+ const port = chrome.port;
19
+ const args = [
20
+ 'lighthouse',
21
+ url,
22
+ `--port=${port}`,
23
+ '--output=json',
24
+ '--quiet'
25
+ ];
26
+ if (opts?.emulateMobile)
27
+ args.push('--preset=mobile');
28
+ if (opts?.categories && opts.categories.length)
29
+ args.push(`--only-categories=${opts.categories.join(',')}`);
30
+ // Run via npx to ensure local package is used
31
+ const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
32
+ const reportJson = await new Promise((resolve, reject) => {
33
+ execFile(cmd, args, { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
34
+ if (err) {
35
+ const message = stderr || (err && err.message) || String(err);
36
+ return reject(new Error(message));
37
+ }
38
+ resolve(stdout);
39
+ });
40
+ });
41
+ // The CLI returns a JSON string containing the report and LHR; parse it
42
+ const parsed = JSON.parse(reportJson);
43
+ // Lighthouse CLI places the LHR inside `lhr` when output=json
44
+ const lhr = parsed.lhr ?? parsed;
45
+ return { lhr, report: parsed };
46
+ }
47
+ catch (err) {
48
+ // If anything goes wrong launching Chrome or running Lighthouse,
49
+ // return a minimal error-shaped report so the MCP server can continue
50
+ // to respond and surface the error to callers instead of crashing.
51
+ const message = (err && (err.stack || err.message)) || String(err);
52
+ return {
53
+ lhr: { categories: {} },
54
+ report: { error: message },
55
+ error: message
56
+ };
57
+ }
58
+ finally {
59
+ try {
60
+ await chrome.kill();
61
+ }
62
+ catch (_) {
63
+ // ignore cleanup errors
64
+ }
65
+ }
66
+ }
package/dist/server.js ADDED
@@ -0,0 +1,79 @@
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
+ }
package/docs/MCP.md ADDED
@@ -0,0 +1,21 @@
1
+ # MCP Server (MCP-Optimizer)
2
+
3
+ This project includes a minimal MCP server implementation using `@modelcontextprotocol/sdk`.
4
+
5
+ Files
6
+ - `src/mcpServer.ts` — minimal MCP server exposing a `run-audit` tool that calls the existing Lighthouse runner and fixer. Defaults to port `4010` (env `MCP_PORT`).
7
+ - `src/index.ts` — starts both the existing HTTP audit endpoint and the MCP server.
8
+
9
+ Extended features
10
+ - The `run-audit` tool now accepts optional parameters: `categories` (string[]), `emulateMobile` (boolean), and `onlyFailures` (boolean). These are passed to the handler and can be used to tune the audit or filter results.
11
+ - A read-only resource `latest-audit` is registered and returns the most recent Lighthouse report stored in memory.
12
+
13
+ Run (development)
14
+
15
+ ```bash
16
+ npm install
17
+ npm run dev
18
+ ```
19
+
20
+ Notes
21
+ - The MCP implementation is intentionally minimal and uses `any` in a few places to remain compatible across SDK versions. If the SDK API differs, consult the SDK examples and docs at https://github.com/model-context-protocol/typescript-sdk and adjust `src/mcpServer.ts` accordingly.
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "mcp-optimizer",
3
+ "version": "0.0.1-alpha.1",
4
+ "description": "An MCP server that runs Lighthouse audits and enables LLMs to automatically optimize your code.",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "mcp-optimizer": "bin/mcp-optimizer.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsc -p tsconfig.json",
11
+ "start": "node dist/index.js",
12
+ "dev": "ts-node-dev --respawn --transpile-only src/index.ts",
13
+ "test": "echo \"Error: no test specified\" && exit 1"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/jayzoou/MCP-Optimizer.git"
18
+ },
19
+ "keywords": [
20
+ "mcp"
21
+ ],
22
+ "author": "",
23
+ "license": "MIT",
24
+ "bugs": {
25
+ "url": "https://github.com/jayzoou/MCP-Optimizer/issues"
26
+ },
27
+ "homepage": "https://github.com/jayzoou/MCP-Optimizer#readme",
28
+ "dependencies": {
29
+ "@modelcontextprotocol/sdk": "^1.25.1",
30
+ "zod": "^4.3.4",
31
+ "lighthouse": "^10.0.0",
32
+ "chrome-launcher": "^1.2.1"
33
+ },
34
+ "devDependencies": {
35
+ "typescript": "^5.1.6",
36
+ "ts-node": "^10.9.1",
37
+ "ts-node-dev": "^2.0.0",
38
+ "@types/node": "^20.5.1"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }