하는 일/ai

[ MCP ] ChatGPT + MCP 서버

yeznable 2025. 6. 5. 16:17
728x90

 

지난번에 아래 글에서 Claude with MCPs에 대해 소개했었다.

 

[ 따라잡기 ] Claude with MCPs (0) - 소개

AI를 활용하는 데에 뒤쳐지지 말아야겠다는 생각에 호기롭게 [ 따라잡기 ]라는 말머리도 달아 윈드서프를 사용해보고 아래 글을 쓴지 3개월도 채 되지 않았다. [ Windsurf ] 윈드서프 파이썬 프로젝

yeznable-blog.tistory.com

 

오랜만에 생각이 나서 이제는 ChatGPT에서도 MCP를 활용할 수 있나? 하고 찾아보니 Custom GPT로 MCP를 활용할 수 있다는걸 알게 되었다.

위 링크의 글을 쓰면서 나도 처음 MCP라는 것에 대해 정리해보았고 "명칭은 MCP 서버라고 되어있어서 헷갈리지만 사실상 pip으로 설치하는 패키지나 다름이 없다" 라고 생각했었다.

하지만 그건 Claude에서 활용한 방식이었고 Custom GPT로 MCP를 활용하려면 실제로 MCP 서버를 운영해야 한다.

Custom GPT 기능은 Free Plan 에서는 활용할 수 없고 Plus 이상의 Plan에서 활용할 수 있다.

 

가장 싸고 쉬운 방식으로 Custom GPT + MCP 서버를 구성하는 방법을 정리해 남겨본다.

이번에 정리하는 내용으로 나는 ChatGPT에게 시켜서 MCP 서버 내의 파일시스템을 제어하는 예제를 만들어본다.


GitHub에 FastAPI로 MCP 서버 준비

GitHub에 새로운 저장소를 만들어서 다음과 같이 3개의 파일을 작성한다.

실제 파일에 적힌 내용들은 다음과 같다.

나도 FastAPI에 대해서 간략하게만 알고 무언가 제대로 만들어본적은 없었다.

이 과정도 ChatGPT에게 "이런 기능의 MCP를 만들자" 하고 물어가며 만든 스크립트들이다.

# main.py
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel
import uvicorn
import os

app = FastAPI()

BASE_PATH = "/tmp/gpt_mcp"

class FileRequest(BaseModel):
    filename: str
    content: str

class FolderRequest(BaseModel):
    foldername: str

@app.get("/ping")
def ping():
    return {"status": "awake"}

@app.post("/create-folder")
def create_folder(req: FolderRequest):
    path = os.path.join("/tmp", req.foldername)
    os.makedirs(path, exist_ok=True)
    return {"message": f"Folder created at {path}"}

@app.post("/write-file")
def write_file(req: FileRequest):
    os.makedirs(BASE_PATH, exist_ok=True)
    filepath = os.path.join(BASE_PATH, req.filename)
    try:
        with open(filepath, "w") as f:
            f.write(req.content)
        return {"message": f"File written to {filepath}"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/list-files")
def list_files():
    if not os.path.exists(BASE_PATH):
        return {"files": []}
    files = os.listdir(BASE_PATH)
    return {"files": files}

@app.get("/read-file")
def read_file(filename: str = Query(...)):
    filepath = os.path.join(BASE_PATH, filename)
    if not os.path.exists(filepath):
        raise HTTPException(status_code=404, detail="File not found")
    try:
        with open(filepath, "r") as f:
            content = f.read()
        return {"filename": filename, "content": content}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.delete("/delete-file")
def delete_file(filename: str = Query(...)):
    filepath = os.path.join(BASE_PATH, filename)
    if not os.path.exists(filepath):
        raise HTTPException(status_code=404, detail="File not found")
    try:
        os.remove(filepath)
        return {"message": f"Deleted {filename}"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/clear-folder")
def clear_folder():
    if not os.path.exists(BASE_PATH):
        return {"message": "Folder does not exist, nothing to clear"}
    try:
        for f in os.listdir(BASE_PATH):
            path = os.path.join(BASE_PATH, f)
            if os.path.isfile(path):
                os.remove(path)
        return {"message": "All files deleted"}
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

# 로컬 실행용 코드 (Render 배포 시 필요 없음)
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=10000)
# render.yaml
services:
  - type: web
    name: mcp-server
    env: python
    buildCommand: ""
    startCommand: uvicorn main:app --host 0.0.0.0 --port 10000
# requirements.txt
fastapi
uvicorn

 

이렇게 3개의 파일을 저장한 저장소의 URL을 복사 해둔다.


Render를 활용한 무료 MCP 서버 배포

 

Cloud Application Platform

On Render, you can build, deploy, and scale your apps with unparalleled ease – from your first user to your billionth.

render.com

 

Render는 GitHub을 연동해서 자동으로 FastAPI, Flask 등의 서비스를 손쉽게 배포할 수 있게 해주는 서비스로 웹서비스를 배포할 때 월 750시간까지 무료로 사용할 수 있다.

단점으로 한동안 안쓰면 슬립 상태로 전환되어 다시 호출할 때 딜레이가 생긴다는 것인데 어차피 서비스를 만드는 것도 아니고 무료로 쓰는걸 생각하면 감안할 수 있는 정도인듯 하다.

 

Render에 로그인하여 우측 상단에 "+New > Web Service" 버튼으로 새 웹 서비스 프로젝트를 생성한다.

 

프로젝트 설정 화면에서 Source Code에는 Public Git Repository를 선택하고 아까 복사해둔 저장소 주소를 입력해 Connect한다.

 

Name, Project, Language, Branch, Region, Root Directory, Build Command 항목들 모두 처음 작성되어있는 기본값을 그대로 두면 된다.

 

Start Command는 기본으로는 비어있지만 다음과 같이 작성해준다.

uvicorn main:app --host 0.0.0.0 --port 10000

 

요금제도 기본으로는 Starter로 선택되어있지만 Free를 선택해준다.

 

이후 가장 아래쪽에 "Deploy Web Service" 버튼을 눌러 서비스를 배포한다.

배포된 주소를 나중에 Custom GPT 설정에 활용하도록 복사해둔다.


Custom GPT 설정

Plus Plan을 사용하고 있다면 좌측의 사이드바 상단에 GPT라는 항목을 찾을 수 있다.

해당 항목에 들어가 우측 상단을 보면 "+ 만들기" 버튼이 있는데 이쪽으로 들어간다.

 

그러면 나타나는 화면에서 "구성" 탭으로 들어간다.

 

이름, 설명 값은 편한대로 작성하고 지침은 나중에 작성하기로 하고 아래쪽에 "새 작업 만들기" 버튼을 누른다.

 

"새 작업 만들기" 버튼을 눌러 들어간 화면에서 "스키마" 창에 다음 값을 입력한다.

해당 값은 아까 배포한 MCP 서버의 FastAPI에 정의된 API들을 활용하는 방법들이 정의되어있다.

내가 배포한 FastAPI 코드를 ChatGPT에게 전달하면서 Custom GPT에 MCP 서버를 적용할 스키마 작성해달라고 하면 어렵지 않게 작성 해줄 것이다.

그것이 내가 생각한 것과 조금 다르게 작동한다면 수정은 필요하다.

{
  "openapi": "3.1.0",
  "info": {
    "title": "Flexible File Manager",
    "version": "1.2.0"
  },
  "servers": [
    {
      "url": "<Render에서 배포한 FastAPI 주소>"
    }
  ],
  "paths": {
    "/ping": {
      "get": {
        "operationId": "ping",
        "summary": "서버 상태 확인",
        "responses": {
          "200": { "description": "OK" }
        }
      }
    },
    "/create-folder": {
      "post": {
        "operationId": "createFolder",
        "summary": "지정한 경로에 폴더 생성",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "path": { "type": "string" }
                },
                "required": ["path"]
              }
            }
          },
          "responses": {
            "200": { "description": "생성 완료 메시지" }
          }
        }
      }
    },
    "/write-file": {
      "post": {
        "operationId": "writeFile",
        "summary": "지정 경로에 파일 작성",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "path": { "type": "string" },
                  "content": { "type": "string" }
                },
                "required": ["path", "content"]
              }
            }
          },
          "responses": {
            "200": { "description": "작성 완료 메시지" }
          }
        }
      }
    },
    "/list-files": {
      "get": {
        "operationId": "listFiles",
        "summary": "지정 폴더의 파일 목록 조회",
        "parameters": [
          {
            "name": "folder",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "파일 목록" }
        }
      }
    },
    "/list-files-recursive": {
      "get": {
        "operationId": "listFilesRecursive",
        "summary": "지정 폴더의 하위 디렉토리 포함 전체 파일 구조 조회",
        "parameters": [
          {
            "name": "folder",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "파일 트리 구조" }
        }
      }
    },
    "/read-file": {
      "get": {
        "operationId": "readFile",
        "summary": "파일 읽기",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "파일 내용" }
        }
      }
    },
    "/delete-file": {
      "delete": {
        "operationId": "deleteFile",
        "summary": "파일 삭제",
        "parameters": [
          {
            "name": "path",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "삭제 완료" }
        }
      }
    },
    "/clear-folder": {
      "post": {
        "operationId": "clearFolder",
        "summary": "폴더 내 모든 파일 삭제",
        "parameters": [
          {
            "name": "folder",
            "in": "query",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "삭제 완료" }
        }
      }
    }
  }
}

 

이후 아까의 구성으로 돌아와 "지침" 항목에 다음과 같이 작성한다.

너는 파일 시스템을 제어하는 도우미야. 사용자의 요청에 따라 아래 기능을 적절히 호출해:

- “/tmp/logs 폴더 만들어줘” → createFolder
- “/tmp/test.txt에 '안녕'이라고 써줘” → writeFile
- “/tmp에 뭐 있어?” → listFiles
- “/tmp 전체 폴더 구조 알려줘” → listFilesRecursive
- “/tmp/test.txt 내용 보여줘” → readFile
- “/tmp/test.txt 지워줘” → deleteFile
- “/tmp 폴더 비워줘” → clearFolder
- “MCP 서버 켜져 있어?” → ping

모든 응답은 친절하고 간결하게 설명해줘.

 

이렇게 MCP를 배포하고 Custom GPT에 연결이 완료되었다.


 

실제로 MCP 서버에 정의된 값들을 잘 활용하는지 테스트 해본다.

위의 작업을 하며 이미 Render에서 배포된 서버가 슬립 상태로 넘어갔기 때문에 우선 서버를 깨우며 시작한다.

 

위와 같은 상황에 Render 프로젝트의 로그를 보면 다음과 같이 슬립 상태에서 다시 Running 상태로 변경되는걸 확인할 수 있다.

 

폴더 및 파일 생성을 진행해본다.

 

Git에 올라와있는 main.py를 수정하면 Render 프로젝트 화면에서 "Manual Deploy > Deploy latest commit" 버튼으로 적용할 수 있다.

 


이렇게 배포된 MCP 서버의 파일시스템을 제어하는 Custom GPT + MCP 서버를 완성 해봤다.

MCP 서버에서 파일시스템 제어 뿐 아닌 외부 API 활용 또는 DB접속 등의 기능이 정의되어있다면 ChatGPT에게 자연어 명령으로 정의된 시스템을 제어할 수 있을 것이다.


 

이후 Render가 아닌 AWS 서버로 MCP를 구성해 다루는 실습도 해보았다.

이 글 만큼 자세히 쓴 글은 아니지만 활용성이 훨씬 높아지는 예제라서 관심 있는 분들은 보시면 좋을듯

 

[ MCP ] ChatGPT + AWS + Terraform

지난 포스팅에서 Render를 활용해 간단하게 무료로 MCP 서버를 만들고 Custom GPT에 연결하는 실습을 했다. [ MCP ] ChatGPT + MCP 서버지난번에 아래 글에서 Claude with MCPs에 대해 소개했었다. [ 따라잡기 ] Cl

yeznable-blog.tistory.com

 

728x90