From c8f3d3a6ea9367dbcda843c3d54602c24c2a24a2 Mon Sep 17 00:00:00 2001 From: jungwoo choi Date: Sun, 15 Feb 2026 09:43:01 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20MCP=20best=20practices=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20-=20tool=20annotations,=20server=20instructions,=20?= =?UTF-8?q?error=20codes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 모든 도구에 annotations 추가 (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) - Server instructions 추가 (워크플로우 안내) - McpError + ErrorCode로 파라미터 검증 및 에러 처리 개선 - server-card.json에 annotations, capabilities, 상세 설명 추가 Co-Authored-By: Claude Opus 4.6 --- mcp/src/index.ts | 59 ++++++++++++++++++++++++++++--------------- mcp/src/server.ts | 64 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 98 insertions(+), 25 deletions(-) diff --git a/mcp/src/index.ts b/mcp/src/index.ts index b98f332..5a8419d 100644 --- a/mcp/src/index.ts +++ b/mcp/src/index.ts @@ -56,20 +56,27 @@ async function startHttp(server: ReturnType, port: number) res.json({ serverInfo: { name: "web-inspector-mcp", version: "1.0.0" }, authentication: { required: false }, + capabilities: { + tools: true, + resources: false, + prompts: false, + }, tools: [ { name: "inspect_page", description: - "Inspect a single web page for HTML/CSS quality, accessibility (WCAG/KWCAG), SEO, and performance/security. Returns scores, grades, and top issues.", + "Inspect a single web page for HTML/CSS quality, accessibility (WCAG/KWCAG), SEO, and performance/security. Returns scores, grades, and top issues. Takes 10-30 seconds.", + annotations: { title: "Inspect Page", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }, inputSchema: { type: "object", properties: { - url: { type: "string", description: "URL to inspect" }, + url: { type: "string", description: "The URL of the web page to inspect (must start with http:// or https://)" }, accessibility_standard: { type: "string", enum: ["wcag_2.0_a", "wcag_2.0_aa", "wcag_2.1_aa", "wcag_2.2_aa", "kwcag_2.1", "kwcag_2.2"], + description: "Accessibility standard to check against (default: wcag_2.1_aa)", }, - language: { type: "string", enum: ["en", "ko"] }, + language: { type: "string", enum: ["en", "ko"], description: "Response language: 'en' for English, 'ko' for Korean" }, }, required: ["url"], }, @@ -77,52 +84,64 @@ async function startHttp(server: ReturnType, port: number) { name: "inspect_site", description: - "Start a site-wide crawl and inspection. Returns a site_inspection_id for tracking.", + "Start a site-wide crawl and inspection. Crawls links from the root URL and inspects each page. Returns a site_inspection_id for tracking. Use get_inspection to check results later.", + annotations: { title: "Inspect Site", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true }, inputSchema: { type: "object", properties: { - url: { type: "string" }, - max_pages: { type: "number" }, - max_depth: { type: "number" }, - language: { type: "string", enum: ["en", "ko"] }, + url: { type: "string", description: "Root URL of the site to crawl and inspect" }, + max_pages: { type: "number", description: "Maximum number of pages to crawl (default: 20, max: 500, 0: unlimited)" }, + max_depth: { type: "number", description: "Maximum link depth to crawl (default: 2, max: 3)" }, + accessibility_standard: { + type: "string", + enum: ["wcag_2.0_a", "wcag_2.0_aa", "wcag_2.1_aa", "wcag_2.2_aa", "kwcag_2.1", "kwcag_2.2"], + description: "Accessibility standard to check against (default: wcag_2.1_aa)", + }, + language: { type: "string", enum: ["en", "ko"], description: "Response language: 'en' for English, 'ko' for Korean" }, }, required: ["url"], }, }, { name: "get_inspection", - description: "Get detailed inspection results by ID.", + description: "Get detailed inspection results by inspection ID. Works for both single-page and site inspections.", + annotations: { title: "Get Inspection", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, inputSchema: { type: "object", - properties: { id: { type: "string" }, language: { type: "string", enum: ["en", "ko"] } }, + properties: { + id: { type: "string", description: "The inspection_id or site_inspection_id to retrieve" }, + language: { type: "string", enum: ["en", "ko"], description: "Response language: 'en' for English, 'ko' for Korean" }, + }, required: ["id"], }, }, { name: "get_issues", description: - "Get filtered issues for single-page or site inspections.", + "Get a filtered list of issues found during an inspection. Works with both single-page inspection_id and site_inspection_id. For site inspections, aggregates issues from all pages.", + annotations: { title: "Get Issues", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, inputSchema: { type: "object", properties: { - id: { type: "string" }, - category: { type: "string" }, - severity: { type: "string" }, - page_url: { type: "string" }, - language: { type: "string", enum: ["en", "ko"] }, + id: { type: "string", description: "The inspection_id or site_inspection_id to get issues for" }, + category: { type: "string", enum: ["html_css", "accessibility", "seo", "performance_security"], description: "Filter by category" }, + severity: { type: "string", enum: ["critical", "major", "minor", "info"], description: "Filter by severity" }, + page_url: { type: "string", description: "For site inspections: filter issues by page URL substring" }, + language: { type: "string", enum: ["en", "ko"], description: "Response language: 'en' for English, 'ko' for Korean" }, }, required: ["id"], }, }, { name: "get_history", - description: "List recent inspection history.", + description: "List recent inspection history. Optionally filter by URL substring.", + annotations: { title: "Get History", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false }, inputSchema: { type: "object", properties: { - url: { type: "string" }, - limit: { type: "number" }, - language: { type: "string", enum: ["en", "ko"] }, + url: { type: "string", description: "Optional URL substring to filter results" }, + limit: { type: "number", description: "Number of results to return (default: 10)" }, + language: { type: "string", enum: ["en", "ko"], description: "Response language: 'en' for English, 'ko' for Korean" }, }, }, }, diff --git a/mcp/src/server.ts b/mcp/src/server.ts index cab804e..3007639 100644 --- a/mcp/src/server.ts +++ b/mcp/src/server.ts @@ -1,7 +1,9 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, + ErrorCode, ListToolsRequestSchema, + McpError, } from "@modelcontextprotocol/sdk/types.js"; import { ApiClient } from "./api-client.js"; import { t } from "./i18n/index.js"; @@ -27,7 +29,18 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server { const server = new Server( { name: "web-inspector-mcp", version: "1.0.0" }, - { capabilities: { tools: {} } }, + { + capabilities: { tools: {} }, + instructions: + "Web Inspector MCP server analyzes web pages and websites for HTML/CSS quality, accessibility (WCAG 2.x / KWCAG 2.x), SEO, and performance/security.\n\n" + + "Typical workflow:\n" + + "1. Use `inspect_page` to audit a single URL (takes 10-30s, returns scores and top issues).\n" + + "2. Use `inspect_site` to start a site-wide crawl (returns an ID immediately; the crawl runs in background).\n" + + "3. Use `get_inspection` to retrieve detailed results by ID.\n" + + "4. Use `get_issues` to filter and drill into specific issues by category or severity.\n" + + "5. Use `get_history` to review past inspections.\n\n" + + "All tools accept a `language` parameter ('en' or 'ko') to control the response language.", + }, ); // ── List tools ────────────────────────────────────────── @@ -37,6 +50,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server { { name: "inspect_page", description: t("inspect_page.description", defaultLang), + annotations: { + title: "Inspect Page", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, + }, inputSchema: { type: "object" as const, properties: { @@ -61,6 +81,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server { { name: "inspect_site", description: t("inspect_site.description", defaultLang), + annotations: { + title: "Inspect Site", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, + }, inputSchema: { type: "object" as const, properties: { @@ -93,6 +120,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server { { name: "get_inspection", description: t("get_inspection.description", defaultLang), + annotations: { + title: "Get Inspection", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, inputSchema: { type: "object" as const, properties: { @@ -112,6 +146,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server { { name: "get_issues", description: t("get_issues.description", defaultLang), + annotations: { + title: "Get Issues", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, inputSchema: { type: "object" as const, properties: { @@ -150,6 +191,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server { { name: "get_history", description: t("get_history.description", defaultLang), + annotations: { + title: "Get History", + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, + }, inputSchema: { type: "object" as const, properties: { @@ -178,6 +226,14 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server { const { name, arguments: args = {} } = request.params; const lang: Lang = (args.language as Lang) || defaultLang; + // Validate required params + if ((name === "inspect_page" || name === "inspect_site") && !args.url) { + throw new McpError(ErrorCode.InvalidParams, "Missing required parameter: url"); + } + if ((name === "get_inspection" || name === "get_issues") && !args.id) { + throw new McpError(ErrorCode.InvalidParams, "Missing required parameter: id"); + } + try { let text: string; @@ -227,14 +283,12 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server { break; default: - return { - content: [{ type: "text", text: `Unknown tool: ${name}` }], - isError: true, - }; + throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } return { content: [{ type: "text", text }] }; } catch (error) { + if (error instanceof McpError) throw error; const message = error instanceof Error ? error.message : String(error); return { content: [{ type: "text", text: `Error: ${message}` }],