mcp-web-validator 1.0.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 +129 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +682 -0
- package/dist/index.js.map +1 -0
- package/dist/screenshot.d.ts +18 -0
- package/dist/screenshot.js +65 -0
- package/dist/screenshot.js.map +1 -0
- package/dist/seo-auditor.d.ts +24 -0
- package/dist/seo-auditor.js +233 -0
- package/dist/seo-auditor.js.map +1 -0
- package/dist/w3c-validator.d.ts +23 -0
- package/dist/w3c-validator.js +62 -0
- package/dist/w3c-validator.js.map +1 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import * as fs from "fs/promises";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import { validateHtmlContent, validateCssContent } from "./w3c-validator.js";
|
|
8
|
+
import { auditSeoMetadata, validateSchemaMarkup, checkBrokenLinks } from "./seo-auditor.js";
|
|
9
|
+
import { captureScreenshots } from "./screenshot.js";
|
|
10
|
+
// Initialize MCP Server
|
|
11
|
+
const server = new Server({
|
|
12
|
+
name: "mcp-web-validator",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
}, {
|
|
15
|
+
capabilities: {
|
|
16
|
+
tools: {},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
// Register Tool Definitions
|
|
20
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
21
|
+
return {
|
|
22
|
+
tools: [
|
|
23
|
+
{
|
|
24
|
+
name: "html_validate_local",
|
|
25
|
+
description: "Validates a local HTML file against the official W3C Nu HTML Checker API. Catches syntax, tags, and compliance errors.",
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
filePath: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "Absolute or relative path to the local HTML file.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["filePath"],
|
|
35
|
+
},
|
|
36
|
+
outputSchema: {
|
|
37
|
+
type: "object",
|
|
38
|
+
description: "Validation result containing W3C HTML parser validation messages.",
|
|
39
|
+
properties: {
|
|
40
|
+
errors: {
|
|
41
|
+
type: "array",
|
|
42
|
+
description: "List of HTML validation error and warning objects returned from W3C.",
|
|
43
|
+
items: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
type: { "type": "string", "description": "The category of message: 'error', 'info', or 'non-document-error'." },
|
|
47
|
+
message: { "type": "string", "description": "The specific validation or parsing error message." },
|
|
48
|
+
extract: { "type": "string", "description": "The HTML snippet around the validation location." },
|
|
49
|
+
lastLine: { "type": "number", "description": "Line number where the issue occurred." },
|
|
50
|
+
lastColumn: { "type": "number", "description": "Column number where the issue occurred." }
|
|
51
|
+
},
|
|
52
|
+
required: ["type", "message"]
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
required: ["errors"]
|
|
57
|
+
},
|
|
58
|
+
annotations: {
|
|
59
|
+
readOnlyHint: true,
|
|
60
|
+
openWorldHint: false
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "html_validate_url",
|
|
65
|
+
description: "Validates the markup of a live public URL using the W3C HTML validation engine.",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
url: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "The live URL to validate (must start with http:// or https://).",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
required: ["url"],
|
|
75
|
+
},
|
|
76
|
+
outputSchema: {
|
|
77
|
+
type: "object",
|
|
78
|
+
description: "Validation result containing W3C HTML parser validation messages.",
|
|
79
|
+
properties: {
|
|
80
|
+
errors: {
|
|
81
|
+
type: "array",
|
|
82
|
+
description: "List of HTML validation error and warning objects returned from W3C.",
|
|
83
|
+
items: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
type: { "type": "string", "description": "The category of message: 'error', 'info', or 'non-document-error'." },
|
|
87
|
+
message: { "type": "string", "description": "The specific validation or parsing error message." },
|
|
88
|
+
extract: { "type": "string", "description": "The HTML snippet around the validation location." },
|
|
89
|
+
lastLine: { "type": "number", "description": "Line number where the issue occurred." },
|
|
90
|
+
lastColumn: { "type": "number", "description": "Column number where the issue occurred." }
|
|
91
|
+
},
|
|
92
|
+
required: ["type", "message"]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
required: ["errors"]
|
|
97
|
+
},
|
|
98
|
+
annotations: {
|
|
99
|
+
readOnlyHint: true,
|
|
100
|
+
openWorldHint: false
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
name: "css_validate_local",
|
|
105
|
+
description: "Validates a local CSS file against the W3C Jigsaw CSS Validator API. Finds styling syntax errors.",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: {
|
|
109
|
+
filePath: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Absolute or relative path to the local CSS file.",
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
required: ["filePath"],
|
|
115
|
+
},
|
|
116
|
+
outputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
description: "CSS validation results.",
|
|
119
|
+
properties: {
|
|
120
|
+
errors: {
|
|
121
|
+
type: "array",
|
|
122
|
+
description: "List of CSS validation errors from W3C Jigsaw API.",
|
|
123
|
+
items: {
|
|
124
|
+
type: "object",
|
|
125
|
+
properties: {
|
|
126
|
+
line: { "type": "number", "description": "Line number of the CSS error." },
|
|
127
|
+
context: { "type": "string", "description": "The CSS selector or context where the error occurred." },
|
|
128
|
+
message: { "type": "string", "description": "The specific CSS validation warning or error message." }
|
|
129
|
+
},
|
|
130
|
+
required: ["line", "message"]
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
required: ["errors"]
|
|
135
|
+
},
|
|
136
|
+
annotations: {
|
|
137
|
+
readOnlyHint: true,
|
|
138
|
+
openWorldHint: false
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "seo_audit_metadata",
|
|
143
|
+
description: "Runs a fast offline audit of HTML metadata, heading structure, viewport responsive tags, image alt tags, and Open Graph cards.",
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: "object",
|
|
146
|
+
properties: {
|
|
147
|
+
htmlContent: {
|
|
148
|
+
type: "string",
|
|
149
|
+
description: "The raw HTML string content to analyze.",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
required: ["htmlContent"],
|
|
153
|
+
},
|
|
154
|
+
outputSchema: {
|
|
155
|
+
type: "object",
|
|
156
|
+
description: "Audited Technical SEO results.",
|
|
157
|
+
properties: {
|
|
158
|
+
issues: {
|
|
159
|
+
type: "array",
|
|
160
|
+
description: "List of audited Technical SEO issues, warnings, and error indicators.",
|
|
161
|
+
items: {
|
|
162
|
+
type: "object",
|
|
163
|
+
properties: {
|
|
164
|
+
category: { "type": "string", "description": "SEO audit category (e.g. metadata, structure, accessibility)." },
|
|
165
|
+
severity: { "type": "string", "description": "Severity of the issue: 'error' or 'warning'." },
|
|
166
|
+
message: { "type": "string", "description": "Detailed explanation of the SEO issue." },
|
|
167
|
+
element: { "type": "string", "description": "Relevant HTML snippet or element if applicable." }
|
|
168
|
+
},
|
|
169
|
+
required: ["category", "severity", "message"]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
required: ["issues"]
|
|
174
|
+
},
|
|
175
|
+
annotations: {
|
|
176
|
+
readOnlyHint: true,
|
|
177
|
+
openWorldHint: false
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: "links_check_broken",
|
|
182
|
+
description: "Extracts all links (a href tags) in the HTML content and tests their HTTP status codes to detect broken internal or external URLs.",
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
htmlContent: {
|
|
187
|
+
type: "string",
|
|
188
|
+
description: "The raw HTML string content to inspect.",
|
|
189
|
+
},
|
|
190
|
+
baseUrl: {
|
|
191
|
+
type: "string",
|
|
192
|
+
description: "Optional base URL to resolve relative paths (e.g., https://example.com).",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
required: ["htmlContent"],
|
|
196
|
+
},
|
|
197
|
+
outputSchema: {
|
|
198
|
+
type: "object",
|
|
199
|
+
description: "Reachability report of page links.",
|
|
200
|
+
properties: {
|
|
201
|
+
links: {
|
|
202
|
+
type: "array",
|
|
203
|
+
description: "Status details of all checked links on the page.",
|
|
204
|
+
items: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
url: { "type": "string", "description": "The destination URL that was tested." },
|
|
208
|
+
status: { "type": "number", "description": "HTTP status code response (e.g., 200, 404)." },
|
|
209
|
+
ok: { "type": "boolean", "description": "Whether the link is reachable and returned a successful status code." },
|
|
210
|
+
message: { "type": "string", "description": "Reachable status details or error description." }
|
|
211
|
+
},
|
|
212
|
+
required: ["url", "status", "ok"]
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
required: ["links"]
|
|
217
|
+
},
|
|
218
|
+
annotations: {
|
|
219
|
+
readOnlyHint: true,
|
|
220
|
+
openWorldHint: false
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
name: "schema_validate_markup",
|
|
225
|
+
description: "Finds and validates the JSON-LD schema blocks within the HTML, catching syntax issues.",
|
|
226
|
+
inputSchema: {
|
|
227
|
+
type: "object",
|
|
228
|
+
properties: {
|
|
229
|
+
htmlContent: {
|
|
230
|
+
type: "string",
|
|
231
|
+
description: "The raw HTML string containing <script type=\"application/ld+json\"> blocks.",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
required: ["htmlContent"],
|
|
235
|
+
},
|
|
236
|
+
outputSchema: {
|
|
237
|
+
type: "object",
|
|
238
|
+
description: "Parsed JSON-LD validation report.",
|
|
239
|
+
properties: {
|
|
240
|
+
issues: {
|
|
241
|
+
type: "array",
|
|
242
|
+
description: "List of parsed JSON-LD validation results.",
|
|
243
|
+
items: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
category: { "type": "string", "description": "Always 'Schema Markup'." },
|
|
247
|
+
severity: { "type": "string", "description": "Severity level: 'error' or 'warning'." },
|
|
248
|
+
message: { "type": "string", "description": "Validation message explaining JSON parsing errors or schema problems." }
|
|
249
|
+
},
|
|
250
|
+
required: ["category", "severity", "message"]
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
required: ["issues"]
|
|
255
|
+
},
|
|
256
|
+
annotations: {
|
|
257
|
+
readOnlyHint: true,
|
|
258
|
+
openWorldHint: false
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
name: "report_generate_validation",
|
|
263
|
+
description: "Runs all validation checks (HTML, CSS, SEO, Schema, Links) on local files and aggregates them into a beautifully formatted Markdown report with summary tables.",
|
|
264
|
+
inputSchema: {
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: {
|
|
267
|
+
htmlFilePath: {
|
|
268
|
+
type: "string",
|
|
269
|
+
description: "Absolute or relative path to the local HTML file to validate.",
|
|
270
|
+
},
|
|
271
|
+
cssFilePath: {
|
|
272
|
+
type: "string",
|
|
273
|
+
description: "Optional absolute or relative path to the local CSS file to validate.",
|
|
274
|
+
},
|
|
275
|
+
baseUrl: {
|
|
276
|
+
type: "string",
|
|
277
|
+
description: "Optional base URL to resolve relative link paths.",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
required: ["htmlFilePath"],
|
|
281
|
+
},
|
|
282
|
+
outputSchema: {
|
|
283
|
+
type: "object",
|
|
284
|
+
description: "Aggregated Markdown validation and SEO report details.",
|
|
285
|
+
properties: {
|
|
286
|
+
content: {
|
|
287
|
+
type: "array",
|
|
288
|
+
items: {
|
|
289
|
+
type: "object",
|
|
290
|
+
properties: {
|
|
291
|
+
type: { "type": "string", "enum": ["text"] },
|
|
292
|
+
text: { "type": "string", "description": "Formatted Markdown report." }
|
|
293
|
+
},
|
|
294
|
+
required: ["type", "text"]
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
required: ["content"]
|
|
299
|
+
},
|
|
300
|
+
annotations: {
|
|
301
|
+
readOnlyHint: true,
|
|
302
|
+
openWorldHint: false
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "screenshot_capture",
|
|
307
|
+
description: "Renders a local HTML file or remote URL using Puppeteer and captures screenshots at different viewport sizes (desktop, tablet, mobile).",
|
|
308
|
+
inputSchema: {
|
|
309
|
+
type: "object",
|
|
310
|
+
properties: {
|
|
311
|
+
targetPath: {
|
|
312
|
+
type: "string",
|
|
313
|
+
description: "Path to the local HTML file or remote URL (e.g. http:// or https://) to screenshot.",
|
|
314
|
+
},
|
|
315
|
+
outputDir: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "Optional absolute or relative directory path where screenshots will be saved. Defaults to '.mcp-validator/screenshots'.",
|
|
318
|
+
},
|
|
319
|
+
viewports: {
|
|
320
|
+
type: "array",
|
|
321
|
+
description: "Optional list of custom viewports to capture. Each viewport object must have name, width, and height.",
|
|
322
|
+
items: {
|
|
323
|
+
type: "object",
|
|
324
|
+
properties: {
|
|
325
|
+
name: { type: "string", description: "Name of the viewport (e.g., mobile-portrait)." },
|
|
326
|
+
width: { type: "number", description: "Width in pixels." },
|
|
327
|
+
height: { type: "number", description: "Height in pixels." }
|
|
328
|
+
},
|
|
329
|
+
required: ["name", "width", "height"]
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
required: ["targetPath"]
|
|
334
|
+
},
|
|
335
|
+
outputSchema: {
|
|
336
|
+
type: "object",
|
|
337
|
+
description: "Status report of screenshot file paths and coordinates generated.",
|
|
338
|
+
properties: {
|
|
339
|
+
content: {
|
|
340
|
+
type: "array",
|
|
341
|
+
items: {
|
|
342
|
+
type: "object",
|
|
343
|
+
properties: {
|
|
344
|
+
type: { "type": "string", "enum": ["text"] },
|
|
345
|
+
text: { "type": "string", "description": "Markdown list of screenshots created and their directories." }
|
|
346
|
+
},
|
|
347
|
+
required: ["type", "text"]
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
required: ["content"]
|
|
352
|
+
},
|
|
353
|
+
annotations: {
|
|
354
|
+
readOnlyHint: false,
|
|
355
|
+
openWorldHint: true
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
// Handle Tool Calls
|
|
362
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
363
|
+
const { name, arguments: args } = request.params;
|
|
364
|
+
try {
|
|
365
|
+
switch (name) {
|
|
366
|
+
case "html_validate_local": {
|
|
367
|
+
const filePath = String(args?.filePath);
|
|
368
|
+
const resolvedPath = path.resolve(filePath);
|
|
369
|
+
try {
|
|
370
|
+
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
371
|
+
const errors = await validateHtmlContent(content);
|
|
372
|
+
return {
|
|
373
|
+
content: [
|
|
374
|
+
{
|
|
375
|
+
type: "text",
|
|
376
|
+
text: JSON.stringify(errors, null, 2),
|
|
377
|
+
},
|
|
378
|
+
],
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
return {
|
|
383
|
+
isError: true,
|
|
384
|
+
content: [
|
|
385
|
+
{
|
|
386
|
+
type: "text",
|
|
387
|
+
text: `Error reading file at "${resolvedPath}": ${e.message}`,
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
case "html_validate_url": {
|
|
394
|
+
const url = String(args?.url);
|
|
395
|
+
try {
|
|
396
|
+
const response = await fetch(url);
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
return {
|
|
399
|
+
isError: true,
|
|
400
|
+
content: [
|
|
401
|
+
{
|
|
402
|
+
type: "text",
|
|
403
|
+
text: `Failed to fetch URL ${url}. Status code: ${response.status}`,
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const content = await response.text();
|
|
409
|
+
const errors = await validateHtmlContent(content);
|
|
410
|
+
return {
|
|
411
|
+
content: [
|
|
412
|
+
{
|
|
413
|
+
type: "text",
|
|
414
|
+
text: JSON.stringify(errors, null, 2),
|
|
415
|
+
},
|
|
416
|
+
],
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
return {
|
|
421
|
+
isError: true,
|
|
422
|
+
content: [
|
|
423
|
+
{
|
|
424
|
+
type: "text",
|
|
425
|
+
text: `Error fetching URL "${url}": ${e.message}`,
|
|
426
|
+
},
|
|
427
|
+
],
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
case "css_validate_local": {
|
|
432
|
+
const filePath = String(args?.filePath);
|
|
433
|
+
const resolvedPath = path.resolve(filePath);
|
|
434
|
+
try {
|
|
435
|
+
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
436
|
+
const errors = await validateCssContent(content);
|
|
437
|
+
return {
|
|
438
|
+
content: [
|
|
439
|
+
{
|
|
440
|
+
type: "text",
|
|
441
|
+
text: JSON.stringify(errors, null, 2),
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
return {
|
|
448
|
+
isError: true,
|
|
449
|
+
content: [
|
|
450
|
+
{
|
|
451
|
+
type: "text",
|
|
452
|
+
text: `Error reading CSS file at "${resolvedPath}": ${e.message}`,
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
case "seo_audit_metadata": {
|
|
459
|
+
const htmlContent = String(args?.htmlContent);
|
|
460
|
+
const issues = auditSeoMetadata(htmlContent);
|
|
461
|
+
return {
|
|
462
|
+
content: [
|
|
463
|
+
{
|
|
464
|
+
type: "text",
|
|
465
|
+
text: JSON.stringify(issues, null, 2),
|
|
466
|
+
},
|
|
467
|
+
],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
case "links_check_broken": {
|
|
471
|
+
const htmlContent = String(args?.htmlContent);
|
|
472
|
+
const baseUrl = args?.baseUrl ? String(args.baseUrl) : undefined;
|
|
473
|
+
const linkStatuses = await checkBrokenLinks(htmlContent, baseUrl);
|
|
474
|
+
return {
|
|
475
|
+
content: [
|
|
476
|
+
{
|
|
477
|
+
type: "text",
|
|
478
|
+
text: JSON.stringify(linkStatuses, null, 2),
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
case "schema_validate_markup": {
|
|
484
|
+
const htmlContent = String(args?.htmlContent);
|
|
485
|
+
const issues = validateSchemaMarkup(htmlContent);
|
|
486
|
+
return {
|
|
487
|
+
content: [
|
|
488
|
+
{
|
|
489
|
+
type: "text",
|
|
490
|
+
text: JSON.stringify(issues, null, 2),
|
|
491
|
+
},
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
case "report_generate_validation": {
|
|
496
|
+
const htmlFilePath = String(args?.htmlFilePath);
|
|
497
|
+
const cssFilePath = args?.cssFilePath ? String(args.cssFilePath) : undefined;
|
|
498
|
+
const baseUrl = args?.baseUrl ? String(args.baseUrl) : undefined;
|
|
499
|
+
const resolvedHtmlPath = path.resolve(htmlFilePath);
|
|
500
|
+
const htmlContent = await fs.readFile(resolvedHtmlPath, "utf-8");
|
|
501
|
+
// Runs checks
|
|
502
|
+
const htmlErrors = await validateHtmlContent(htmlContent);
|
|
503
|
+
const seoIssues = auditSeoMetadata(htmlContent);
|
|
504
|
+
const schemaIssues = validateSchemaMarkup(htmlContent);
|
|
505
|
+
const linkStatuses = await checkBrokenLinks(htmlContent, baseUrl);
|
|
506
|
+
let cssErrors = [];
|
|
507
|
+
if (cssFilePath) {
|
|
508
|
+
try {
|
|
509
|
+
const resolvedCssPath = path.resolve(cssFilePath);
|
|
510
|
+
const cssContent = await fs.readFile(resolvedCssPath, "utf-8");
|
|
511
|
+
cssErrors = await validateCssContent(cssContent);
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
cssErrors = [{ line: 0, type: "error", message: `Failed to read CSS: ${e.message}` }];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Generate Report Markdown
|
|
518
|
+
const htmlErrCount = htmlErrors.filter(e => e.type === "error").length;
|
|
519
|
+
const htmlWarnCount = htmlErrors.filter(e => e.type !== "error").length;
|
|
520
|
+
const cssErrCount = cssErrors.length;
|
|
521
|
+
const seoErrCount = seoIssues.filter(i => i.severity === "error").length;
|
|
522
|
+
const seoWarnCount = seoIssues.filter(i => i.severity !== "error").length;
|
|
523
|
+
const schemaErrCount = schemaIssues.filter(i => i.severity === "error").length;
|
|
524
|
+
const brokenLinkCount = linkStatuses.filter(l => !l.ok).length;
|
|
525
|
+
// Calculate Scores (PageSpeed style: Base 100)
|
|
526
|
+
let htmlScore = 100 - (htmlErrCount * 15) - (htmlWarnCount * 2);
|
|
527
|
+
let cssScore = cssFilePath ? (100 - (cssErrCount * 20)) : 100;
|
|
528
|
+
let seoScore = 100 - (seoErrCount * 15) - (seoWarnCount * 4) - (schemaErrCount * 15);
|
|
529
|
+
let linkScore = linkStatuses.length > 0 ? (100 - (brokenLinkCount * 25)) : 100;
|
|
530
|
+
// Clamp to [0, 100]
|
|
531
|
+
htmlScore = Math.max(0, Math.min(100, htmlScore));
|
|
532
|
+
cssScore = Math.max(0, Math.min(100, cssScore));
|
|
533
|
+
seoScore = Math.max(0, Math.min(100, seoScore));
|
|
534
|
+
linkScore = Math.max(0, Math.min(100, linkScore));
|
|
535
|
+
const getCircle = (score) => {
|
|
536
|
+
if (score >= 90)
|
|
537
|
+
return "đĸ";
|
|
538
|
+
if (score >= 50)
|
|
539
|
+
return "đ ";
|
|
540
|
+
return "đ´";
|
|
541
|
+
};
|
|
542
|
+
const totalAuditCount = cssFilePath ? 4 : 3;
|
|
543
|
+
const totalScoreSum = htmlScore + seoScore + linkScore + (cssFilePath ? cssScore : 0);
|
|
544
|
+
const overallScore = Math.round(totalScoreSum / totalAuditCount);
|
|
545
|
+
const report = [
|
|
546
|
+
`# đ Web Validation & SEO Audit Report â ${getCircle(overallScore)} **${overallScore}**/100`,
|
|
547
|
+
`*Generated for: \`${path.basename(htmlFilePath)}\`*`,
|
|
548
|
+
``,
|
|
549
|
+
`## ⥠Page Health Scores (PageSpeed Inspired)`,
|
|
550
|
+
``,
|
|
551
|
+
`| Score Card | Status | Score |`,
|
|
552
|
+
`| :--- | :---: | :---: |`,
|
|
553
|
+
`| **W3C HTML Validation** | ${getCircle(htmlScore)} ${htmlScore >= 90 ? "Excellent" : (htmlScore >= 50 ? "Needs Work" : "Poor")} | **${htmlScore}** / 100 |`,
|
|
554
|
+
`| **W3C CSS Validation** | ${cssFilePath ? `${getCircle(cssScore)} ${cssScore >= 90 ? "Excellent" : (cssScore >= 50 ? "Needs Work" : "Poor")}` : "âšī¸ Not Audited"} | ${cssFilePath ? `**${cssScore}** / 100` : "N/A"} |`,
|
|
555
|
+
`| **SEO & Accessibility** | ${getCircle(seoScore)} ${seoScore >= 90 ? "Optimized" : (seoScore >= 50 ? "Warnings" : "Poor")} | **${seoScore}** / 100 |`,
|
|
556
|
+
`| **Links Integrity** | ${linkStatuses.length > 0 ? `${getCircle(linkScore)} ${linkScore >= 90 ? "All Good" : "Broken Links"}` : "âšī¸ No Links"} | ${linkStatuses.length > 0 ? `**${linkScore}** / 100` : "N/A"} |`,
|
|
557
|
+
``,
|
|
558
|
+
`---`,
|
|
559
|
+
``,
|
|
560
|
+
`## đ Audit Details Overview`,
|
|
561
|
+
`| Audit Category | Status | Details |`,
|
|
562
|
+
`| :--- | :---: | :--- |`,
|
|
563
|
+
`| **W3C HTML Validation** | ${htmlErrCount > 0 ? "â Failed" : "â
Passed"} | ${htmlErrCount} Errors, ${htmlWarnCount} Warnings |`,
|
|
564
|
+
`| **W3C CSS Validation** | ${cssFilePath ? (cssErrCount > 0 ? "â Failed" : "â
Passed") : "âšī¸ Not Audited"} | ${cssErrCount} Errors |`,
|
|
565
|
+
`| **Technical SEO & Accessibility** | ${seoErrCount > 0 ? "â Critical Issues" : (seoWarnCount > 0 ? "â ī¸ Warnings" : "â
Optimized")} | ${seoErrCount} Errors, ${seoWarnCount} Warnings |`,
|
|
566
|
+
`| **JSON-LD Schema Verification** | ${schemaErrCount > 0 ? "â Invalid" : "â
Valid"} | ${schemaErrCount} Syntax Errors |`,
|
|
567
|
+
`| **Broken Link Check** | ${brokenLinkCount > 0 ? "â Broken Links Found" : "â
All Links OK"} | ${brokenLinkCount} Dead Links, ${linkStatuses.length} Total Links Checked |`,
|
|
568
|
+
`---`,
|
|
569
|
+
``,
|
|
570
|
+
`## đ´ HTML Syntax & Compliance Issues (${htmlErrors.length})`,
|
|
571
|
+
];
|
|
572
|
+
if (htmlErrors.length === 0) {
|
|
573
|
+
report.push("*No HTML syntax or markup validation errors found! Excellent job.*");
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
report.push("| Line | Col | Severity | Message | Extract |");
|
|
577
|
+
report.push("| :---: | :---: | :--- | :--- | :--- |");
|
|
578
|
+
for (const err of htmlErrors) {
|
|
579
|
+
const extract = err.extract ? `\`${err.extract.replace(/\n/g, " ").trim()}\`` : "N/A";
|
|
580
|
+
report.push(`| ${err.lastLine || "N/A"} | ${err.lastColumn || "N/A"} | ${err.type === "error" ? "đ´ Error" : "â ī¸ Warning"} | ${err.message} | ${extract} |`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (cssFilePath) {
|
|
584
|
+
report.push(``, `---`, ``, `## đ¨ CSS Styling Issues (${cssErrors.length})`);
|
|
585
|
+
if (cssErrors.length === 0) {
|
|
586
|
+
report.push("*No CSS syntax errors found! Stylesheet is fully compliant.*");
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
report.push("| Line | Context | Message |");
|
|
590
|
+
report.push("| :---: | :--- | :--- |");
|
|
591
|
+
for (const err of cssErrors) {
|
|
592
|
+
report.push(`| ${err.line} | \`${err.context || "N/A"}\` | ${err.message} |`);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
report.push(``, `---`, ``, `## đ Technical SEO & Accessibility Issues (${seoIssues.length + schemaIssues.length})`);
|
|
597
|
+
const allSeo = [...seoIssues, ...schemaIssues];
|
|
598
|
+
if (allSeo.length === 0) {
|
|
599
|
+
report.push("*No technical SEO or schema issues found! Page is search-engine ready.*");
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
report.push("| Category | Severity | Message | Element Snippet |");
|
|
603
|
+
report.push("| :--- | :--- | :--- | :--- |");
|
|
604
|
+
for (const issue of allSeo) {
|
|
605
|
+
const severityLabel = issue.severity === "error" ? "đ´ Error" : (issue.severity === "warning" ? "â ī¸ Warning" : "âšī¸ Info");
|
|
606
|
+
const snippet = issue.element ? `\`${issue.element.trim()}\`` : "N/A";
|
|
607
|
+
report.push(`| ${issue.category} | ${severityLabel} | ${issue.message} | ${snippet} |`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
report.push(``, `---`, ``, `## đ Link Health Check (${linkStatuses.length} links checked)`);
|
|
611
|
+
if (linkStatuses.length === 0) {
|
|
612
|
+
report.push("*No hyperlinks found in the document.*");
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
report.push("| Link URL | Status Code | Health | Details |");
|
|
616
|
+
report.push("| :--- | :---: | :---: | :--- |");
|
|
617
|
+
for (const link of linkStatuses) {
|
|
618
|
+
report.push(`| [${link.url}](${link.url}) | ${link.status} | ${link.ok ? "â
Healthy" : "â Broken"} | ${link.message || "Accessible"} |`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
content: [
|
|
623
|
+
{
|
|
624
|
+
type: "text",
|
|
625
|
+
text: report.join("\n"),
|
|
626
|
+
},
|
|
627
|
+
],
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
case "screenshot_capture": {
|
|
631
|
+
const targetPath = String(args?.targetPath);
|
|
632
|
+
const outputDir = args?.outputDir ? String(args.outputDir) : ".mcp-validator/screenshots";
|
|
633
|
+
const viewports = args?.viewports;
|
|
634
|
+
const resolvedOutputDir = path.resolve(outputDir);
|
|
635
|
+
const results = await captureScreenshots(targetPath, resolvedOutputDir, viewports);
|
|
636
|
+
const responseText = [
|
|
637
|
+
`# đ¸ Viewport Screenshot Generation Complete`,
|
|
638
|
+
`Captured **${results.length}** viewport rendering(s):`,
|
|
639
|
+
``,
|
|
640
|
+
`| Viewport | Dimensions | Output Path |`,
|
|
641
|
+
`| :--- | :---: | :--- |`,
|
|
642
|
+
...results.map(r => `| **${r.viewportName}** | ${r.width}x${r.height} px | [${path.basename(r.outputPath)}](file:///${r.outputPath.replace(/\\/g, "/")}) |`),
|
|
643
|
+
``,
|
|
644
|
+
`> [!NOTE]`,
|
|
645
|
+
`> Screenshots have been saved successfully to [${outputDir}](file:///${resolvedOutputDir.replace(/\\/g, "/")})`
|
|
646
|
+
].join("\n");
|
|
647
|
+
return {
|
|
648
|
+
content: [
|
|
649
|
+
{
|
|
650
|
+
type: "text",
|
|
651
|
+
text: responseText,
|
|
652
|
+
},
|
|
653
|
+
],
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
default:
|
|
657
|
+
throw new Error(`Tool "${name}" not found.`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
return {
|
|
662
|
+
isError: true,
|
|
663
|
+
content: [
|
|
664
|
+
{
|
|
665
|
+
type: "text",
|
|
666
|
+
text: `Server error executing tool "${name}": ${error.message}`,
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
// Run server using stdio transport
|
|
673
|
+
async function run() {
|
|
674
|
+
const transport = new StdioServerTransport();
|
|
675
|
+
await server.connect(transport);
|
|
676
|
+
console.error("mcp-web-validator server successfully started on stdio");
|
|
677
|
+
}
|
|
678
|
+
run().catch((error) => {
|
|
679
|
+
console.error("Fatal error starting mcp-web-validator server:", error);
|
|
680
|
+
process.exit(1);
|
|
681
|
+
});
|
|
682
|
+
//# sourceMappingURL=index.js.map
|