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 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
- Quick start
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
- 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.
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
- // 返回所有信息,便于 HTTP 路由复用
109
+ // 返回精简信息:只返回 summary 与 reportId,完整报告保存在服务器以便按需获取
109
110
  return {
110
111
  content: [
111
112
  { 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) }
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: 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) }
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
- const port = Number(process.env.PORT || process.env.AUDIT_PORT || 5000);
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
- execFile(cmd, args, { maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-optimizer",
3
- "version": "0.0.2-alpha.1",
3
+ "version": "0.0.2",
4
4
  "description": "An MCP server that runs Lighthouse audits and enables LLMs to automatically optimize your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
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
- // 返回所有信息,便于 HTTP 路由复用
83
+ // 返回精简信息:只返回 summary 与 reportId,完整报告保存在服务器以便按需获取
83
84
  return {
84
85
  content: [
85
86
  { type: "text", text: JSON.stringify(summary, null, 2) },
86
- { type: "text", text: JSON.stringify(lhr, null, 2) },
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: JSON.stringify(result.lhr || {}, null, 2) },
156
- { type: "text", text: JSON.stringify(result.report || {}, null, 2) },
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
- const port = Number(process.env.PORT || process.env.AUDIT_PORT || 5000);
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
- execFile(cmd, args, { maxBuffer: 10 * 1024 * 1024 }, (err: any, stdout: string, stderr: string) => {
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
- }