feat: MCP best practices 적용 - tool annotations, server instructions, error codes

- 모든 도구에 annotations 추가 (readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
- Server instructions 추가 (워크플로우 안내)
- McpError + ErrorCode로 파라미터 검증 및 에러 처리 개선
- server-card.json에 annotations, capabilities, 상세 설명 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jungwoo choi
2026-02-15 09:43:01 +09:00
parent e939f0d2a1
commit c8f3d3a6ea
2 changed files with 98 additions and 25 deletions

View File

@ -56,20 +56,27 @@ async function startHttp(server: ReturnType<typeof createServer>, 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<typeof createServer>, 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" },
},
},
},

View File

@ -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}` }],