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.
- package/.github/workflows/publish.yml +34 -0
- package/README.md +31 -0
- package/bin/mcp-optimizer.js +16 -0
- package/dist/fix/fixer.js +19 -0
- package/dist/index.js +12 -0
- package/dist/mcpServer.js +351 -0
- package/dist/runner/lighthouseRunner.js +66 -0
- package/dist/server.js +79 -0
- package/docs/MCP.md +21 -0
- package/package.json +43 -0
- package/server.ts +600 -0
- package/src/fix/fixer.ts +18 -0
- package/src/index.ts +10 -0
- package/src/mcpServer.ts +335 -0
- package/src/runner/lighthouseRunner.ts +66 -0
- package/src/types/modelcontextprotocol__sdk.d.ts +4 -0
- package/tsconfig.json +13 -0
|
@@ -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
|
+
}
|