RunArt
Runnywhere(러니웨어) — 어디서든 러닝 코스 짜기!
카카오 PlayMCP Agentic Player 10 공모전 출품작. AI 채팅에 "시청에서 5km, 오르막 없이, 고래 모양으로" 라고 말하면 서울 보행 도로망 위에 뛸 수 있는 코스를 생성한다.
서울시 경사도(표고·등고선), 보행자 신호등, 가로등 위치 데이터를 러닝 친화도 점수(RFS)에 반영해 평지 우선·밤안심 코스를 제공한다. 서울시 공중화장실과 OSM 편의점 데이터도 반영해 "화장실/편의점 지나가게" 같은 요청을 코스 후보 선택에 활용한다.
PRD: ../runart-mcp-prd/PRD.md
실행
python3 -m venv .venv && source .venv/bin/activate
pip install -e '.[dev]'
python -m runart.server # http://localhost:8000/mcp (Streamable HTTP, JSON)
pytest # 테스트 (시나리오 수용 테스트 포함)
python scripts/loadtest.py 1000 10 # 콜드 미스 포함 부하 테스트 (평균 100ms / p99 3s)PlayMCP 심사 증빙용 JSON 보고서는 다음과 같이 저장한다.
RUNART_LOADTEST_REPORT=artifacts/playmcp-loadtest.json python scripts/loadtest.py 1000 10주소 검색, 재시도, 코스 생성을 포함한 MCP Tool 전체 응답에는 2.85초 상한을 적용하며, 내부 코스 생성과 외부 지오코딩도 더 짧은 같은 데드라인을 공유한다.
환경변수: HOST(로컬 기본 127.0.0.1, 컨테이너 0.0.0.0) · PORT(기본 8000) · RUNART_BASE_URL(미리보기 링크 도메인) · RUNART_RELEASE_SHA(배포 Git SHA, Docker 빌드 인자와 연동) · KAKAO_JAVASCRIPT_KEY(카카오맵 Web API, 필수) · KAKAO_REST_API_KEY(지오코딩; 없으면 지하철역·주요 지명만 해석되고 상호명·주소는 불가) · RUNART_KAKAO_CACHE_TTL_S(Kakao Local 성공 응답과 원문 검색어의 메모리 캐시 유효시간, 기본 600초) · RUNART_TOKEN_SECRET(32자 이상 도감·릴레이 토큰 서명키, 운영 환경 필수) · RUNART_LEGAL_CONTACT(정책 문의 이메일, 공개 배포 시 필수) · WEB_CONCURRENCY(웹 워커 수, 기본 1) · RUNART_POOL_WORKERS(코스 탐색 프로세스 수, 기본 2) · RATE_LIMIT_RPS(IP당, 기본 20) · RUNART_MAX_BODY_BYTES(MCP 요청 본문, 기본 65,536) · RUNART_MAX_CONCURRENT_MCP(컨테이너당 동시 MCP 요청, 기본 10) · RUNART_MAX_QUEUED_MCP(동시 처리 한도 초과 시 대기 가능한 요청, 기본 32) · RUNART_MCP_QUEUE_TIMEOUT_S(대기열 최대 체류시간, 기본 1초) · RUNART_KAKAO_WIDGETS(Kakao Tools 코스 카드, 기본 1; 문제 시 0으로 Markdown 폴백) · RUNART_ROUTE_EDIT(코스 편집, 기본 1) · RUNART_MAX_CONCURRENT_ROUTE_EDITS(동시 편집 재계산, 기본 1) · RUNART_ETL_LOCAL_ONLY=1(기존 OSM 속성은 보존하고 로컬 경사도/신호등/가로등만 재반영)
MCP 요청은 컨테이너당 10개를 동시에 처리하고, 순간적으로 몰린 요청은 최대
32개까지 1초 동안 대기한다. 그래도 용량이 부족하면 Retry-After: 1과
함께 HTTP 429를 반환한다. 이 값은 단일 컨테이너를 무제한 확장하는 설정이
아니다. 공개 배포에서는 로드밸런서 뒤에 최소 2개 복제본을 두고, 실제 KC
CPU·메모리와 429 비율을 보면서 복제본 수를 늘린다. RUNART_TRUST_PROXY_HOPS는
KC가 보장하는 프록시 홉 수를 확인한 경우에만 설정한다.
실그래프(data/seoul_graph.pkl)가 없으면 서울 시청 일대 데모 그리드로 구동된다(전체 파이프라인 동작 확인용). 공모전 제출 전 반드시 ETL 실행:
pip install -e '.[etl]'
python etl/build_graph.py # OSM 서울 전역 보행망 -> data/seoul_graph.pkl
python scripts/build_animal_presets.py --workers 2 --fresh # 고유 역 좌표 × 동물 4종 품질 우선 사전 계산
RUNART_ETL_LOCAL_ONLY=1 python etl/build_rfs.py
# 로컬 서울시 경사도 + 보행자 신호등 + 가로등 위치 + 공중화장실을 반영현재 스냅숏(data/snapshot.json, 2026-07-11): 서울 전역 보행 그래프 163,848 노드 / 232,006 엣지에 경사도 232,006개 엣지, 보행자 신호등 26,769개 포인트 기반 횡단 점수, 가로등 19,316개 포인트 기반 조명 점수를 반영했다. 편의시설은 편의점 6,693개, 화장실 4,985개, 공원 2,237개, 음수대 213개가 포함되어 있다.
pickle 형식의 그래프·시설·인프라 파일은 로드 전에 src/runart/data_integrity.py의 SHA-256과 대조한다. ETL로 이 세 파일을 다시 만들 때만 RUNART_ALLOW_UNVERIFIED_DATA=1로 검증을 임시 해제하고, 검수 후 새 체크섬을 코드에 반영해야 한다. 운영 환경에서는 검증 해제 변수를 설정하지 않는다.
동물 코스 프리셋은 stations.py의 289행을 동일 좌표 기준으로 합친 뒤 강아지·고양이·고래·토끼를 각각 11km까지 전 거리 탐색한다. 런타임의 3초 제한이나 조기 종료를 적용하지 않고 레퍼런스 실루엣 유사도를 최우선으로 선택하며, 유사도가 비슷할 때만 더 짧은 코스를 고른다. 결과는 data/animal_station_presets.json.gz에 저장되며, 적절한 코스가 없는 조합도 명시적으로 저장해 런타임 재탐색을 막는다. 그래프가 변경되면 fingerprint가 달라져 기존 프리셋은 자동 무효화된다.
런타임에는 요청 지점의 정확한 프리셋을 먼저 사용하고, 없으면 같은 동물의 검증 코스를 주변 2km에서 찾아 실제 출발역·이동 거리·도보 시간을 명시한다. 품질 게이트를 낮추지 않으면서 역×동물 즉시 추천 범위를 421개에서 905개 조합으로 넓힌다.
구조
경로 | 역할 (PRD 매핑) |
| MCP 툴 6개 + 미리보기/GPX/공유 라우트 (§5.1, §5.6) |
| RFS 가중 순환 코스 생성, ±5% 거리 허용 (§5.3) |
| 동물 모양 템플릿·스냅핑·유사도 게이트 0.7 (§5.4) |
| 러닝 친화도 점수 — 기본/야간 가중 프로파일 (§5.7) |
| 코스 10m 반경 편의시설 (§5.5) |
| 자기완결형 course_id — stateless (§5.1) |
| 오프라인 데이터 파이프라인 — 경로 생성 중 데이터 API 호출 없음 (§5.7) |
경로·안전·시설 데이터는 컨테이너에 미리 적재되어 런타임에 외부로 조회하지 않는다. 단, 출발지 해석(지오코딩)은 KAKAO_REST_API_KEY가 설정된 경우 Kakao Local API를 사용한다. 지원하는 입력은 네 가지다 — 지하철역(강남역), 가게·건물 상호명(스타벅스 서울숲점), 도로명 주소(서울 성동구 아차산로 100), 지번 주소(마포구 상암동 1601). 지하철역 289개와 주요 지명은 네트워크 없이 해석하므로 키가 없어도 동작하지만, 상호명과 임의 주소는 키가 있어야 해석된다.
툴 (7개, 모두 stateless·idempotent)
create_seoul_running_course · list_available_shapes · find_facilities_near_course · refine_course · get_course_status · record_animal_completion · extend_shape_relay
새 코스 요청 계약
등록된 툴은 위 7개와 캐시된 Preview 호환용 generate_running_course,
generate_animal_course를 합쳐 9개다. 세 생성 진입점은 같은 dispatcher를 사용한다.
출발지 없음 또는
서울 시내·아무데나: 호출은 하되missing_start로 구·역·주소를 묻는다. 거리·모양·야간 조건으로 출발지를 대신하지 않는다.서울 25개 구: 해당 구에 속한 역/프리셋만 사용한다. 주소 속 구 이름은 범위로 오해하지 않는다.
특정 역·상호명·주소 또는 좌표 쌍: 실제 해석한 출발점으로 생성한다. 한쪽 좌표만 있으면
invalid_coordinates다.공원·수변 목적지 요청(
park)만 무출발지 카탈로그 추천을 유지한다. 구를 지정하면 그 구의 등록 목적지만 반환한다.명시 거리 ±10%, 평지/언덕, 조명 ≥0.4, 추가 시설 10m 조건은 필수다. 통과한 1~3개만 반환하며 0개면 위젯 없는
insufficient_courses다. 야간 조명은 안전 보장이 아니다.include_hills=null은 지형 언급 없음,false는 평지,true는 언덕이다. 구형 두 툴의false만null로 해석하며 저장된 코스 파라미터 기본값은 변경하지 않는다.특정 출발지에서 요청한 코스가 없고 인근 대안만 있으면
start_change_confirmation_required로 질문만 반환한다. 위젯·코스 ID·지도 링크는 보내지 않는다. 사용자가 가까운 출발지의 같은 모양 또는 원래 출발지의 일반 코스를 선택한 다음 턴에confirmation_options의 인자로 통합 툴을 호출한다. 두 선택지에 모호한 “네”만 답하면 다시 구분해서 묻는다.allow_nearby_start는 기본false다. 가까운 출발지 선택에 명시적으로 동의한 경우에만true로 호출하며 원래 거리·지형·야간·시설 조건은 유지한다. 동의 전에는 정확한 출발지 카드만 제공하고, 다른 출발지 카드로 빈자리를 채우지 않는다(역 출구 허용 범위 150m 미만). 구/공원 목적지 카탈로그 계약은 유지한다.일반 코스의 첫 카드는 해당 출발지의 일반 코스다. 동의한 경우에만 2km 안의 조건 충족 동물 프리셋을 추가하고, 그 외에는 같은 출발지 변형만 제공한다. 첫 코스가 없으면 다른 출발점/종류로 대체하지 않는다.
동물 프리셋의 파라미터를 야간으로 바꿔 재사용하지 않는다. 야간 동물은 실제 생성 결과만 허용한다.
코스 수정은 지도 링크의 웹 편집기를 사용한다.
refine_course·시설 조회는 유효한course_id를 직접 전달한 경우에만 사용한다.
park는 이 경로에서 시설 POI가 아닌 검증된 공원·수변 목적지를 뜻한다.
추가 화장실·편의점·음수대는 기존 지도 경로 기준 10m로 검사한다.
기존 POI 스냅숏에는 등록 공원 경로 5곳 모두 10m 내 park 포인트가 없어,
목적지까지 POI 조건으로 검사하면 보존해야 하는 공원 추천이 전부 사라진다.
검증: .venv/bin/pytest tests/test_citywide_start.py -q.
이 테스트는 25개 구, 입력 분류, 조건 경계, 캐시 제거 후 복원,
실제 ASGI MCP initialize/list/call과 지도·편집·GPX HTTP 응답을 포함한다.
서울 동물지도(/animals)는 검증된 421개 GPS 아트를 한 화면에서 탐색하게 한다. 완주 기록은 서버 DB나 로그인 대신 자기완결형 passport_token으로 이어지며, 4종 도감·지역별 4종 배지·주간 최인접 미발견 동물을 제공한다. Shape Relay(/relay/{token})도 최대 8개 동네의 같은 동물 course_id를 자기완결형 토큰에 담아 나란히 비교하고 공동 GPS 작품으로 겹쳐 보여준다. 따라서 PlayMCP 권장 stateless/no-session 구조를 유지한다.
배포 (PlayMCP in KC)
docker build --build-arg RUNART_RELEASE_SHA=$(git rev-parse HEAD) -t runnywhere .
docker run -p 8000:8000 -e RUNART_BASE_URL=https://<kc-endpoint> runnywhereMCP Endpoint: https://<kc-endpoint>/mcp — PlayMCP 등록 전 MCP Inspector로 검증할 것.
승격·롤백 게이트
작업 트리의 검증 이미지는 운영 이미지가 아니다. 배포 전 변경을 커밋으로 고정하고
RUNART_RELEASE_SHA를 그 SHA로 다시 빌드한다. 직전 운영 이미지 digest와
9개 툴 스키마/설명을 함께 보관한다. 별도 스테이징에서 /healthz.ready=true,
release_sha 일치, 9개 툴 설명(통합 툴 900자 목표·상한 1024자)을 확인한 뒤
PlayMCP 재등록과 새 대화 Preview 발화 매트릭스를 통과해야 운영으로 승격한다.
이 변경은 DB·데이터 파일 마이그레이션이 없다. 장애 시 직전 이미지 digest로 endpoint를 되돌리고 툴 스키마/설명도 같은 버전으로 복구한다. 이후 healthz와 특정 출발지·구·무출발지 공원 요청을 다시 확인한다. 이전 이미지가 구 정책을 지원하지 않으면 이전 계약대로 동작함을 확인하고 구 정책 제공을 중단한다.
course_selection 로그는 release SHA, 범위, 구, 후보/통과 수, 조건별 탈락 수,
결과 코드, 소요 시간을 남기며 원문 주소·상호명·좌표를 기록하지 않는다.
missing_start 비율과 다음 턴 요청 전환은 호스트에서 집계해야 한다.
서버는 사용자를 연결하는 세션이나 추적 식별자를 추가하지 않는다.
라이선스·데이터·안전
소스 코드는 MIT License로 배포한다. OSM 파생 DB는 ODbL 1.0, 서울시 경사도 OA-22241·가로등 OA-22205·보행자 신호등 OA-22356·공중화장실 OA-22586·서울교통공사 역주소는 공공누리 1유형으로 이용한다. 역 좌표는 서울교통공사 1–8호선 좌표 공공데이터(이용허락 제한 없음)를 기준으로 한다. Mapzen Terrain Tiles에서 취득한 NASA SRTM 30m를 고도 폴백으로 사용하며, 상세 출처와 USGS 고지는 데이터 고지 문서에 기록한다. 안심이 CCTV 포인트는 서비스 종료로 사용하지 않고 OSM surveillance 태그만 사용한다.
세부 출처·가공·재배포 조건은 DATA_LICENSES.md, 의존성 고지는 THIRD_PARTY_NOTICES.md를 참고한다. 웹 UI의 /terms, /privacy, /data-licenses에서 이용·안전, 정보 처리, 출처를 확인할 수 있다. 코스는 실시간 내비게이션이 아닌 참고용이며, 사용자가 현장 통행·공사·날씨·건강 상태를 확인해야 한다.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Querry-AI/Runnywhere-PlayMCP'
If you have feedback or need assistance with the MCP directory API, please join our Discord server