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:
@ -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" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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}` }],
|
||||||
|
|||||||
Reference in New Issue
Block a user