본문 바로가기
Agentic AI/AI_AGENT

[3/3] Harness와 MCP를 결합한 AI Agent 구축

by 아톨 2026. 10. 5.

지금까지 진행한 Python → Tool Calling → Agent Loop → MCP → Skill → Harness가 실제 코드 하나로 어떻게 합쳐지는지 이해하는 단계입니다.

특히 이번에는 코드를 단순히 한 줄씩 번역하기보다는,

“이 코드가 왜 필요한가 → 실행하면 무슨 일이 일어나는가 → 데이터가 어디로 이동하는가”

를 중심으로 설명하겠습니다.


1. 먼저 전체 그림부터 잡겠습니다

우리가 지금 만든 시스템은 크게 두 개의 프로그램입니다.

┌──────────────────────────────────────────────────────────┐
│                    Harness / AI Agent                    │
│                                                          │
│  harness.py                                              │
│                                                          │
│  Plan → Skill → LLM → Tool Call → MCP Client             │
│                          ↑                │               │
│                          │                │               │
│                          └──── Tool 결과 ─┘               │
│                                                          │
│  Persistence / Retry / Context Reset                     │
└──────────────────────────┬───────────────────────────────┘
                           │
                    MCP Protocol
                           │
                           ▼
┌──────────────────────────────────────────────────────────┐
│                  MCP Server                              │
│                                                          │
│  weather_mcp_server.py                                   │
│                                                          │
│       get_weather_info()                                 │
│              │                                           │
│              ▼                                           │
│         Open-Meteo API                                   │
└──────────────────────────────────────────────────────────┘
 

아주 간단하게 말하면:

weather_mcp_server.py

"날씨를 가져오는 능력을 MCP Tool로 만들어 제공하는 프로그램"

입니다.

반면,

harness.py

"그 Tool을 AI Agent가 사용하도록 관리하고, 여러 도시의 작업을 계획하고 실행하고 저장하는 프로그램"

입니다.


2. 두 프로그램의 역할 차이

이것을 먼저 확실히 구분해 두면 코드가 훨씬 쉬워집니다.

프로그램핵심 역할
weather_mcp_server.py 날씨 Tool 제공
harness.py AI Agent 작업 관리
SKILL.md AI가 어떻게 판단할지 지시
OpenAI LLM 판단/추론
MCP Client ↔ Server 연결 규격
Open-Meteo 실제 날씨 데이터 제공
plan.json 작업 상태 저장
reports/*.md 결과 저장

따라서:

SKILL.md
   ↓
"날씨를 이렇게 분석해라"

LLM
   ↓
"날씨 데이터가 필요하군.
 get_weather_info를 호출해야겠다."

MCP Client
   ↓
"get_weather_info 호출"

MCP Server
   ↓
"알겠습니다. 제가 실행하겠습니다."

Open-Meteo
   ↓
실제 날씨 데이터

MCP Server
   ↓
결과 전달

MCP Client
   ↓
결과 전달

LLM
   ↓
SKILL.md 기준으로 분석

Harness
   ↓
report 저장 + plan 완료 처리
 

이 흐름입니다.


3. 먼저 weather_mcp_server.py부터 보겠습니다

전체 코드를 다시 놓고 보겠습니다.

 
import requests

from mcp.server.fastmcp import FastMCP


mcp = FastMCP("Open-Meteo Weather Server")


def get_coordinates(location: str):

    url = "https://geocoding-api.open-meteo.com/v1/search"

    params = {
        "name": location,
        "count": 1,
        "language": "en",
        "format": "json",
    }

    response = requests.get(
        url,
        params=params,
        timeout=10,
    )

    response.raise_for_status()

    data = response.json()

    if not data.get("results"):
        raise ValueError(
            f"도시를 찾을 수 없습니다: {location}"
        )

    result = data["results"][0]

    return {
        "name": result["name"],
        "latitude": result["latitude"],
        "longitude": result["longitude"],
        "country": result.get("country", ""),
    }


@mcp.tool()
def get_weather_info(location: str) -> dict:

    coordinates = get_coordinates(location)

    latitude = coordinates["latitude"]
    longitude = coordinates["longitude"]

    url = "https://api.open-meteo.com/v1/forecast"

    params = {
        "latitude": latitude,
        "longitude": longitude,
        "current": [
            "temperature_2m",
            "relative_humidity_2m",
            "weather_code",
        ],
        "timezone": "auto",
    }

    response = requests.get(
        url,
        params=params,
        timeout=10,
    )

    response.raise_for_status()

    data = response.json()

    current = data["current"]

    weather_code = current["weather_code"]

    condition_map = {
        0: "맑음",
        1: "대체로 맑음",
        2: "부분적으로 흐림",
        3: "흐림",
        45: "안개",
        48: "안개",
        51: "이슬비",
        53: "이슬비",
        55: "이슬비",
        61: "비",
        63: "비",
        65: "강한 비",
        71: "눈",
        73: "눈",
        75: "강한 눈",
        80: "소나기",
        81: "소나기",
        82: "강한 소나기",
        95: "뇌우",
        96: "뇌우",
        99: "강한 뇌우",
    }

    condition = condition_map.get(
        weather_code,
        "알 수 없는 날씨",
    )

    return {
        "location": coordinates["name"],
        "country": coordinates["country"],
        "temperature_c": current["temperature_2m"],
        "humidity": current["relative_humidity_2m"],
        "condition": condition,
        "weather_code": weather_code,
        "timezone": data.get("timezone"),
    }


if __name__ == "__main__":

    mcp.run(
        transport="sse"
    )
 

이제 하나씩 보겠습니다.


4. requests

 
import requests
 

이것은 Python에서 HTTP 요청을 보내기 위한 라이브러리입니다.

우리가 Open-Meteo API에:

"서울의 날씨를 알려줘."

라고 요청하려면 인터넷으로 HTTP 요청을 보내야 합니다.

그 역할을 requests가 합니다.

구조적으로 보면:

Python
  │
  │ requests.get()
  ▼
인터넷
  │
  ▼
Open-Meteo API
 

입니다.


5. FastMCP 가져오기

 
from mcp.server.fastmcp import FastMCP
 

여기서 MCP Server를 만들기 위한 FastMCP를 가져옵니다.

예전에는 MCP Server를 만들려면 여러 가지 설정을 직접 해야 했지만 FastMCP를 사용하면 비교적 간단하게 Tool을 만들 수 있습니다.


6. MCP Server 생성

 
mcp = FastMCP("Open-Meteo Weather Server")
 

이 한 줄이 중요합니다.

우리는 여기서:

"이 프로그램은 MCP Server입니다."

라고 선언하고 있는 것입니다.

이 객체 mcp가 앞으로:

  • Tool 등록
  • MCP protocol 처리
  • Client 요청 처리
  • Server 실행

등을 담당합니다.

즉:

mcp
 │
 ├── Tool 등록
 ├── Client 요청 수신
 ├── Tool 실행
 └── 결과 반환
 

이라고 생각하면 됩니다.


7. get_coordinates() 함수

다음 함수가 있습니다.

 
def get_coordinates(location: str):
 

이 함수의 역할은 단순합니다.

도시 이름을 위도/경도로 바꾸는 함수

입니다.

왜 필요할까요?

Open-Meteo의 날씨 API는 보통 다음처럼 좌표를 이용합니다.

latitude = 37.5665
longitude = 126.9780
 

그런데 사용자는:

서울
 

이라고 합니다.

따라서 중간에:

서울
 ↓
위도/경도
 ↓
날씨 API
 

가 필요합니다.


8. Geocoding API 주소

 
url = "https://geocoding-api.open-meteo.com/v1/search"
 

Open-Meteo의 Geocoding API 주소입니다.

쉽게 말하면:

"도시 이름으로 위치를 찾아주는 API"

입니다.


9. API에 전달할 파라미터

 
params = {
    "name": location,
    "count": 1,
    "language": "en",
    "format": "json",
}
 

예를 들어 location이 "서울"이라면:

name = 서울
 

입니다.

count

 
"count": 1
 

검색 결과를 하나만 가져오겠다는 의미입니다.

language

 
"language": "en"
 

검색 결과의 언어를 영어로 요청합니다.

format

 
"format": "json"
 

결과를 JSON 형태로 받겠다는 뜻입니다.


10. 실제 API 호출

 
response = requests.get(
    url,
    params=params,
    timeout=10,
)
 

이 부분에서 실제 인터넷 요청이 발생합니다.

개념적으로:

GET
https://geocoding-api.open-meteo.com/v1/search
        ?name=서울
        &count=1
        &language=en
        &format=json
 

와 비슷한 요청이 만들어집니다.


11. raise_for_status()

 
response.raise_for_status()
 

이것은 API 호출이 정상인지 확인합니다.

예를 들어:

200 OK
 

이면 정상입니다.

반대로:

404
500
503
 

같은 HTTP 오류가 발생하면 예외를 발생시킵니다.

즉:

API 호출
   ↓
정상?
 ┌─┴─┐
Yes  No
 │    │
 ▼    ▼
계속  Exception
 

입니다.


12. JSON으로 변환

 
data = response.json()
 

Open-Meteo가 보내준 JSON 데이터를 Python 객체로 변환합니다.

예를 들어 API가:

 
{
    "results": [
        {
            "name": "Seoul",
            "latitude": 37.566,
            "longitude": 126.978
        }
    ]
}
 

을 보내면 Python에서는:

 
data["results"]
 

처럼 접근할 수 있게 됩니다.


13. 도시가 없으면

 
if not data.get("results"):
 

검색 결과가 없는지 확인합니다.

예를 들어:

"아무도시"
 

라고 했는데 검색 결과가 없다면:

 
raise ValueError(
    f"도시를 찾을 수 없습니다: {location}"
)
 

를 실행합니다.

이것도 중요한 부분입니다.

예전 Mock 데이터에서는 모르는 도시가 들어오면:

 
20°C
50%
 

같은 가짜 데이터를 반환했습니다.

그런데 지금은 그렇게 하지 않습니다.

모르는 도시
   ↓
에러
 

입니다.

이것이 우리가 SKILL.md에서 강조한:

추측하지 말라

와도 맞습니다.


14. 검색 결과 하나 가져오기

 
result = data["results"][0]
 

count=1로 요청했기 때문에 첫 번째 결과를 가져옵니다.


15. 필요한 데이터만 정리해서 반환

 
return {
    "name": result["name"],
    "latitude": result["latitude"],
    "longitude": result["longitude"],
    "country": result.get("country", ""),
}
 

여기서 중요한 개념이 있습니다.

API가 제공하는 모든 데이터를 그대로 넘기지 않고 우리가 필요한 데이터만 추려서 반환합니다.

예:

Open-Meteo Geocoding API
       ↓
많은 데이터
       ↓
필요한 데이터만
       ↓
name
latitude
longitude
country
 

이런 식입니다.


16. 이제 진짜 MCP Tool입니다

가장 중요한 부분입니다.

 
@mcp.tool()
def get_weather_info(location: str) -> dict:
 

여기서:

 
@mcp.tool()
 

가 핵심입니다.

이것은 단순한 Python 함수였던:

 
get_weather_info()
 

를

MCP를 통해 외부에서 호출할 수 있는 Tool

로 등록합니다.

즉:

그냥 Python 함수

def get_weather_info():
 

에서

MCP Tool

@mcp.tool()
def get_weather_info():
 

로 바뀐 것입니다.


17. MCP Tool의 의미

이제 MCP Client가 Server에게:

"어떤 Tool이 있습니까?"
 

라고 물으면 Server는:

get_weather_info
 

를 알려줄 수 있습니다.

우리가 실제 실행 로그에서 본:

>> MCP Tools:
 - get_weather_info
 

가 바로 이것입니다.

즉, @mcp.tool()이 없으면 MCP Client가 이 함수를 Tool로 발견할 수 없습니다.


18. location: str

 
def get_weather_info(location: str) -> dict:
 

여기서:

 
location: str
 

은:

도시 이름을 문자열로 받는다.

라는 뜻입니다.

예:

 
get_weather_info("서울")
 

그리고:

 
-> dict
 

는:

결과는 dictionary 형태로 반환한다.

는 뜻입니다.


19. 도시 → 좌표

Tool 내부에서:

 
coordinates = get_coordinates(location)
 

을 호출합니다.

즉:

get_weather_info("서울")
        │
        ▼
get_coordinates("서울")
        │
        ▼
latitude / longitude
 

입니다.


20. 좌표 꺼내기

 
latitude = coordinates["latitude"]
longitude = coordinates["longitude"]
 

예를 들어:

latitude  = 37.5665
longitude = 126.9780
 

같은 값이 들어갑니다.


21. Forecast API

이제 두 번째 API를 호출합니다.

 
url = "https://api.open-meteo.com/v1/forecast"
 

이것은 실제 날씨 데이터를 가져오는 API입니다.

앞의 API와 구분해야 합니다.

Geocoding API
     ↓
"서울이 어디인가?"
     ↓
위도 / 경도

Forecast API
     ↓
"그 위치의 날씨는?"
     ↓
기온 / 습도 / 날씨 상태
 

즉 Open-Meteo API를 두 번 사용하는 것입니다.


22. 필요한 날씨 데이터 지정

 
params = {
    "latitude": latitude,
    "longitude": longitude,
    "current": [
        "temperature_2m",
        "relative_humidity_2m",
        "weather_code",
    ],
    "timezone": "auto",
}
 

여기서:

 
"latitude": latitude
"longitude": longitude
 

로 위치를 지정합니다.

그리고:

 
"current"
 

안에서 현재 필요한 날씨 데이터를 지정합니다.

기온

 
"temperature_2m"
 

습도

 
"relative_humidity_2m"
 

날씨 코드

 
"weather_code"
 

입니다.


23. 왜 날씨 상태를 문자열로 직접 받지 않을까요?

Open-Meteo는 날씨 상태를 코드로 제공합니다.

예:

0
1
2
3
45
61
95
 

등입니다.

그래서 우리가 아래에서 사람이 읽을 수 있는 한국어로 변환합니다.


24. API 호출

 
response = requests.get(
    url,
    params=params,
    timeout=10,
)
 

실제 날씨 API를 호출합니다.


25. JSON → Python

 
data = response.json()
 

API 결과를 Python 객체로 바꿉니다.

그리고:

 
current = data["current"]
 

현재 날씨 데이터만 꺼냅니다.


26. 날씨 코드

 
weather_code = current["weather_code"]
 

예를 들어:

weather_code = 0
 

일 수 있습니다.

그런데 AI에게:

weather_code = 0
 

이라고 주는 것보다:

condition = 맑음
 

이라고 주는 것이 훨씬 좋습니다.

그래서 다음 코드가 필요합니다.


27. condition_map

 
condition_map = {
    0: "맑음",
    1: "대체로 맑음",
    2: "부분적으로 흐림",
    3: "흐림",
    45: "안개",
    ...
}
 

이것은 단순한 Python dictionary입니다.

예:

 
condition_map[0]
 

→

맑음
 

입니다.


28. .get()

 
condition = condition_map.get(
    weather_code,
    "알 수 없는 날씨",
)
 

여기서 .get()의 두 번째 인자는 기본값입니다.

예를 들어:

 
weather_code = 61
 

이면:

비
 

가 됩니다.

하지만 우리가 모르는 코드:

 
weather_code = 999
 

가 들어오면:

알 수 없는 날씨
 

가 됩니다.


29. 최종 결과

가장 중요한 반환 부분입니다.

 
return {
    "location": coordinates["name"],
    "country": coordinates["country"],
    "temperature_c": current["temperature_2m"],
    "humidity": current["relative_humidity_2m"],
    "condition": condition,
    "weather_code": weather_code,
    "timezone": data.get("timezone"),
}
 

예를 들어 최종적으로:

 
{
    "location": "Seoul",
    "country": "South Korea",
    "temperature_c": 22.3,
    "humidity": 48,
    "condition": "맑음",
    "weather_code": 0,
    "timezone": "Asia/Seoul"
}
 

같은 결과가 됩니다.

이것이 MCP Tool의 결과입니다.


30. 마지막 MCP Server 실행

 
if __name__ == "__main__":
 

이것은 Python 파일을 직접 실행했을 때만 아래 코드를 실행하라는 의미입니다.

 
mcp.run(
    transport="sse"
)
 

여기서 MCP Server가 실제로 시작됩니다.

즉:

python weather_mcp_server.py
 

하면:

Python 프로그램 시작
       ↓
FastMCP Server 생성
       ↓
get_weather_info Tool 등록
       ↓
mcp.run()
       ↓
MCP Server 대기
 

상태가 됩니다.


31. 여기까지가 weather_mcp_server.py

한 문장으로 정리하면:

weather_mcp_server.py는 Open-Meteo API를 이용해 실제 날씨를 조회하는 Python 함수를 MCP Tool로 등록하고, MCP Client가 이 Tool을 호출할 수 있도록 Server로 실행하는 프로그램입니다.


32. 이제 harness.py를 보겠습니다

이 파일은 훨씬 중요합니다.

왜냐하면 지금까지 우리가 배운 개념들이 대부분 들어 있기 때문입니다.

전체 구조는:

harness.py

환경설정
   ↓
경로 설정
   ↓
Plan
   ↓
Skill 읽기
   ↓
MCP 연결
   ↓
MCP Tool 발견
   ↓
OpenAI Tool Schema 변환
   ↓
도시별 Agent 실행
   ↓
LLM
   ↓
Tool Call
   ↓
MCP Tool 실행
   ↓
결과를 LLM에 전달
   ↓
최종 답변
   ↓
파일 저장
   ↓
Plan 완료
 

입니다.


33. import

 
import asyncio
import json
import os
import pathlib
import time
 

각각의 역할을 보면:

asyncio

MCP Client가 비동기 방식으로 동작하기 때문에 필요합니다.

 
async def
await
asyncio.run()
 

등에 사용합니다.


json

plan.json을 읽고 저장하거나 Tool 인자를 처리할 때 사용합니다.


os

환경변수:

 
os.getenv()
 

를 사용하기 위해 필요합니다.


pathlib

파일 경로를 다루기 쉽게 해줍니다.

 
BASE_DIR / "skills" / "SKILL.md"
 

같은 방식이 가능합니다.


time

현재 코드에서는 사실상 사용하지 않아도 됩니다.

나중에 retry나 로그 시간 처리 등에 사용할 수 있습니다.


34. MCP Client 관련 import

 
from mcp import ClientSession
from mcp.client.sse import sse_client
 

여기가 MCP Client의 핵심입니다.

sse_client

MCP Server와 SSE 방식으로 연결합니다.

Harness
   ↓
sse_client()
   ↓
MCP Server
 

ClientSession

MCP 연결 이후 실제 MCP 작업을 수행하는 객체입니다.

예:

 
session.list_tools()
 
 
session.call_tool()
 

입니다.

즉:

sse_client
    = 연결 통로

ClientSession
    = 실제 MCP 대화 담당
 

라고 이해하면 좋습니다.


35. OpenAI Client

 
openai_client = OpenAI(
    api_key=os.getenv("OPENAI_API_KEY", "")
)
 

LLM을 호출하기 위한 OpenAI Client입니다.

그리고:

 
MODEL = os.getenv(
    "OPENAI_MODEL",
    "gpt-5.4-mini",
)
 

에서 사용할 모델을 결정합니다.


36. BASE_DIR

 
BASE_DIR = pathlib.Path(__file__).parent
 

이것은 매우 좋은 습관입니다.

현재 실행 중인 Python 파일의 폴더를 기준으로 잡습니다.

예를 들어:

C:\Dev\book_agentic_ai\test_\project_1
 

에 harness.py가 있다면:

BASE_DIR
=
C:\Dev\book_agentic_ai\test_\project_1
 

가 됩니다.


37. 파일 경로

 
SKILL_PATH = BASE_DIR / "skills" / "SKILL.md"

REPORT_DIR = BASE_DIR / "reports"

PLAN_FILE = BASE_DIR / "plan.json"
 

따라서:

project_1/
│
├── harness.py
├── plan.json
│
├── skills/
│   └── SKILL.md
│
└── reports/
 

구조가 됩니다.


38. reports 폴더 생성

 
REPORT_DIR.mkdir(exist_ok=True)
 

reports 폴더가 없으면 만들어줍니다.

이미 있으면 그냥 넘어갑니다.


39. MCP Server 주소

 
MCP_SERVER_URL = os.getenv(
    "MCP_SERVER_URL",
    "http://localhost:8000/sse",
)
 

.env에:

MCP_SERVER_URL=http://localhost:8000/sse
 

가 있다면 그것을 사용합니다.

없으면 기본값:

http://localhost:8000/sse
 

를 사용합니다.


40. Retry 설정

 
MAX_RETRIES = 3

RETRY_DELAYS = [1, 2, 4]
 

최대 3번 시도합니다.

실패하면:

1초
↓
2초
↓
4초
 

식으로 재시도하도록 만든 것입니다.

이것은 Harness의 중요한 역할 중 하나입니다.


41. initialize_plan()

 
def initialize_plan(cities):

    plan = {
        city: "pending"
        for city in cities
    }
 

예를 들어:

 
cities = [
    "서울",
    "도쿄",
    "뉴욕",
]
 

이면:

 
{
    "서울": "pending",
    "도쿄": "pending",
    "뉴욕": "pending"
}
 

이 만들어집니다.


42. Plan 저장

 
with open(
    PLAN_FILE,
    "w",
    encoding="utf-8",
) as f:

    json.dump(
        plan,
        f,
        indent=4,
        ensure_ascii=False,
    )
 

파일에 저장합니다.

결과:

 
{
    "서울": "pending",
    "도쿄": "pending",
    "뉴욕": "pending"
}
 

이 됩니다.

이것이 Harness의 State입니다.


43. load_plan()

 
def load_plan():

    with open(
        PLAN_FILE,
        "r",
        encoding="utf-8",
    ) as f:

        return json.load(f)
 

저장되어 있는 작업 상태를 읽습니다.


44. save_plan()

 
def save_plan(plan):
 

작업 상태를 다시 저장합니다.

예를 들어:

서울 completed
도쿄 pending
뉴욕 pending
 

을 저장합니다.


45. load_skill()

 
def load_skill():

    with open(
        SKILL_PATH,
        "r",
        encoding="utf-8",
    ) as f:

        return f.read()
 

SKILL.md 전체를 문자열로 읽습니다.

이 부분이 중요합니다.

Harness가 Skill을 Python 코드로 해석하는 것이 아닙니다.

그냥 텍스트로 읽어서:

LLM의 system message
 

로 전달합니다.

즉:

SKILL.md
   ↓
문자열
   ↓
LLM system message
 

입니다.


46. MCP Tool을 OpenAI Tool로 변환

여기가 아까 오류가 발생했던 부분입니다.

현재 수정된 코드는:

 
def convert_mcp_tool_to_openai(mcp_tool):

    return {
        "type": "function",
        "name": mcp_tool.name,
        "description": mcp_tool.description or "",
        "parameters": mcp_tool.input_schema,
    }
 

이 함수는 굉장히 중요합니다.

왜냐하면:

MCP Tool Schema
       ↓
OpenAI Tool Schema
 

로 바꾸기 때문입니다.


47. 왜 변환이 필요한가?

MCP와 OpenAI는 서로 다른 시스템입니다.

MCP Server는:

"나는 get_weather_info라는 Tool을 가지고 있다."
 

라고 MCP 방식으로 알려줍니다.

하지만 OpenAI LLM에게는:

 
{
    "type": "function",
    "name": "get_weather_info",
    ...
}
 

형태로 Tool을 알려줘야 합니다.

그래서 중간에서 변환합니다.

MCP
 │
 │ Tool Schema
 ▼
Harness
 │
 │ 변환
 ▼
OpenAI Tool Schema
 │
 ▼
LLM
 

이것이 Harness가 하는 통합/Orchestration 역할 중 하나입니다.


48. 우리가 아까 수정한 input_schema

현재 MCP SDK의 Tool 객체에서는:

 
mcp_tool.input_schema
 

를 사용합니다.

이것을:

 
"parameters": mcp_tool.input_schema
 

로 OpenAI Tool의 parameters에 넣습니다.

즉:

MCP Tool
 ├── name
 ├── description
 └── input_schema
          │
          ▼
OpenAI Tool
 ├── name
 ├── description
 └── parameters
 

입니다.


49. MCP 결과를 문자열로 만드는 함수

 
def extract_mcp_result(result):

    parts = []

    for content in result.content:

        if hasattr(content, "text"):
            parts.append(content.text)

        else:
            parts.append(str(content))

    return "\n".join(parts)
 

이것도 중요한 함수입니다.

MCP Tool의 결과가 단순히 Python 문자열 하나라고 가정하지 않고, MCP 결과 안의 content를 확인합니다.


50. result.content

예를 들어 MCP Tool 결과가:

content
 ├── text
 └── ...
 

형태일 수 있습니다.

그래서:

 
for content in result.content:
 

로 하나씩 확인합니다.


51. hasattr()

 
if hasattr(content, "text"):
 

이것은:

이 객체에 text라는 속성이 있는가?

를 확인합니다.

있으면:

 
parts.append(content.text)
 

로 텍스트를 꺼냅니다.


52. 최종 문자열

 
return "\n".join(parts)
 

여러 결과를 하나의 문자열로 합칩니다.

그래서 최종적으로:

{
  "location": "Seoul",
  "temperature_c": 22,
  ...
}
 

같은 텍스트가 만들어집니다.

그리고 이것을 다시 LLM에게 줍니다.


53. 이제 핵심 함수 process_city()

 
async def process_city(
    city,
    skill_instruction,
    session,
    tools,
):
 

이 함수가:

도시 하나를 AI Agent가 처리하는 부분

입니다.

예를 들어:

 
process_city("서울", ...)
 

이면 서울 하나를 처리합니다.


54. Context Reset

 
messages = [

    {
        "role": "system",
        "content": skill_instruction,
    },

    {
        "role": "user",
        "content": (
            f"{city}의 현재 날씨를 조회하고 "
            f"기상 분석 리포트를 작성해줘."
        ),
    },
]
 

이 부분이 매우 중요합니다.

매 도시마다 새로운 messages를 만듭니다.

즉:

서울
messages = 새로 생성

도쿄
messages = 새로 생성

뉴욕
messages = 새로 생성
 

입니다.

이것이 Context Reset입니다.


55. 왜 Context Reset이 필요할까요?

만약 하나의 messages를 계속 사용한다면:

서울 날씨
서울 분석
서울 결과

도쿄 날씨
도쿄 분석
...
 

이 모두 하나의 대화 기록에 들어갑니다.

그러면 Agent가 이전 도시의 정보를 참고할 가능성이 있습니다.

우리는 도시별로 독립된 작업을 원합니다.

따라서:

서울 Agent
   = 서울만 알고 시작

도쿄 Agent
   = 도쿄만 알고 시작
 

하도록 합니다.


56. Agent Loop

 
for step in range(1, 6):
 

최대 5번의 Agent Loop를 허용합니다.

왜 필요할까요?

LLM이 한 번에 끝내지 않을 수도 있기 때문입니다.

예:

Step 1
LLM → Tool Call

Step 2
Tool 결과 → LLM

Step 3
LLM → 최종 답변
 

이런 구조가 가능합니다.


57. LLM 호출

 
response = openai_client.responses.create(
    model=MODEL,
    input=messages,
    tools=tools,
    tool_choice="auto",
)
 

이 부분이 AI Agent의 두뇌입니다.

LLM에게:

  1. 지금까지의 대화
  2. Skill
  3. 사용할 수 있는 Tool

을 전달합니다.


58. LLM은 Tool을 실행하지 않습니다

이것을 반드시 기억하면 좋습니다.

LLM은:

"get_weather_info를 호출해야겠다."
 

라고 결정합니다.

그리고:

function_call
 

을 반환합니다.

실제 Tool 실행은 Harness가 합니다.

즉:

LLM
 │
 │ 판단
 ▼
Tool Call
 │
 ▼
Harness
 │
 │ 실제 실행
 ▼
MCP
 

입니다.


59. messages.extend(response.output)

 
messages.extend(response.output)
 

이것은 LLM의 출력을 대화 기록에 추가합니다.

즉:

messages
   ↓
LLM 응답
   ↓
messages에 추가
 

합니다.

이것이 중요한 이유는 다음 LLM 호출 때:

"아까 내가 Tool을 호출하려고 했다는 사실"

도 알고 있어야 하기 때문입니다.


60. Tool Call 찾기

 
tool_calls = [
    item
    for item in response.output
    if item.type == "function_call"
]
 

LLM의 응답 중에서:

function_call
 

인 것만 골라냅니다.

예를 들어 LLM이:

function_call:
    name = get_weather_info
    arguments = {"location":"서울"}
 

을 반환했다면 이것을 잡습니다.


61. Tool Call이 없다면?

 
if not tool_calls:
 

Tool을 호출하지 않았다면:

 
report_content = response.output_text
 

로 최종 답변을 가져옵니다.

즉:

LLM
 ↓
Tool Call 없음
 ↓
최종 답변
 

입니다.


62. Tool Call이 있으면

 
for tool_call in tool_calls:
 

Tool 호출 요청을 하나씩 처리합니다.


63. Tool 이름

 
tool_name = tool_call.name
 

예:

get_weather_info
 

가 됩니다.


64. Tool arguments

 
tool_args = json.loads(
    tool_call.arguments
)
 

LLM이 반환한 arguments는 JSON 문자열 형태이기 때문에 Python dictionary로 바꿉니다.

예:

'{"location":"서울"}'
 

↓

 
{
    "location": "서울"
}
 

65. 드디어 MCP 호출

가장 중요한 코드 중 하나입니다.

 
result = await session.call_tool(
    tool_name,
    arguments=tool_args,
)
 

이 한 줄이:

Harness
   ↓
MCP Client
   ↓
MCP Server
 

를 실행합니다.

예를 들어:

 
tool_name = "get_weather_info"

tool_args = {
    "location": "서울"
}
 

이면:

MCP Server에게

get_weather_info(
    location="서울"
)

실행해 주세요.
 

라고 요청하는 것입니다.


66. MCP Server에서는 무슨 일이 일어날까요?

weather_mcp_server.py에서:

 
@mcp.tool()
def get_weather_info(location):
 

가 실행됩니다.

그리고:

서울
 ↓
Geocoding API
 ↓
위도/경도
 ↓
Forecast API
 ↓
기온/습도/weather code
 ↓
한국어 날씨 상태 변환
 ↓
dict 반환
 

합니다.


67. 결과가 Harness로 돌아옵니다

 
result = await session.call_tool(...)
 

의 result에 MCP Server의 결과가 들어옵니다.

그 다음:

 
result_text = extract_mcp_result(result)
 

로 LLM에게 전달할 문자열로 바꿉니다.


68. 가장 중요한 부분: Tool 결과를 다시 LLM에게

 
messages.append(
    {
        "type": "function_call_output",
        "call_id": tool_call.call_id,
        "output": result_text,
    }
)
 

이것이 Agent Loop의 핵심입니다.

전체를 보면:

                 LLM
                  │
                  │ Tool Call
                  ▼
               Harness
                  │
                  ▼
             MCP Client
                  │
                  ▼
             MCP Server
                  │
                  ▼
             Open-Meteo
                  │
                  ▼
              결과
                  │
                  ▼
             MCP Client
                  │
                  ▼
               Harness
                  │
                  │ function_call_output
                  ▼
                 LLM
 

입니다.


69. 그래서 이것이 진짜 Agent Loop입니다

우리가 이전에 공부했던 개념과 정확히 연결됩니다.

Think
  ↓
Act
  ↓
Observe
  ↓
Think
  ↓
Respond
 

구체적으로:

LLM 판단
  ↓
Tool Call
  ↓
MCP Tool 실행
  ↓
Open-Meteo 결과
  ↓
LLM이 결과 관찰
  ↓
Skill 정책 적용
  ↓
최종 리포트
 

입니다.


70. run_harness()가 전체 지휘자입니다

이제:

 
async def run_harness():
 

를 봅니다.

이 함수는 말 그대로 Harness 전체를 실행하는 지휘자입니다.


71. Plan 확인

 
if not PLAN_FILE.exists():
    print("계획 파일이 없습니다.")
    return
 

계획이 없으면 실행하지 않습니다.

실제로 프로그램 맨 아래에서 처음 한 번 생성합니다.


72. Plan 읽기

 
plan = load_plan()
 

예:

 
{
    "서울": "pending",
    "도쿄": "completed",
    "뉴욕": "pending"
}
 

73. Skill 읽기

 
skill_instruction = load_skill()
 

SKILL.md를 읽어서 문자열로 가져옵니다.


74. MCP Server 연결

 
async with sse_client(
    MCP_SERVER_URL
) as (
    read_stream,
    write_stream,
):
 

여기서 MCP Server에 연결합니다.

우리가 실행할 때 보았던:

>> MCP Server 연결 성공
 

이 단계입니다.


75. ClientSession 생성

 
async with ClientSession(
    read_stream,
    write_stream,
) as session:
 

이제 session이라는 MCP Client 세션이 만들어집니다.

이후:

 
session.list_tools()
 
 
session.call_tool()
 

을 사용할 수 있습니다.


76. MCP 초기화

 
await session.initialize()
 

MCP Client와 Server가 서로:

"너는 누구고 어떤 기능을 지원하는가?"

등을 확인하면서 MCP 연결을 초기화합니다.


77. Tool 목록 조회

 
tool_result = await session.list_tools()
 

Server에게:

"사용할 수 있는 Tool을 알려줘."

라고 합니다.

Server가:

get_weather_info
 

를 알려줍니다.

그래서 실제 실행 결과에서:

>> MCP Tools:
 - get_weather_info
 

가 나온 것입니다.


78. MCP Tool을 OpenAI Tool로 변환

 
tools = [
    convert_mcp_tool_to_openai(tool)
    for tool in tool_result.tools
]
 

이제:

MCP 세계
 

에서:

OpenAI LLM 세계
 

로 Tool 정보를 전달할 수 있도록 바꿉니다.


79. 도시별 반복

 
for city, status in plan.items():
 

예:

서울
도쿄
뉴욕
런던
파리
 

를 하나씩 처리합니다.


80. 완료된 도시는 건너뛰기

 
if status == "completed":
    continue
 

이것이 Persistence의 핵심입니다.

예를 들어:

 
{
    "서울": "completed",
    "도쿄": "completed",
    "뉴욕": "pending"
}
 

이면:

서울 → Skip
도쿄 → Skip
뉴욕 → 실행
 

합니다.


81. Retry

 
for attempt in range(
    1,
    MAX_RETRIES + 1,
):
 

도시 하나를 최대 3번 시도합니다.

1회
 ↓ 실패
2회
 ↓ 실패
3회
 

입니다.


82. process_city() 호출

 
report_content = await process_city(
    city=city,
    skill_instruction=skill_instruction,
    session=session,
    tools=tools,
)
 

여기서 실제 Agent 작업이 시작됩니다.

즉:

run_harness()
       │
       ▼
process_city()
       │
       ▼
LLM
       │
       ▼
MCP
       │
       ▼
Open-Meteo
       │
       ▼
LLM
       │
       ▼
report
 

입니다.


83. 결과 저장

 
report_path = (
    REPORT_DIR
    / f"{city}.md"
)
 

예를 들어:

reports/서울.md
 

가 됩니다.

그리고:

 
with open(
    report_path,
    "w",
    encoding="utf-8",
) as f:

    f.write(report_content)
 

최종 결과를 파일로 저장합니다.

이것이 Harness의 Persistence입니다.


84. 상태 업데이트

 
plan[city] = "completed"
 

예:

서울 pending
 

이:

서울 completed
 

로 바뀝니다.

그리고:

 
save_plan(plan)
 

으로 저장합니다.


85. 실패하면

모든 retry가 실패하면:

 
if not success:

    plan[city] = "failed"

    save_plan(plan)
 

이 됩니다.

따라서 최종적으로:

 
{
    "서울": "completed",
    "도쿄": "completed",
    "뉴욕": "failed",
    "런던": "completed",
    "파리": "pending"
}
 

같은 상태가 가능합니다.

이것이 실제 업무 시스템에서 상당히 중요한 개념입니다.


86. 마지막 asyncio.run()

파일 마지막:

 
if __name__ == "__main__":

    cities = [
        "서울",
        "도쿄",
        "뉴욕",
        "런던",
        "파리",
    ]

    if not PLAN_FILE.exists():
        initialize_plan(cities)

    asyncio.run(
        run_harness()
    )
 

입니다.

Python 파일을 직접 실행하면:

cities 정의
     ↓
plan.json 없나?
     ↓
없음 → Plan 생성
     ↓
asyncio.run()
     ↓
run_harness()
 

가 실행됩니다.


87. 이제 정말 중요한 "전체 실행 과정"

사용자가:

 
python harness.py
 

를 실행했다고 생각해보겠습니다.

STEP 1. Plan

plan.json 확인
 

없으면:

 
{
    "서울": "pending",
    "도쿄": "pending",
    "뉴욕": "pending",
    "런던": "pending",
    "파리": "pending"
}
 

생성.


STEP 2. Skill

SKILL.md 읽기
 

↓

system message
 

로 준비.


STEP 3. MCP 연결

Harness
   ↓
SSE
   ↓
MCP Server
 

STEP 4. Tool 발견

list_tools()
 

↓

get_weather_info
 

STEP 5. Tool Schema 변환

MCP Tool Schema
       ↓
OpenAI Tool Schema
 

STEP 6. 서울 Agent 생성

system:
SKILL.md

user:
서울의 현재 날씨를 조회하고
기상 분석 리포트를 작성해줘.
 

STEP 7. LLM 판단

LLM:

날씨 데이터가 필요하다.

↓

function_call
get_weather_info
location="서울"
 

STEP 8. Harness가 MCP Tool 실행

Harness
   ↓
MCP Client
   ↓
MCP Server
 

STEP 9. MCP Server

get_weather_info("서울")
 

↓

Geocoding API
 

↓

서울의 위도/경도
 

↓

Forecast API
 

↓

기온
습도
weather_code
 

STEP 10. MCP 결과

예:

 
{
    "location": "Seoul",
    "temperature_c": 22.4,
    "humidity": 47,
    "condition": "맑음"
}
 

STEP 11. 다시 LLM

Harness가:

function_call_output
 

으로 전달합니다.

LLM:

기온 22.4℃
습도 47%
맑음
 

을 확인합니다.


STEP 12. Skill 적용

Skill:

15~25℃
습도 60% 미만
 

이면:

최적
 

이라고 판단.


STEP 13. 최종 리포트

 
### 서울 기상 분석 리포트

- 현재 날씨: 22.4°C, 맑음
  (습도: 47%)

- 전문가 분석: ...

- 오늘의 권고:
  조깅 적합도: 최적
  ...
 

STEP 14. Persistence

reports/서울.md
 

저장.

그리고:

 
{
    "서울": "completed"
}
 

로 Plan 업데이트.


88. 결국 두 파일의 관계는 이렇게 이해하면 됩니다

weather_mcp_server.py

"나는 이런 능력이 있다."

┌──────────────────────┐
│ Weather MCP Server   │
│                      │
│ get_weather_info()   │
│        ↓             │
│   Open-Meteo         │
└──────────────────────┘
 

harness.py

"그 능력을 이용해서 이 업무를 끝까지 수행하겠다."

┌─────────────────────────────┐
│ Harness                     │
│                             │
│ Plan                        │
│ Skill                       │
│ LLM                         │
│ Context Reset               │
│ MCP Client                  │
│ Agent Loop                  │
│ Retry                       │
│ Persistence                 │
└─────────────────────────────┘
 

89. 특히 SKILL.md와 MCP를 혼동하면 안 됩니다

이 부분은 앞으로 AI Agent 공부에서 굉장히 중요합니다.

SKILL

"어떻게 일할 것인가?"
 

입니다.

예:

기온과 습도를 분석해서 조깅 적합도를 판단해라.


MCP Tool

"무엇을 할 수 있는가?"
 

입니다.

예:

현재 서울 날씨를 가져올 수 있다.


LLM

"어떻게 판단할 것인가?"
 

입니다.


Harness

"전체 일을 어떻게 안정적으로 끝낼 것인가?"
 

입니다.


90. 네 가지를 한 문장으로 연결하면

SKILL은 업무 방법을 알려주고, LLM은 판단하고, MCP Tool은 실제 행동을 수행하며, Harness는 그 전체 업무가 계획대로 끝까지 수행되도록 관리합니다.

이 문장을 기억하시면 좋습니다.


91. 지금 우리가 만든 시스템을 역할별로 다시 정리

요소질문현재 구현
Skill 어떻게 일할까? SKILL.md
LLM 무엇을 판단할까? OpenAI
Tool 무엇을 실행할까? get_weather_info
MCP Server Tool을 어디서 제공할까? weather_mcp_server.py
MCP Client Server와 어떻게 연결할까? harness.py
Agent Loop 판단→실행→결과→판단을 어떻게 반복할까? process_city()
Harness 여러 작업을 어떻게 관리할까? run_harness()
Plan 어디까지 했나? plan.json
Persistence 결과를 어디에 저장하나? reports/*.md
Retry 실패하면 어떻게 하나? MAX_RETRIES

92. 그리고 지금 코드에서 가장 중요한 함수 6개

처음에는 전체 코드가 길어서 복잡해 보이지만, 실제로는 아래 6개를 중심으로 이해하면 됩니다.

① get_coordinates()

도시 → 좌표
 

② get_weather_info()

좌표 → 실제 날씨
 

③ convert_mcp_tool_to_openai()

MCP Tool → LLM이 사용할 Tool
 

④ process_city()

한 도시를 AI Agent가 처리
 

⑤ run_harness()

전체 도시 작업 관리
 

⑥ initialize_plan()

작업 계획 생성
 

이 6개가 머릿속에 들어오면 전체 코드가 상당히 단순해집니다.


93. 지금 단계에서 가장 중요한 그림

마지막으로 이것 하나는 꼭 기억하셨으면 합니다.

                  ┌───────────────┐
                  │   SKILL.md    │
                  │ "어떻게 할까" │
                  └───────┬───────┘
                          │
                          ▼
┌──────────┐        ┌───────────┐
│ Harness  │───────▶│    LLM    │
│          │        │ "판단"    │
│ Plan     │        └─────┬─────┘
│ State    │              │
│ Retry    │         Tool Call
│ Context  │              │
│ Save     │              ▼
└──────────┘        ┌───────────┐
      ▲             │ MCP Client│
      │             └─────┬─────┘
      │                   │
      │                   ▼
      │             ┌───────────┐
      │             │MCP Server │
      │             └─────┬─────┘
      │                   │
      │                   ▼
      │            get_weather_info()
      │                   │
      │                   ▼
      │             Open-Meteo
      │                   │
      │                   ▼
      │             실제 날씨
      │                   │
      │                   ▼
      └────────── LLM ← 결과
                         │
                         ▼
                    최종 리포트
                         │
                         ▼
                  reports/서울.md
 

지금까지 공부한 것들이 여기에서 하나로 합쳐졌습니다.

특히 이번 코드를 이해할 때는 weather_mcp_server.py를 "Tool 제공자", harness.py를 "AI Agent + 작업 관리자"라고 구분해서 생각하면 훨씬 쉽습니다.

그리고 다음 단계에서는 이 코드에서 **responses.create() → function_call → session.call_tool() → function_call_output → 두 번째 responses.create()**가 실제로 어떻게 이어지는지를 별도로 떼어서 보면 좋습니다.


    # ============================================================
    # Harness + AI Agent + MCP Client
    # Open-Meteo Weather Agent
    # ============================================================
    import asyncio
    import json
    import os
    import pathlib
    import time

    from dotenv import load_dotenv
    from openai import OpenAI

    from mcp import ClientSession
    from mcp.client.sse import sse_client

    # ============================================================
    # 1. 환경 설정
    # ============================================================
    load_dotenv(override=True)

    # ============================================================
    # 2. 기본 경로 설정
    # ============================================================
    BASE_DIR = pathlib.Path(__file__).parent
    SKILL_PATH = BASE_DIR / "skills" / "SKILL.md"
    REPORT_DIR = BASE_DIR / "reports"
    PLAN_FILE = BASE_DIR / "plan.json"

    # reports 폴더가 없으면 생성
    REPORT_DIR.mkdir(exist_ok=True)

    # ============================================================
    # 3. MCP Server 주소
    # ============================================================
    MCP_SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:8000/sse",)

    # ============================================================
    # 4. Retry 설정
    # ============================================================
    MAX_RETRIES = 3
    RETRY_DELAYS = [1, 2, 4]

    # ============================================================
    # 5. OpenAI Client
    # ============================================================
    openai_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY", ""))
    MODEL = os.getenv("OPENAI_MODEL", "gpt-5.4-mini",)

    # ============================================================
    # 6. Plan 초기화
    # ============================================================
    def initialize_plan(cities):
        plan = {city: "pending" for city in cities}
        with open (PLAN_FILE, "w", encoding="utf-8", ) as f:
            json.dump(plan, f, indent=4, ensure_ascii=False,)
        print(">> 새로운 작업 계획을 생성했습니다.")
       
    # ============================================================
    # 7. Plan 읽기
    # ============================================================
    def load_plan():
        with open (PLAN_FILE, "r", encoding="utf-8",) as f:
            return json.load(f)

    # ============================================================
    # 8. Plan 저장
    # ============================================================
    def save_plan(plan):
        with open(PLAN_FILE, "w", encoding="utf-8") as f:
            json.dump(plan, f, indent=4, ensure_ascii=False,)

    # ============================================================
    # 9. SKILL.md 읽기
    # ============================================================
    def load_skill():
        with open(SKILL_PATH, "r", encoding="utf-8",) as f:
            return f.read()

    # ============================================================
    # 10. MCP Tool → OpenAI Tool Schema 변환
    # ============================================================
    def convert_mcp_tool_to_openai(mcp_tool):
        return {
            "type":"function",
            "name":mcp_tool.name,
            "description":mcp_tool.description or "",
            "parameters":mcp_tool.input_schema,
        }

    # ============================================================
    # 11. MCP Tool 결과를 문자열로 변환
    # ============================================================
    def extract_mcp_result(result):
        # extract_mcp_result()는 쉽게 말하면 MCP의 데이터 표현을 LLM이 읽을 수 있는 문자열 표현으로 바꾸는 어댑터(adapter)
        parts = []
        for content in result.content:
            # result.content의 content는 MCP SDK가 만들어서 반환하는 결과 객체의 속성(즉,MCP Result 객체가 원래 가지고 있는 속성)
            # for content의 content는 우리가 만든 지역 변수(즉, 우리가 반복문에서 임의로 붙인 변수)
            if hasattr(content, "text"):
                parts.append(content.text)
            else:
                parts.append(str(content))
        return "\n".join(parts)

    # ============================================================
    # 12. 한 도시 작업
    # ============================================================
    async def process_city(city, skill_instruction, session, tools,):
        print()
        print("="*100)
        print(f">> [Agent] {city} 작업 시작")
        print("="*100)
       
        # --------------------------------------------------------
        # Context Reset
        # --------------------------------------------------------
        messages = [
            {"role":"system", "content":skill_instruction},                   ###########
            {"role":"user", "content":f"{city}의 현재 날씨를 조회하고, 기상분석 리포트를 작성해줘."},
        ]
       
        # --------------------------------------------------------
        # Agent Loop
        # --------------------------------------------------------
        for step in range(1,6):
            print(f">> [Agent Loop] Step {step}")
           
            # ----------------------------------------------------
            # LLM 호출
            # ----------------------------------------------------
            response = openai_client.responses.create(
                model=MODEL,
                input=messages,
                tools=tools,              #####################
                tool_choice="auto",
            )
            # ----------------------------------------------------
            # 모델 출력 기록
            # ----------------------------------------------------
            messages.extend(response.output)
           
            # ----------------------------------------------------
            # Tool Call 찾기
            # ----------------------------------------------------
            tool_calls = [
                item
                for item in response.output
                if item.type == "function_call"
            ]
           
            # ----------------------------------------------------
            # Tool Call이 없다면 최종 답변
            # ---------------------------------------------------
            if not tool_calls:
                report_content = (response.output_text)
                print()
                print(">> 최종 Agent 브리핑")
                print(report_content)
                return report_content
           
            # ----------------------------------------------------
            # MCP Tool 실행
            # ----------------------------------------------------
            for tool_call in tool_calls:
                tool_name = tool_call.name
                tool_args = json.loads(tool_call.arguments)
                print(f">> MCP Tool 호출: {tool_name}")
                print(f">> MCP Tool arguments: {tool_args}")
               
                # ------------------------------------------------
                # MCP Server의 Tool 실행
                # ------------------------------------------------
                try:
                    result = await session.call_tool(
                        tool_name,
                        arguments = tool_args,
                    )
                except Exception as e:
                    print(f"!! MCP Tool 오류: {e}")
                    raise
               
                # ------------------------------------------------
                # MCP 결과 → 문자열
                # ------------------------------------------------
                result_text = extract_mcp_result(result)
                print(f">> MCP Tool 결과: {result_text}")
               
                # ------------------------------------------------
                # Tool 결과를 LLM에 전달
                # ------------------------------------------------
                messages.append(
                    {
                        "type":"function_call_output",
                        "call_id":tool_call.call_id,
                        "output":result_text,
                    }
                )
       
        raise RuntimeError(f"{city} 작업이 최대 Agent Loop 횟수를 초과했습니다.")

    # ============================================================
    # 13. 전체 Harness
    # ============================================================
    async def run_harness():

        # --------------------------------------------------------
        # Plan 확인
        # --------------------------------------------------------
        if not PLAN_FILE.exists():
            print("계획 파일이 없습니다.")
            return
       
        # --------------------------------------------------------
        # Plan 읽기
        # --------------------------------------------------------
        plan = load_plan()
       
        # --------------------------------------------------------
        # Skill 읽기
        # --------------------------------------------------------
        skill_instruction = load_skill()
       
        print()
        print("="*100)
        print("Harness 시작")
        print("="*100)
       
        print(f">> MCP Server: {MCP_SERVER_URL}")
        print(f">> Model: {MODEL}")
           
        # ========================================================
        # MCP Server 연결
        # ========================================================
        async with sse_client(MCP_SERVER_URL) as (read_stream, write_stream,):
            async with ClientSession(read_stream, write_stream,) as session:
               
                # ------------------------------------------------
                # MCP 초기화
                # ------------------------------------------------
                await session.initialize()
                print()
                print(">> MCP Server 연결 성공")
               
                # ------------------------------------------------
                # MCP Tool 목록 조회
                # ------------------------------------------------
                tool_result = (await session.list_tools())
               
                print()
                print(">> MCP Tools: ")
               
                for tool in tool_result.tools:
                    print(f" - {tool.name}")
                   
                # ------------------------------------------------
                # MCP Tool → OpenAI Tool 변환
                # ------------------------------------------------
                tools = [convert_mcp_tool_to_openai(tool) for tool in tool_result.tools]
               
                # =================================================
                # 도시별 작업
                # =================================================
                for city, status in plan.items():
                   
                    # ---------------------------------------------
                    # 이미 완료된 작업은 Skip
                    # ---------------------------------------------
                    if status == "completed":
                        print(f">> {city}: 이미 완료 -> Skip")
                        continue   #pending이면 continue하지 않음.
                   
                    # ---------------------------------------------
                    # Retry
                    # ---------------------------------------------
                    success = False
                   
                    for attempt in range(1, MAX_RETRIES+1,):
                        try:
                            print()
                            print(f">> {city} 시도 {attempt}/{MAX_RETRIES}")
                           
                            # -------------------------------------
                            # 도시 처리
                            # -------------------------------------
                            report_content = (
                                await process_city(
                                    city=city,
                                    skill_instruction=(skill_instruction),
                                    session=session,
                                    tools=tools,
                                )
                            )

                            # -------------------------------------
                            # 결과 저장
                            # -------------------------------------
                            report_path = (REPORT_DIR/f"{city}.md")
                            with open (report_path, "w", encoding="utf-8") as f:
                                f.write(report_content)
                            print()
                            print(f">> 리포트 저장: {report_path}")
                           
                            # -------------------------------------
                            # State 업데이트
                            # -------------------------------------
                            plan[city]="completed"
                            save_plan(plan)
                            print(f">>{city} 작업 완료")
                           
                            success = True
                            break
                        except Exception as e:
                            print()
                            print(f"!! {city} 처리 실패: {e}")
                           
                            # -------------------------------------
                            # Retry
                            # -------------------------------------
                            if attempt < MAX_RETRIES:
                                delay = (RETRY_DELAYS[attempt-1])
                                print(f">> {delay}초 후, 재시도합니다.")
                               
                                await asyncio.sleep(delay)

                    # ------------------------------------------------
                    # 최종 실패
                    # ------------------------------------------------
                    if not success:
                        plan[city]="failed"
                        save_plan(plan)
                        print(f"!! {city} 최종실패")
       
        print()
        print("="*100)
        print("Harness 작업 종료")
        print("="*100)                
           
    # ============================================================
    # 14. 프로그램 시작
    # ============================================================
    if __name__ == "__main__":
        cities=[
            "서울특별시","Tokyo","New York","London","Paris"
        ]
       
        # --------------------------------------------------------
        # Plan이 없으면 최초 생성
        # --------------------------------------------------------
        if not PLAN_FILE.exists():
            initialize_plan(cities)
       
        # --------------------------------------------------------
        # Plan이 없으면 최초 생성
        # --------------------------------------------------------
        asyncio.run(run_harness())       
 

 

반응형