feat: MCP 서버 추가 — AI 에이전트용 웹 검사 도구

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>
This commit is contained in:
jungwoo choi
2026-02-14 15:44:35 +09:00
parent bffce65aca
commit 69e0f80282
51 changed files with 4327 additions and 0 deletions

83
mcp/dist/api-client.js vendored Normal file
View File

@ -0,0 +1,83 @@
/**
* 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 {
baseUrl;
constructor(baseUrl) {
this.baseUrl = baseUrl;
// Strip trailing slash
this.baseUrl = baseUrl.replace(/\/+$/, "");
}
// ── Single-page inspection ──────────────────────────────
async startInspection(url, accessibilityStandard) {
const body = { url };
if (accessibilityStandard)
body.accessibility_standard = accessibilityStandard;
return this.post("/api/inspections", body);
}
async getInspection(inspectionId) {
return this.get(`/api/inspections/${inspectionId}`);
}
async getIssues(inspectionId, category, severity) {
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, limit = 10) {
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, maxPages, maxDepth, accessibilityStandard) {
const body = { 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) {
return this.get(`/api/site-inspections/${siteInspectionId}`);
}
async getSiteInspections(limit = 10) {
return this.get(`/api/site-inspections?limit=${limit}`);
}
// ── HTTP helpers ────────────────────────────────────────
async get(path) {
return this.request("GET", path);
}
async post(path, body) {
return this.request("POST", path, body);
}
async request(method, path, body) {
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());
}
finally {
clearTimeout(timer);
}
}
}
//# sourceMappingURL=api-client.js.map