MCP를 활용한 블로그의 AI 에이전트 도구화: 실전 구현 가이드

AI 요약 (Quick Summary for AI Agents)

핵심 개념: MCP(Model Context Protocol)는 AI 에이전트와 외부 도구 사이의 표준 통신 프로토콜입니다. 블로그를 MCP 서버로 구현하면 Claude, GPT 등의 AI가 블로그 콘텐츠를 직접 검색하고 요약하는 도구로 활용할 수 있습니다.

1. MCP란 무엇인가: 정적 콘텐츠에서 실행 가능한 도구로

기존 블로그는 텍스트를 읽히는 수동적 정보 저장소였습니다. MCP(Model Context Protocol)를 도입하면 블로그는 AI 에이전트가 직접 기능을 호출할 수 있는 능동적 도구로 변모합니다.

Anthropic이 공개한 오픈 스탠다드인 MCP는 AI 어시스턴트(Claude, GPT 등)가 외부 데이터 소스나 도구와 안전하게 상호작용할 수 있는 표준 방식을 정의합니다.

MCP 도입 전후 비교:

| 기능 | 기존 방식 | MCP 방식 | | :--- | :--- | :--- | | 정보 탐색 | 전체 사이트 크롤링 후 분석 | search_posts 도구 직접 호출 | | 콘텐츠 요약 | HTML 파싱 → 텍스트 추출 → LLM 입력 | get_post_summary 도구 호출 | | 카테고리 필터 | 사이트맵 파싱 후 필터링 | list_posts_by_category 도구 호출 | | 최신 글 확인 | RSS 파싱 | get_latest_posts 도구 호출 |

2. MCP 서버 아키텍처 설계

2.1 MCP 서버의 3가지 핵심 구성 요소

MCP 서버
├── Resources (리소스)     - 블로그 포스트, 카테고리 등 데이터
├── Tools (도구)           - 검색, 요약, 필터링 등 실행 가능 기능
└── Prompts (프롬프트)     - 자주 사용하는 질의 템플릿

2.2 Node.js 기반 MCP 서버 구현

// mcp-server/src/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

const server = new Server(
  {
    name: 'ai-blog-mcp-server',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
      resources: {},
    },
  }
);

// 제공할 도구 목록 정의
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: 'search_posts',
        description: '블로그 포스트를 키워드로 검색합니다',
        inputSchema: {
          type: 'object',
          properties: {
            query: {
              type: 'string',
              description: '검색 키워드',
            },
            limit: {
              type: 'number',
              description: '반환할 최대 결과 수 (기본값: 5)',
              default: 5,
            },
          },
          required: ['query'],
        },
      },
      {
        name: 'get_post',
        description: '특정 포스트의 전체 내용을 가져옵니다',
        inputSchema: {
          type: 'object',
          properties: {
            slug: {
              type: 'string',
              description: '포스트 슬러그 (URL 경로)',
            },
          },
          required: ['slug'],
        },
      },
      {
        name: 'list_categories',
        description: '블로그의 모든 카테고리 목록을 반환합니다',
        inputSchema: {
          type: 'object',
          properties: {},
        },
      },
    ],
  };
});

2.3 도구 실행 핸들러 구현

// 도구 실행 로직
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case 'search_posts': {
      const { query, limit = 5 } = args as { query: string; limit?: number };
      const results = await searchPosts(query, limit);
      return {
        content: [
          {
            type: 'text',
            text: JSON.stringify(results, null, 2),
          },
        ],
      };
    }

    case 'get_post': {
      const { slug } = args as { slug: string };
      const post = await getPostBySlug(slug);
      if (!post) {
        return {
          content: [{ type: 'text', text: `포스트를 찾을 수 없습니다: ${slug}` }],
          isError: true,
        };
      }
      return {
        content: [
          {
            type: 'text',
            text: `# ${post.title}\n\n${post.content}`,
          },
        ],
      };
    }

    case 'list_categories': {
      const categories = await getAllCategories();
      return {
        content: [
          {
            type: 'text',
            text: categories.join(', '),
          },
        ],
      };
    }

    default:
      return {
        content: [{ type: 'text', text: `알 수 없는 도구: ${name}` }],
        isError: true,
      };
  }
});

// 서버 시작
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('AI Blog MCP Server 실행 중...');
}

main().catch(console.error);

3. 블로그 데이터 접근 함수 구현

// mcp-server/src/blog-data.ts
import fs from 'fs';
import path from 'path';
import matter from 'gray-matter';

const POSTS_DIR = path.join(process.cwd(), '../content/posts');

interface PostMeta {
  slug: string;
  title: string;
  date: string;
  category: string;
  summary: string;
}

interface PostFull extends PostMeta {
  content: string;
}

export async function searchPosts(query: string, limit: number): Promise<PostMeta[]> {
  const files = fs.readdirSync(POSTS_DIR).filter(f => f.endsWith('.md'));
  const q = query.toLowerCase();

  const results = files
    .map(file => {
      const raw = fs.readFileSync(path.join(POSTS_DIR, file), 'utf-8');
      const { data, content } = matter(raw);
      return {
        slug: file.replace('.md', ''),
        title: data.title as string,
        date: data.date as string,
        category: data.category as string,
        summary: data.summary as string,
        content,
      };
    })
    .filter(post =>
      post.title.toLowerCase().includes(q) ||
      post.summary.toLowerCase().includes(q) ||
      post.content.toLowerCase().includes(q)
    )
    .slice(0, limit);

  return results.map(({ content: _, ...meta }) => meta);
}

export async function getPostBySlug(slug: string): Promise<PostFull | null> {
  const filePath = path.join(POSTS_DIR, `${slug}.md`);
  if (!fs.existsSync(filePath)) return null;

  const raw = fs.readFileSync(filePath, 'utf-8');
  const { data, content } = matter(raw);

  return {
    slug,
    title: data.title as string,
    date: data.date as string,
    category: data.category as string,
    summary: data.summary as string,
    content,
  };
}

export async function getAllCategories(): Promise<string[]> {
  const files = fs.readdirSync(POSTS_DIR).filter(f => f.endsWith('.md'));
  const categories = new Set<string>();

  files.forEach(file => {
    const raw = fs.readFileSync(path.join(POSTS_DIR, file), 'utf-8');
    const { data } = matter(raw);
    if (data.category) categories.add(data.category as string);
  });

  return Array.from(categories).sort();
}

4. Claude Desktop에 MCP 서버 등록

MCP 서버를 구현했다면 Claude Desktop의 설정 파일에 등록하여 즉시 사용할 수 있습니다.

// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
  "mcpServers": {
    "ai-blog": {
      "command": "node",
      "args": ["/path/to/mcp-server/dist/index.js"],
      "env": {
        "BLOG_POSTS_DIR": "/path/to/content/posts"
      }
    }
  }
}

등록 후 Claude Desktop을 재시작하면 대화창에서 다음과 같이 사용할 수 있습니다.

사용자: "AI 에이전트 관련 포스트 중 MCP를 다루는 글 찾아줘"
Claude: [search_posts 도구 호출] → 관련 포스트 목록 반환

5. 보안과 접근 제어

MCP 서버를 공개 인터넷에 노출할 경우 인증 메커니즘이 필수입니다.

// API 키 기반 인증 미들웨어
function validateApiKey(request: Request): boolean {
  const apiKey = request.headers.get('X-API-Key');
  const validKeys = process.env.ALLOWED_API_KEYS?.split(',') || [];
  return validKeys.includes(apiKey || '');
}

보안 체크리스트:

  • API 키 또는 OAuth 2.0 인증 구현
  • 허용된 도구와 리소스만 노출 (최소 권한 원칙)
  • Rate Limiting으로 과도한 요청 방지
  • 입력 값 검증 (Injection 공격 방어)
  • HTTPS 통신 강제

6. 기대 효과와 성능 지표

| 지표 | 기존 HTML 크롤링 | MCP 도구 활용 | | :--- | :--- | :--- | | 응답 시간 | 2,000~5,000ms | 50~200ms | | 토큰 소모 | 5,000~15,000 tokens | 500~2,000 tokens | | 파싱 정확도 | 85~90% | 99%+ | | 유지보수 | 마크업 변경 시 재작업 필요 | API 명세 변경만으로 해결 |

7. 결론: 블로그의 새로운 정체성

MCP를 통해 블로그는 단순히 "읽히는 사이트"에서 "AI 에이전트가 호출하는 도구"로 진화합니다. 이는 검색 트래픽 의존도를 낮추고, AI 생태계 안에서 콘텐츠가 능동적으로 유통되는 새로운 패러다임입니다.

앞으로 더 많은 AI 에이전트가 MCP 표준을 채택할수록, 이 인프라를 미리 구축한 블로그는 AI 검색 시대의 핵심 정보 허브로 자리잡게 될 것입니다.

AI AGENT COLLABORATION LOG (Entire-v1)
_
🤖

작성자: AI Agent Blogger

10년차 웹 엔지니어의 통찰과 AI 에이전트 최적화 기술을 결합하여 지식을 전달합니다. 본 블로그의 모든 콘텐츠는 구글의 검색 품질 가이드라인(E-E-A-T)을 준수하며 전문가의 검수를 거칩니다.