framewatch-mcp-server 0.1.0
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/LICENSE +21 -0
- package/README.md +537 -0
- package/dist/constants.d.ts +172 -0
- package/dist/constants.js +168 -0
- package/dist/constants.js.map +1 -0
- package/dist/engine/browser.d.ts +56 -0
- package/dist/engine/browser.js +142 -0
- package/dist/engine/browser.js.map +1 -0
- package/dist/engine/differ.d.ts +88 -0
- package/dist/engine/differ.js +373 -0
- package/dist/engine/differ.js.map +1 -0
- package/dist/engine/interaction.d.ts +76 -0
- package/dist/engine/interaction.js +254 -0
- package/dist/engine/interaction.js.map +1 -0
- package/dist/engine/layers/console.d.ts +63 -0
- package/dist/engine/layers/console.js +118 -0
- package/dist/engine/layers/console.js.map +1 -0
- package/dist/engine/layers/dom.d.ts +53 -0
- package/dist/engine/layers/dom.js +282 -0
- package/dist/engine/layers/dom.js.map +1 -0
- package/dist/engine/layers/index.d.ts +95 -0
- package/dist/engine/layers/index.js +184 -0
- package/dist/engine/layers/index.js.map +1 -0
- package/dist/engine/layers/network.d.ts +62 -0
- package/dist/engine/layers/network.js +169 -0
- package/dist/engine/layers/network.js.map +1 -0
- package/dist/engine/layers/performance.d.ts +55 -0
- package/dist/engine/layers/performance.js +215 -0
- package/dist/engine/layers/performance.js.map +1 -0
- package/dist/engine/layers/probe.d.ts +50 -0
- package/dist/engine/layers/probe.js +39 -0
- package/dist/engine/layers/probe.js.map +1 -0
- package/dist/engine/layers/session.d.ts +46 -0
- package/dist/engine/layers/session.js +131 -0
- package/dist/engine/layers/session.js.map +1 -0
- package/dist/engine/recorder.d.ts +61 -0
- package/dist/engine/recorder.js +256 -0
- package/dist/engine/recorder.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +125 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/accessibility.d.ts +140 -0
- package/dist/tools/accessibility.js +357 -0
- package/dist/tools/accessibility.js.map +1 -0
- package/dist/tools/capture.d.ts +279 -0
- package/dist/tools/capture.js +275 -0
- package/dist/tools/capture.js.map +1 -0
- package/dist/tools/compare.d.ts +86 -0
- package/dist/tools/compare.js +247 -0
- package/dist/tools/compare.js.map +1 -0
- package/dist/tools/index.d.ts +10 -0
- package/dist/tools/index.js +25 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/interact.d.ts +160 -0
- package/dist/tools/interact.js +203 -0
- package/dist/tools/interact.js.map +1 -0
- package/dist/tools/responsive.d.ts +89 -0
- package/dist/tools/responsive.js +197 -0
- package/dist/tools/responsive.js.map +1 -0
- package/dist/tools/screenshot.d.ts +76 -0
- package/dist/tools/screenshot.js +117 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/server.d.ts +89 -0
- package/dist/tools/server.js +201 -0
- package/dist/tools/server.js.map +1 -0
- package/dist/types.d.ts +123 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/bounded-log.d.ts +41 -0
- package/dist/utils/bounded-log.js +78 -0
- package/dist/utils/bounded-log.js.map +1 -0
- package/dist/utils/format.d.ts +56 -0
- package/dist/utils/format.js +130 -0
- package/dist/utils/format.js.map +1 -0
- package/dist/utils/image.d.ts +44 -0
- package/dist/utils/image.js +81 -0
- package/dist/utils/image.js.map +1 -0
- package/dist/utils/server-process.d.ts +84 -0
- package/dist/utils/server-process.js +251 -0
- package/dist/utils/server-process.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
import { createInterface } from "node:readline";
|
|
4
|
+
import { MAX_SERVER_LINE_LENGTH, MAX_SERVER_LOG_LINES, SERVER_PORT_POLL_MS, SERVER_PORT_PROBE_TIMEOUT_MS, SERVER_STOP_GRACE_MS, } from "../constants.js";
|
|
5
|
+
import { BoundedLog } from "./bounded-log.js";
|
|
6
|
+
/** A start that failed, carrying the output that explains why. */
|
|
7
|
+
export class DevServerError extends Error {
|
|
8
|
+
output;
|
|
9
|
+
constructor(message, output = []) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "DevServerError";
|
|
12
|
+
this.output = output;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
let current = null;
|
|
16
|
+
/** The running server, or null. A process that has since died counts as null. */
|
|
17
|
+
export function getDevServer() {
|
|
18
|
+
if (!current || current.isExited)
|
|
19
|
+
return null;
|
|
20
|
+
return { ...current.info };
|
|
21
|
+
}
|
|
22
|
+
/** The last `count` output lines of the running server, oldest first. */
|
|
23
|
+
export function devServerOutput(count) {
|
|
24
|
+
if (!current)
|
|
25
|
+
return [];
|
|
26
|
+
const lines = current.log.items;
|
|
27
|
+
return lines.slice(Math.max(0, lines.length - count));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Spawn a dev server and wait until its port answers.
|
|
31
|
+
*
|
|
32
|
+
* @throws DevServerError when a server is already running, when the port is
|
|
33
|
+
* already taken by something else, when the process exits before the port
|
|
34
|
+
* opens, or when it never opens at all. Each of those carries the output the
|
|
35
|
+
* server produced, because that is where the reason actually is.
|
|
36
|
+
*/
|
|
37
|
+
export async function startDevServer(options) {
|
|
38
|
+
if (getDevServer()) {
|
|
39
|
+
const running = current.info;
|
|
40
|
+
throw new DevServerError(`a dev server is already running (pid ${running.pid}, port ${running.port}: ${running.command}). ` +
|
|
41
|
+
"Stop it with framewatch_stop_server first.");
|
|
42
|
+
}
|
|
43
|
+
// A dead server from an earlier call is just history; clear it out.
|
|
44
|
+
current = null;
|
|
45
|
+
let ready;
|
|
46
|
+
try {
|
|
47
|
+
ready = new RegExp(options.ready_pattern, "i");
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new DevServerError(`\`ready_pattern\` is not a valid regular expression: ${error instanceof Error ? error.message : String(error)}`);
|
|
51
|
+
}
|
|
52
|
+
// Something already on the port would make every readiness check pass
|
|
53
|
+
// instantly and hand the caller a server that is not theirs.
|
|
54
|
+
if (await isPortOpen(options.port)) {
|
|
55
|
+
throw new DevServerError(`port ${options.port} is already in use by another process. Stop whatever is on it, or start this ` +
|
|
56
|
+
"server on a different port.");
|
|
57
|
+
}
|
|
58
|
+
const cwd = options.cwd ?? process.cwd();
|
|
59
|
+
const child = spawn(options.command, {
|
|
60
|
+
shell: true,
|
|
61
|
+
cwd,
|
|
62
|
+
env: { ...process.env, ...options.env },
|
|
63
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
64
|
+
// Its own process group, so stopping it stops the whole tree: `npm run
|
|
65
|
+
// dev` is a shell that spawns node that spawns a bundler, and killing the
|
|
66
|
+
// shell alone would orphan every one of its children.
|
|
67
|
+
detached: process.platform !== "win32",
|
|
68
|
+
});
|
|
69
|
+
const log = new BoundedLog(MAX_SERVER_LOG_LINES, isNotable);
|
|
70
|
+
const startedAt = Date.now();
|
|
71
|
+
let readyLine;
|
|
72
|
+
const watch = (stream) => {
|
|
73
|
+
if (!stream)
|
|
74
|
+
return;
|
|
75
|
+
createInterface({ input: stream }).on("line", (line) => {
|
|
76
|
+
const text = elide(line);
|
|
77
|
+
log.add(text);
|
|
78
|
+
// Keep the *first* matching line: later ones are usually a rebuild
|
|
79
|
+
// saying "ready" again, and the first is the one that named the URL.
|
|
80
|
+
if (readyLine === undefined && ready.test(line))
|
|
81
|
+
readyLine = text;
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
watch(child.stdout);
|
|
85
|
+
watch(child.stderr);
|
|
86
|
+
const exited = new Promise((resolve) => {
|
|
87
|
+
child.once("exit", (code, signal) => {
|
|
88
|
+
if (current?.child === child)
|
|
89
|
+
current.isExited = true;
|
|
90
|
+
resolve({ code, signal });
|
|
91
|
+
});
|
|
92
|
+
// A command that cannot be spawned at all (no shell) never emits "exit".
|
|
93
|
+
child.once("error", (error) => {
|
|
94
|
+
log.add(`spawn failed: ${elide(error.message)}`);
|
|
95
|
+
if (current?.child === child)
|
|
96
|
+
current.isExited = true;
|
|
97
|
+
resolve({ code: null, signal: null });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
const info = {
|
|
101
|
+
command: options.command,
|
|
102
|
+
port: options.port,
|
|
103
|
+
pid: child.pid ?? -1,
|
|
104
|
+
cwd,
|
|
105
|
+
url: `http://localhost:${options.port}`,
|
|
106
|
+
ready_ms: 0,
|
|
107
|
+
};
|
|
108
|
+
current = { child, info, log, startedAt, exited, isExited: false };
|
|
109
|
+
const outcome = await waitForPort(child, exited, options.port, options.timeout_ms);
|
|
110
|
+
const tail = log.toArray();
|
|
111
|
+
if (outcome === "exited") {
|
|
112
|
+
const { code, signal } = await exited;
|
|
113
|
+
current = null;
|
|
114
|
+
throw new DevServerError(`the server exited before port ${options.port} opened ` +
|
|
115
|
+
`(${signal ? `killed by ${signal}` : `exit code ${code ?? "unknown"}`}).`, tail);
|
|
116
|
+
}
|
|
117
|
+
if (outcome === "timeout") {
|
|
118
|
+
await killTree(child, exited);
|
|
119
|
+
current = null;
|
|
120
|
+
throw new DevServerError(`the server did not open port ${options.port} within ${options.timeout_ms}ms` +
|
|
121
|
+
(readyLine !== undefined
|
|
122
|
+
? ` — it printed a line matching \`ready_pattern\` ("${readyLine}"), so it may be listening on a ` +
|
|
123
|
+
"different port than the one given."
|
|
124
|
+
: ". Check the command, the port, and `ready_pattern`."), tail);
|
|
125
|
+
}
|
|
126
|
+
info.ready_ms = Date.now() - startedAt;
|
|
127
|
+
if (readyLine !== undefined)
|
|
128
|
+
info.ready_line = readyLine;
|
|
129
|
+
return { ...info };
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Stop the running server, or return null if there is none.
|
|
133
|
+
*
|
|
134
|
+
* SIGTERM to the whole process group first — dev servers use it to clean up
|
|
135
|
+
* their own children and their sockets — then SIGKILL if it is still there
|
|
136
|
+
* after SERVER_STOP_GRACE_MS.
|
|
137
|
+
*/
|
|
138
|
+
export async function stopDevServer() {
|
|
139
|
+
const state = current;
|
|
140
|
+
current = null;
|
|
141
|
+
if (!state)
|
|
142
|
+
return null;
|
|
143
|
+
const { code, signal, forced } = state.isExited
|
|
144
|
+
? { ...(await state.exited), forced: false }
|
|
145
|
+
: await killTree(state.child, state.exited);
|
|
146
|
+
return {
|
|
147
|
+
command: state.info.command,
|
|
148
|
+
port: state.info.port,
|
|
149
|
+
pid: state.info.pid,
|
|
150
|
+
uptime_ms: Date.now() - state.startedAt,
|
|
151
|
+
exit_code: code,
|
|
152
|
+
exit_signal: signal,
|
|
153
|
+
forced,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Stop the server on the way out. Never throws — shutdown must not be blocked by it. */
|
|
157
|
+
export async function shutdownDevServer() {
|
|
158
|
+
try {
|
|
159
|
+
await stopDevServer();
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
// Nothing left to do about it at shutdown.
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/** Poll the port until it answers, the process dies, or `timeoutMs` runs out. */
|
|
166
|
+
async function waitForPort(child, exited, port, timeoutMs) {
|
|
167
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
168
|
+
let dead = false;
|
|
169
|
+
void exited.then(() => {
|
|
170
|
+
dead = true;
|
|
171
|
+
});
|
|
172
|
+
for (;;) {
|
|
173
|
+
if (await isPortOpen(port))
|
|
174
|
+
return "open";
|
|
175
|
+
// Checked after the port: a server that answered and then exited in the
|
|
176
|
+
// same instant still counts as having started.
|
|
177
|
+
if (dead || child.exitCode !== null || child.signalCode !== null)
|
|
178
|
+
return "exited";
|
|
179
|
+
if (Date.now() >= deadline)
|
|
180
|
+
return "timeout";
|
|
181
|
+
await sleep(Math.min(SERVER_PORT_POLL_MS, Math.max(0, deadline - Date.now())));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* True when something accepts a TCP connection on `port`.
|
|
186
|
+
*
|
|
187
|
+
* Both loopback addresses are tried: a server bound only to `::1` (Node's
|
|
188
|
+
* default when the host resolves to IPv6 first) does not answer on 127.0.0.1,
|
|
189
|
+
* and reporting that as "never started" would be wrong.
|
|
190
|
+
*/
|
|
191
|
+
export function isPortOpen(port, timeoutMs = SERVER_PORT_PROBE_TIMEOUT_MS) {
|
|
192
|
+
return Promise.all(["127.0.0.1", "::1"].map((host) => probe(host, port, timeoutMs))).then((results) => results.some(Boolean));
|
|
193
|
+
}
|
|
194
|
+
function probe(host, port, timeoutMs) {
|
|
195
|
+
return new Promise((resolve) => {
|
|
196
|
+
const socket = createConnection({ host, port });
|
|
197
|
+
const done = (open) => {
|
|
198
|
+
socket.removeAllListeners();
|
|
199
|
+
socket.destroy();
|
|
200
|
+
resolve(open);
|
|
201
|
+
};
|
|
202
|
+
socket.setTimeout(timeoutMs, () => done(false));
|
|
203
|
+
socket.once("connect", () => done(true));
|
|
204
|
+
socket.once("error", () => done(false));
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* End the process and everything it spawned.
|
|
209
|
+
*
|
|
210
|
+
* The negative pid signals the process group created by `detached: true`; if
|
|
211
|
+
* that fails (the group is gone, or this is Windows) the child alone is
|
|
212
|
+
* signalled instead.
|
|
213
|
+
*/
|
|
214
|
+
async function killTree(child, exited) {
|
|
215
|
+
signalTree(child, "SIGTERM");
|
|
216
|
+
const graceful = await Promise.race([exited, sleep(SERVER_STOP_GRACE_MS).then(() => null)]);
|
|
217
|
+
if (graceful !== null)
|
|
218
|
+
return { ...graceful, forced: false };
|
|
219
|
+
signalTree(child, "SIGKILL");
|
|
220
|
+
return { ...(await exited), forced: true };
|
|
221
|
+
}
|
|
222
|
+
function signalTree(child, signal) {
|
|
223
|
+
const pid = child.pid;
|
|
224
|
+
if (pid !== undefined && pid > 0 && process.platform !== "win32") {
|
|
225
|
+
try {
|
|
226
|
+
process.kill(-pid, signal);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// No such group (already reaped, or never detached): signal the child itself.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
child.kill(signal);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Already gone.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Lines worth keeping when the log is full: the ones that say what went wrong. */
|
|
241
|
+
function isNotable(line) {
|
|
242
|
+
return /error|fail|fatal|exception|EADDRINUSE|ENOENT|not found/i.test(line);
|
|
243
|
+
}
|
|
244
|
+
function elide(line) {
|
|
245
|
+
const flat = line.replace(/\s+$/, "");
|
|
246
|
+
return flat.length > MAX_SERVER_LINE_LENGTH ? `${flat.slice(0, MAX_SERVER_LINE_LENGTH)}…` : flat;
|
|
247
|
+
}
|
|
248
|
+
function sleep(ms) {
|
|
249
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=server-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-process.js","sourceRoot":"","sources":["../../src/utils/server-process.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn, type ChildProcess } from \"node:child_process\";\nimport { createConnection } from \"node:net\";\nimport { createInterface } from \"node:readline\";\nimport {\n MAX_SERVER_LINE_LENGTH,\n MAX_SERVER_LOG_LINES,\n SERVER_PORT_POLL_MS,\n SERVER_PORT_PROBE_TIMEOUT_MS,\n SERVER_STOP_GRACE_MS,\n} from \"../constants.js\";\nimport { BoundedLog } from \"./bounded-log.js\";\n\n/**\n * Dev server process manager.\n *\n * FrameWatch can start the app it is about to look at. One server runs at a\n * time — the tools that use it take no server argument, so a second would have\n * no way of being addressed — and it is owned for as long as the MCP server\n * lives, then stopped on shutdown. Nothing here is allowed to outlive the\n * process that spawned it.\n *\n * **Readiness is the port, not the log line.** `ready_pattern` is matched and\n * reported (dev servers print the URL they actually bound to, which is worth\n * repeating back), but what makes a server \"running\" is that something answers\n * on its port: that is the condition the next tool call depends on, and a\n * pattern that fires early — or a regex that never matches a server that is\n * working perfectly — would report the wrong thing in both directions.\n */\n\n/** A server that is up, with everything worth telling the caller about it. */\nexport interface RunningServer {\n command: string;\n port: number;\n pid: number;\n cwd: string;\n url: string;\n /** How long the port took to answer. */\n ready_ms: number;\n /** The output line that matched `ready_pattern`, if one did. */\n ready_line?: string;\n}\n\n/** What a stopped server left behind. */\nexport interface StoppedServer {\n command: string;\n port: number;\n pid: number;\n uptime_ms: number;\n /** How it went: exit code, or the signal that ended it. */\n exit_code: number | null;\n exit_signal: string | null;\n /** True when SIGTERM was ignored and the process had to be killed outright. */\n forced: boolean;\n}\n\nexport interface StartDevServerOptions {\n command: string;\n port: number;\n ready_pattern: string;\n cwd?: string;\n env?: Record<string, string>;\n timeout_ms: number;\n}\n\n/** A start that failed, carrying the output that explains why. */\nexport class DevServerError extends Error {\n readonly output: string[];\n constructor(message: string, output: string[] = []) {\n super(message);\n this.name = \"DevServerError\";\n this.output = output;\n }\n}\n\ninterface ServerState {\n child: ChildProcess;\n info: RunningServer;\n log: BoundedLog<string>;\n startedAt: number;\n /** Resolves when the process is gone, whatever ended it. */\n exited: Promise<{ code: number | null; signal: NodeJS.Signals | null }>;\n isExited: boolean;\n}\n\nlet current: ServerState | null = null;\n\n/** The running server, or null. A process that has since died counts as null. */\nexport function getDevServer(): RunningServer | null {\n if (!current || current.isExited) return null;\n return { ...current.info };\n}\n\n/** The last `count` output lines of the running server, oldest first. */\nexport function devServerOutput(count: number): string[] {\n if (!current) return [];\n const lines = current.log.items;\n return lines.slice(Math.max(0, lines.length - count)) as string[];\n}\n\n/**\n * Spawn a dev server and wait until its port answers.\n *\n * @throws DevServerError when a server is already running, when the port is\n * already taken by something else, when the process exits before the port\n * opens, or when it never opens at all. Each of those carries the output the\n * server produced, because that is where the reason actually is.\n */\nexport async function startDevServer(options: StartDevServerOptions): Promise<RunningServer> {\n if (getDevServer()) {\n const running = current!.info;\n throw new DevServerError(\n `a dev server is already running (pid ${running.pid}, port ${running.port}: ${running.command}). ` +\n \"Stop it with framewatch_stop_server first.\",\n );\n }\n // A dead server from an earlier call is just history; clear it out.\n current = null;\n\n let ready: RegExp;\n try {\n ready = new RegExp(options.ready_pattern, \"i\");\n } catch (error) {\n throw new DevServerError(\n `\\`ready_pattern\\` is not a valid regular expression: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n // Something already on the port would make every readiness check pass\n // instantly and hand the caller a server that is not theirs.\n if (await isPortOpen(options.port)) {\n throw new DevServerError(\n `port ${options.port} is already in use by another process. Stop whatever is on it, or start this ` +\n \"server on a different port.\",\n );\n }\n\n const cwd = options.cwd ?? process.cwd();\n const child = spawn(options.command, {\n shell: true,\n cwd,\n env: { ...process.env, ...options.env },\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n // Its own process group, so stopping it stops the whole tree: `npm run\n // dev` is a shell that spawns node that spawns a bundler, and killing the\n // shell alone would orphan every one of its children.\n detached: process.platform !== \"win32\",\n });\n\n const log = new BoundedLog<string>(MAX_SERVER_LOG_LINES, isNotable);\n const startedAt = Date.now();\n let readyLine: string | undefined;\n\n const watch = (stream: NodeJS.ReadableStream | null): void => {\n if (!stream) return;\n createInterface({ input: stream }).on(\"line\", (line) => {\n const text = elide(line);\n log.add(text);\n // Keep the *first* matching line: later ones are usually a rebuild\n // saying \"ready\" again, and the first is the one that named the URL.\n if (readyLine === undefined && ready.test(line)) readyLine = text;\n });\n };\n watch(child.stdout);\n watch(child.stderr);\n\n const exited = new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => {\n child.once(\"exit\", (code, signal) => {\n if (current?.child === child) current.isExited = true;\n resolve({ code, signal });\n });\n // A command that cannot be spawned at all (no shell) never emits \"exit\".\n child.once(\"error\", (error) => {\n log.add(`spawn failed: ${elide(error.message)}`);\n if (current?.child === child) current.isExited = true;\n resolve({ code: null, signal: null });\n });\n });\n\n const info: RunningServer = {\n command: options.command,\n port: options.port,\n pid: child.pid ?? -1,\n cwd,\n url: `http://localhost:${options.port}`,\n ready_ms: 0,\n };\n current = { child, info, log, startedAt, exited, isExited: false };\n\n const outcome = await waitForPort(child, exited, options.port, options.timeout_ms);\n const tail = log.toArray();\n\n if (outcome === \"exited\") {\n const { code, signal } = await exited;\n current = null;\n throw new DevServerError(\n `the server exited before port ${options.port} opened ` +\n `(${signal ? `killed by ${signal}` : `exit code ${code ?? \"unknown\"}`}).`,\n tail,\n );\n }\n if (outcome === \"timeout\") {\n await killTree(child, exited);\n current = null;\n throw new DevServerError(\n `the server did not open port ${options.port} within ${options.timeout_ms}ms` +\n (readyLine !== undefined\n ? ` — it printed a line matching \\`ready_pattern\\` (\"${readyLine}\"), so it may be listening on a ` +\n \"different port than the one given.\"\n : \". Check the command, the port, and `ready_pattern`.\"),\n tail,\n );\n }\n\n info.ready_ms = Date.now() - startedAt;\n if (readyLine !== undefined) info.ready_line = readyLine;\n return { ...info };\n}\n\n/**\n * Stop the running server, or return null if there is none.\n *\n * SIGTERM to the whole process group first — dev servers use it to clean up\n * their own children and their sockets — then SIGKILL if it is still there\n * after SERVER_STOP_GRACE_MS.\n */\nexport async function stopDevServer(): Promise<StoppedServer | null> {\n const state = current;\n current = null;\n if (!state) return null;\n\n const { code, signal, forced } = state.isExited\n ? { ...(await state.exited), forced: false }\n : await killTree(state.child, state.exited);\n\n return {\n command: state.info.command,\n port: state.info.port,\n pid: state.info.pid,\n uptime_ms: Date.now() - state.startedAt,\n exit_code: code,\n exit_signal: signal,\n forced,\n };\n}\n\n/** Stop the server on the way out. Never throws — shutdown must not be blocked by it. */\nexport async function shutdownDevServer(): Promise<void> {\n try {\n await stopDevServer();\n } catch {\n // Nothing left to do about it at shutdown.\n }\n}\n\ntype PortOutcome = \"open\" | \"exited\" | \"timeout\";\n\n/** Poll the port until it answers, the process dies, or `timeoutMs` runs out. */\nasync function waitForPort(\n child: ChildProcess,\n exited: Promise<unknown>,\n port: number,\n timeoutMs: number,\n): Promise<PortOutcome> {\n const deadline = Date.now() + Math.max(0, timeoutMs);\n let dead = false;\n void exited.then(() => {\n dead = true;\n });\n\n for (;;) {\n if (await isPortOpen(port)) return \"open\";\n // Checked after the port: a server that answered and then exited in the\n // same instant still counts as having started.\n if (dead || child.exitCode !== null || child.signalCode !== null) return \"exited\";\n if (Date.now() >= deadline) return \"timeout\";\n await sleep(Math.min(SERVER_PORT_POLL_MS, Math.max(0, deadline - Date.now())));\n }\n}\n\n/**\n * True when something accepts a TCP connection on `port`.\n *\n * Both loopback addresses are tried: a server bound only to `::1` (Node's\n * default when the host resolves to IPv6 first) does not answer on 127.0.0.1,\n * and reporting that as \"never started\" would be wrong.\n */\nexport function isPortOpen(port: number, timeoutMs: number = SERVER_PORT_PROBE_TIMEOUT_MS): Promise<boolean> {\n return Promise.all([\"127.0.0.1\", \"::1\"].map((host) => probe(host, port, timeoutMs))).then((results) =>\n results.some(Boolean),\n );\n}\n\nfunction probe(host: string, port: number, timeoutMs: number): Promise<boolean> {\n return new Promise((resolve) => {\n const socket = createConnection({ host, port });\n const done = (open: boolean): void => {\n socket.removeAllListeners();\n socket.destroy();\n resolve(open);\n };\n socket.setTimeout(timeoutMs, () => done(false));\n socket.once(\"connect\", () => done(true));\n socket.once(\"error\", () => done(false));\n });\n}\n\n/**\n * End the process and everything it spawned.\n *\n * The negative pid signals the process group created by `detached: true`; if\n * that fails (the group is gone, or this is Windows) the child alone is\n * signalled instead.\n */\nasync function killTree(\n child: ChildProcess,\n exited: Promise<{ code: number | null; signal: NodeJS.Signals | null }>,\n): Promise<{ code: number | null; signal: NodeJS.Signals | null; forced: boolean }> {\n signalTree(child, \"SIGTERM\");\n const graceful = await Promise.race([exited, sleep(SERVER_STOP_GRACE_MS).then(() => null)]);\n if (graceful !== null) return { ...graceful, forced: false };\n\n signalTree(child, \"SIGKILL\");\n return { ...(await exited), forced: true };\n}\n\nfunction signalTree(child: ChildProcess, signal: NodeJS.Signals): void {\n const pid = child.pid;\n if (pid !== undefined && pid > 0 && process.platform !== \"win32\") {\n try {\n process.kill(-pid, signal);\n return;\n } catch {\n // No such group (already reaped, or never detached): signal the child itself.\n }\n }\n try {\n child.kill(signal);\n } catch {\n // Already gone.\n }\n}\n\n/** Lines worth keeping when the log is full: the ones that say what went wrong. */\nfunction isNotable(line: string): boolean {\n return /error|fail|fatal|exception|EADDRINUSE|ENOENT|not found/i.test(line);\n}\n\nfunction elide(line: string): string {\n const flat = line.replace(/\\s+$/, \"\");\n return flat.length > MAX_SERVER_LINE_LENGTH ? `${flat.slice(0, MAX_SERVER_LINE_LENGTH)}…` : flat;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "framewatch-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for smart visual testing of web apps — frame capture, diffing, and interaction replay",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "kekoDev (https://github.com/kekoDev)",
|
|
7
|
+
"homepage": "https://github.com/kekoDev/framewatch#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/kekoDev/framewatch.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/kekoDev/framewatch/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"bin": {
|
|
17
|
+
"framewatch-mcp-server": "dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"types": "dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"default": "./dist/index.js"
|
|
25
|
+
},
|
|
26
|
+
"./package.json": "./package.json"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"start": "node dist/index.js",
|
|
36
|
+
"dev": "tsc --watch",
|
|
37
|
+
"test": "vitest run",
|
|
38
|
+
"test:watch": "vitest",
|
|
39
|
+
"coverage": "vitest run --coverage",
|
|
40
|
+
"prepublishOnly": "npm run build",
|
|
41
|
+
"pretest": "npm run build && npm run typecheck",
|
|
42
|
+
"typecheck": "tsc -p tsconfig.test.json --noEmit"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
|
+
"playwright": "^1.62.0",
|
|
47
|
+
"sharp": "^0.35.0",
|
|
48
|
+
"zod": "^3.25.0",
|
|
49
|
+
"axe-core": "^4.10.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^22.0.0",
|
|
53
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
54
|
+
"typescript": "^5.9.0",
|
|
55
|
+
"vitest": "^4.1.0"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=20.9.0"
|
|
59
|
+
},
|
|
60
|
+
"keywords": [
|
|
61
|
+
"mcp",
|
|
62
|
+
"model-context-protocol",
|
|
63
|
+
"visual-testing",
|
|
64
|
+
"visual-regression",
|
|
65
|
+
"screenshot",
|
|
66
|
+
"playwright",
|
|
67
|
+
"chromium",
|
|
68
|
+
"claude-code",
|
|
69
|
+
"ai-testing",
|
|
70
|
+
"accessibility",
|
|
71
|
+
"axe-core",
|
|
72
|
+
"diff"
|
|
73
|
+
]
|
|
74
|
+
}
|