rpc-agent 0.4.0 → 0.6.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/CHANGELOG.md +100 -0
- package/README.md +449 -5
- package/dist/args.js +197 -31
- package/dist/browser.js +57 -3
- package/dist/capture.js +45 -42
- package/dist/check-format.js +120 -0
- package/dist/check-page.js +351 -0
- package/dist/check.js +685 -0
- package/dist/cli.js +88 -1
- package/dist/errors.js +8 -0
- package/dist/gif.js +212 -0
- package/dist/help.js +56 -2
- package/dist/mcp.js +218 -16
- package/dist/motion.js +487 -0
- package/dist/options.js +33 -0
- package/dist/png.js +47 -0
- package/dist/record.js +333 -0
- package/dist/ui.js +199 -0
- package/package.json +2 -2
package/dist/mcp.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
5
|
import { z } from 'zod';
|
|
5
6
|
import { describeAuth, listSessions, resolveAuth, sessionsDir, toOrigin } from './auth.js';
|
|
6
7
|
import { capture, launchBrowser } from './capture.js';
|
|
8
|
+
import { runCheck } from './check.js';
|
|
9
|
+
import { textSummary } from './check-format.js';
|
|
7
10
|
import { renderPreview } from './composite.js';
|
|
8
11
|
import { CATEGORY_LABELS, CATEGORY_ORDER, DEFAULT_DEVICES, DEVICE_ALIASES, DEVICES, resolveDevices, } from './devices.js';
|
|
9
|
-
import { COLOR_SCHEMES, DEFAULT_MAX_SLICES, DEFAULT_OUT_DIR, DEFAULT_SLICE_WIDTH, DEFAULT_TIMEOUT_MS, normalizeUrl, } from './options.js';
|
|
12
|
+
import { COLOR_SCHEMES, DEFAULT_MAX_SLICES, DEFAULT_OUT_DIR, DEFAULT_SLICE_WIDTH, DEFAULT_TIMEOUT_MS, defaultCheckOptions, defaultRecordOptions, DEFAULT_RECORD_DEVICE, DEFAULT_RECORD_SECONDS, MAX_RECORD_SECONDS, normalizeUrl, } from './options.js';
|
|
13
|
+
import { unpinnedLines, warningWhere } from './motion.js';
|
|
14
|
+
import { record } from './record.js';
|
|
10
15
|
import { DEFAULT_MAX_HEIGHT } from './slices.js';
|
|
11
16
|
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
12
17
|
const screenshotInput = {
|
|
@@ -56,6 +61,17 @@ const screenshotInput = {
|
|
|
56
61
|
.boolean()
|
|
57
62
|
.optional()
|
|
58
63
|
.describe('Emulate prefers-reduced-motion: reduce. Helps when scroll-triggered entrance animations (GSAP, Framer Motion) leave sections blank.'),
|
|
64
|
+
finishAnimations: z
|
|
65
|
+
.boolean()
|
|
66
|
+
.optional()
|
|
67
|
+
.describe('Jump GSAP tweens to their end before capture when gsap/ScrollTrigger are on window (scrubbed and pinned ones keep their ' +
|
|
68
|
+
'scroll position), and finite Web Animations too (the screenshot already ends CSS animations). Use it when the result warns ' +
|
|
69
|
+
'that elements were still animating. Default false.'),
|
|
70
|
+
unpin: z
|
|
71
|
+
.boolean()
|
|
72
|
+
.optional()
|
|
73
|
+
.describe('Full page: collapse pinned (GSAP ScrollTrigger-style or sticky) sections so each shows once instead of over a tall blank spacer. ' +
|
|
74
|
+
'Listed under motion.unpinned. Default true.'),
|
|
59
75
|
waitFor: z.string().optional().describe('CSS selector to wait for (visible) before capturing.'),
|
|
60
76
|
waitMs: z.number().int().min(0).max(30_000).optional().describe('Extra settle delay after load, in ms.'),
|
|
61
77
|
colorScheme: z.enum(COLOR_SCHEMES).optional().describe('Emulate prefers-color-scheme.'),
|
|
@@ -65,6 +81,8 @@ const screenshotInput = {
|
|
|
65
81
|
.boolean()
|
|
66
82
|
.optional()
|
|
67
83
|
.describe('Return JPEG previews inline: one per device, or the full-page slices when fullPage. Default true.'),
|
|
84
|
+
};
|
|
85
|
+
const authInput = {
|
|
68
86
|
cookies: z
|
|
69
87
|
.array(z.object({
|
|
70
88
|
name: z.string().min(1),
|
|
@@ -93,6 +111,67 @@ const screenshotInput = {
|
|
|
93
111
|
.optional()
|
|
94
112
|
.describe('Extra origins (e.g. an API host) that also get the headers and loaded cookies.'),
|
|
95
113
|
};
|
|
114
|
+
const checkInput = {
|
|
115
|
+
url: z
|
|
116
|
+
.string()
|
|
117
|
+
.describe('Page to audit. http(s)://, file://, a bare host like localhost:3000, or a path to a local .html file.'),
|
|
118
|
+
devices: z
|
|
119
|
+
.array(z.string())
|
|
120
|
+
.min(1)
|
|
121
|
+
.max(8)
|
|
122
|
+
.optional()
|
|
123
|
+
.describe(`Device ids or aliases (see list_devices). Default: ${DEFAULT_DEVICES.join(', ')}. ` +
|
|
124
|
+
'The mobile viewport checks (100vh, sideways scroll, tap targets, input zoom) run on phones only.'),
|
|
125
|
+
shots: z
|
|
126
|
+
.boolean()
|
|
127
|
+
.optional()
|
|
128
|
+
.describe('Save PNGs of the elements behind layout-shift and horizontal-overflow findings (paths in the report). Default false.'),
|
|
129
|
+
outDir: z.string().optional().describe(`Where shots go, relative to the server's cwd. Default ${DEFAULT_OUT_DIR}.`),
|
|
130
|
+
waitFor: z.string().optional().describe('CSS selector to wait for (visible) before checking.'),
|
|
131
|
+
waitMs: z.number().int().min(0).max(30_000).optional().describe('Extra settle delay after the scroll, in ms.'),
|
|
132
|
+
colorScheme: z.enum(COLOR_SCHEMES).optional().describe('Emulate prefers-color-scheme.'),
|
|
133
|
+
timeoutMs: z.number().int().min(1000).max(120_000).optional().describe(`Navigation timeout. Default ${DEFAULT_TIMEOUT_MS}.`),
|
|
134
|
+
};
|
|
135
|
+
const recordInput = {
|
|
136
|
+
url: z
|
|
137
|
+
.string()
|
|
138
|
+
.describe('Page to record. http(s)://, file://, a bare host like localhost:3000, or a path to a local .html file.'),
|
|
139
|
+
device: z.string().optional().describe(`One device id or alias (see list_devices). Default ${DEFAULT_RECORD_DEVICE}.`),
|
|
140
|
+
selector: z
|
|
141
|
+
.string()
|
|
142
|
+
.trim()
|
|
143
|
+
.min(1)
|
|
144
|
+
.optional()
|
|
145
|
+
.describe('CSS selector of the element to record (a carousel, a marquee, a pinned section). It is scrolled into view and the video is cropped to its box. Default: the whole viewport.'),
|
|
146
|
+
duration: z
|
|
147
|
+
.number()
|
|
148
|
+
.min(0.5)
|
|
149
|
+
.max(MAX_RECORD_SECONDS)
|
|
150
|
+
.optional()
|
|
151
|
+
.describe(`Seconds of video. Default ${DEFAULT_RECORD_SECONDS}, max ${MAX_RECORD_SECONDS}.`),
|
|
152
|
+
gif: z.boolean().optional().describe('Also write a GIF (10 fps, at most 640 px wide). Default false.'),
|
|
153
|
+
mp4: z.boolean().optional().describe('Also write an mp4; needs an ffmpeg with H.264 on PATH, skipped with a warning otherwise. Default false.'),
|
|
154
|
+
reducedMotion: z.boolean().optional().describe('Emulate prefers-reduced-motion: reduce.'),
|
|
155
|
+
waitFor: z.string().optional().describe('CSS selector to wait for (visible) before recording.'),
|
|
156
|
+
waitMs: z.number().int().min(0).max(30_000).optional().describe('Extra delay before recording, in ms.'),
|
|
157
|
+
colorScheme: z.enum(COLOR_SCHEMES).optional().describe('Emulate prefers-color-scheme.'),
|
|
158
|
+
outDir: z.string().optional().describe(`Output directory, relative to the server's cwd. Default ${DEFAULT_OUT_DIR}.`),
|
|
159
|
+
timeoutMs: z.number().int().min(1000).max(120_000).optional().describe(`Navigation timeout. Default ${DEFAULT_TIMEOUT_MS}.`),
|
|
160
|
+
};
|
|
161
|
+
function authFrom(args) {
|
|
162
|
+
const auth = {};
|
|
163
|
+
if (args.cookies?.length)
|
|
164
|
+
auth.cookies = args.cookies;
|
|
165
|
+
if (args.headers && Object.keys(args.headers).length)
|
|
166
|
+
auth.headers = args.headers;
|
|
167
|
+
if (args.storageState)
|
|
168
|
+
auth.storageState = args.storageState;
|
|
169
|
+
if (args.useSession === false)
|
|
170
|
+
auth.useSession = false;
|
|
171
|
+
if (args.allowOrigins?.length)
|
|
172
|
+
auth.allowOrigins = args.allowOrigins.map(toOrigin);
|
|
173
|
+
return auth;
|
|
174
|
+
}
|
|
96
175
|
export function createServer() {
|
|
97
176
|
const server = new McpServer({ name: 'rpc', version: pkg.version });
|
|
98
177
|
server.registerTool('screenshot', {
|
|
@@ -100,22 +179,12 @@ export function createServer() {
|
|
|
100
179
|
description: 'Load a URL in headless Chromium at one or more device viewports (with device pixel ratio, mobile user agent and touch) ' +
|
|
101
180
|
'and save PNG screenshots, optionally framed in a device bezel. Use it to check responsive layout of a page you are building, ' +
|
|
102
181
|
'including local dev servers. Returns file paths, page title, HTTP status and console errors, plus inline previews.',
|
|
103
|
-
inputSchema: screenshotInput,
|
|
182
|
+
inputSchema: { ...screenshotInput, ...authInput },
|
|
104
183
|
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
105
184
|
}, async (args) => {
|
|
106
185
|
let options;
|
|
107
186
|
try {
|
|
108
|
-
const auth =
|
|
109
|
-
if (args.cookies?.length)
|
|
110
|
-
auth.cookies = args.cookies;
|
|
111
|
-
if (args.headers && Object.keys(args.headers).length)
|
|
112
|
-
auth.headers = args.headers;
|
|
113
|
-
if (args.storageState)
|
|
114
|
-
auth.storageState = args.storageState;
|
|
115
|
-
if (args.useSession === false)
|
|
116
|
-
auth.useSession = false;
|
|
117
|
-
if (args.allowOrigins?.length)
|
|
118
|
-
auth.allowOrigins = args.allowOrigins.map(toOrigin);
|
|
187
|
+
const auth = authFrom(args);
|
|
119
188
|
options = {
|
|
120
189
|
url: normalizeUrl(args.url),
|
|
121
190
|
devices: resolveDevices(args.devices ?? DEFAULT_DEVICES).map(d => d.id),
|
|
@@ -128,6 +197,8 @@ export function createServer() {
|
|
|
128
197
|
timeoutMs: args.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
129
198
|
emulate: true,
|
|
130
199
|
reducedMotion: args.reducedMotion ?? false,
|
|
200
|
+
finishAnimations: args.finishAnimations ?? false,
|
|
201
|
+
unpin: args.unpin ?? true,
|
|
131
202
|
maxHeight: args.maxHeight ?? DEFAULT_MAX_HEIGHT,
|
|
132
203
|
saveSlices: args.saveSlices,
|
|
133
204
|
maxSlices: args.maxSlices ?? DEFAULT_MAX_SLICES,
|
|
@@ -183,9 +254,112 @@ export function createServer() {
|
|
|
183
254
|
await browser.close();
|
|
184
255
|
}
|
|
185
256
|
});
|
|
257
|
+
server.registerTool('check', {
|
|
258
|
+
title: 'Pre-launch check of a page',
|
|
259
|
+
description: 'Audit one URL before calling the work done: meta and social tags (title, description, canonical, lang, viewport zoom, ' +
|
|
260
|
+
'favicon, Open Graph, twitter:card, and whether og:image loads and its size), layout shift (CLS with the elements that moved), ' +
|
|
261
|
+
'buttons/links/images/fields without an accessible name, web fonts (font-display, preloads), mobile viewport bugs on phones ' +
|
|
262
|
+
'(100vh, sideways scroll, tap targets under 44px, inputs under 16px) and basics (HTTP status, mixed content, console errors). ' +
|
|
263
|
+
'Returns a short summary, then the JSON report: each finding has id, severity (error|warn|info), message, selector or url, ' +
|
|
264
|
+
'devices and a fix. Fix the errors (and ideally warnings), then run it again.',
|
|
265
|
+
inputSchema: { ...checkInput, ...authInput },
|
|
266
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
267
|
+
}, async (args) => {
|
|
268
|
+
let options;
|
|
269
|
+
try {
|
|
270
|
+
const auth = authFrom(args);
|
|
271
|
+
options = {
|
|
272
|
+
...defaultCheckOptions(args.url),
|
|
273
|
+
devices: resolveDevices(args.devices ?? DEFAULT_DEVICES).map(d => d.id),
|
|
274
|
+
outDir: args.outDir ?? DEFAULT_OUT_DIR,
|
|
275
|
+
waitFor: args.waitFor,
|
|
276
|
+
waitMs: args.waitMs ?? 0,
|
|
277
|
+
colorScheme: args.colorScheme,
|
|
278
|
+
timeoutMs: args.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
279
|
+
shots: args.shots ?? false,
|
|
280
|
+
...(Object.keys(auth).length ? { auth } : {}),
|
|
281
|
+
};
|
|
282
|
+
resolveAuth(options.url, auth);
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
return { isError: true, content: [{ type: 'text', text: err.message }] };
|
|
286
|
+
}
|
|
287
|
+
let browser;
|
|
288
|
+
try {
|
|
289
|
+
browser = await launchBrowser();
|
|
290
|
+
}
|
|
291
|
+
catch (err) {
|
|
292
|
+
return { isError: true, content: [{ type: 'text', text: `Could not start the browser: ${err.message}` }] };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const report = await runCheck(options, { browser });
|
|
296
|
+
const content = [
|
|
297
|
+
{ type: 'text', text: (report.auth ? `auth: ${describeAuth(report.auth)}\n` : '') + textSummary(report) },
|
|
298
|
+
{ type: 'text', text: JSON.stringify(report, null, 2) },
|
|
299
|
+
];
|
|
300
|
+
return report.devices.every(d => d.error) ? { isError: true, content } : { content };
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
await browser.close();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
server.registerTool('record', {
|
|
307
|
+
title: 'Record a short video of a page or element',
|
|
308
|
+
description: 'Record a few seconds of a page, or of one element, as a VP9 webm (plus a GIF or mp4 on request), for motion a ' +
|
|
309
|
+
'screenshot cannot show: carousels, marquees, pinned scroll sections, looping animations. screenshot warns ' +
|
|
310
|
+
'"carousel: use rpc record" when it sees one. Returns the file paths, a short summary and the first frame (poster) inline.',
|
|
311
|
+
inputSchema: { ...recordInput, ...authInput },
|
|
312
|
+
annotations: { readOnlyHint: false, openWorldHint: true },
|
|
313
|
+
}, async (args) => {
|
|
314
|
+
let options;
|
|
315
|
+
try {
|
|
316
|
+
const auth = authFrom(args);
|
|
317
|
+
options = {
|
|
318
|
+
...defaultRecordOptions(args.url),
|
|
319
|
+
device: resolveDevices([args.device ?? DEFAULT_RECORD_DEVICE])[0].id,
|
|
320
|
+
selector: args.selector,
|
|
321
|
+
duration: args.duration ?? DEFAULT_RECORD_SECONDS,
|
|
322
|
+
gif: args.gif ?? false,
|
|
323
|
+
mp4: args.mp4 ?? false,
|
|
324
|
+
reducedMotion: args.reducedMotion ?? false,
|
|
325
|
+
outDir: args.outDir ?? DEFAULT_OUT_DIR,
|
|
326
|
+
waitFor: args.waitFor,
|
|
327
|
+
waitMs: args.waitMs ?? 0,
|
|
328
|
+
colorScheme: args.colorScheme,
|
|
329
|
+
timeoutMs: args.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
330
|
+
...(Object.keys(auth).length ? { auth } : {}),
|
|
331
|
+
};
|
|
332
|
+
resolveAuth(options.url, auth);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
return { isError: true, content: [{ type: 'text', text: err.message }] };
|
|
336
|
+
}
|
|
337
|
+
let browser;
|
|
338
|
+
try {
|
|
339
|
+
browser = await launchBrowser();
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
return { isError: true, content: [{ type: 'text', text: `Could not start the browser: ${err.message}` }] };
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const r = await record(options, { browser });
|
|
346
|
+
const content = [{ type: 'text', text: recordSummary(r) + '\n\n' + JSON.stringify(r, null, 2) }];
|
|
347
|
+
if (r.error)
|
|
348
|
+
return { isError: true, content };
|
|
349
|
+
if (r.poster) {
|
|
350
|
+
const p = await renderPreview(browser, await readFile(r.poster.path), r.poster);
|
|
351
|
+
content.push({ type: 'text', text: `${r.device}${r.selector ? ` ${r.selector}` : ''}: first frame (poster) ${p.width}x${p.height}` });
|
|
352
|
+
content.push({ type: 'image', data: p.data.toString('base64'), mimeType: 'image/jpeg' });
|
|
353
|
+
}
|
|
354
|
+
return { content };
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
await browser.close();
|
|
358
|
+
}
|
|
359
|
+
});
|
|
186
360
|
server.registerTool('list_devices', {
|
|
187
361
|
title: 'List device presets',
|
|
188
|
-
description: 'List device ids, CSS viewport sizes and pixel ratios usable in screenshot.devices, plus short aliases.',
|
|
362
|
+
description: 'List device ids, CSS viewport sizes and pixel ratios usable in screenshot.devices and check.devices, plus short aliases.',
|
|
189
363
|
inputSchema: {
|
|
190
364
|
category: z
|
|
191
365
|
.enum(CATEGORY_ORDER)
|
|
@@ -264,8 +438,15 @@ export function headline(shots, options, auth) {
|
|
|
264
438
|
continue;
|
|
265
439
|
}
|
|
266
440
|
const size = s.shot ? `${s.shot.width}x${s.shot.height}px` : '';
|
|
441
|
+
const motion = [
|
|
442
|
+
...unpinnedLines(s.motion?.unpinned).map(u => ` unpinned: ${u}`),
|
|
443
|
+
...(s.warnings ?? []).map(w => {
|
|
444
|
+
const where = warningWhere(w);
|
|
445
|
+
return ` WARN [${w.id}] ${w.message}${where ? ` at ${where}` : ''}\n fix: ${w.fix}`;
|
|
446
|
+
}),
|
|
447
|
+
];
|
|
267
448
|
if (!options.fullPage) {
|
|
268
|
-
lines.push(`ok ${s.device}: ${size}${s.shot ? ` ${s.shot.path}` : ''}
|
|
449
|
+
lines.push(`ok ${s.device}: ${size}${s.shot ? ` ${s.shot.path}` : ''}`, ...motion);
|
|
269
450
|
continue;
|
|
270
451
|
}
|
|
271
452
|
const parts = [
|
|
@@ -294,8 +475,29 @@ export function headline(shots, options, auth) {
|
|
|
294
475
|
parts.push(` full PNG: ${s.shot ? s.shot.path : ''}`);
|
|
295
476
|
}
|
|
296
477
|
}
|
|
297
|
-
lines.push(parts.join('\n'));
|
|
478
|
+
lines.push(parts.join('\n'), ...motion);
|
|
479
|
+
}
|
|
480
|
+
return lines.join('\n');
|
|
481
|
+
}
|
|
482
|
+
/** First lines of the record tool's result: what was written, or why not. */
|
|
483
|
+
export function recordSummary(r) {
|
|
484
|
+
const lines = [];
|
|
485
|
+
if (r.auth)
|
|
486
|
+
lines.push(`auth: ${describeAuth(r.auth)}`);
|
|
487
|
+
if (r.error) {
|
|
488
|
+
lines.push(`FAILED ${r.device}: ${r.error}${r.hint ? `\n hint: ${r.hint}` : ''}`);
|
|
489
|
+
return lines.join('\n');
|
|
298
490
|
}
|
|
491
|
+
const v = r.video;
|
|
492
|
+
lines.push(`ok ${r.device}${r.selector ? ` ${r.selector}` : ''}: ${v.duration}s webm ${v.width}x${v.height} at ${v.fps} fps ${v.path}`);
|
|
493
|
+
if (r.poster)
|
|
494
|
+
lines.push(` poster: ${r.poster.path}`);
|
|
495
|
+
if (r.gif)
|
|
496
|
+
lines.push(` gif: ${r.gif.path} (${r.gif.width}x${r.gif.height}, ${r.gif.frames} frames)`);
|
|
497
|
+
if (r.mp4)
|
|
498
|
+
lines.push(` mp4: ${r.mp4.path}`);
|
|
499
|
+
for (const w of r.warnings ?? [])
|
|
500
|
+
lines.push(` WARN ${w}`);
|
|
299
501
|
return lines.join('\n');
|
|
300
502
|
}
|
|
301
503
|
function summarise(url, outDir, shots, auth) {
|