Node.js + TypeScript MCP 서버 구현: - 5개 도구: inspect_page, inspect_site, get_inspection, get_issues, get_history - 듀얼 트랜스포트: stdio (Claude Desktop) + Streamable HTTP (Docker/원격) - i18n 지원 (영어/한국어) - Docker 통합 (port 3100) + Nginx /mcp 프록시 - Smithery 레지스트리 배포 설정 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
91 lines
3.7 KiB
JavaScript
91 lines
3.7 KiB
JavaScript
import { t } from "../i18n/index.js";
|
|
const POLL_INTERVAL = 2_000; // 2 seconds
|
|
const MAX_POLLS = 60; // 120 seconds total
|
|
export async function inspectPage(client, url, lang, standard) {
|
|
// 1. Start inspection
|
|
const { inspection_id } = await client.startInspection(url, standard);
|
|
// 2. Poll until completion
|
|
let result = null;
|
|
for (let i = 0; i < MAX_POLLS; i++) {
|
|
await sleep(POLL_INTERVAL);
|
|
const data = await client.getInspection(inspection_id);
|
|
if (data.status === "completed") {
|
|
result = data;
|
|
break;
|
|
}
|
|
if (data.status === "error") {
|
|
throw new Error(`Inspection failed for ${url}`);
|
|
}
|
|
}
|
|
// 3. Timeout — return partial info
|
|
if (!result) {
|
|
return t("inspect_page.timeout", lang, { url, inspectionId: inspection_id });
|
|
}
|
|
// 4. Format result as markdown
|
|
return formatResult(result, lang);
|
|
}
|
|
function formatResult(r, lang) {
|
|
const lines = [];
|
|
const duration = r.duration_seconds
|
|
? `${r.duration_seconds.toFixed(1)}s`
|
|
: "—";
|
|
lines.push(`# ${t("result.title", lang)}`);
|
|
lines.push(`**URL**: ${r.url}`);
|
|
lines.push(`**${t("result.overall_score", lang)}**: ${r.overall_score}/100 (${r.grade})`);
|
|
lines.push(`**${t("result.duration", lang)}**: ${duration}`);
|
|
if (r.accessibility_standard) {
|
|
lines.push(`**${t("result.standard", lang)}**: ${r.accessibility_standard}`);
|
|
}
|
|
lines.push("");
|
|
// Category scores table
|
|
lines.push(`## ${t("result.category_scores", lang)}`);
|
|
lines.push(`| ${t("result.category", lang)} | ${t("result.score", lang)} | ${t("result.grade", lang)} | ${t("result.issues", lang)} |`);
|
|
lines.push("|---|---|---|---|");
|
|
const catNames = [
|
|
["html_css", "HTML/CSS"],
|
|
["accessibility", "Accessibility"],
|
|
["seo", "SEO"],
|
|
["performance_security", "Performance/Security"],
|
|
];
|
|
for (const [key, label] of catNames) {
|
|
const cat = r.categories[key];
|
|
if (!cat)
|
|
continue;
|
|
const issueStr = `${cat.total_issues} (C:${cat.critical} M:${cat.major} m:${cat.minor} i:${cat.info})`;
|
|
lines.push(`| ${label} | ${cat.score} | ${cat.grade} | ${issueStr} |`);
|
|
}
|
|
lines.push("");
|
|
// Issue summary
|
|
const s = r.summary;
|
|
lines.push(`## ${t("result.issue_summary", lang)}`);
|
|
lines.push(`**${t("result.total", lang)}**: ${s.total_issues} (Critical: ${s.critical}, Major: ${s.major}, Minor: ${s.minor}, Info: ${s.info})`);
|
|
lines.push("");
|
|
// Top issues (critical + major, max 5)
|
|
const topIssues = collectTopIssues(r, 5);
|
|
if (topIssues.length > 0) {
|
|
lines.push(`## ${t("result.top_issues", lang)}`);
|
|
for (const issue of topIssues) {
|
|
const sevLabel = issue.severity.toUpperCase();
|
|
lines.push(`### [${sevLabel}] ${issue.code}`);
|
|
lines.push(`- **${t("result.message", lang)}**: ${issue.message}`);
|
|
if (issue.element) {
|
|
lines.push(`- **${t("result.element", lang)}**: \`${issue.element}\``);
|
|
}
|
|
lines.push(`- **${t("result.suggestion", lang)}**: ${issue.suggestion}`);
|
|
lines.push("");
|
|
}
|
|
}
|
|
// Hint for more issues
|
|
lines.push(`> ${t("result.more_issues_hint", lang, { inspectionId: r.inspection_id })}`);
|
|
return lines.join("\n");
|
|
}
|
|
function collectTopIssues(r, max) {
|
|
const all = Object.values(r.categories).flatMap((cat) => cat.issues);
|
|
return all
|
|
.filter((i) => i.severity === "critical" || i.severity === "major")
|
|
.slice(0, max);
|
|
}
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
//# sourceMappingURL=inspect-page.js.map
|