visitkorea-medicaltourism
by leejaew
README.md
# VisitKorea Medical Tourism MCP Server


[](LICENSE)
An MCP (Model Context Protocol) server that wraps the **Korea Tourism Organization (KTO) Medical Tourism Open API** (`MdclTursmService`), exposing 8 structured tools that AI agents — including Claude and Manus AI — can call directly via Streamable HTTP.
The underlying data is curated by the **Korea Tourism Organization (KTO)** and published as an Open API on [data.go.kr](https://www.data.go.kr/data/15143913/openapi.do) (Korea's Public Data Portal) under service ID `15143913`. It covers KTO-certified medical tourism facilities across South Korea — hospitals, specialist clinics, and wellness centres — with multilingual support for 5 international languages.
## Features
- **Area-based search** — list facilities by province or city/county
- **Location-based search** — find facilities within a GPS radius (up to 20 km), using WGS84 coordinates
- **Keyword search** — full-text search across all regions
- **Sync list** — full dataset synchronisation list for building and maintaining local databases
- **Detail records** — common info, introductory info (hours, parking, capacity), and medical-specific info (specialties, languages, reservation status)
- **8 MCP tools** — one per API operation, with full parameter documentation in docstrings
- **5 languages** — English, Japanese, Simplified Chinese, Korean, Russian
- **TTL response cache** — district codes cached 24 h; all other responses cached 5 min (≈6× speedup on cache hits)
- **Rate limiter** — 10 upstream calls/min, burst of 5; fast-fail design returns an immediate error when the limit is hit rather than blocking
- **Input validation** — all parameters validated before the upstream request; GPS bounds, date format, sort codes, and language codes checked with clear error messages
- **Health endpoint** — `GET /healthz` for production liveness probes
## Prerequisites
- Python 3.11+
- A valid API key from [data.go.kr](https://www.data.go.kr/data/15143913/openapi.do) (service ID `15143913`)
- Replit account (for deployment)
## Installation & Setup
### 1. Get an API key
1. Visit [https://www.data.go.kr/data/15143913/openapi.do](https://www.data.go.kr/data/15143913/openapi.do)
2. Sign in or create a 공공데이터포털 account
3. Click **활용신청** (Request API access) for service `MdclTursmService`
4. After approval (~10 minutes), retrieve your key from My Page
Either key variant works — the server normalises both automatically at startup:
| Key variant | Where to find it | Notes |
|---|---|---|
| 일반 인증키 **(Encoding)** | My Page → 발급받은 키 | URL-percent-encoded; the server decodes it once before use |
| 일반 인증키 **(Decoding)** | My Page → 발급받은 키 | Raw base64; used as-is |
### 2. Set the Replit Secret
Open the **Secrets** tab in Replit and add:
| Secret name | Value |
|---|---|
| `VISITKOREA_API_KEY` | Your API key (Encoding or Decoding variant — either works) |
### 3. Install dependencies
Replit handles this automatically on first run. To install manually:
```bash
pip install -r mcp-server/requirements.txt
```
**`requirements.txt`:**
```
mcp[cli]
httpx
python-dotenv
```
### 4. Run the server
```bash
python mcp-server/main.py
```
The server starts on the `PORT` environment variable (default `8000`).
| Endpoint | Description |
|---|---|
| `POST /mcp` | Streamable HTTP MCP endpoint (all tool calls) |
| `GET /healthz` | Liveness probe — returns `{"status":"ok","server":"visitkorea-medicaltourism"}` |
## Connecting an AI Agent
### Connector JSON
Paste this into your AI agent's custom connector settings:
```json
{
"mcpServers": {
"visitkorea-medicaltourism": {
"url": "https://<your-replit-url>/mcp"
}
}
}
```
Replace `<your-replit-url>` with your deployed Replit project domain.
### Manus AI
Go to **Settings → Connectors → Add Connectors → Custom MCP**, click **Import by JSON**, and paste the JSON above. Do not add a `"type"` field — Manus AI infers Streamable HTTP from the URL automatically.
### Claude Desktop
Add the JSON to your `claude_desktop_config.json` under the `"mcpServers"` key.
## Tool Reference
All tools follow the same call pattern — they accept a `lang_div_cd` and return a list of dicts. Pagination defaults to 10 results per page starting at page 1.
### Recommended workflows
| Goal | Sequence |
|---|---|
| Search by region | `get_ldong_code` → `get_area_based_list` → `get_detail_common` / `get_detail_medical` |
| Search near a location | `get_location_based_list` → `get_detail_common` / `get_detail_medical` |
| Search by keyword | `search_medical_by_keyword` → `get_detail_common` / `get_detail_medical` |
| Build a local database | `get_medical_sync_list` |
---
### Tool 1 — `get_ldong_code`
Retrieve legal administrative district (법정동) codes for province/city and district filtering.
**Upstream endpoint:** `GET /ldongCode`
**Cache TTL:** 24 hours (codes are static reference data)
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang_div_cd` | string | Optional | Language code (default `ENG`) |
| `l_dong_regn_cd` | string | Optional | Province code — e.g. `11` = Seoul. Omit to list all provinces. |
| `l_dong_list_yn` | string | Optional | `N` = 시도/시군구 codes only (default); `Y` = full 법정동 list |
| `num_of_rows` | int | Optional | Results per page — clamped to 1–100 (default 10) |
| `page_no` | int | Optional | Page number (default 1) |
**Example response item:**
```json
{ "rnum": 1, "code": "11", "name": "Seoul" }
```
---
### Tool 2 — `get_area_based_list`
List medical tourism facilities filtered by administrative region.
**Upstream endpoint:** `GET /areaBasedList`
**Cache TTL:** 5 minutes
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang_div_cd` | string | Optional | Language code |
| `arrange` | string | Optional | Sort order: `A`=title, `C`=modified, `D`=created, `O`/`Q`/`R`=image-only variants |
| `l_dong_regn_cd` | string | Optional | Province code from `get_ldong_code` |
| `l_dong_signgu_cd` | string | Optional | District code (requires `l_dong_regn_cd`) |
| `mdfcn_dt` | string | Optional | Modified-since date filter in **YYYYMMDD** format |
| `num_of_rows` | int | Optional | Results per page (default 10) |
| `page_no` | int | Optional | Page number (default 1) |
---
### Tool 3 — `get_location_based_list`
Find medical tourism facilities within a GPS radius. Coordinates must be within South Korea's WGS84 bounding box (longitude 124–132, latitude 33–39).
**Upstream endpoint:** `GET /locationBasedList`
**Cache TTL:** 5 minutes
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang_div_cd` | string | Optional | Language code |
| `map_x` | float | **Required** | Longitude in WGS84 — e.g. `126.9780` (Seoul) |
| `map_y` | float | **Required** | Latitude in WGS84 — e.g. `37.5665` (Seoul) |
| `radius` | int | **Required** | Search radius in metres — 1 to 20,000 |
| `arrange` | string | Optional | Sort: `E`=distance asc, `S`=distance desc+image; or standard A/C/D/O/Q/R |
| `l_dong_regn_cd` | string | Optional | Optional province filter |
| `l_dong_signgu_cd` | string | Optional | Optional district filter (requires `l_dong_regn_cd`) |
| `mdfcn_dt` | string | Optional | Modified-since date filter in **YYYYMMDD** format |
| `num_of_rows` | int | Optional | Results per page (default 10) |
| `page_no` | int | Optional | Page number (default 1) |
Response items include a `dist` field (distance in metres from the provided coordinates).
---
### Tool 4 — `search_medical_by_keyword`
Full-text keyword search across all medical tourism facilities.
**Upstream endpoint:** `GET /searchKeyword`
**Cache TTL:** 5 minutes
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang_div_cd` | string | Optional | Language code |
| `keyword` | string | **Required** | Search term (automatically URL-encoded) |
| `arrange` | string | Optional | Sort order (A/C/D/O/Q/R) |
| `l_dong_regn_cd` | string | Optional | Province filter |
| `l_dong_signgu_cd` | string | Optional | District filter (requires `l_dong_regn_cd`) |
| `num_of_rows` | int | Optional | Results per page (default 10) |
| `page_no` | int | Optional | Page number (default 1) |
---
### Tool 5 — `get_medical_sync_list`
Retrieve the full medical tourism synchronisation list. Use for building or refreshing a local database of all facilities.
**Upstream endpoint:** `GET /mdclTursmSyncList`
**Cache TTL:** 5 minutes
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang_div_cd` | string | Optional | Language code |
| `showflag` | string | Optional | `1` = publicly visible only; `0` = hidden only |
| `old_content_id` | string | Optional | Last known content ID — fetch only newer records |
| `mdfcn_dt` | string | Optional | Modified-since date in **YYYYMMDD** format |
| `num_of_rows` | int | Optional | Results per page (default 10) |
| `page_no` | int | Optional | Page number (default 1) |
---
### Tool 6 — `get_detail_common`
Fetch full common detail for a specific facility: title, address, GPS, phone, homepage, and overview text.
**Upstream endpoint:** `GET /detailCommon`
**Cache TTL:** 5 minutes
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang_div_cd` | string | Optional | Language code |
| `content_id` | string | **Required** | Content ID returned by any list or search tool |
| `num_of_rows` | int | Optional | Results per page (default 1) |
| `page_no` | int | Optional | Page number (default 1) |
**Key response fields:** `contentId`, `title`, `overview`, `homepage`, `tel`, `baseAddr`, `detailAddr`, `zipCd`, `mapX`, `mapY`, `orgImage`, `thumbImage`, `lDongRegnCd`, `lDongSignguCd`, `regDt`, `mdfcnDt`
> **Note:** The upstream API returns GPS fields as lowercase `mapx`/`mapy`. This tool normalises them to `mapX`/`mapY` for consistency with the list-endpoint field names.
---
### Tool 7 — `get_detail_intro`
Fetch type-specific introductory details: opening hours, rest days, parking, capacity, and age suitability.
**Upstream endpoint:** `GET /detailIntro`
**Cache TTL:** 5 minutes
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang_div_cd` | string | Optional | Language code |
| `content_id` | string | **Required** | Content ID |
| `num_of_rows` | int | Optional | Results per page (default 1) |
| `page_no` | int | Optional | Page number (default 1) |
---
### Tool 8 — `get_detail_medical`
Fetch medical-specific details: specialties, foreign languages served, reservation status, and SNS info.
**Upstream endpoint:** `GET /detailMdclTursm`
**Cache TTL:** 5 minutes
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang_div_cd` | string | Optional | Language code |
| `content_id` | string | **Required** | Content ID |
| `num_of_rows` | int | Optional | Results per page (default 1) |
| `page_no` | int | Optional | Page number (default 1) |
**Key response fields:** `mainMdclSubjInfo` (main specialties), `svcLangInfo` (supported languages), `onlineRsvtPsblYn` (online reservation available)
---
## Language Code Reference
| Code | Language |
|---|---|
| `ENG` | English |
| `JPN` | Japanese (日本語) |
| `CHS` | Simplified Chinese (简体中文) |
| `KOR` | Korean (한국어) |
| `RUS` | Russian (Русский) |
## Error Handling
### Upstream API errors
Tool functions surface data.go.kr errors as Python exceptions with descriptive messages. The raw API key is masked (`[REDACTED]`) in all error strings before they leave the process.
| Upstream code | Exception type | Meaning |
|---|---|---|
| `00` / `0000` | — | Success; returns results |
| `03` | — | No data; returns `[]` |
| `10` | `ValueError` | Invalid request parameter |
| `11` | `ValueError` | Missing required parameter |
| `22` | `RuntimeError` | Upstream daily quota (1,000 requests/day) exceeded |
| `30` | `PermissionError` | Service key not registered |
| `31` | `PermissionError` | Service key expired |
| other | `RuntimeError` | Unexpected API error with code and message |
### Local rate limiter
The server enforces a **10 calls/minute** limit (burst of 5) on requests that reach the upstream API. Cache hits bypass the limiter entirely. When the limit is exceeded, the tool returns immediately with:
```
Rate limit: too many requests to upstream API. Please retry after Xs.
(Limit: 10 calls/min, burst 5)
```
This is a fast-fail design — no blocking sleep — so the agent receives the retry-after guidance instantly and can back off gracefully.
## Project Structure
```
visitkorea-medicaltourism-mcp/
├── mcp-server/
│ ├── main.py # FastMCP entrypoint — Streamable HTTP, /healthz endpoint
│ ├── requirements.txt # mcp[cli], httpx, python-dotenv
│ ├── README.md
│ ├── tools/
│ │ ├── ldong_code.py # get_ldong_code
│ │ ├── area_based_list.py # get_area_based_list
│ │ ├── location_based_list.py # get_location_based_list
│ │ ├── search_keyword.py # search_medical_by_keyword
│ │ ├── sync_list.py # get_medical_sync_list
│ │ ├── detail_common.py # get_detail_common
│ │ ├── detail_intro.py # get_detail_intro
│ │ └── detail_mdcl_tursm.py # get_detail_medical
│ └── utils/
│ ├── config.py # API key loading, FIXED_PARAMS, key masking
│ ├── cache.py # TTL dict cache (24h / 5min by endpoint)
│ ├── rate_limiter.py # Token bucket — 10/min, burst 5, fast-fail
│ ├── api_client.py # call_api() — wires cache, limiter, httpx, retry
│ └── validation.py # Input validation for all parameter types
└── artifacts/
├── landing/ # Developer landing page (React + Vite)
└── api-server/ # Express proxy (CORS, /api/healthz)
```
### `utils/` module responsibilities
Each module has a single, clearly bounded responsibility:
| Module | LOC | Responsibility |
|---|---|---|
| `config.py` | 41 | Loads `VISITKOREA_API_KEY` at import time, normalises Encoding/Decoding variant, exposes `FIXED_PARAMS` and `mask_key()` |
| `cache.py` | 49 | SHA-256-keyed TTL dict cache; `make_key`, `get`, `set`, `ttl_for` |
| `rate_limiter.py` | 60 | `TokenBucket` class; module-level `limiter` singleton used by `call_api` |
| `api_client.py` | 141 | `call_api(endpoint, params, num_of_rows, page_no)` — the only public function; handles the full request lifecycle |
| `validation.py` | 93 | `validate_lang`, `validate_pagination`, `validate_gps`, `validate_radius`, `validate_date`, `validate_arrange`, `validate_showflag` |
## Performance
| Scenario | Typical latency |
|---|---|
| Cache hit | < 5 ms |
| Cache miss — first `get_ldong_code` call | ~600 ms (upstream API) |
| Subsequent `get_ldong_code` calls (24h TTL) | < 5 ms |
| Cache miss — other tools | ~600 ms |
| Subsequent calls within 5 min TTL | < 5 ms |
The httpx client maintains a persistent TCP connection pool (`max_connections=10`, `max_keepalive_connections=5`) to avoid TLS handshake overhead on every request. Total request timeout is 15 s; connection timeout is 5 s.
## Environment Variables
| Variable | Required | Description |
|---|---|---|
| `VISITKOREA_API_KEY` | Yes | API key from data.go.kr (Encoding or Decoding variant) |
| `PORT` | No | Port for uvicorn to listen on (default `8000`) |
## Contributing
Contributions are welcome. Please open an issue before submitting a pull request. Ensure all changes are tested against the live API and that no API keys are committed to the repository.
## License
MIT License
Tourism data provided by the Korea Tourism Organization (KTO) via the 공공데이터포털 open API platform (`data.go.kr`). Data usage is subject to KTO terms — attribution is required for `Type1` content; `Type3` content additionally prohibits modification.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues