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>
235 lines
6.2 KiB
TypeScript
235 lines
6.2 KiB
TypeScript
/**
|
|
* REST API client for Web Inspector backend.
|
|
* Uses native fetch (Node 20+) with AbortController timeout.
|
|
*/
|
|
|
|
const DEFAULT_TIMEOUT = 120_000; // 120 seconds
|
|
|
|
export class ApiClient {
|
|
constructor(private baseUrl: string) {
|
|
// Strip trailing slash
|
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
}
|
|
|
|
// ── Single-page inspection ──────────────────────────────
|
|
|
|
async startInspection(
|
|
url: string,
|
|
accessibilityStandard?: string,
|
|
): Promise<{ inspection_id: string; status: string }> {
|
|
const body: Record<string, unknown> = { url };
|
|
if (accessibilityStandard) body.accessibility_standard = accessibilityStandard;
|
|
return this.post("/api/inspections", body);
|
|
}
|
|
|
|
async getInspection(inspectionId: string): Promise<InspectionResult> {
|
|
return this.get(`/api/inspections/${inspectionId}`);
|
|
}
|
|
|
|
async getIssues(
|
|
inspectionId: string,
|
|
category?: string,
|
|
severity?: string,
|
|
): Promise<IssuesResult> {
|
|
const params = new URLSearchParams();
|
|
if (category) params.set("category", category);
|
|
if (severity) params.set("severity", severity);
|
|
const qs = params.toString();
|
|
return this.get(`/api/inspections/${inspectionId}/issues${qs ? `?${qs}` : ""}`);
|
|
}
|
|
|
|
async getInspections(
|
|
url?: string,
|
|
limit = 10,
|
|
): Promise<InspectionsListResult> {
|
|
const params = new URLSearchParams({ limit: String(limit) });
|
|
if (url) params.set("url", url);
|
|
return this.get(`/api/inspections?${params.toString()}`);
|
|
}
|
|
|
|
// ── Site-wide inspection ────────────────────────────────
|
|
|
|
async startSiteInspection(
|
|
url: string,
|
|
maxPages?: number,
|
|
maxDepth?: number,
|
|
accessibilityStandard?: string,
|
|
): Promise<{ site_inspection_id: string; status: string }> {
|
|
const body: Record<string, unknown> = { url };
|
|
if (maxPages !== undefined) body.max_pages = maxPages;
|
|
if (maxDepth !== undefined) body.max_depth = maxDepth;
|
|
if (accessibilityStandard) body.accessibility_standard = accessibilityStandard;
|
|
return this.post("/api/site-inspections", body);
|
|
}
|
|
|
|
async getSiteInspection(siteInspectionId: string): Promise<SiteInspectionResult> {
|
|
return this.get(`/api/site-inspections/${siteInspectionId}`);
|
|
}
|
|
|
|
async getSiteInspections(limit = 10): Promise<SiteInspectionsListResult> {
|
|
return this.get(`/api/site-inspections?limit=${limit}`);
|
|
}
|
|
|
|
// ── HTTP helpers ────────────────────────────────────────
|
|
|
|
private async get<T>(path: string): Promise<T> {
|
|
return this.request("GET", path);
|
|
}
|
|
|
|
private async post<T>(path: string, body: unknown): Promise<T> {
|
|
return this.request("POST", path, body);
|
|
}
|
|
|
|
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
|
|
try {
|
|
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
method,
|
|
headers: body ? { "Content-Type": "application/json" } : undefined,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
signal: controller.signal,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const text = await res.text().catch(() => "");
|
|
throw new Error(`API ${method} ${path} failed: ${res.status} ${text}`);
|
|
}
|
|
|
|
return (await res.json()) as T;
|
|
} finally {
|
|
clearTimeout(timer);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Response types ──────────────────────────────────────
|
|
|
|
export interface Issue {
|
|
code: string;
|
|
category: string;
|
|
severity: "critical" | "major" | "minor" | "info";
|
|
message: string;
|
|
element?: string;
|
|
line?: number;
|
|
suggestion: string;
|
|
wcag_criterion?: string;
|
|
kwcag_criterion?: string;
|
|
kwcag_name?: string;
|
|
kwcag_principle?: string;
|
|
}
|
|
|
|
export interface CategoryResult {
|
|
score: number;
|
|
grade: string;
|
|
total_issues: number;
|
|
critical: number;
|
|
major: number;
|
|
minor: number;
|
|
info: number;
|
|
issues: Issue[];
|
|
wcag_level?: string;
|
|
}
|
|
|
|
export interface InspectionResult {
|
|
inspection_id: string;
|
|
url: string;
|
|
status: "running" | "completed" | "error";
|
|
created_at: string;
|
|
completed_at?: string;
|
|
duration_seconds?: number;
|
|
overall_score: number;
|
|
grade: string;
|
|
accessibility_standard?: string;
|
|
categories: {
|
|
html_css: CategoryResult;
|
|
accessibility: CategoryResult;
|
|
seo: CategoryResult;
|
|
performance_security: CategoryResult;
|
|
};
|
|
summary: {
|
|
total_issues: number;
|
|
critical: number;
|
|
major: number;
|
|
minor: number;
|
|
info: number;
|
|
};
|
|
}
|
|
|
|
export interface IssuesResult {
|
|
inspection_id: string;
|
|
total: number;
|
|
filters: Record<string, string | null>;
|
|
issues: Issue[];
|
|
}
|
|
|
|
export interface InspectionsListResult {
|
|
items: Array<{
|
|
inspection_id: string;
|
|
url: string;
|
|
created_at: string;
|
|
overall_score: number;
|
|
grade: string;
|
|
total_issues: number;
|
|
}>;
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
total_pages: number;
|
|
}
|
|
|
|
export interface SiteInspectionResult {
|
|
site_inspection_id: string;
|
|
root_url: string;
|
|
domain: string;
|
|
status: "crawling" | "inspecting" | "completed" | "error";
|
|
created_at: string;
|
|
completed_at?: string;
|
|
config: {
|
|
max_pages: number;
|
|
max_depth: number;
|
|
concurrency: number;
|
|
accessibility_standard: string;
|
|
};
|
|
discovered_pages: Array<{
|
|
url: string;
|
|
depth: number;
|
|
parent_url?: string;
|
|
inspection_id?: string;
|
|
status: string;
|
|
title?: string;
|
|
overall_score?: number;
|
|
grade?: string;
|
|
}>;
|
|
aggregate_scores?: {
|
|
overall_score: number;
|
|
grade: string;
|
|
html_css: number;
|
|
accessibility: number;
|
|
seo: number;
|
|
performance_security: number;
|
|
total_issues: number;
|
|
pages_inspected: number;
|
|
pages_total: number;
|
|
};
|
|
}
|
|
|
|
export interface SiteInspectionsListResult {
|
|
items: Array<{
|
|
site_inspection_id: string;
|
|
root_url: string;
|
|
domain: string;
|
|
status: string;
|
|
created_at: string;
|
|
pages_total: number;
|
|
pages_inspected: number;
|
|
overall_score?: number;
|
|
grade?: string;
|
|
}>;
|
|
total: number;
|
|
page: number;
|
|
limit: number;
|
|
total_pages: number;
|
|
}
|