instruction.md
# 마이리얼트립 검색
## 이 스킬이 하는 일
마이리얼트립 공식 개발자센터의 MCP 문서에 공개된 엔드포인트를 사용해 여행 검색을 수행한다.
- MCP 엔드포인트: `https://mcp-servers.myrealtrip.com/mcp`
- 문서: <https://docs.myrealtrip.com/#/api/mcp/overview>
- 연결 가이드: <https://docs.myrealtrip.com/#/api/mcp/setup>
- 인증: 문서 기준 별도 인증 없음
지원 범위:
- 국내선/국제선 항공권 검색
- 항공권 날짜별 최저가 캘린더와 프로모션 항공사 조회
- 숙소 검색 및 상세/객실/리뷰/편의시설 조회
- 투어·티켓·액티비티 검색, 상세, 날짜별 옵션/가격/예약 가능 여부 조회
- 현재 한국 시간(KST) 조회
## 동작 방식
```
Claude Code / Codex / Hermes Agent
→ myrealtrip-search/scripts/myrealtrip_mcp.py
→ https://mcp-servers.myrealtrip.com/mcp (Streamable HTTP MCP)
├─ searchDomesticFlights / searchInternationalFlights
├─ flightsFareCalendar / getPromotionAirlines
├─ searchStays / getStayDetail
├─ getCategoryList / searchTnas / getTnaDetail / getTnaOptions
└─ getCurrentTime
```
이 스킬은 화면 scraping이 아니라 마이리얼트립이 공개한 MCP 서버를 직접 호출한다. 따라서 DOM 선택자나 브라우저 자동화에 의존하지 않는다.
## 사용할 때
- "마이리얼트립에서 제주 항공권 찾아줘"
- "다음 주 오사카 가는 최저가 항공권 봐줘"
- "부산 해운대 호텔 2박 검색해줘"
- "이 호텔 객실 타입이랑 취소정책 자세히 봐줘"
- "오사카 유니버설 스튜디오 티켓 가격 확인해줘"
- "도쿄 디즈니랜드 날짜별 옵션 확인해줘"
- "항공권 특가 항공사 있어?"
## 사용하지 않을 때
- 실제 예약/결제/로그인이 필요한 경우
- 마이리얼트립 계정의 예약 내역이나 개인화 데이터가 필요한 경우
- 항공권·숙소·투어 외의 파트너 API 수익/예약 내역 조회가 목적일 때
- 가격·재고를 법적/회계적 증빙 수준으로 확정해야 할 때
## 준비 사항
Python MCP SDK가 필요하다. 없으면 설치한다.
```bash
python3 -m pip install mcp
```
래퍼는 MCP Python SDK 1.x(`streamablehttp_client`)와 2.x(`streamable_http_client`) 모두에서 동작한다. `pip install mcp`는 최신 2.x를 설치한다.
기본 엔드포인트는 공식 문서의 URL이다. 필요할 때만 환경변수로 바꾼다.
```bash
export MYREALTRIP_MCP_ENDPOINT="https://mcp-servers.myrealtrip.com/mcp"
```
Hermes Agent에 MCP 서버로 직접 등록하려면 다음 설정을 쓸 수 있다.
```yaml
mcp_servers:
myrealtrip:
url: "https://mcp-servers.myrealtrip.com/mcp"
```
CLI별 공식 연결 명령은 문서 기준 다음과 같다.
```bash
claude mcp add --transport http myrealtrip https://mcp-servers.myrealtrip.com/mcp
codex mcp add myrealtrip --url https://mcp-servers.myrealtrip.com/mcp
gemini mcp add -t http -s user myrealtrip https://mcp-servers.myrealtrip.com/mcp
```
## 사용 흐름
### 1. 요청 유형을 분류한다
- 국내 항공: 출발지와 도착지가 모두 한국 도시/공항이면 `searchDomesticFlights`
- 국제 항공: 목적지가 해외면 `searchInternationalFlights`
- 날짜 유연 항공 최저가: `flightsFareCalendar` 후 실제 검색 도구로 재확인
- 숙소 목록: `searchStays`
- 특정 숙소 상세/객실/리뷰/편의시설: `searchStays`로 `gid` 확보 후 `getStayDetail`
- 투어·티켓·액티비티 목록: `searchTnas`
- 도시별 카테고리 필터가 필요하면 `getCategoryList` 후 `searchTnas`
- 특정 투어 상세: `searchTnas` 결과의 `gid`, `url`로 `getTnaDetail`
- 날짜별 실제 옵션/가격/가능 여부: `getTnaOptions`
### 2. 도구 목록과 입력 스키마를 확인한다
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- tools
```
### 3. 항공권 검색
국내선은 두 공항이 모두 한국일 때만 사용한다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call searchDomesticFlights \
--arg origin=GMP \
--arg destination=CJU \
--arg departDate=2026-05-20 \
--arg maxResults=10
```
국제선은 해외 목적지일 때 사용한다. 사용자가 출발 공항을 말하지 않으면 인천(`ICN`)을 기본값으로 둔다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call searchInternationalFlights \
--json '{"tripType":"ROUND_TRIP","origin":"ICN","destination":"KIX","departDate":"2026-06-10","returnDate":"2026-06-14","passengers":{"adults":1,"children":0,"infants":0},"maxResults":5}'
```
날짜가 유동적인 최저가 요청은 캘린더를 먼저 본다. 캘린더 가격은 캐시/추정값이므로, 최종 답변 전에 실제 검색 도구로 후보 날짜를 다시 확인한다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call flightsFareCalendar \
--arg from=ICN \
--arg to=NRT \
--arg departureDate=2026-06-01 \
--arg period=4 \
--arg maxResults=30
```
특가/할인 항공사 요청은 프로모션 항공사를 먼저 확인한다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call getPromotionAirlines
```
### 4. 숙소 검색과 상세 조회
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call searchStays \
--json '{"keyword":"부산 해운대","checkIn":"2026-06-10","checkOut":"2026-06-12","adultCount":2,"childCount":0,"isDomestic":true,"order":"recommended","minReviewRating":4.0}'
```
사용자가 특정 숙소의 객실, 가격, 편의시설, 리뷰, 취소정책을 묻거나 "자세히"를 요청하면 `searchStays` 결과의 `gid`로 상세를 이어서 호출한다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call getStayDetail \
--json '{"gid":123456,"checkIn":"2026-06-10","checkOut":"2026-06-12","adultCount":2,"childCount":0}'
```
### 5. 투어/티켓/액티비티 검색과 옵션 확인
도시별 카테고리 필터를 쓰려면 카테고리 값을 추측하지 말고 먼저 조회한다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call getCategoryList --arg city=Osaka
```
검색어는 한국어가 가장 잘 맞는다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call searchTnas \
--arg query="오사카 유니버설 스튜디오" \
--arg perPage=5
```
상세는 검색 결과의 `gid`와 `url`을 같이 넘긴다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call getTnaDetail \
--json '{"gid":"123456","url":"https://www.myrealtrip.com/offers/123456"}'
```
날짜별 예약 가능 여부와 실제 가격은 `getTnaOptions`로 확인한다.
```bash
npx -y @nomadamas/k-skill@0 exec myrealtrip-search scripts/myrealtrip_mcp.py -- call getTnaOptions \
--json '{"gid":"123456","url":"https://www.myrealtrip.com/offers/123456","selectedDate":"2026-06-10"}'
```
## 사용 가능한 도구
| 도구명 | 용도 | 주요 입력 |
|--------|------|-----------|
| `getCurrentTime` | 현재 KST 조회 | 없음 |
| `searchDomesticFlights` | 국내선 항공권 검색 | `origin`, `destination`, `departDate`, `returnDate`, `passengers`, `cabinClass`, `maxResults` |
| `searchInternationalFlights` | 국제선 항공권 검색 | `origin`, `destination`, `departDate`, `returnDate`, `passengers`, `cabinClass`, `directFlightOnly`, `preferredAirline` |
| `flightsFareCalendar` | 날짜별 최저가 캘린더 | `from`, `to`, `departureDate`, `period`, `international`, `transfer` |
| `getPromotionAirlines` | 프로모션 항공사 조회 | 없음 |
| `searchStays` | 숙소 목록 검색 | `keyword`, `checkIn`, `checkOut`, `adultCount`, `childCount`, `isDomestic`, 가격/평점/시설 필터 |
| `getStayDetail` | 숙소 상세/객실/리뷰/편의시설 | `gid`, `checkIn`, `checkOut`, `adultCount`, `childCount` |
| `getCategoryList` | 도시별 TNA 카테고리 조회 | `city` |
| `searchTnas` | 투어/티켓/액티비티 검색 | `query`, `page`, `perPage`, `category`, `sort` |
| `getTnaDetail` | TNA 상품 상세 | `gid`, `url` |
| `getTnaOptions` | 날짜별 예약 가능 여부와 실제 가격 | `gid`, `url`, `selectedDate` |
## 응답 형식
래퍼는 MCP `CallToolResult`를 JSON으로 출력한다. 보통 `content[].text`에 사람이 읽을 수 있는 검색 결과가 들어온다.
```json
{
"content": [
{
"type": "text",
"text": "검색 결과 ..."
}
],
"isError": false
}
```
답변할 때는 원문을 그대로 길게 붙이지 말고 다음 기준으로 정리한다.
- 항공: 항공사, 출발/도착 시각, 총 소요시간, 직항/경유, 가격, 예약 URL
- 숙소: 숙소명, 지역, 1박/총액 가격, 평점, 핵심 장점, 상세 확인 필요 여부
- 투어/TNA: 상품명, 시작가, 평점/리뷰, 포함사항 핵심, 날짜별 옵션 확인 결과, URL
- 후보가 많으면 상위 3~5개만 비교
- 가격·좌석·재고·예약 가능 여부는 실시간 변동 가능하다고 명시
## 실패 상황과 대응
- `Python 패키지 'mcp'가 필요합니다` 또는 ImportError 내용이 함께 나온 경우: `python3 -m pip install mcp` 후 재시도한다. 패키지는 설치됐는데 심볼을 못 찾는다면 SDK 1.x/2.x 호환 래퍼가 최신 k-skill인지 확인한다.
- MCP 엔드포인트 연결 실패: `MYREALTRIP_MCP_ENDPOINT` 오타, 네트워크, upstream 장애를 확인한다.
- 빈 결과: 날짜/지역/인원/공항코드를 바꿔 재검색한다.
- 항공 검색 실패: 국내/국제 도구 선택이 맞는지 확인한다. 해외 목적지는 반드시 `searchInternationalFlights`다.
- 숙소 상세 실패: `searchStays` 결과의 `gid`가 최신인지 확인하고 같은 날짜로 다시 검색한다.
- TNA 옵션 실패: `getTnaOptions`에는 `searchTnas` 결과의 `gid`와 `url`, `YYYY-MM-DD` 날짜가 모두 필요하다.
- 캘린더 가격 불일치: `flightsFareCalendar`는 캐시/추정값이므로 실제 검색 결과를 우선한다.
- 예약/결제 요청: 자동화하지 말고 예약 URL을 제공해 사용자가 직접 진행하게 한다.
## 완료 기준
- 요청 유형에 맞는 MCP 도구를 선택했다.
- 필요한 날짜, 인원, 공항/도시, `gid`/`url` 등 필수 입력을 확보했다.
- 실제 MCP 호출 결과를 바탕으로 상위 후보를 요약했다.
- 가격·재고·예약 가능 여부의 변동 가능성을 안내했다.
- 예약/결제는 자동화하지 않고 마이리얼트립 URL을 제공했다.
references/DISCLAIMER.md
# DISCLAIMER — `myrealtrip-search`
이 스킬은 마이리얼트립·항공사·숙박업체·예약 제공자의 공식 기능 또는 공식 지원 도구가 아니며, 공식 제휴·후원·승인·인증 또는 협업한 사실이 전혀 없습니다. 상표와 서비스명은 여행상품 조회·예약 대상을 설명하기 위해서만 사용합니다.
대법원 2005. 6. 10. 선고 [2005도1637 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=83920)(소니용 리모컨 사건)은 기능 설명용 표장과 출처표시를 구별했습니다. [상표법 제2조](https://www.law.go.kr/법령/상표법/제2조), [제89조](https://www.law.go.kr/법령/상표법/제89조), [제90조](https://www.law.go.kr/법령/상표법/제90조), [제108조](https://www.law.go.kr/법령/상표법/제108조)에 따른 출처 혼동 판단은 별도입니다.
대법원 2022. 5. 12. 선고 [2021도1533 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=221765)은 공개정보 수집만으로 곧바로 [정보통신망법 제48조](https://www.law.go.kr/법령/정보통신망이용촉진및정보보호등에관한법률/제48조) 위반이 되는 것은 아닌 사정을 제시했지만, 크롤링·예약 선점·결제 자동화를 일반적으로 허용하지 않습니다. [저작권법 제93조](https://www.law.go.kr/법령/저작권법/제93조)과 [형법 제314조 제2항](https://www.law.go.kr/법령/형법/제314조)은 별도입니다.
- 공개 여행정보 자동 수집은 반드시 개인의 정보 조회용으로만 사용합니다.
- 조직적·대량 크롤링, 별도 DB 구축, 반복 예약 선점이나 구매 자동화에 사용하지 않습니다.
- 접근통제·CAPTCHA·차단·rate limit을 우회하거나 예약 서비스의 정상 영업을 방해하지 않습니다.
이 문서는 적법성을 보증하는 법률 자문이 아닙니다.
references/TRADEMARK-LEGAL-STATEMENT.md
# 상표 사용 법적 고지 — `myrealtrip-search`
이 스킬에서 `마이리얼트립` 및 `MyRealTrip` 명칭은 항공권·숙소·투어·티켓 검색의 **대상 서비스**를 식별하기 위해 사용한다. k-skill의 출처를 마이리얼트립 운영사로 표시하려는 사용이 아니다.
대법원 2005. 6. 10. 선고 [2005도1637 판결](https://www.law.go.kr/LSW/precInfoP.do?precSeq=83920)은 타인의 표장을 출처표시가 아니라 상품 기능 또는 적용 기종을 밝히기 위해 사용하고 상표 사용으로 인식될 수 없는 경우 침해가 아니라고 판시했다. [상표법 제2조](https://www.law.go.kr/법령/상표법/제2조), [제89조](https://www.law.go.kr/법령/상표법/제89조), [제90조](https://www.law.go.kr/법령/상표법/제90조), [제108조](https://www.law.go.kr/법령/상표법/제108조)와 대법원 [2011다18802](https://www.law.go.kr/LSW/precInfoP.do?precSeq=167457), [2019후10418](https://law.go.kr/LSW/precInfoP.do?mode=0&precSeq=230725) 판결에 따라 실제 거래계에서 출처표시로 기능하는지는 표시 태양과 사용 경위 등을 종합해 판단해야 한다.
따라서 이 명칭은 필요한 범위의 평문으로만 사용하고, 별도 근거 없이 로고, 공식·제휴·후원·인증·파트너 표현을 사용하지 않는다. 각 상표의 권리는 해당 권리자에게 있다. 이 문서는 여행상품 계약, 저작권, 데이터베이스권, 부정경쟁방지법, 약관·계약, 개인정보 또는 예약·결제 흐름의 적법성을 판단하지 않는다.
전체 검토: [제3자 상표의 기능 설명·호환 대상 표시 검토](https://github.com/NomaDamas/k-skill/blob/dev/docs/legal/trademark-use-review.md)
scripts/myrealtrip_mcp.py
#!/usr/bin/env python3
"""마이리얼트립 공개 Streamable HTTP MCP 서버를 호출한다.
엔드포인트는 https://docs.myrealtrip.com/#/api/mcp/overview 에 문서화되어
있으며 현재 별도 API 키가 필요하지 않다. 이 래퍼는 k-skill 사용자가
단순 조회와 도구 호출을 위해 MCP 클라이언트 코드를 직접 작성하지 않아도
되게 해준다.
"""
from __future__ import annotations
import argparse
import asyncio
import importlib
import json
import os
import sys
from typing import Any, Sequence
DEFAULT_ENDPOINT = "https://mcp-servers.myrealtrip.com/mcp"
DEFAULT_TIMEOUT_SECONDS = 30.0
class MyRealTripMcpError(RuntimeError):
"""설정 또는 MCP 호출 실패 때 발생한다."""
def parse_json_object(raw: str, *, arg_name: str) -> dict[str, Any]:
try:
value = json.loads(raw)
except json.JSONDecodeError as exc:
raise argparse.ArgumentTypeError(f"{arg_name}은 올바른 JSON이어야 합니다: {exc}") from exc
if not isinstance(value, dict):
raise argparse.ArgumentTypeError(f"{arg_name}은 JSON 객체여야 합니다")
return value
def parse_positive_float(raw: str) -> float:
try:
value = float(raw)
except ValueError as exc:
raise argparse.ArgumentTypeError("timeout은 숫자여야 합니다") from exc
if value <= 0:
raise argparse.ArgumentTypeError("timeout은 0보다 커야 합니다")
return value
def parse_kv_pairs(pairs: Sequence[str]) -> dict[str, Any]:
args: dict[str, Any] = {}
for pair in pairs:
if "=" not in pair:
raise argparse.ArgumentTypeError(f"인자 '{pair}'는 key=value 형식이어야 합니다")
key, raw_value = pair.split("=", 1)
if not key:
raise argparse.ArgumentTypeError(f"인자 '{pair}'의 key가 비어 있습니다")
try:
value = json.loads(raw_value)
except json.JSONDecodeError:
value = raw_value
args[key] = value
return args
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Streamable HTTP로 마이리얼트립 MCP 도구를 호출합니다.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"예시:\n"
" myrealtrip_mcp.py tools\n"
" myrealtrip_mcp.py call getCurrentTime\n"
" myrealtrip_mcp.py call searchTnas --arg query='오사카 유니버설 스튜디오' --arg perPage=5\n"
" myrealtrip_mcp.py call searchDomesticFlights --json '{\"origin\":\"GMP\",\"destination\":\"CJU\",\"departDate\":\"2026-05-20\"}'\n"
),
)
parser.add_argument(
"--endpoint",
default=os.getenv("MYREALTRIP_MCP_ENDPOINT", DEFAULT_ENDPOINT),
help="마이리얼트립 MCP 엔드포인트(기본값: %(default)s).",
)
parser.add_argument(
"--timeout-seconds",
type=parse_positive_float,
default=DEFAULT_TIMEOUT_SECONDS,
help="MCP 연결/호출 전체 제한 시간(기본값: %(default)s초).",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("tools", help="사용 가능한 MCP 도구와 입력 스키마를 JSON으로 출력합니다.")
call_parser = subparsers.add_parser("call", help="MCP 도구 하나를 호출하고 CallToolResult를 JSON으로 출력합니다.")
call_parser.add_argument("tool", help="도구명. 예: searchStays, searchTnas, searchDomesticFlights.")
call_parser.add_argument(
"--json",
dest="json_args",
type=lambda raw: parse_json_object(raw, arg_name="--json"),
default=None,
help="도구 인자를 JSON 객체로 전달합니다.",
)
call_parser.add_argument(
"--arg",
dest="kv_args",
action="append",
default=[],
metavar="KEY=VALUE",
help="도구 인자입니다. VALUE는 가능하면 JSON으로 파싱하고, 아니면 문자열로 처리합니다. 반복 지정할 수 있습니다.",
)
return parser.parse_args(argv)
def tool_input_schema(tool: Any) -> dict[str, Any]:
schema = getattr(tool, "input_schema", None) or getattr(tool, "inputSchema", None) or {}
return schema if isinstance(schema, dict) else {}
def load_mcp_client() -> tuple[Any, Any]:
try:
mcp = importlib.import_module("mcp")
http = importlib.import_module("mcp.client.streamable_http")
except ImportError as exc:
raise MyRealTripMcpError(
"Python 패키지 'mcp'가 필요합니다. 다음 명령으로 설치하세요: python3 -m pip install mcp "
f"({type(exc).__name__}: {exc})"
) from exc
client_session = getattr(mcp, "ClientSession", None)
streamable = getattr(http, "streamable_http_client", None) or getattr(http, "streamablehttp_client", None)
if client_session is None or streamable is None:
raise MyRealTripMcpError(
"설치된 'mcp' 패키지에서 Streamable HTTP 클라이언트를 찾을 수 없습니다. "
"다음 명령으로 재설치하세요: python3 -m pip install mcp"
)
return client_session, streamable
async def _run_mcp_once(endpoint: str, command: str, tool: str | None, arguments: dict[str, Any] | None) -> Any:
client_session, streamable_http_client = load_mcp_client()
try:
async with streamable_http_client(endpoint) as (read_stream, write_stream, _):
async with client_session(read_stream, write_stream) as session:
await session.initialize()
if command == "tools":
result = await session.list_tools()
return [
{
"name": item.name,
"description": item.description,
"inputSchema": tool_input_schema(item),
}
for item in result.tools
]
if command == "call" and tool:
return await session.call_tool(tool, arguments or {})
except MyRealTripMcpError:
raise
except Exception as exc:
raise MyRealTripMcpError(f"마이리얼트립 MCP 엔드포인트 호출 실패 {endpoint}: {exc}") from exc
raise MyRealTripMcpError(f"지원하지 않는 명령입니다: {command}")
async def run_mcp(
endpoint: str,
command: str,
tool: str | None = None,
arguments: dict[str, Any] | None = None,
*,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
) -> Any:
try:
return await asyncio.wait_for(
_run_mcp_once(endpoint, command, tool, arguments),
timeout=timeout_seconds,
)
except TimeoutError as exc:
raise MyRealTripMcpError(
f"마이리얼트립 MCP 엔드포인트 호출 시간이 {timeout_seconds:g}초를 초과했습니다: {endpoint}"
) from exc
def jsonable(value: Any) -> Any:
if hasattr(value, "model_dump"):
return value.model_dump(mode="json")
if hasattr(value, "dict"):
return value.dict()
return value
def main(argv: Sequence[str] | None = None) -> int:
args = parse_args(argv)
tool_args: dict[str, Any] | None = None
if args.command == "call":
tool_args = dict(args.json_args or {})
tool_args.update(parse_kv_pairs(args.kv_args))
try:
result = asyncio.run(
run_mcp(
args.endpoint,
args.command,
getattr(args, "tool", None),
tool_args,
timeout_seconds=args.timeout_seconds,
)
)
except MyRealTripMcpError as exc:
print(f"myrealtrip_mcp.py: {exc}", file=sys.stderr)
return 2
print(json.dumps(jsonable(result), ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
scripts/test_myrealtrip_mcp.py
import argparse
import asyncio
import importlib.util
import pathlib
import types
import unittest
from unittest import mock
MODULE_PATH = pathlib.Path(__file__).with_name("myrealtrip_mcp.py")
spec = importlib.util.spec_from_file_location("myrealtrip_mcp", MODULE_PATH)
myrealtrip_mcp = importlib.util.module_from_spec(spec)
assert spec.loader is not None
spec.loader.exec_module(myrealtrip_mcp)
class ParseHelpersTest(unittest.TestCase):
def test_parse_json_object_requires_object(self):
self.assertEqual(myrealtrip_mcp.parse_json_object('{"gid": 123}', arg_name="--json"), {"gid": 123})
with self.assertRaises(argparse.ArgumentTypeError):
myrealtrip_mcp.parse_json_object('["not", "object"]', arg_name="--json")
def test_parse_kv_pairs_json_decodes_values_when_possible(self):
self.assertEqual(
myrealtrip_mcp.parse_kv_pairs(["query=오사카", "perPage=5", "directFlightOnly=true"]),
{"query": "오사카", "perPage": 5, "directFlightOnly": True},
)
def test_parse_kv_pairs_rejects_malformed_pairs(self):
with self.assertRaises(argparse.ArgumentTypeError):
myrealtrip_mcp.parse_kv_pairs(["missing_separator"])
with self.assertRaises(argparse.ArgumentTypeError):
myrealtrip_mcp.parse_kv_pairs(["=empty_key"])
def test_timeout_must_be_positive_number(self):
self.assertEqual(myrealtrip_mcp.parse_positive_float("1.5"), 1.5)
for raw in ["0", "-1", "not-a-number"]:
with self.subTest(raw=raw):
with self.assertRaises(argparse.ArgumentTypeError):
myrealtrip_mcp.parse_positive_float(raw)
class CliAssemblyTest(unittest.TestCase):
def test_json_and_arg_inputs_are_merged_before_call(self):
captured = {}
async def fake_run_mcp(endpoint, command, tool=None, arguments=None, *, timeout_seconds):
captured.update(
endpoint=endpoint,
command=command,
tool=tool,
arguments=arguments,
timeout_seconds=timeout_seconds,
)
return {"ok": True}
with mock.patch.object(myrealtrip_mcp, "run_mcp", side_effect=fake_run_mcp):
exit_code = myrealtrip_mcp.main(
[
"--endpoint",
"https://example.invalid/mcp",
"--timeout-seconds",
"2.5",
"call",
"searchTnas",
"--json",
'{"query":"오사카","perPage":3}',
"--arg",
"perPage=5",
]
)
self.assertEqual(exit_code, 0)
self.assertEqual(captured["endpoint"], "https://example.invalid/mcp")
self.assertEqual(captured["command"], "call")
self.assertEqual(captured["tool"], "searchTnas")
self.assertEqual(captured["arguments"], {"query": "오사카", "perPage": 5})
self.assertEqual(captured["timeout_seconds"], 2.5)
class TimeoutTest(unittest.TestCase):
def test_run_mcp_reports_timeout(self):
async def never_finishes(*args, **kwargs):
await asyncio.sleep(60)
with mock.patch.object(myrealtrip_mcp, "_run_mcp_once", side_effect=never_finishes):
with self.assertRaisesRegex(myrealtrip_mcp.MyRealTripMcpError, "초과"):
asyncio.run(
myrealtrip_mcp.run_mcp(
"https://example.invalid/mcp",
"tools",
timeout_seconds=0.001,
)
)
class McpSdkCompatTest(unittest.TestCase):
def test_tool_input_schema_accepts_v1_and_v2_attribute_names(self):
class V2:
input_schema = {"type": "object"}
class V1:
inputSchema = {"type": "object", "properties": {"q": {}}}
class Empty:
pass
self.assertEqual(myrealtrip_mcp.tool_input_schema(V2()), {"type": "object"})
self.assertEqual(myrealtrip_mcp.tool_input_schema(V1()), {"type": "object", "properties": {"q": {}}})
self.assertEqual(myrealtrip_mcp.tool_input_schema(Empty()), {})
def test_load_mcp_client_prefers_sdk_v2_name(self):
fake_session = object()
fake_v2 = object()
fake_mcp = types.SimpleNamespace(ClientSession=fake_session)
fake_http = types.SimpleNamespace(streamable_http_client=fake_v2, streamablehttp_client=object())
with mock.patch.object(myrealtrip_mcp.importlib, "import_module", side_effect=lambda name: fake_mcp if name == "mcp" else fake_http):
session, client = myrealtrip_mcp.load_mcp_client()
self.assertIs(session, fake_session)
self.assertIs(client, fake_v2)
def test_load_mcp_client_falls_back_to_sdk_v1_name(self):
fake_session = object()
fake_v1 = object()
fake_mcp = types.SimpleNamespace(ClientSession=fake_session)
fake_http = types.SimpleNamespace(streamablehttp_client=fake_v1)
with mock.patch.object(myrealtrip_mcp.importlib, "import_module", side_effect=lambda name: fake_mcp if name == "mcp" else fake_http):
session, client = myrealtrip_mcp.load_mcp_client()
self.assertIs(session, fake_session)
self.assertIs(client, fake_v1)
def test_load_mcp_client_includes_import_error_detail(self):
with mock.patch.object(myrealtrip_mcp.importlib, "import_module", side_effect=ImportError("No module named mcp")):
with self.assertRaisesRegex(myrealtrip_mcp.MyRealTripMcpError, "No module named mcp"):
myrealtrip_mcp.load_mcp_client()
if __name__ == "__main__":
def test_run_mcp_reports_timeout(self):
async def never_finishes(*args, **kwargs):
await asyncio.sleep(60)
with mock.patch.object(myrealtrip_mcp, "_run_mcp_once", side_effect=never_finishes):
with self.assertRaisesRegex(myrealtrip_mcp.MyRealTripMcpError, "초과"):
asyncio.run(
myrealtrip_mcp.run_mcp(
"https://example.invalid/mcp",
"tools",
timeout_seconds=0.001,
)
)
if __name__ == "__main__":
unittest.main()
skill.json
{
"name": "myrealtrip-search",
"description": "마이리얼트립 공식 Streamable HTTP MCP 서버로 항공권, 숙소, 투어/티켓/액티비티를 검색하고 상세·가격·예약 링크를 확인한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.",
"profiles": [
"browser",
"action:booking"
],
"frontmatter": "name: myrealtrip-search\ndescription: 마이리얼트립 공식 Streamable HTTP MCP 서버로 항공권, 숙소, 투어/티켓/액티비티를 검색하고 상세·가격·예약 링크를 확인한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.\nlicense: MIT\nmetadata:\n category: travel\n locale: ko-KR\n phase: v1"
}
SKILL.md
---
name: myrealtrip-search
description: 마이리얼트립 공식 Streamable HTTP MCP 서버로 항공권, 숙소, 투어/티켓/액티비티를 검색하고 상세·가격·예약 링크를 확인한다. 돌쇠에서는 공식 표면을 통한 후속 액션까지 진행한다.
license: MIT
metadata:
category: travel
locale: ko-KR
phase: v1
---
# myrealtrip-search
<!-- k-skill:cli-stub — generated by scripts/generate-skill-stubs.js; edit skill.json / instruction.md instead -->
## Get the full instructions (required first step)
Run this and follow its output as the primary instructions for this skill:
```bash
npx -y @nomadamas/k-skill@0 instruct myrealtrip-search
```
The CLI detects the current runtime (Dolshoi vault/CloakBrowser vs generic) and prints only the applicable instructions, always up to date. Helper files bundled with the CLI are listed by:
```bash
npx -y @nomadamas/k-skill@0 files myrealtrip-search
```
Keep the CLI and every coding-agent skill install current (including Vercel Agent Skills) with:
```bash
npx -y @nomadamas/k-skill@0 update
```
If `npx` is unavailable, install Node.js 18+ or follow https://github.com/NomaDamas/k-skill#readme, or read the source instructions at https://github.com/NomaDamas/k-skill/blob/main/myrealtrip-search/instruction.md.
## Legal disclaimer (required)
This skill is not an official feature of, officially supported by, affiliated with, sponsored by, approved by, or developed in collaboration with any third-party trademark owner or service operator it identifies. Third-party names are used only to describe the skill's function, lookup target, or compatibility.
Any automated collection of publicly accessible information must be limited to personal, non-organizational lookup. Do not use this skill for systematic or bulk crawling, database building, access-control or block circumvention, or conduct that interferes with a third party's business or service.
Read the full Korean legal disclaimer, including the cited Korean Supreme Court precedents and statutory limits, before use:
```bash
npx -y @nomadamas/k-skill@0 read myrealtrip-search references/DISCLAIMER.md
```
## Hard rules even without the CLI
- Never execute payment, message/email delivery, final submission, cancellation, or public posting without the user's explicit approval immediately beforehand.
- Never ask for, print, or store plaintext credentials in chat, files, or shell arguments.
- Never bypass legal, physical-presence, CAPTCHA, identity-proofing, or electronic-signature boundaries.