instruction.md
# 동물약국·동물용의약품 취급 약국 조회
## What this skill does
홍익메디케어가 운영하는 인증 없는 공개 Streamable HTTP MCP 서버를 직접 호출한다.
- 엔드포인트: `https://hkmedi.co.kr/pharmacy-mcp`
- 지역별 동물약국 목록 조회
- 제품명·분류·증상 키워드로 동물용의약품 검색
- 특정 제품을 최근 6개월 안에 홍익메디케어에서 구매한 약국 조회
- 별도 API key나 `k-skill-proxy` 없이 사용자 머신에서 직접 호출
이 데이터는 민간 유통사인 홍익메디케어의 거래·디렉터리 데이터다. 공공기관의
동물약국 인허가 원장이나 전국 모든 유통사의 판매 자료가 아니다.
## When to use
- "서울 강남구 동물약국 알려줘"
- "목포시 동물약국 리스트 찾아줘"
- "동물용 항생제 제품 뭐가 있어?"
- "오리더밀 취급하는 서울 약국 찾아줘"
- "피부 관련 동물약 파는 인천 약국 있어?"
## When not to use
- 동물의 증상을 진단하거나 약을 처방·추천해야 하는 요청
- 용량, 투여 주기, 병용 가능 여부를 결정하는 요청
- 현재 매장 재고를 확정하거나 구매를 자동화하는 요청
- 공공기관의 공식 인허가 상태·행정처분 확인이 필요한 요청
동물의 상태가 위급하거나 약물 선택이 필요한 경우 수의사 진료를 우선 안내한다.
## Access path
기본 경로는 bundled helper다.
```bash
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- tools
```
helper는 Python 표준 라이브러리만 사용해 MCP `initialize` 후 세션 ID를 유지하며
`tools/list` 또는 `tools/call`을 실행한다. 서버가 JSON 또는 SSE로 응답해도
동일한 JSON 결과로 정규화한다.
직접 MCP 클라이언트에 등록할 수도 있다.
```bash
claude mcp add --transport http animal-pharmacy https://hkmedi.co.kr/pharmacy-mcp
codex mcp add animal-pharmacy --url https://hkmedi.co.kr/pharmacy-mcp
```
## Tool selection
| 사용자 요청 | MCP 도구 | 주요 입력 |
| --- | --- | --- |
| 지역별 동물약국 목록 | `find_animal_pharmacies` | `city`, 선택 `gu`, `keyword`, `limit` |
| 제품명·분류·증상 키워드 검색 | `search_product` | `keyword` |
| 제품 취급 약국 조회 | `find_pharmacies_by_product` | `item_srl` 또는 `product_name`, 선택 `city`, `gu`, `limit` |
### 지역별 동물약국
```bash
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- \
call find_animal_pharmacies \
--arg city=서울 \
--arg gu=강남구 \
--arg limit=5
```
`result._meta.pharmacies`에서 약국명, 전화번호, 주소, 행정구역, 좌표를 읽는다.
사용자가 지역을 주지 않았다면 시·도와 시·군·구를 먼저 묻는다.
### 제품 검색
```bash
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- \
call search_product \
--arg keyword=항생제
```
제품명뿐 아니라 서버가 등록한 분류·증상 태그도 검색한다. `keyword`는 최소
2글자여야 한다. 결과의 `item_srl`과 `item_name`을 제시하되, 검색 결과를
진단·처방·효능 보증으로 해석하지 않는다.
### 제품 취급 약국
제품명이 충분히 구체적이면 `product_name`으로 바로 검색할 수 있다.
```bash
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- \
call find_pharmacies_by_product \
--arg product_name=오리더밀 \
--arg city=서울 \
--arg limit=5
```
제품명이 모호하거나 동명이 여러 개면 먼저 `search_product`로 `item_srl`을
확인한 뒤 정확 조회한다.
```bash
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- \
call find_pharmacies_by_product \
--arg item_srl=4452 \
--arg city=서울 \
--arg limit=5
```
## Provenance and interpretation
`find_pharmacies_by_product` 결과에는 반드시 아래 의미를 함께 전달한다.
- 약국은 **최근 6개월 안에 홍익메디케어에서 해당 제품을 구매한 이력** 기준이다.
- 이 기준은 해당 약국의 과거 취급 근거이지 현재 재고·판매 가능 여부 보장이 아니다.
- 다른 유통사를 통한 구매나 전국 모든 동물약국을 포괄하지 않을 수 있다.
- 방문 전에 전화로 제품명과 현재 재고를 확인하도록 안내한다.
`find_animal_pharmacies`는 지역 디렉터리이며 제품 취급 여부를 뜻하지 않는다.
제품까지 확인하려면 별도로 `find_pharmacies_by_product`를 호출한다.
## Response style
- 보통 3~5곳만 약국명, 전화번호, 주소 순으로 정리한다.
- 제품 검색은 제품명과 `item_srl`을 함께 보여준다.
- 취급 약국 결과에는 최근 6개월 홍익메디케어 구매 이력 기준임을 한 문장으로 명시한다.
- 좌표는 사용자가 지도 연결을 원할 때만 보조 정보로 쓴다.
- 전화번호와 주소는 조회 목적에 필요한 공개 사업장 정보로만 사용한다.
## Failure modes
- `406 Not Acceptable` 또는 SSE 요구: `Accept: application/json, text/event-stream`을 모두 보낸다.
- `keyword must be at least 2 characters`: 2글자 이상의 키워드로 다시 검색한다.
- 빈 제품 결과: 다른 제품명·분류·증상 키워드를 제안한다.
- 빈 약국 결과: `gu`를 빼고 시·도 단위로 넓히거나 지역 표기를 확인한다.
- MCP 세션 오류: 새 `initialize`로 세션을 다시 만든다. 무한 재시도하지 않는다.
- 연결 실패·5xx: 홍익메디케어 민간 MCP 장애로 보고 현재 조회 불가를 알린다.
- 현재 재고 확인 요청: MCP 결과만으로 확정하지 않고 약국 전화 확인을 안내한다.
## Privacy
- 인증·로그인·개인정보 입력이 없는 공개 조회 전용이다.
- 사용자나 반려동물의 의료정보를 서버에 전달하지 않는다.
- 진단·처방·복약 결정을 대신하지 않는다.
## Done when
- 지역 목록, 제품 검색, 제품 취급 약국 중 맞는 도구를 선택했다.
- 실제 MCP 응답의 `_meta` 구조를 기준으로 결과를 정리했다.
- 제품 취급 약국에는 최근 6개월 홍익메디케어 구매 이력이라는 출처와 한계를 명시했다.
- 현재 재고는 보장되지 않으므로 방문 전 전화 확인을 안내했다.
- 진단·처방 없이 조회 결과만 제공했다.
scripts/animal_pharmacy_mcp.py
#!/usr/bin/env python3
"""홍익메디케어 공개 Streamable HTTP MCP 서버를 호출한다."""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.request
from typing import Any, Sequence
DEFAULT_ENDPOINT = "https://hkmedi.co.kr/pharmacy-mcp"
DEFAULT_TIMEOUT_SECONDS = 30.0
PROTOCOL_VERSION = "2025-03-26"
USER_AGENT = "k-skill-animal-pharmacy/1.0"
class AnimalPharmacyMcpError(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="홍익메디케어 동물약국 MCP 도구를 호출합니다.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"예시:\n"
" animal_pharmacy_mcp.py tools\n"
" animal_pharmacy_mcp.py call find_animal_pharmacies --arg city=서울 --arg gu=강남구\n"
" animal_pharmacy_mcp.py call search_product --arg keyword=항생제\n"
" animal_pharmacy_mcp.py call find_pharmacies_by_product --arg product_name=오리더밀 --arg city=서울\n"
),
)
parser.add_argument(
"--endpoint",
default=os.getenv("ANIMAL_PHARMACY_MCP_ENDPOINT", DEFAULT_ENDPOINT),
help="동물약국 MCP 엔드포인트(기본값: %(default)s).",
)
parser.add_argument(
"--timeout-seconds",
type=parse_positive_float,
default=DEFAULT_TIMEOUT_SECONDS,
help="각 MCP HTTP 요청 제한 시간(기본값: %(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 도구 하나를 호출하고 결과를 JSON으로 출력합니다."
)
call_parser.add_argument(
"tool",
choices=[
"find_animal_pharmacies",
"search_product",
"find_pharmacies_by_product",
],
help="호출할 동물약국 MCP 도구명.",
)
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="도구 인자입니다. 반복 지정할 수 있습니다.",
)
return parser.parse_args(argv)
def parse_mcp_response(raw: bytes, content_type: str) -> dict[str, Any]:
text = raw.decode("utf-8")
if "text/event-stream" in content_type:
data_lines = [
line[6:] for line in text.splitlines() if line.startswith("data: ")
]
if not data_lines:
raise AnimalPharmacyMcpError("MCP SSE 응답에 data 이벤트가 없습니다")
text = data_lines[-1]
try:
payload = json.loads(text)
except json.JSONDecodeError as exc:
raise AnimalPharmacyMcpError(
f"MCP 응답이 올바른 JSON이 아닙니다: {exc}"
) from exc
if not isinstance(payload, dict):
raise AnimalPharmacyMcpError("MCP 응답이 JSON 객체가 아닙니다")
if "error" in payload:
error = payload["error"]
message = error.get("message", str(error)) if isinstance(error, dict) else str(error)
raise AnimalPharmacyMcpError(message)
return payload
def post_rpc(
endpoint: str,
payload: dict[str, Any],
*,
timeout_seconds: float,
session_id: str | None = None,
) -> tuple[dict[str, Any], str | None]:
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"User-Agent": USER_AGENT,
}
if session_id:
headers["Mcp-Session-Id"] = session_id
request = urllib.request.Request(
endpoint,
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers=headers,
method="POST",
)
try:
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
parsed = parse_mcp_response(
response.read(),
response.headers.get("Content-Type", ""),
)
return parsed, response.headers.get("Mcp-Session-Id") or session_id
except urllib.error.HTTPError as exc:
detail = exc.read().decode("utf-8", errors="replace")
raise AnimalPharmacyMcpError(
f"동물약국 MCP HTTP {exc.code}: {detail}"
) from exc
except urllib.error.URLError as exc:
raise AnimalPharmacyMcpError(
f"동물약국 MCP 연결 실패 {endpoint}: {exc.reason}"
) from exc
except TimeoutError as exc:
raise AnimalPharmacyMcpError(
f"동물약국 MCP 요청 시간이 {timeout_seconds:g}초를 초과했습니다"
) from exc
def initialize(endpoint: str, *, timeout_seconds: float) -> str:
payload, session_id = post_rpc(
endpoint,
{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {
"name": "k-skill-animal-pharmacy",
"version": "1.0.0",
},
},
},
timeout_seconds=timeout_seconds,
)
if "result" not in payload:
raise AnimalPharmacyMcpError("MCP initialize 응답에 result가 없습니다")
if not session_id:
raise AnimalPharmacyMcpError("MCP initialize 응답에 세션 ID가 없습니다")
return session_id
def run_mcp(
endpoint: str,
command: str,
tool: str | None = None,
arguments: dict[str, Any] | None = None,
*,
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
) -> Any:
session_id = initialize(endpoint, timeout_seconds=timeout_seconds)
if command == "tools":
payload = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {},
}
elif command == "call" and tool:
payload = {
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {"name": tool, "arguments": arguments or {}},
}
else:
raise AnimalPharmacyMcpError(f"지원하지 않는 명령입니다: {command}")
response, _ = post_rpc(
endpoint,
payload,
timeout_seconds=timeout_seconds,
session_id=session_id,
)
return response.get("result")
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 = run_mcp(
args.endpoint,
args.command,
getattr(args, "tool", None),
tool_args,
timeout_seconds=args.timeout_seconds,
)
except AnimalPharmacyMcpError as exc:
print(f"animal_pharmacy_mcp.py: {exc}", file=sys.stderr)
return 2
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
SKILL.md
---
name: animal-pharmacy-search
description: 홍익메디케어 공개 MCP 서버로 지역별 동물약국 목록, 동물용의약품 검색, 최근 6개월 구매 이력 기반 취급 약국을 조회한다. 조회 전용.
license: MIT
metadata:
category: health
locale: ko-KR
phase: v1
---
# animal-pharmacy-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 animal-pharmacy-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 animal-pharmacy-search
```
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/animal-pharmacy-search/instruction.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.