@nomadamas/k-skill 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/animal-pharmacy-search/instruction.md +150 -0
- package/skills/animal-pharmacy-search/scripts/animal_pharmacy_mcp.py +276 -0
- package/skills/animal-pharmacy-search/skill.json +8 -0
- package/skills/consumer-price-safety-search/instruction.md +30 -0
- package/skills/consumer-price-safety-search/skill.json +6 -0
- package/skills/coupang-product-search/instruction.md +59 -188
- package/skills/coupang-product-search/skill.json +6 -2
- package/skills/daiso-product-search/instruction.md +11 -1
- package/skills/fsc-corporate-info/instruction.md +2 -1
- package/skills/fsc-corporate-info/scripts/test_fsc_corporate_info.py +42 -0
- package/skills/g2b-sanctioned-supplier/instruction.md +2 -1
- package/skills/g2b-sanctioned-supplier/scripts/test_g2b_sanctioned_supplier.py +33 -0
- package/skills/government-support-survey/instruction.md +52 -0
- package/skills/government-support-survey/references/NOTICE.md +30 -0
- package/skills/government-support-survey/scripts/run_survey.py +71 -0
- package/skills/government-support-survey/skill.json +10 -0
- package/skills/kamis-food-price/instruction.md +66 -0
- package/skills/kamis-food-price/scripts/run_kamis.py +110 -0
- package/skills/kamis-food-price/skill.json +9 -0
- package/skills/korean-cinema-search/instruction.md +7 -1
- package/skills/market-kurly-search/instruction.md +9 -1
- package/skills/mofa-travel-safety/instruction.md +62 -0
- package/skills/mofa-travel-safety/scripts/run_mofa_travel_safety.py +57 -0
- package/skills/mofa-travel-safety/skill.json +9 -0
- package/skills/nts-tax-delinquency/instruction.md +2 -1
- package/skills/nts-tax-delinquency/scripts/nts_tax_delinquency.py +15 -1
- package/skills/nts-tax-delinquency/scripts/test_nts_tax_delinquency.py +32 -0
- package/skills/seoul-weather-risk/instruction.md +4 -1
- package/skills/seoul-weather-risk/scripts/seoul_weather_risk.py +99 -3
- package/skills/store-longevity-radar/scripts/__pycache__/store_longevity_download.cpython-312.pyc +0 -0
- package/templates/browser.md +1 -1
- package/skills/coupang-product-search/scripts/coupang_partners_mcp.py +0 -146
package/package.json
CHANGED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# 동물약국·동물용의약품 취급 약국 조회
|
|
2
|
+
|
|
3
|
+
## What this skill does
|
|
4
|
+
|
|
5
|
+
홍익메디케어가 운영하는 인증 없는 공개 Streamable HTTP MCP 서버를 직접 호출한다.
|
|
6
|
+
|
|
7
|
+
- 엔드포인트: `https://hkmedi.co.kr/pharmacy-mcp`
|
|
8
|
+
- 지역별 동물약국 목록 조회
|
|
9
|
+
- 제품명·분류·증상 키워드로 동물용의약품 검색
|
|
10
|
+
- 특정 제품을 최근 6개월 안에 홍익메디케어에서 구매한 약국 조회
|
|
11
|
+
- 별도 API key나 `k-skill-proxy` 없이 사용자 머신에서 직접 호출
|
|
12
|
+
|
|
13
|
+
이 데이터는 민간 유통사인 홍익메디케어의 거래·디렉터리 데이터다. 공공기관의
|
|
14
|
+
동물약국 인허가 원장이나 전국 모든 유통사의 판매 자료가 아니다.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
- "서울 강남구 동물약국 알려줘"
|
|
19
|
+
- "목포시 동물약국 리스트 찾아줘"
|
|
20
|
+
- "동물용 항생제 제품 뭐가 있어?"
|
|
21
|
+
- "오리더밀 취급하는 서울 약국 찾아줘"
|
|
22
|
+
- "피부 관련 동물약 파는 인천 약국 있어?"
|
|
23
|
+
|
|
24
|
+
## When not to use
|
|
25
|
+
|
|
26
|
+
- 동물의 증상을 진단하거나 약을 처방·추천해야 하는 요청
|
|
27
|
+
- 용량, 투여 주기, 병용 가능 여부를 결정하는 요청
|
|
28
|
+
- 현재 매장 재고를 확정하거나 구매를 자동화하는 요청
|
|
29
|
+
- 공공기관의 공식 인허가 상태·행정처분 확인이 필요한 요청
|
|
30
|
+
|
|
31
|
+
동물의 상태가 위급하거나 약물 선택이 필요한 경우 수의사 진료를 우선 안내한다.
|
|
32
|
+
|
|
33
|
+
## Access path
|
|
34
|
+
|
|
35
|
+
기본 경로는 bundled helper다.
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- tools
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
helper는 Python 표준 라이브러리만 사용해 MCP `initialize` 후 세션 ID를 유지하며
|
|
42
|
+
`tools/list` 또는 `tools/call`을 실행한다. 서버가 JSON 또는 SSE로 응답해도
|
|
43
|
+
동일한 JSON 결과로 정규화한다.
|
|
44
|
+
|
|
45
|
+
직접 MCP 클라이언트에 등록할 수도 있다.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
claude mcp add --transport http animal-pharmacy https://hkmedi.co.kr/pharmacy-mcp
|
|
49
|
+
codex mcp add animal-pharmacy --url https://hkmedi.co.kr/pharmacy-mcp
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tool selection
|
|
53
|
+
|
|
54
|
+
| 사용자 요청 | MCP 도구 | 주요 입력 |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| 지역별 동물약국 목록 | `find_animal_pharmacies` | `city`, 선택 `gu`, `keyword`, `limit` |
|
|
57
|
+
| 제품명·분류·증상 키워드 검색 | `search_product` | `keyword` |
|
|
58
|
+
| 제품 취급 약국 조회 | `find_pharmacies_by_product` | `item_srl` 또는 `product_name`, 선택 `city`, `gu`, `limit` |
|
|
59
|
+
|
|
60
|
+
### 지역별 동물약국
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- \
|
|
64
|
+
call find_animal_pharmacies \
|
|
65
|
+
--arg city=서울 \
|
|
66
|
+
--arg gu=강남구 \
|
|
67
|
+
--arg limit=5
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`result._meta.pharmacies`에서 약국명, 전화번호, 주소, 행정구역, 좌표를 읽는다.
|
|
71
|
+
사용자가 지역을 주지 않았다면 시·도와 시·군·구를 먼저 묻는다.
|
|
72
|
+
|
|
73
|
+
### 제품 검색
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- \
|
|
77
|
+
call search_product \
|
|
78
|
+
--arg keyword=항생제
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
제품명뿐 아니라 서버가 등록한 분류·증상 태그도 검색한다. `keyword`는 최소
|
|
82
|
+
2글자여야 한다. 결과의 `item_srl`과 `item_name`을 제시하되, 검색 결과를
|
|
83
|
+
진단·처방·효능 보증으로 해석하지 않는다.
|
|
84
|
+
|
|
85
|
+
### 제품 취급 약국
|
|
86
|
+
|
|
87
|
+
제품명이 충분히 구체적이면 `product_name`으로 바로 검색할 수 있다.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- \
|
|
91
|
+
call find_pharmacies_by_product \
|
|
92
|
+
--arg product_name=오리더밀 \
|
|
93
|
+
--arg city=서울 \
|
|
94
|
+
--arg limit=5
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
제품명이 모호하거나 동명이 여러 개면 먼저 `search_product`로 `item_srl`을
|
|
98
|
+
확인한 뒤 정확 조회한다.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx -y @nomadamas/k-skill@0 exec animal-pharmacy-search scripts/animal_pharmacy_mcp.py -- \
|
|
102
|
+
call find_pharmacies_by_product \
|
|
103
|
+
--arg item_srl=4452 \
|
|
104
|
+
--arg city=서울 \
|
|
105
|
+
--arg limit=5
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Provenance and interpretation
|
|
109
|
+
|
|
110
|
+
`find_pharmacies_by_product` 결과에는 반드시 아래 의미를 함께 전달한다.
|
|
111
|
+
|
|
112
|
+
- 약국은 **최근 6개월 안에 홍익메디케어에서 해당 제품을 구매한 이력** 기준이다.
|
|
113
|
+
- 이 기준은 해당 약국의 과거 취급 근거이지 현재 재고·판매 가능 여부 보장이 아니다.
|
|
114
|
+
- 다른 유통사를 통한 구매나 전국 모든 동물약국을 포괄하지 않을 수 있다.
|
|
115
|
+
- 방문 전에 전화로 제품명과 현재 재고를 확인하도록 안내한다.
|
|
116
|
+
|
|
117
|
+
`find_animal_pharmacies`는 지역 디렉터리이며 제품 취급 여부를 뜻하지 않는다.
|
|
118
|
+
제품까지 확인하려면 별도로 `find_pharmacies_by_product`를 호출한다.
|
|
119
|
+
|
|
120
|
+
## Response style
|
|
121
|
+
|
|
122
|
+
- 보통 3~5곳만 약국명, 전화번호, 주소 순으로 정리한다.
|
|
123
|
+
- 제품 검색은 제품명과 `item_srl`을 함께 보여준다.
|
|
124
|
+
- 취급 약국 결과에는 최근 6개월 홍익메디케어 구매 이력 기준임을 한 문장으로 명시한다.
|
|
125
|
+
- 좌표는 사용자가 지도 연결을 원할 때만 보조 정보로 쓴다.
|
|
126
|
+
- 전화번호와 주소는 조회 목적에 필요한 공개 사업장 정보로만 사용한다.
|
|
127
|
+
|
|
128
|
+
## Failure modes
|
|
129
|
+
|
|
130
|
+
- `406 Not Acceptable` 또는 SSE 요구: `Accept: application/json, text/event-stream`을 모두 보낸다.
|
|
131
|
+
- `keyword must be at least 2 characters`: 2글자 이상의 키워드로 다시 검색한다.
|
|
132
|
+
- 빈 제품 결과: 다른 제품명·분류·증상 키워드를 제안한다.
|
|
133
|
+
- 빈 약국 결과: `gu`를 빼고 시·도 단위로 넓히거나 지역 표기를 확인한다.
|
|
134
|
+
- MCP 세션 오류: 새 `initialize`로 세션을 다시 만든다. 무한 재시도하지 않는다.
|
|
135
|
+
- 연결 실패·5xx: 홍익메디케어 민간 MCP 장애로 보고 현재 조회 불가를 알린다.
|
|
136
|
+
- 현재 재고 확인 요청: MCP 결과만으로 확정하지 않고 약국 전화 확인을 안내한다.
|
|
137
|
+
|
|
138
|
+
## Privacy
|
|
139
|
+
|
|
140
|
+
- 인증·로그인·개인정보 입력이 없는 공개 조회 전용이다.
|
|
141
|
+
- 사용자나 반려동물의 의료정보를 서버에 전달하지 않는다.
|
|
142
|
+
- 진단·처방·복약 결정을 대신하지 않는다.
|
|
143
|
+
|
|
144
|
+
## Done when
|
|
145
|
+
|
|
146
|
+
- 지역 목록, 제품 검색, 제품 취급 약국 중 맞는 도구를 선택했다.
|
|
147
|
+
- 실제 MCP 응답의 `_meta` 구조를 기준으로 결과를 정리했다.
|
|
148
|
+
- 제품 취급 약국에는 최근 6개월 홍익메디케어 구매 이력이라는 출처와 한계를 명시했다.
|
|
149
|
+
- 현재 재고는 보장되지 않으므로 방문 전 전화 확인을 안내했다.
|
|
150
|
+
- 진단·처방 없이 조회 결과만 제공했다.
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""홍익메디케어 공개 Streamable HTTP MCP 서버를 호출한다."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.request
|
|
12
|
+
from typing import Any, Sequence
|
|
13
|
+
|
|
14
|
+
DEFAULT_ENDPOINT = "https://hkmedi.co.kr/pharmacy-mcp"
|
|
15
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
16
|
+
PROTOCOL_VERSION = "2025-03-26"
|
|
17
|
+
USER_AGENT = "k-skill-animal-pharmacy/1.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AnimalPharmacyMcpError(RuntimeError):
|
|
21
|
+
"""설정 또는 MCP 호출 실패 때 발생한다."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_json_object(raw: str, *, arg_name: str) -> dict[str, Any]:
|
|
25
|
+
try:
|
|
26
|
+
value = json.loads(raw)
|
|
27
|
+
except json.JSONDecodeError as exc:
|
|
28
|
+
raise argparse.ArgumentTypeError(
|
|
29
|
+
f"{arg_name}은 올바른 JSON이어야 합니다: {exc}"
|
|
30
|
+
) from exc
|
|
31
|
+
if not isinstance(value, dict):
|
|
32
|
+
raise argparse.ArgumentTypeError(f"{arg_name}은 JSON 객체여야 합니다")
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_positive_float(raw: str) -> float:
|
|
37
|
+
try:
|
|
38
|
+
value = float(raw)
|
|
39
|
+
except ValueError as exc:
|
|
40
|
+
raise argparse.ArgumentTypeError("timeout은 숫자여야 합니다") from exc
|
|
41
|
+
if value <= 0:
|
|
42
|
+
raise argparse.ArgumentTypeError("timeout은 0보다 커야 합니다")
|
|
43
|
+
return value
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_kv_pairs(pairs: Sequence[str]) -> dict[str, Any]:
|
|
47
|
+
args: dict[str, Any] = {}
|
|
48
|
+
for pair in pairs:
|
|
49
|
+
if "=" not in pair:
|
|
50
|
+
raise argparse.ArgumentTypeError(
|
|
51
|
+
f"인자 '{pair}'는 key=value 형식이어야 합니다"
|
|
52
|
+
)
|
|
53
|
+
key, raw_value = pair.split("=", 1)
|
|
54
|
+
if not key:
|
|
55
|
+
raise argparse.ArgumentTypeError(
|
|
56
|
+
f"인자 '{pair}'의 key가 비어 있습니다"
|
|
57
|
+
)
|
|
58
|
+
try:
|
|
59
|
+
value = json.loads(raw_value)
|
|
60
|
+
except json.JSONDecodeError:
|
|
61
|
+
value = raw_value
|
|
62
|
+
args[key] = value
|
|
63
|
+
return args
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
description="홍익메디케어 동물약국 MCP 도구를 호출합니다.",
|
|
69
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
70
|
+
epilog=(
|
|
71
|
+
"예시:\n"
|
|
72
|
+
" animal_pharmacy_mcp.py tools\n"
|
|
73
|
+
" animal_pharmacy_mcp.py call find_animal_pharmacies --arg city=서울 --arg gu=강남구\n"
|
|
74
|
+
" animal_pharmacy_mcp.py call search_product --arg keyword=항생제\n"
|
|
75
|
+
" animal_pharmacy_mcp.py call find_pharmacies_by_product --arg product_name=오리더밀 --arg city=서울\n"
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--endpoint",
|
|
80
|
+
default=os.getenv("ANIMAL_PHARMACY_MCP_ENDPOINT", DEFAULT_ENDPOINT),
|
|
81
|
+
help="동물약국 MCP 엔드포인트(기본값: %(default)s).",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--timeout-seconds",
|
|
85
|
+
type=parse_positive_float,
|
|
86
|
+
default=DEFAULT_TIMEOUT_SECONDS,
|
|
87
|
+
help="각 MCP HTTP 요청 제한 시간(기본값: %(default)s초).",
|
|
88
|
+
)
|
|
89
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
90
|
+
subparsers.add_parser(
|
|
91
|
+
"tools", help="사용 가능한 MCP 도구와 입력 스키마를 JSON으로 출력합니다."
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
call_parser = subparsers.add_parser(
|
|
95
|
+
"call", help="MCP 도구 하나를 호출하고 결과를 JSON으로 출력합니다."
|
|
96
|
+
)
|
|
97
|
+
call_parser.add_argument(
|
|
98
|
+
"tool",
|
|
99
|
+
choices=[
|
|
100
|
+
"find_animal_pharmacies",
|
|
101
|
+
"search_product",
|
|
102
|
+
"find_pharmacies_by_product",
|
|
103
|
+
],
|
|
104
|
+
help="호출할 동물약국 MCP 도구명.",
|
|
105
|
+
)
|
|
106
|
+
call_parser.add_argument(
|
|
107
|
+
"--json",
|
|
108
|
+
dest="json_args",
|
|
109
|
+
type=lambda raw: parse_json_object(raw, arg_name="--json"),
|
|
110
|
+
default=None,
|
|
111
|
+
help="도구 인자를 JSON 객체로 전달합니다.",
|
|
112
|
+
)
|
|
113
|
+
call_parser.add_argument(
|
|
114
|
+
"--arg",
|
|
115
|
+
dest="kv_args",
|
|
116
|
+
action="append",
|
|
117
|
+
default=[],
|
|
118
|
+
metavar="KEY=VALUE",
|
|
119
|
+
help="도구 인자입니다. 반복 지정할 수 있습니다.",
|
|
120
|
+
)
|
|
121
|
+
return parser.parse_args(argv)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_mcp_response(raw: bytes, content_type: str) -> dict[str, Any]:
|
|
125
|
+
text = raw.decode("utf-8")
|
|
126
|
+
if "text/event-stream" in content_type:
|
|
127
|
+
data_lines = [
|
|
128
|
+
line[6:] for line in text.splitlines() if line.startswith("data: ")
|
|
129
|
+
]
|
|
130
|
+
if not data_lines:
|
|
131
|
+
raise AnimalPharmacyMcpError("MCP SSE 응답에 data 이벤트가 없습니다")
|
|
132
|
+
text = data_lines[-1]
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
payload = json.loads(text)
|
|
136
|
+
except json.JSONDecodeError as exc:
|
|
137
|
+
raise AnimalPharmacyMcpError(
|
|
138
|
+
f"MCP 응답이 올바른 JSON이 아닙니다: {exc}"
|
|
139
|
+
) from exc
|
|
140
|
+
if not isinstance(payload, dict):
|
|
141
|
+
raise AnimalPharmacyMcpError("MCP 응답이 JSON 객체가 아닙니다")
|
|
142
|
+
if "error" in payload:
|
|
143
|
+
error = payload["error"]
|
|
144
|
+
message = error.get("message", str(error)) if isinstance(error, dict) else str(error)
|
|
145
|
+
raise AnimalPharmacyMcpError(message)
|
|
146
|
+
return payload
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def post_rpc(
|
|
150
|
+
endpoint: str,
|
|
151
|
+
payload: dict[str, Any],
|
|
152
|
+
*,
|
|
153
|
+
timeout_seconds: float,
|
|
154
|
+
session_id: str | None = None,
|
|
155
|
+
) -> tuple[dict[str, Any], str | None]:
|
|
156
|
+
headers = {
|
|
157
|
+
"Content-Type": "application/json",
|
|
158
|
+
"Accept": "application/json, text/event-stream",
|
|
159
|
+
"User-Agent": USER_AGENT,
|
|
160
|
+
}
|
|
161
|
+
if session_id:
|
|
162
|
+
headers["Mcp-Session-Id"] = session_id
|
|
163
|
+
|
|
164
|
+
request = urllib.request.Request(
|
|
165
|
+
endpoint,
|
|
166
|
+
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
|
|
167
|
+
headers=headers,
|
|
168
|
+
method="POST",
|
|
169
|
+
)
|
|
170
|
+
try:
|
|
171
|
+
with urllib.request.urlopen(request, timeout=timeout_seconds) as response:
|
|
172
|
+
parsed = parse_mcp_response(
|
|
173
|
+
response.read(),
|
|
174
|
+
response.headers.get("Content-Type", ""),
|
|
175
|
+
)
|
|
176
|
+
return parsed, response.headers.get("Mcp-Session-Id") or session_id
|
|
177
|
+
except urllib.error.HTTPError as exc:
|
|
178
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
179
|
+
raise AnimalPharmacyMcpError(
|
|
180
|
+
f"동물약국 MCP HTTP {exc.code}: {detail}"
|
|
181
|
+
) from exc
|
|
182
|
+
except urllib.error.URLError as exc:
|
|
183
|
+
raise AnimalPharmacyMcpError(
|
|
184
|
+
f"동물약국 MCP 연결 실패 {endpoint}: {exc.reason}"
|
|
185
|
+
) from exc
|
|
186
|
+
except TimeoutError as exc:
|
|
187
|
+
raise AnimalPharmacyMcpError(
|
|
188
|
+
f"동물약국 MCP 요청 시간이 {timeout_seconds:g}초를 초과했습니다"
|
|
189
|
+
) from exc
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def initialize(endpoint: str, *, timeout_seconds: float) -> str:
|
|
193
|
+
payload, session_id = post_rpc(
|
|
194
|
+
endpoint,
|
|
195
|
+
{
|
|
196
|
+
"jsonrpc": "2.0",
|
|
197
|
+
"id": 1,
|
|
198
|
+
"method": "initialize",
|
|
199
|
+
"params": {
|
|
200
|
+
"protocolVersion": PROTOCOL_VERSION,
|
|
201
|
+
"capabilities": {},
|
|
202
|
+
"clientInfo": {
|
|
203
|
+
"name": "k-skill-animal-pharmacy",
|
|
204
|
+
"version": "1.0.0",
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
timeout_seconds=timeout_seconds,
|
|
209
|
+
)
|
|
210
|
+
if "result" not in payload:
|
|
211
|
+
raise AnimalPharmacyMcpError("MCP initialize 응답에 result가 없습니다")
|
|
212
|
+
if not session_id:
|
|
213
|
+
raise AnimalPharmacyMcpError("MCP initialize 응답에 세션 ID가 없습니다")
|
|
214
|
+
return session_id
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def run_mcp(
|
|
218
|
+
endpoint: str,
|
|
219
|
+
command: str,
|
|
220
|
+
tool: str | None = None,
|
|
221
|
+
arguments: dict[str, Any] | None = None,
|
|
222
|
+
*,
|
|
223
|
+
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
|
|
224
|
+
) -> Any:
|
|
225
|
+
session_id = initialize(endpoint, timeout_seconds=timeout_seconds)
|
|
226
|
+
if command == "tools":
|
|
227
|
+
payload = {
|
|
228
|
+
"jsonrpc": "2.0",
|
|
229
|
+
"id": 2,
|
|
230
|
+
"method": "tools/list",
|
|
231
|
+
"params": {},
|
|
232
|
+
}
|
|
233
|
+
elif command == "call" and tool:
|
|
234
|
+
payload = {
|
|
235
|
+
"jsonrpc": "2.0",
|
|
236
|
+
"id": 2,
|
|
237
|
+
"method": "tools/call",
|
|
238
|
+
"params": {"name": tool, "arguments": arguments or {}},
|
|
239
|
+
}
|
|
240
|
+
else:
|
|
241
|
+
raise AnimalPharmacyMcpError(f"지원하지 않는 명령입니다: {command}")
|
|
242
|
+
|
|
243
|
+
response, _ = post_rpc(
|
|
244
|
+
endpoint,
|
|
245
|
+
payload,
|
|
246
|
+
timeout_seconds=timeout_seconds,
|
|
247
|
+
session_id=session_id,
|
|
248
|
+
)
|
|
249
|
+
return response.get("result")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
253
|
+
args = parse_args(argv)
|
|
254
|
+
tool_args: dict[str, Any] | None = None
|
|
255
|
+
if args.command == "call":
|
|
256
|
+
tool_args = dict(args.json_args or {})
|
|
257
|
+
tool_args.update(parse_kv_pairs(args.kv_args))
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
result = run_mcp(
|
|
261
|
+
args.endpoint,
|
|
262
|
+
args.command,
|
|
263
|
+
getattr(args, "tool", None),
|
|
264
|
+
tool_args,
|
|
265
|
+
timeout_seconds=args.timeout_seconds,
|
|
266
|
+
)
|
|
267
|
+
except AnimalPharmacyMcpError as exc:
|
|
268
|
+
print(f"animal_pharmacy_mcp.py: {exc}", file=sys.stderr)
|
|
269
|
+
return 2
|
|
270
|
+
|
|
271
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
272
|
+
return 0
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
if __name__ == "__main__":
|
|
276
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "animal-pharmacy-search",
|
|
3
|
+
"description": "홍익메디케어 공개 MCP 서버로 지역별 동물약국 목록, 동물용의약품 검색, 최근 6개월 구매 이력 기반 취급 약국을 조회한다. 조회 전용.",
|
|
4
|
+
"profiles": [
|
|
5
|
+
"lookup"
|
|
6
|
+
],
|
|
7
|
+
"frontmatter": "name: animal-pharmacy-search\ndescription: 홍익메디케어 공개 MCP 서버로 지역별 동물약국 목록, 동물용의약품 검색, 최근 6개월 구매 이력 기반 취급 약국을 조회한다. 조회 전용.\nlicense: MIT\nmetadata:\n category: health\n locale: ko-KR\n phase: v1"
|
|
8
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# 소비자 가격·안전 조회
|
|
2
|
+
|
|
3
|
+
## Draft status
|
|
4
|
+
|
|
5
|
+
이 스킬은 #390의 **draft scaffold**다. 참가격과 소비자24는 서로 다른
|
|
6
|
+
upstream이다. 소비자24는 `openapiSvcId`(서비스 카탈로그 ID), 서비스별
|
|
7
|
+
승인 `serviceKey`, 리콜 메뉴 `cntntsId`를 분리해 관리해야 한다.
|
|
8
|
+
|
|
9
|
+
## Planned v1
|
|
10
|
+
|
|
11
|
+
- 참가격 품목·지역별 가격
|
|
12
|
+
- 소비자24 물품정보
|
|
13
|
+
- 소비자24 전체 신청 서비스의 리콜 정보
|
|
14
|
+
- 서비스별 키 allowlist와 공식 오류 코드 보존
|
|
15
|
+
|
|
16
|
+
## Credential model
|
|
17
|
+
|
|
18
|
+
`serviceKey`만 secret이며 gpu01 runtime env에 보관한다. `openapiSvcId`와
|
|
19
|
+
`cntntsId`는 공개 매핑이므로 저장소 코드에 둘 수 있다. 최종 env 이름과
|
|
20
|
+
서비스별 성공 여부는 live probe 후 확정한다.
|
|
21
|
+
|
|
22
|
+
## Official sources
|
|
23
|
+
|
|
24
|
+
- 참가격: https://www.data.go.kr/dataset/3043385/openapi.do
|
|
25
|
+
- 소비자24 목록: https://www.consumer.go.kr/user/ftc/consumer/openApiSvcUser/120/selectOpenApiSvcList.do
|
|
26
|
+
|
|
27
|
+
## Draft stop condition
|
|
28
|
+
|
|
29
|
+
전체 신청 서비스별 키가 실제 성공하는지 확인하고 실패하는 live E2E부터
|
|
30
|
+
추가한 뒤 proxy route, helper, parser, 문서와 manual QA를 완성한다.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "consumer-price-safety-search",
|
|
3
|
+
"description": "한국소비자원 참가격과 소비자24 물품·리콜정보를 공식 API로 조회한다. 현재는 서비스별 인증키·메뉴 매핑을 검증하는 draft scaffold다.",
|
|
4
|
+
"profiles": ["proxy", "lookup"],
|
|
5
|
+
"frontmatter": "name: consumer-price-safety-search\ndescription: 한국소비자원 참가격과 소비자24 물품·리콜정보를 공식 API로 조회한다. 현재는 서비스별 인증키·메뉴 매핑을 검증하는 draft scaffold다.\nlicense: MIT\nmetadata:\n category: consumer\n locale: ko-KR\n phase: draft"
|
|
6
|
+
}
|