본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 10장: 실전 프로젝트 - 풀스택 MCP 시스템 구축
2026년 2월 8일·AI / ML·

10장: 실전 프로젝트 - 풀스택 MCP 시스템 구축

여러 MCP 서버를 조합하고 클라이언트 애플리케이션을 구축하여, 프로덕션 수준의 풀스택 MCP 시스템을 완성하는 실전 프로젝트입니다.

22분2,187자8개 섹션
mcptypescriptpython
공유
mcp-guide10 / 10
12345678910
이전9장: 보안, 인증, 권한 관리

프로젝트 개요

이 장에서는 시리즈 전체에서 학습한 내용을 종합하여 DevOps 어시스턴트를 구축합니다. 이 시스템은 개발팀이 자연어로 인프라를 조회하고, 배포 상태를 확인하며, 로그를 분석할 수 있는 AI 기반 도구입니다.

시스템 구성

세 개의 MCP 서버를 구축하고, 하나의 CLI 클라이언트 애플리케이션으로 통합합니다.

  1. Git 서버: 커밋 히스토리, 브랜치, 변경 파일 조회
  2. 로그 분석 서버: 로그 파일 검색, 에러 집계, 패턴 분석
  3. 시스템 모니터링 서버: CPU, 메모리, 디스크 사용량 조회

프로젝트 구조

devops-assistant/
  servers/
    git-server/
      src/index.ts
      package.json
    log-server/
      src/index.ts
      package.json
    system-server/
      src/index.ts
      package.json
  client/
    src/
      index.ts
      mcp-manager.ts
      agent.ts
    package.json
  package.json          # 워크스페이스 루트

서버 1: Git 서버

Git 저장소의 정보를 조회하는 MCP 서버입니다. 읽기 전용 작업만 제공하여 안전성을 보장합니다.

servers/git-server/src/index.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { execFile } from "child_process";
import { promisify } from "util";
 
const execFileAsync = promisify(execFile);
 
const server = new McpServer({
  name: "git-server",
  version: "1.0.0",
  description: "Git 저장소 정보를 조회하는 MCP 서버",
});
 
const REPO_PATH = process.env.REPO_PATH || process.cwd();
 
// 리소스: 저장소 기본 정보
server.resource(
  "repo-info",
  "git://info",
  { description: "Git 저장소 기본 정보" },
  async () => {
    const [branch, remoteUrl, lastCommit] = await Promise.all([
      execFileAsync("git", ["-C", REPO_PATH, "branch", "--show-current"]),
      execFileAsync("git", ["-C", REPO_PATH, "remote", "get-url", "origin"]).catch(() => ({ stdout: "없음" })),
      execFileAsync("git", ["-C", REPO_PATH, "log", "-1", "--format=%H|%an|%ad|%s", "--date=iso"]),
    ]);
 
    const [hash, author, date, ...msgParts] = lastCommit.stdout.trim().split("|");
 
    const info = {
      currentBranch: branch.stdout.trim(),
      remoteUrl: remoteUrl.stdout.trim(),
      lastCommit: {
        hash: hash,
        author: author,
        date: date,
        message: msgParts.join("|"),
      },
      repoPath: REPO_PATH,
    };
 
    return {
      contents: [{
        uri: "git://info",
        text: JSON.stringify(info, null, 2),
        mimeType: "application/json",
      }],
    };
  }
);
 
// 도구: 커밋 히스토리
server.tool(
  "git_log",
  "Git 커밋 히스토리를 조회합니다. 최근 커밋부터 시간 역순으로 반환합니다.",
  {
    count: z.number().min(1).max(50).default(10).describe("조회할 커밋 수"),
    author: z.string().optional().describe("특정 작성자의 커밋만 필터링"),
    since: z.string().optional().describe("시작 날짜 (YYYY-MM-DD 형식)"),
    path: z.string().optional().describe("특정 파일/디렉토리의 변경만 조회"),
  },
  async (params) => {
    const args = ["-C", REPO_PATH, "log", "--format=%H|%an|%ad|%s", "--date=short"];
    args.push("-" + params.count);
 
    if (params.author) {
      args.push("--author=" + params.author);
    }
    if (params.since) {
      args.push("--since=" + params.since);
    }
    if (params.path) {
      args.push("--", params.path);
    }
 
    const { stdout } = await execFileAsync("git", args);
 
    const commits = stdout.trim().split("\n").filter(Boolean).map((line) => {
      const [hash, author, date, ...msgParts] = line.split("|");
      return {
        hash: hash.substring(0, 8),
        author: author,
        date: date,
        message: msgParts.join("|"),
      };
    });
 
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify(commits, null, 2),
      }],
    };
  }
);
 
// 도구: 변경된 파일 목록
server.tool(
  "git_diff_stat",
  "두 커밋 사이에서 변경된 파일 목록과 통계를 조회합니다.",
  {
    from: z.string().default("HEAD~1").describe("시작 커밋 (기본: HEAD~1)"),
    to: z.string().default("HEAD").describe("끝 커밋 (기본: HEAD)"),
  },
  async (params) => {
    const { stdout } = await execFileAsync("git", [
      "-C", REPO_PATH, "diff", "--stat", params.from, params.to,
    ]);
 
    return {
      content: [{ type: "text" as const, text: stdout }],
    };
  }
);
 
// 도구: 브랜치 목록
server.tool(
  "git_branches",
  "모든 브랜치 목록을 조회합니다. 현재 브랜치를 표시합니다.",
  {},
  async () => {
    const { stdout } = await execFileAsync("git", [
      "-C", REPO_PATH, "branch", "-a", "--format=%(refname:short)|%(committerdate:short)|%(subject)",
    ]);
 
    const branches = stdout.trim().split("\n").filter(Boolean).map((line) => {
      const [name, date, subject] = line.split("|");
      return { name: name, lastCommitDate: date, lastCommitMessage: subject };
    });
 
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify(branches, null, 2),
      }],
    };
  }
);
 
const transport = new StdioServerTransport();
await server.connect(transport);

서버 2: 로그 분석 서버

애플리케이션 로그 파일을 분석하는 MCP 서버입니다.

servers/log-server/src/index.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
 
const server = new McpServer({
  name: "log-server",
  version: "1.0.0",
  description: "애플리케이션 로그를 분석하는 MCP 서버",
});
 
const LOG_DIR = process.env.LOG_DIR || "/var/log/app";
 
// 리소스: 사용 가능한 로그 파일 목록
server.resource(
  "log-files",
  "logs://files",
  { description: "분석 가능한 로그 파일 목록" },
  async () => {
    const files = await fs.readdir(LOG_DIR);
    const logFiles = [];
 
    for (const file of files.filter((f) => f.endsWith(".log"))) {
      const stat = await fs.stat(path.join(LOG_DIR, file));
      logFiles.push({
        name: file,
        size: stat.size,
        sizeMB: (stat.size / (1024 * 1024)).toFixed(2) + " MB",
        lastModified: stat.mtime.toISOString(),
      });
    }
 
    return {
      contents: [{
        uri: "logs://files",
        text: JSON.stringify(logFiles, null, 2),
        mimeType: "application/json",
      }],
    };
  }
);
 
// 도구: 로그 검색
server.tool(
  "search_logs",
  "로그 파일에서 특정 패턴을 검색합니다. 정규식을 지원합니다.",
  {
    file: z.string().describe("로그 파일 이름"),
    pattern: z.string().describe("검색 패턴 (정규식 지원)"),
    limit: z.number().default(50).describe("최대 결과 수"),
    level: z.enum(["ALL", "ERROR", "WARN", "INFO", "DEBUG"]).default("ALL")
      .describe("로그 레벨 필터"),
  },
  async (params) => {
    const filePath = path.join(LOG_DIR, path.basename(params.file));
 
    try {
      await fs.access(filePath);
    } catch {
      return {
        content: [{ type: "text" as const, text: "로그 파일을 찾을 수 없습니다: " + params.file }],
        isError: true,
      };
    }
 
    const content = await fs.readFile(filePath, "utf-8");
    const lines = content.split("\n");
    const regex = new RegExp(params.pattern, "i");
    const matches: string[] = [];
 
    for (const line of lines) {
      if (params.level !== "ALL" && !line.includes("[" + params.level + "]")) {
        continue;
      }
      if (regex.test(line)) {
        matches.push(line);
        if (matches.length >= params.limit) break;
      }
    }
 
    return {
      content: [{
        type: "text" as const,
        text: matches.length > 0
          ? matches.length + "건의 결과:\n\n" + matches.join("\n")
          : "일치하는 로그가 없습니다.",
      }],
    };
  }
);
 
// 도구: 에러 집계
server.tool(
  "error_summary",
  "로그 파일의 에러를 집계하여 빈도순으로 정렬합니다.",
  {
    file: z.string().describe("로그 파일 이름"),
    hours: z.number().default(24).describe("최근 N시간 내의 에러만 집계"),
  },
  async (params) => {
    const filePath = path.join(LOG_DIR, path.basename(params.file));
    const content = await fs.readFile(filePath, "utf-8");
    const lines = content.split("\n");
 
    const cutoff = new Date();
    cutoff.setHours(cutoff.getHours() - params.hours);
 
    const errorCounts: Map<string, number> = new Map();
    let totalErrors = 0;
 
    for (const line of lines) {
      if (!line.includes("[ERROR]")) continue;
 
      // 타임스탬프 파싱 시도
      const timestampMatch = line.match(/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}/);
      if (timestampMatch) {
        const lineDate = new Date(timestampMatch[0]);
        if (lineDate < cutoff) continue;
      }
 
      // 에러 메시지 추출 (타임스탬프와 레벨 제거 후)
      const errorMsg = line.replace(/.*\[ERROR\]\s*/, "").substring(0, 100);
      errorCounts.set(errorMsg, (errorCounts.get(errorMsg) || 0) + 1);
      totalErrors++;
    }
 
    const sorted = Array.from(errorCounts.entries())
      .sort((a, b) => b[1] - a[1])
      .slice(0, 20)
      .map(([message, count]) => ({ message, count }));
 
    const summary = {
      totalErrors: totalErrors,
      uniqueErrors: errorCounts.size,
      period: "최근 " + params.hours + "시간",
      topErrors: sorted,
    };
 
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify(summary, null, 2),
      }],
    };
  }
);
 
// 도구: 최근 로그
server.tool(
  "recent_logs",
  "로그 파일의 마지막 N줄을 반환합니다.",
  {
    file: z.string().describe("로그 파일 이름"),
    lines: z.number().default(50).describe("반환할 줄 수"),
  },
  async (params) => {
    const filePath = path.join(LOG_DIR, path.basename(params.file));
    const content = await fs.readFile(filePath, "utf-8");
    const allLines = content.split("\n");
    const lastLines = allLines.slice(-params.lines).join("\n");
 
    return {
      content: [{ type: "text" as const, text: lastLines }],
    };
  }
);
 
const transport = new StdioServerTransport();
await server.connect(transport);

서버 3: 시스템 모니터링 서버

시스템 리소스 사용량을 조회하는 MCP 서버입니다.

servers/system-server/src/index.ts
typescript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import os from "os";
import { execFile } from "child_process";
import { promisify } from "util";
 
const execFileAsync = promisify(execFile);
 
const server = new McpServer({
  name: "system-server",
  version: "1.0.0",
  description: "시스템 리소스 모니터링 MCP 서버",
});
 
// 리소스: 시스템 기본 정보
server.resource(
  "system-info",
  "system://info",
  { description: "운영체제, CPU, 메모리 등 시스템 기본 정보" },
  async () => {
    const info = {
      hostname: os.hostname(),
      platform: os.platform(),
      arch: os.arch(),
      release: os.release(),
      uptime: formatUptime(os.uptime()),
      cpus: os.cpus().length + " cores (" + os.cpus()[0]?.model + ")",
      totalMemory: formatBytes(os.totalmem()),
    };
 
    return {
      contents: [{
        uri: "system://info",
        text: JSON.stringify(info, null, 2),
        mimeType: "application/json",
      }],
    };
  }
);
 
// 도구: CPU 및 메모리 사용량
server.tool(
  "get_resource_usage",
  "현재 CPU 및 메모리 사용량을 조회합니다.",
  {},
  async () => {
    const totalMem = os.totalmem();
    const freeMem = os.freemem();
    const usedMem = totalMem - freeMem;
 
    const cpus = os.cpus();
    const cpuUsage = cpus.map((cpu) => {
      const total = Object.values(cpu.times).reduce((a, b) => a + b, 0);
      const idle = cpu.times.idle;
      return ((total - idle) / total) * 100;
    });
    const avgCpuUsage = cpuUsage.reduce((a, b) => a + b, 0) / cpuUsage.length;
 
    const loadAvg = os.loadavg();
 
    const usage = {
      cpu: {
        usagePercent: avgCpuUsage.toFixed(1) + "%",
        loadAverage: {
          "1min": loadAvg[0].toFixed(2),
          "5min": loadAvg[1].toFixed(2),
          "15min": loadAvg[2].toFixed(2),
        },
        cores: cpus.length,
      },
      memory: {
        total: formatBytes(totalMem),
        used: formatBytes(usedMem),
        free: formatBytes(freeMem),
        usagePercent: ((usedMem / totalMem) * 100).toFixed(1) + "%",
      },
    };
 
    return {
      content: [{
        type: "text" as const,
        text: JSON.stringify(usage, null, 2),
      }],
    };
  }
);
 
// 도구: 디스크 사용량
server.tool(
  "get_disk_usage",
  "디스크 파티션별 사용량을 조회합니다.",
  {},
  async () => {
    try {
      const { stdout } = await execFileAsync("df", ["-h", "--output=source,size,used,avail,pcent,target"]);
      return {
        content: [{ type: "text" as const, text: stdout }],
      };
    } catch {
      // macOS 호환
      const { stdout } = await execFileAsync("df", ["-h"]);
      return {
        content: [{ type: "text" as const, text: stdout }],
      };
    }
  }
);
 
// 도구: 프로세스 목록
server.tool(
  "get_top_processes",
  "CPU 또는 메모리 사용량이 높은 프로세스를 조회합니다.",
  {
    sortBy: z.enum(["cpu", "memory"]).default("cpu").describe("정렬 기준"),
    count: z.number().default(10).describe("표시할 프로세스 수"),
  },
  async (params) => {
    const sortFlag = params.sortBy === "cpu" ? "-pcpu" : "-pmem";
 
    try {
      const { stdout } = await execFileAsync("ps", [
        "aux", "--sort=" + sortFlag,
      ]);
 
      const lines = stdout.split("\n");
      const header = lines[0];
      const processes = lines.slice(1, params.count + 1).join("\n");
 
      return {
        content: [{
          type: "text" as const,
          text: header + "\n" + processes,
        }],
      };
    } catch {
      // macOS 호환
      const { stdout } = await execFileAsync("ps", ["aux"]);
      const lines = stdout.split("\n");
      const header = lines[0];
 
      const sortIdx = params.sortBy === "cpu" ? 2 : 3;
      const sorted = lines.slice(1)
        .filter(Boolean)
        .sort((a, b) => {
          const valA = parseFloat(a.split(/\s+/)[sortIdx] || "0");
          const valB = parseFloat(b.split(/\s+/)[sortIdx] || "0");
          return valB - valA;
        })
        .slice(0, params.count);
 
      return {
        content: [{
          type: "text" as const,
          text: header + "\n" + sorted.join("\n"),
        }],
      };
    }
  }
);
 
function formatBytes(bytes: number): string {
  const units = ["B", "KB", "MB", "GB", "TB"];
  let unitIndex = 0;
  let value = bytes;
  while (value >= 1024 && unitIndex < units.length - 1) {
    value /= 1024;
    unitIndex++;
  }
  return value.toFixed(1) + " " + units[unitIndex];
}
 
function formatUptime(seconds: number): string {
  const days = Math.floor(seconds / 86400);
  const hours = Math.floor((seconds % 86400) / 3600);
  const mins = Math.floor((seconds % 3600) / 60);
  return days + "일 " + hours + "시간 " + mins + "분";
}
 
const transport = new StdioServerTransport();
await server.connect(transport);

클라이언트 애플리케이션

세 개의 MCP 서버를 통합하는 CLI 클라이언트 애플리케이션을 구축합니다.

client/src/mcp-manager.ts
typescript
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 
interface ServerConfig {
  name: string;
  command: string;
  args: string[];
  env?: Record<string, string>;
}
 
export class McpManager {
  private clients: Map<string, Client> = new Map();
  private toolToClient: Map<string, Client> = new Map();
 
  async addServer(config: ServerConfig) {
    const transport = new StdioClientTransport({
      command: config.command,
      args: config.args,
      env: { ...process.env, ...config.env },
    });
 
    const client = new Client({
      name: "devops-assistant-" + config.name,
      version: "1.0.0",
    });
 
    await client.connect(transport);
    this.clients.set(config.name, client);
 
    // 도구 이름 -> 클라이언트 매핑
    const tools = await client.listTools();
    for (const tool of tools.tools) {
      this.toolToClient.set(tool.name, client);
    }
 
    console.error("[" + config.name + "] 연결 완료 (" + tools.tools.length + "개 도구)");
  }
 
  async getAllTools() {
    const allTools: Array<{ name: string; description: string; inputSchema: unknown }> = [];
 
    for (const [, client] of this.clients) {
      const result = await client.listTools();
      allTools.push(...result.tools.map((t) => ({
        name: t.name,
        description: t.description || "",
        inputSchema: t.inputSchema,
      })));
    }
 
    return allTools;
  }
 
  async callTool(name: string, args: Record<string, unknown>) {
    const client = this.toolToClient.get(name);
    if (!client) {
      throw new Error("도구를 찾을 수 없습니다: " + name);
    }
    return client.callTool({ name, arguments: args });
  }
 
  async getAllResources() {
    const allResources: Array<{ uri: string; description: string }> = [];
 
    for (const [, client] of this.clients) {
      try {
        const result = await client.listResources();
        allResources.push(...result.resources.map((r) => ({
          uri: r.uri,
          description: r.description || "",
        })));
      } catch {
        // 리소스를 지원하지 않는 서버는 건너뜀
      }
    }
 
    return allResources;
  }
 
  async disconnectAll() {
    for (const [name, client] of this.clients) {
      await client.close();
      console.error("[" + name + "] 연결 해제");
    }
  }
}
client/src/agent.ts
typescript
import Anthropic from "@anthropic-ai/sdk";
import { McpManager } from "./mcp-manager.js";
 
export class DevOpsAgent {
  private anthropic: Anthropic;
  private mcpManager: McpManager;
  private conversationHistory: Anthropic.MessageParam[] = [];
 
  constructor(mcpManager: McpManager) {
    this.anthropic = new Anthropic();
    this.mcpManager = mcpManager;
  }
 
  async chat(userMessage: string): Promise<string> {
    this.conversationHistory.push({
      role: "user",
      content: userMessage,
    });
 
    const tools = await this.mcpManager.getAllTools();
    const anthropicTools: Anthropic.Tool[] = tools.map((t) => ({
      name: t.name,
      description: t.description,
      input_schema: t.inputSchema as Anthropic.Tool.InputSchema,
    }));
 
    const systemPrompt = [
      "당신은 DevOps 어시스턴트입니다.",
      "개발팀의 Git 저장소, 로그 파일, 시스템 리소스를 관리하고 분석하는 것을 돕습니다.",
      "",
      "규칙:",
      "- 데이터를 조회한 후 핵심 내용을 요약하여 보고합니다.",
      "- 에러가 발견되면 가능한 원인과 해결 방안을 함께 제시합니다.",
      "- 시스템 리소스가 임계치(CPU 80%, 메모리 90%, 디스크 90%)를 초과하면 경고합니다.",
      "- 한국어로 응답합니다.",
    ].join("\n");
 
    while (true) {
      const response = await this.anthropic.messages.create({
        model: "claude-sonnet-4-20250514",
        max_tokens: 4096,
        system: systemPrompt,
        tools: anthropicTools,
        messages: this.conversationHistory,
      });
 
      if (response.stop_reason === "end_turn") {
        const textBlock = response.content.find((b) => b.type === "text");
        const text = textBlock && "text" in textBlock ? textBlock.text : "";
 
        this.conversationHistory.push({
          role: "assistant",
          content: response.content,
        });
 
        return text;
      }
 
      if (response.stop_reason === "tool_use") {
        this.conversationHistory.push({
          role: "assistant",
          content: response.content,
        });
 
        const toolResults: Anthropic.ToolResultBlockParam[] = [];
 
        for (const block of response.content) {
          if (block.type === "tool_use") {
            try {
              const result = await this.mcpManager.callTool(
                block.name,
                block.input as Record<string, unknown>
              );
 
              const text = (result.content as Array<{ text?: string }>)
                .map((c) => c.text || "")
                .join("\n");
 
              toolResults.push({
                type: "tool_result",
                tool_use_id: block.id,
                content: text,
                is_error: result.isError === true,
              });
            } catch (error) {
              toolResults.push({
                type: "tool_result",
                tool_use_id: block.id,
                content: "도구 실행 오류: " + (error as Error).message,
                is_error: true,
              });
            }
          }
        }
 
        this.conversationHistory.push({
          role: "user",
          content: toolResults,
        });
      }
    }
  }
 
  clearHistory() {
    this.conversationHistory = [];
  }
}
client/src/index.ts
typescript
import readline from "readline";
import { McpManager } from "./mcp-manager.js";
import { DevOpsAgent } from "./agent.js";
 
async function main() {
  const mcpManager = new McpManager();
 
  // MCP 서버들 연결
  await mcpManager.addServer({
    name: "git",
    command: "node",
    args: ["../servers/git-server/dist/index.js"],
    env: { REPO_PATH: process.env.REPO_PATH || process.cwd() },
  });
 
  await mcpManager.addServer({
    name: "logs",
    command: "node",
    args: ["../servers/log-server/dist/index.js"],
    env: { LOG_DIR: process.env.LOG_DIR || "/var/log/app" },
  });
 
  await mcpManager.addServer({
    name: "system",
    command: "node",
    args: ["../servers/system-server/dist/index.js"],
  });
 
  const agent = new DevOpsAgent(mcpManager);
 
  // 대화형 CLI
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });
 
  console.log("DevOps 어시스턴트가 준비되었습니다. 질문을 입력하세요. (종료: exit)");
  console.log("---");
 
  const prompt = () => {
    rl.question("> ", async (input) => {
      const trimmed = input.trim();
 
      if (trimmed === "exit" || trimmed === "quit") {
        await mcpManager.disconnectAll();
        rl.close();
        return;
      }
 
      if (trimmed === "clear") {
        agent.clearHistory();
        console.log("대화 기록이 초기화되었습니다.");
        prompt();
        return;
      }
 
      if (!trimmed) {
        prompt();
        return;
      }
 
      try {
        const response = await agent.chat(trimmed);
        console.log("\n" + response + "\n");
      } catch (error) {
        console.error("오류:", (error as Error).message);
      }
 
      prompt();
    });
  };
 
  prompt();
}
 
main().catch(console.error);

실행과 사용 예시

빌드 및 실행
bash
# 각 서버 빌드
cd servers/git-server && npm run build && cd ../..
cd servers/log-server && npm run build && cd ../..
cd servers/system-server && npm run build && cd ../..
 
# 클라이언트 빌드 및 실행
cd client
REPO_PATH=/path/to/your/repo LOG_DIR=/var/log/app npm run start

실행 후 자연어로 질문할 수 있습니다.

> 오늘 커밋된 내용을 요약해 주세요
> 최근 에러 로그를 분석하고 원인을 추정해 주세요
> 현재 시스템 리소스 사용량은 어떤가요? 이상 징후가 있나요?
> 지난 일주일간 가장 많이 수정된 파일은 무엇인가요?
> ERROR 로그에서 "timeout" 관련 항목을 검색해 주세요

프로덕션 고려사항

실전 배포를 위해 추가로 고려해야 할 사항을 정리합니다.

에러 복구

MCP 서버가 비정상 종료될 경우 자동으로 재연결하는 로직이 필요합니다.

자동 재연결
typescript
async addServerWithRetry(config: ServerConfig, maxRetries: number = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      await this.addServer(config);
      return;
    } catch (error) {
      console.error(
        "[" + config.name + "] 연결 실패 (시도 " + attempt + "/" + maxRetries + "): " +
        (error as Error).message
      );
      if (attempt < maxRetries) {
        await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
      }
    }
  }
  throw new Error("[" + config.name + "] 서버 연결에 실패했습니다.");
}

모니터링

각 도구 호출의 성능과 에러율을 측정하여 운영 상태를 파악합니다.

도구 호출 메트릭
typescript
interface ToolMetrics {
  callCount: number;
  errorCount: number;
  totalDuration: number;
  lastCallAt: string;
}
 
const metrics: Map<string, ToolMetrics> = new Map();
 
async callToolWithMetrics(name: string, args: Record<string, unknown>) {
  const start = Date.now();
  const existing = metrics.get(name) || {
    callCount: 0, errorCount: 0, totalDuration: 0, lastCallAt: "",
  };
 
  try {
    const result = await this.callTool(name, args);
    existing.callCount++;
    existing.totalDuration += Date.now() - start;
    existing.lastCallAt = new Date().toISOString();
    if (result.isError) existing.errorCount++;
    metrics.set(name, existing);
    return result;
  } catch (error) {
    existing.callCount++;
    existing.errorCount++;
    existing.totalDuration += Date.now() - start;
    metrics.set(name, existing);
    throw error;
  }
}

설정 파일 관리

서버 구성을 코드에서 분리하여 설정 파일로 관리합니다.

mcp-config.json
json
{
  "servers": [
    {
      "name": "git",
      "command": "node",
      "args": ["servers/git-server/dist/index.js"],
      "env": { "REPO_PATH": "/path/to/repo" }
    },
    {
      "name": "logs",
      "command": "node",
      "args": ["servers/log-server/dist/index.js"],
      "env": { "LOG_DIR": "/var/log/app" }
    },
    {
      "name": "system",
      "command": "node",
      "args": ["servers/system-server/dist/index.js"]
    }
  ]
}

시리즈를 마치며

이 시리즈를 통해 MCP의 핵심 개념부터 프로덕션 수준의 시스템 구축까지 전체 과정을 다루었습니다.

1~4장에서는 MCP의 기초를 다졌습니다. 프로토콜의 아키텍처, JSON-RPC 기반 통신, 전송 계층, 서버 프리미티브를 이해했습니다.

5~7장에서는 실제 구현을 수행했습니다. TypeScript SDK와 Python FastMCP로 서버를 구축하고, 클라이언트를 구현하여 LLM과 통합했습니다.

8~9장에서는 프로덕션 관점을 다루었습니다. 기존 시스템과의 연동 패턴과 보안 모범 사례를 학습했습니다.

10장에서는 모든 내용을 종합하여 DevOps 어시스턴트라는 실전 프로젝트를 완성했습니다.

MCP는 AI와 기존 시스템을 연결하는 표준으로 빠르게 자리 잡고 있습니다. 이 시리즈에서 다룬 지식을 바탕으로, 여러분의 시스템에 MCP를 도입하여 AI의 가능성을 확장해 보시기 바랍니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#mcp#typescript#python

관련 글

AI / ML

9장: 보안, 인증, 권한 관리

MCP 서버의 OAuth 2.1 인증, 입력 유효성 검사, 명령 주입 방지, 데이터 유출 방지 등 프로덕션 보안 모범 사례를 다룹니다.

2026년 2월 6일·20분
AI / ML

8장: 기존 시스템과 MCP 연동하기

데이터베이스, REST API, 레거시 시스템을 MCP 서버로 래핑하여 AI 모델이 접근할 수 있도록 만드는 실전 패턴을 다룹니다.

2026년 2월 4일·18분
AI / ML

7장: MCP 클라이언트 구현하기

MCP 클라이언트를 직접 구현하여 서버에 연결하고, LLM과 통합하여 도구 호출 파이프라인을 완성하는 방법을 다룹니다.

2026년 2월 2일·15분
이전 글9장: 보안, 인증, 권한 관리

댓글

목차

약 22분 남음
  • 프로젝트 개요
    • 시스템 구성
    • 프로젝트 구조
  • 서버 1: Git 서버
  • 서버 2: 로그 분석 서버
  • 서버 3: 시스템 모니터링 서버
  • 클라이언트 애플리케이션
  • 실행과 사용 예시
  • 프로덕션 고려사항
    • 에러 복구
    • 모니터링
    • 설정 파일 관리
  • 시리즈를 마치며