ABOUT ME

Unity로 만드는 게임 개발의 즐거움과 배움을 함께 나누는 공간입니다. 실전 코드 예제와 개발 팁, 프로젝트 경험을 공유하며, 창의적인 아이디어와 함께 성장하는 개발자 커뮤니티를 만들어갑니다.

Today
Yesterday
Total
  • MCP(Model Context Protocol)이란 무엇인가? + Claude Desktop으로 MCP Server 연결하기
    개발 가이드 2025. 4. 2. 21:07

    개요

    최근 MCP라는 주제를 다루는 유튜브 영상이나 블로그 글들이 많아졌다. 

    AI를 더 잘 사용하는 방법이라는건 알겠지만, 이게 왜 갑자기 이렇게 인기가 많아졌을까? 하는 의문이 들었다. 

    이에 리서치를 통해 알게 된 지식과, 간단하게 사용할 수 있는 방법을 알아보려고 한다. 

    또한 이 기술을 통해, 어떤 방향으로 이 기술을 활용할 수 있을 지 생각해보자.

     

    다음은 앤트로픽 MCP 가이드 문서이다. 직접 만들어보고 싶은 사람은 다음 가이드를 참조하면 좋을듯 하다.

     

    Introduction - Model Context Protocol

    Understand how MCP connects clients, servers, and LLMs

    modelcontextprotocol.io

     

    1.  MCP란 무엇인가?

    MCP란 대규모 언어 모델이 외부 데이터와 시스템을 더 효과적으로 활용할 수 있도록 설계된 일종의 연결 프로토콜

     

    MCP는 Claude AI를 제작하는 앤트로픽(Anthropic)에서 제작하였다. 

    앤트로픽에서는 MCP를 USB-C 포트에 비유한다. 

     

    이 예시를 이해하여 보자. 

    • 컴퓨터, TV, 모니터 등 다양한 기기는 USB-A나 USB-C를 통해 주변 장치들과 연결된다.
    • 마우스, 키보드처럼 규격이 다른 기기들도 USB-C라는 표준 포맷을 통해 호환이 가능해진다.

    마찬가지로, MCP는 언어 모델이 여러 외부 도구와 표준화된 방식으로 연결될 수 있게 해주는 범용 인터페이스 역할을 한다.

     

    ✅ MCP의 주요 특징

    1. 데이터 접근성 향상

    • 기존 방식: API 호출은 한 번 요청하면 응답하고 종료되는 단방향 구조였다.
    • MCP는 지속적인 양방향 스트리밍 통신을 지원해, AI가 다양한 데이터 소스와 실시간으로 정보를 주고받으며 상호작용할 수 있게 만든다.

    2. 개발 효율성 증가

    • 기존에는 각 데이터 소스마다 별도의 커넥터와 로직을 따로 구성해야 했다.
    • MCP는 표준화된 프로토콜 하나로 다양한 시스템과 연결할 수 있어, 유지보수 비용을 대폭 절감시켜준다.

    3. 확장성과 개방성

    • AI는 MCP를 통해 다양한 툴, 서비스, 데이터셋 간 컨텍스트를 유지하며 복합적인 작업을 처리할 수 있다.
    • MCP는 오픈소스로 제공되어, 누구나 참여하고 개선할 수 있는 확장 가능한 생태계를 지향한다.
    • Claude 뿐만 아니라 ChatGPT, Perplexity 등 다양한 LLM 시스템에서 활용 가능하다.

    4. 보안 중심 설계

    • 기존에는 LLM이 외부 API에 접근하려면 API 키를 서버에 전달해야 했고, 개인정보 유출 가능성이 있었다.
    • MCP는 보안 계층이 내장된 구조로, 데이터는 로컬 시스템에서만 처리되며, 민감한 정보가 LLM 서버로 전송되지 않는다.
    결론
    MCP는 AI 활용 방식의 패러다임을 바꾸는 연결 표준이다.
    LLM이 실제 세계의 도구 및 데이터와 더 긴밀하게 상호작용 할 수 있게 해주는 인프라다. 

    2. 그렇다면, MCP는 어떻게 작동하는데?

    MCP 에는 세가지 핵심 역할이 있다. 

    • MCP 서버(도구 및 데이터 액세스)
    • MCP 클라이언트(LLM에 포함되는 MCP 서버 및 통신)
    • MCP 호스트(Claude Desktop, Cursor 등)

    이제 하나씩 어떤식으로 작동하는지 살펴보자

    2-1. MCP 서버

    LLM이  사용할 수 있는 도구와 데이터 엑세스 기능을 제공하는 프로그램
    • 사용자의 장치에서 로컬로 실행되거나 원격 서버에 배포 가능
    • 로컬 데이터나 원격 서비스에서 정보를 검색하는 특정 도구 세트를 제공
    • LLM이 작업을 처리하는 동안 특정 도구를 사용해야 한다면, MCP 서버가 제공하는 도구를 사용하여 필요한 데이터를 얻음

    예시: 회의 일정 요약 및 이메일 전송 자동화

    사용자는 다음과 같은 요청을 LLM에게 전달한다

    “이번 주 일정 중 회의 내용을 요약해서 관련 팀원들에게 이메일로 보내줘.”

     

    기존 API 방식이라면?

    • LLM이 로컬 캘린더에 접근할 수 없음 → 사용자가 수동으로 일정 정보를 복사해서 붙여넣어야 함
    • 요약 후 이메일 전송도 별도의 앱이나 서비스에서 직접 처리해야 함

    MCP 서버가 있다면?

    1. MCP 서버는 사용자의 장치에서 실행되며, 로컬 캘린더와 이메일 시스템에 연결된 도구 세트를 가지고 있음

    2. LLM은 MCP 서버를 통해 다음과 같은 요청을 보냄

    • 캘린더에서 이번 주 회의 일정 및 메모 데이터를 검색
    • 자연어로 회의 요약 생성
    • 이메일 도구를 통해 요약본을 팀원에게 전송

    3. 사용자는 아무 입력 없이도 메일 발송 가능

     

    2-2. MCP 클라이언트

    LLM과 MCP 서버를 연결하는 브릿지. 클라이언트는 LLM에 포함된다. 
    • LLM으로부터 요청 수신
    • 적절한 MCP 서버로 요청 전달
    • MCP 서버로부터 결과를 LLM에 반환

    즉, 우리가 Claude에게 프롬프트를 입력하면, LLM이 해당 작업에 외부 도구가 필요하다고 판단할 경우 MCP 클라이언트를 통해 적절한 MCP 서버에 요청을 전송하게 된다.

    그 후, 결과를 받아 다시 응답에 반영해준다.

     

    🧩 예시: PDF 문서 요약 요청

     

    사용자가 Claude에게 다음과 같이 지시한다:

     

    “내 다운로드 폴더에 있는 ‘2024_회의록.pdf’ 파일 내용을 요약해줘.”

     

    진행 흐름

    1. LLM은 해당 작업을 처리하려면 로컬 파일 시스템에 접근해야 한다고 판단한다.
    2. MCP 클라이언트는 요청을 수신한 후, ‘파일 읽기 및 문서 요약’ 기능을 가진 MCP 서버로 요청을 전달한다.
    3. MCP 서버는 다운로드 폴더에서 파일을 열고 내용을 분석하여 요약본을 생성한다.
    4. 클라이언트는 이 결과를 받아 LLM에 전달, Claude는 자연스럽게 요약 결과를 사용자에게 응답한다.

    이처럼 클라이언트는 LLM의 판단력과 MCP 서버의 실행력을 매끄럽게 연결해주는 통신 허브다.

    LLM은 똑똑하게 결정하고, 클라이언트는 정확히 연결하고, 서버는 실제로 실행하는 구조라고 보면 된다.

    2-3. MCP 호스트

    Claude Desktop, IDE(Cursor 등), 또는 MCP를 통해 데이터를 액세스하고자 하는 AI 도구와 같은 프로그램
    • 사용자가 LLM과 상호작용 할 수 있는 인터페이스를 제공
    • MCP 클라이언트를 통합하여 MCP 서버가 제공하는 도구를 사용하여 LLM 기능 확장

    Claude에서 제공하는 MCP 구조도

    3. 이제 MCP를 만들어보자.

     

    앤트로픽에서는 서버 / 클라이언트 개발 가이드를 제공한다. 

    이번에는 MCP 서버만 만들어보자.

     

    앤트로픽에서 제공하는 가이드를 그대로 따라가보자.

     

    기상 정보 수집 및 경고 도구

    LLM들은 현재 날씨 데이터를 가져올 수 있는 능력이 없다. 
    이를 MCP를 구현해서 해결해보자.

     

    먼저 기본세팅을 진행해주자.

    • 구현 언어: TypeScript ( 클로드에서 python, Java, Kotlin, C#도 가이드를 제공한다!)
    • Node 버전: 22.14.0(최신 lts 버전. 16버전 이상으로 사용하는 것을 추천한다.)

    다음 커맨드를 입력하여 버전을 확인해주자. 

    node --version
    npm --version

     

    만약 설치가 되어있지 않거나, 버전이 16 미만이라면 새로 설치해주어야 한다. 

    npm install 22.14.0 #가장 최신의 LTS 버전으로 설치

     

    노드 세팅이 완료되었다면, 프로젝트 세팅을 진행해주자. 

    #맥의 경우
    # 폴더 생성 및 이동
    mkdir weather
    cd weather
    
    # node 초기화
    npm init -y
    
    # 노드 모듈 설치
    npm install @modelcontextprotocol/sdk zod
    npm install -D @types/node typescript
    
    # Create our files
    mkdir src
    touch src/index.ts
    
    #윈도우의 경우
    
    md weather
    cd weather
    
    npm init -y
    
    npm install @modelcontextprotocol/sdk zod
    npm install -D @types/node typescript
    
    md src
    new-item src\index.ts

    폴더 생성 완료

    이제, package.json을 들어가서 type: module과 빌드 커맨드를 적어주자. 

    {
      "name": "weather",
      "version": "1.0.0",
      "description": "",
      "main": "./build/index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "tsc && chmod 755 build/index.js"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "@modelcontextprotocol/sdk": "^1.8.0",
        "node-fetch": "^3.3.2",
        "zod": "^3.24.2"
      },
      "devDependencies": {
        "@types/node": "^22.13.17",
        "typescript": "^5.8.2"
      },
      "type": "module",
      "bin": {
        "weather": "./build/index.js"
      },
      "files": [
        "build"
      ]
    }

    그리고 루트에 tsconfig.json 파일을 생성해주고, 다음을 붙여넣어주자.

    {
        "compilerOptions": {
            "target": "ES2024",
            "module": "Node16",
            "moduleResolution": "Node16",
            "outDir": "./build",
            "rootDir": "./src",
            "strict": true,
            "esModuleInterop": true,
            "skipLibCheck": true,
            "forceConsistentCasingInFileNames": true
        },
        "include": [
            "src/**/*"
        ],
        "exclude": [
            "node_modules"
        ],
        "lib": [
            "ES2024",
            "DOM"
        ]
    }

    일단 기본적인 설정이 완료되었다. 

    이제 우리는 다음 도구를 작성할 것이다.

    • get-alerts: 경고 수준의 날씨 예보 시 알람을 제공
    • get-forecast: 현재 날씨 예보 데이터를 제공
    •  
     

    https://api.weather.gov api를 사용하여 미국 주의 데이터를 받을 수 있도록 할것이다. 

    먼저 import와 변수를 정의해주고, 타입 안정성을 위한 변수 타입을 지정해주자. 

    #!/usr/bin/env node
    
    import fetch from 'node-fetch';
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    import { z } from "zod";
    
    const NWS_API_BASE = "https://api.weather.gov";
    const USER_AGENT = "weather-app/1.0";
    const MAX_RETRIES = 3;
    const RETRY_DELAY_MS = 1000;
    
    // Type definitions
    interface AlertFeature {
        properties: {
            event?: string;
            areaDesc?: string;
            severity?: string;
            status?: string;
            headline?: string;
        };
    }
    
    interface ForecastPeriod {
        name?: string;
        temperature?: number;
        temperatureUnit?: string;
        windSpeed?: string;
        windDirection?: string;
        shortForecast?: string;
    }
    
    interface AlertsResponse {
        features: AlertFeature[];
    }
    
    interface PointsResponse {
        properties: {
            forecast?: string;
        };
    }
    
    interface ForecastResponse {
        properties: {
            periods: ForecastPeriod[];
        };
    }
    
    interface ApiError {
        status: number;
        message: string;
        details?: string;
    }
    
    // Create server instance
    const server = new McpServer({
        name: "weather",
        version: "1.0.0",
        capabilities: {
            resources: {},
            tools: {},
        },
    });

    이제 api를 호출해서 응답을 처리하는 함수를 작성해준다. 

    async function makeNWSRequest<T>(url: string, retryCount = 0): Promise<T | ApiError> {
        const headers = {
            "User-Agent": USER_AGENT,
            "Accept": "application/geo+json",
        };
    
    
        try {
            const response = await fetch(url, { headers });
    
            if (!response.ok) {
                const errorText = await response.text().catch(() => "Unable to read error response");
    
                // Retry logic for server errors (5xx)
                if (response.status >= 500 && retryCount < MAX_RETRIES) {
                    await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
                    return makeNWSRequest<T>(url, retryCount + 1);
                }
    
                return {
                    status: response.status,
                    message: `HTTP error! status: ${response.status}`,
                    details: errorText
                };
            }
    
            const json = await response.json();
            return json as T;
        } catch (error) {
    
            // Retry on network errors
            if (retryCount < MAX_RETRIES) {
                await new Promise(resolve => setTimeout(resolve, RETRY_DELAY_MS));
                return makeNWSRequest<T>(url, retryCount + 1);
            }
    
            return {
                status: 0,
                message: "Network or parsing error",
                details: error instanceof Error ? error.message : String(error)
            };
        }
    }
    
    // Format alert data
    function formatAlert(feature: AlertFeature): string {
        const props = feature.properties;
        return [
            `Event: ${props.event || "Unknown"}`,
            `Area: ${props.areaDesc || "Unknown"}`,
            `Severity: ${props.severity || "Unknown"}`,
            `Status: ${props.status || "Unknown"}`,
            `Headline: ${props.headline || "No headline"}`,
            "---",
        ].join("\n");
    }
    
    // Check if response is an API error
    function isApiError(response: any): response is ApiError {
        return response && typeof response === 'object' && 'status' in response && 'message' in response;
    }

     

    이제 클로드에서 인식할 수 있는 실행 도구를 작성해준다. 

    server.tool(
        "get-alerts",
        "Get weather alerts for a state",
        {
            state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"),
        },
        async ({ state }) => {
            const stateCode = state.toUpperCase();
            const alertsUrl = `${NWS_API_BASE}/alerts/active?area=${stateCode}`;
            const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);
    
            if (isApiError(alertsData)) {
                return {
                    content: [
                        {
                            type: "text",
                            text: `Failed to retrieve alerts data: ${alertsData.message}`,
                        },
                    ],
                };
            }
    
            const features = alertsData.features || [];
            if (features.length === 0) {
                return {
                    content: [
                        {
                            type: "text",
                            text: `No active alerts for ${stateCode}`,
                        },
                    ],
                };
            }
    
            const formattedAlerts = features.map(formatAlert);
            const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;
    
            return {
                content: [
                    {
                        type: "text",
                        text: alertsText,
                    },
                ],
            };
        },
    );
    
    server.tool(
        "get-forecast",
        "Get weather forecast for a location",
        {
            latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
            longitude: z.number().min(-180).max(180).describe("Longitude of the location"),
        },
        async ({ latitude, longitude }) => {
            // Get grid point data - use full precision for latitude/longitude
            const pointsUrl = `${NWS_API_BASE}/points/${latitude},${longitude}`;
            const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);
    
            if (isApiError(pointsData)) {
                return {
                    content: [
                        {
                            type: "text",
                            text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. Error: ${pointsData.message}${pointsData.details ? ` - ${pointsData.details}` : ''} (Note: Only US locations are supported by the NWS API).`,
                        },
                    ],
                };
            }
    
            const forecastUrl = pointsData.properties?.forecast;
    
            if (!forecastUrl) {
                return {
                    content: [
                        {
                            type: "text",
                            text: "Failed to get forecast URL from grid point data. The API response did not include a forecast URL.",
                        },
                    ],
                };
            }
    
            // Get forecast data
            const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
    
            if (isApiError(forecastData)) {
                return {
                    content: [
                        {
                            type: "text",
                            text: `Failed to retrieve forecast data: ${forecastData.message}${forecastData.details ? ` - ${forecastData.details}` : ''}`,
                        },
                    ],
                };
            }
    
            const periods = forecastData.properties?.periods || [];
            if (periods.length === 0) {
                return {
                    content: [
                        {
                            type: "text",
                            text: "No forecast periods available in the API response.",
                        },
                    ],
                };
            }
    
            // Format forecast periods
            const formattedForecast = periods.map((period: ForecastPeriod) =>
                [
                    `${period.name || "Unknown"}:`,
                    `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
                    `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
                    `${period.shortForecast || "No forecast available"}`,
                    "---",
                ].join("\n"),
            );
    
            const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;
    
            return {
                content: [
                    {
                        type: "text",
                        text: forecastText,
                    },
                ],
            };
        },
    );

     

    이제 마지막으로, 서버를 실행시키기 위한 main 함수를 작성해주자. 

    async function main() {
        const transport = new StdioServerTransport();
        await server.connect(transport);
        console.error("Weather MCP Server running on stdio");
    }
    
    main().catch((error) => {
        console.error("Fatal error in main():", error);
        process.exit(1);
    });

     

    코드가 다 작성되었으면, npm run build를 통해서 서버를 빌드해주자. 

    클로드는 빌드 파일의 index.js로 접근하게 만들것이기 때문에, 어떤 수정사항이 있다면 꼭 빌드해줘야 한다. 

     

    4. 클로드 데스크탑에 MCP 서버 연결하기

    이제 클로드 데스크탑에서 서버를 연결해보자.

    현재 클로드 데스크탑은 윈도우, 맥은 지원하지만 리눅스는 아직 지원되지 않는다고 한다. 

    클로드 데스크탑 앱이 다운받은 상태가 아니라면 

    https://claude.ai/download에서 다운로드를 진행해주자. 

    **크롬에서 다운받은 웹앱 형태가 아닌 데스크탑 앱을 받아야 함을 인지

     

    이제 다운이 완료되었다면, 우리는 클로드가 MCP 서버를 인식할 수 있도록 해야한다. 

    터미널에 다음 커맨드를 입력해보자(VS Code를 쓴다는 가정)

    code ~/Library/Application\ Support/Claude/claude_desktop_config.json

    해당 파일을 열면, 이제 서버를 지정해주어야 한다. 

    여기에 우리가 원하는 서버를 지정해주면 된다. 

    {
        "mcpServers": {
          "weather": {
            "command": "npx",
            "args": ["-y", "/Users/userpc/Documents/GitHub/weather"]
          }
        }
      }

    userpc 부분에 본인의 PC 이름으로 지정해주자. 

    정 찾기 힘들다면, weather 파일 경로를 복사해서 붙여넣기 해주자. 

     

    이제 세팅은 완료되었으니 클로드를 들어가보자.

    도구 연동 완료

    도구 버튼이 보이는가?

    해당 버튼을 누르면 

    MCP 도구 리스트

    이런식으로 사용 가능한 도구를 확인할 수 있다. 

     

    이제 질문을 진행해보자. 

    "현재 LA의 날씨를 알려줘"

    실행하면 다음과 같이 도구를 허용하겠냐는 문구가 뜬다. 

    허용을 해주면 api를 통해 데이터를 요청하고 아까 설정해둔 템플릿에 맞게 알려준다. 

    잘 나왔군

    마무리하며....

    꽤나 도구가 유용해보인다. 

    MCP Server에 다양한 로직을 작성해두고, AI를 통해 파일을 정리해볼 수도 있겠고, 이메일 자동화, 게임 플레이 등등...

    다양한 재밌는 용도 뿐만 아니라 굉장히 유용한 기능을 끌어낼 수 있을 것으로 보인다. 

    그리고 찾아보니, 다양한 좋은 Server를 정리해 둔 깃 링크를 발견했다. 

     

    awesome-mcp-servers/README-ko.md at main · punkpeye/awesome-mcp-servers

    A collection of MCP servers. Contribute to punkpeye/awesome-mcp-servers development by creating an account on GitHub.

    github.com

    심지어 Unity도 있는걸 보고 많이 놀랐다. 

     

    GitHub - CoderGamester/mcp-unity: MCP Server to integrate Unity Editor game engine with different AI Model clients (e.g. Claude

    MCP Server to integrate Unity Editor game engine with different AI Model clients (e.g. Claude Desktop, Windsurf, Cursor) - CoderGamester/mcp-unity

    github.com

    세상엔 참 대단한 사람들이 많구나 싶다. 이게 나온게 24년 11월 일텐데..

    이것도 연동해보고, 가능하다면 오픈소스 기여도 해보고 싶다. 

    일단 마치면서, AI를 사용해 모든걸 자동화 시키고 싶은 모든분들을 위해

    😘즐코(즐거운 코딩 되시길)!

     

    '개발 가이드' 카테고리의 다른 글

    [Apple Developers] 애플 개발자 계정 가입하기  (4) 2025.02.04
Designed by Tistory.