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({ res.json({
serverInfo: { name: "web-inspector-mcp", version: "1.0.0" }, serverInfo: { name: "web-inspector-mcp", version: "1.0.0" },
authentication: { required: false }, authentication: { required: false },
capabilities: {
tools: true,
resources: false,
prompts: false,
},
tools: [ tools: [
{ {
name: "inspect_page", name: "inspect_page",
description: 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: { inputSchema: {
type: "object", type: "object",
properties: { 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: { accessibility_standard: {
type: "string", 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"], 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"], required: ["url"],
}, },
@ -77,52 +84,64 @@ async function startHttp(server: ReturnType<typeof createServer>, port: number)
{ {
name: "inspect_site", name: "inspect_site",
description: 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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
url: { type: "string" }, url: { type: "string", description: "Root URL of the site to crawl and inspect" },
max_pages: { type: "number" }, max_pages: { type: "number", description: "Maximum number of pages to crawl (default: 20, max: 500, 0: unlimited)" },
max_depth: { type: "number" }, max_depth: { type: "number", description: "Maximum link depth to crawl (default: 2, max: 3)" },
language: { type: "string", enum: ["en", "ko"] }, 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"], required: ["url"],
}, },
}, },
{ {
name: "get_inspection", 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: { inputSchema: {
type: "object", 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"], required: ["id"],
}, },
}, },
{ {
name: "get_issues", name: "get_issues",
description: 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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
id: { type: "string" }, id: { type: "string", description: "The inspection_id or site_inspection_id to get issues for" },
category: { type: "string" }, category: { type: "string", enum: ["html_css", "accessibility", "seo", "performance_security"], description: "Filter by category" },
severity: { type: "string" }, severity: { type: "string", enum: ["critical", "major", "minor", "info"], description: "Filter by severity" },
page_url: { type: "string" }, page_url: { type: "string", description: "For site inspections: filter issues by page URL substring" },
language: { type: "string", enum: ["en", "ko"] }, language: { type: "string", enum: ["en", "ko"], description: "Response language: 'en' for English, 'ko' for Korean" },
}, },
required: ["id"], required: ["id"],
}, },
}, },
{ {
name: "get_history", 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: { inputSchema: {
type: "object", type: "object",
properties: { properties: {
url: { type: "string" }, url: { type: "string", description: "Optional URL substring to filter results" },
limit: { type: "number" }, limit: { type: "number", description: "Number of results to return (default: 10)" },
language: { type: "string", enum: ["en", "ko"] }, 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 { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { import {
CallToolRequestSchema, CallToolRequestSchema,
ErrorCode,
ListToolsRequestSchema, ListToolsRequestSchema,
McpError,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { ApiClient } from "./api-client.js"; import { ApiClient } from "./api-client.js";
import { t } from "./i18n/index.js"; import { t } from "./i18n/index.js";
@ -27,7 +29,18 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server {
const server = new Server( const server = new Server(
{ name: "web-inspector-mcp", version: "1.0.0" }, { 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 ────────────────────────────────────────── // ── List tools ──────────────────────────────────────────
@ -37,6 +50,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server {
{ {
name: "inspect_page", name: "inspect_page",
description: t("inspect_page.description", defaultLang), description: t("inspect_page.description", defaultLang),
annotations: {
title: "Inspect Page",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
},
inputSchema: { inputSchema: {
type: "object" as const, type: "object" as const,
properties: { properties: {
@ -61,6 +81,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server {
{ {
name: "inspect_site", name: "inspect_site",
description: t("inspect_site.description", defaultLang), description: t("inspect_site.description", defaultLang),
annotations: {
title: "Inspect Site",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true,
},
inputSchema: { inputSchema: {
type: "object" as const, type: "object" as const,
properties: { properties: {
@ -93,6 +120,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server {
{ {
name: "get_inspection", name: "get_inspection",
description: t("get_inspection.description", defaultLang), description: t("get_inspection.description", defaultLang),
annotations: {
title: "Get Inspection",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
inputSchema: { inputSchema: {
type: "object" as const, type: "object" as const,
properties: { properties: {
@ -112,6 +146,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server {
{ {
name: "get_issues", name: "get_issues",
description: t("get_issues.description", defaultLang), description: t("get_issues.description", defaultLang),
annotations: {
title: "Get Issues",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
inputSchema: { inputSchema: {
type: "object" as const, type: "object" as const,
properties: { properties: {
@ -150,6 +191,13 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server {
{ {
name: "get_history", name: "get_history",
description: t("get_history.description", defaultLang), description: t("get_history.description", defaultLang),
annotations: {
title: "Get History",
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
},
inputSchema: { inputSchema: {
type: "object" as const, type: "object" as const,
properties: { properties: {
@ -178,6 +226,14 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server {
const { name, arguments: args = {} } = request.params; const { name, arguments: args = {} } = request.params;
const lang: Lang = (args.language as Lang) || defaultLang; 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 { try {
let text: string; let text: string;
@ -227,14 +283,12 @@ export function createServer(apiUrl: string, defaultLang: Lang): Server {
break; break;
default: default:
return { throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
content: [{ type: "text", text: `Unknown tool: ${name}` }],
isError: true,
};
} }
return { content: [{ type: "text", text }] }; return { content: [{ type: "text", text }] };
} catch (error) { } catch (error) {
if (error instanceof McpError) throw error;
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
return { return {
content: [{ type: "text", text: `Error: ${message}` }], content: [{ type: "text", text: `Error: ${message}` }],