kr-medicine-mcp 0.1.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/LICENSE +21 -0
- package/README.md +231 -0
- package/dist/core/context.d.ts +17 -0
- package/dist/core/context.js +2 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/middleware.d.ts +19 -0
- package/dist/core/middleware.js +82 -0
- package/dist/core/middleware.js.map +1 -0
- package/dist/core/registry.d.ts +2 -0
- package/dist/core/registry.js +17 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/tool.d.ts +42 -0
- package/dist/core/tool.js +15 -0
- package/dist/core/tool.js.map +1 -0
- package/dist/domains/drug/alternatives.tool.d.ts +2 -0
- package/dist/domains/drug/alternatives.tool.js +65 -0
- package/dist/domains/drug/alternatives.tool.js.map +1 -0
- package/dist/domains/drug/client.d.ts +21 -0
- package/dist/domains/drug/client.js +47 -0
- package/dist/domains/drug/client.js.map +1 -0
- package/dist/domains/drug/detail.tool.d.ts +2 -0
- package/dist/domains/drug/detail.tool.js +34 -0
- package/dist/domains/drug/detail.tool.js.map +1 -0
- package/dist/domains/drug/index.d.ts +1 -0
- package/dist/domains/drug/index.js +5 -0
- package/dist/domains/drug/index.js.map +1 -0
- package/dist/domains/drug/search.tool.d.ts +2 -0
- package/dist/domains/drug/search.tool.js +60 -0
- package/dist/domains/drug/search.tool.js.map +1 -0
- package/dist/domains/drug/types.d.ts +50 -0
- package/dist/domains/drug/types.js +2 -0
- package/dist/domains/drug/types.js.map +1 -0
- package/dist/domains/dur/age-caution.tool.d.ts +2 -0
- package/dist/domains/dur/age-caution.tool.js +55 -0
- package/dist/domains/dur/age-caution.tool.js.map +1 -0
- package/dist/domains/dur/client.d.ts +17 -0
- package/dist/domains/dur/client.js +51 -0
- package/dist/domains/dur/client.js.map +1 -0
- package/dist/domains/dur/index.d.ts +1 -0
- package/dist/domains/dur/index.js +5 -0
- package/dist/domains/dur/index.js.map +1 -0
- package/dist/domains/dur/interaction.tool.d.ts +2 -0
- package/dist/domains/dur/interaction.tool.js +87 -0
- package/dist/domains/dur/interaction.tool.js.map +1 -0
- package/dist/domains/dur/pregnancy.tool.d.ts +2 -0
- package/dist/domains/dur/pregnancy.tool.js +47 -0
- package/dist/domains/dur/pregnancy.tool.js.map +1 -0
- package/dist/domains/dur/types.d.ts +66 -0
- package/dist/domains/dur/types.js +6 -0
- package/dist/domains/dur/types.js.map +1 -0
- package/dist/domains/food/allergen.tool.d.ts +2 -0
- package/dist/domains/food/allergen.tool.js +55 -0
- package/dist/domains/food/allergen.tool.js.map +1 -0
- package/dist/domains/food/client.d.ts +23 -0
- package/dist/domains/food/client.js +50 -0
- package/dist/domains/food/client.js.map +1 -0
- package/dist/domains/food/index.d.ts +1 -0
- package/dist/domains/food/index.js +5 -0
- package/dist/domains/food/index.js.map +1 -0
- package/dist/domains/food/nutrition.tool.d.ts +2 -0
- package/dist/domains/food/nutrition.tool.js +54 -0
- package/dist/domains/food/nutrition.tool.js.map +1 -0
- package/dist/domains/food/recall.tool.d.ts +2 -0
- package/dist/domains/food/recall.tool.js +62 -0
- package/dist/domains/food/recall.tool.js.map +1 -0
- package/dist/domains/food/types.d.ts +64 -0
- package/dist/domains/food/types.js +2 -0
- package/dist/domains/food/types.js.map +1 -0
- package/dist/domains/index.d.ts +6 -0
- package/dist/domains/index.js +10 -0
- package/dist/domains/index.js.map +1 -0
- package/dist/domains/pill/client.d.ts +24 -0
- package/dist/domains/pill/client.js +40 -0
- package/dist/domains/pill/client.js.map +1 -0
- package/dist/domains/pill/identify.tool.d.ts +2 -0
- package/dist/domains/pill/identify.tool.js +85 -0
- package/dist/domains/pill/identify.tool.js.map +1 -0
- package/dist/domains/pill/index.d.ts +1 -0
- package/dist/domains/pill/index.js +3 -0
- package/dist/domains/pill/index.js.map +1 -0
- package/dist/domains/pill/types.d.ts +48 -0
- package/dist/domains/pill/types.js +2 -0
- package/dist/domains/pill/types.js.map +1 -0
- package/dist/http-server.d.ts +8 -0
- package/dist/http-server.js +52 -0
- package/dist/http-server.js.map +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +46 -0
- package/dist/main.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +83 -0
- package/dist/server.js.map +1 -0
- package/dist/shared/cache.d.ts +10 -0
- package/dist/shared/cache.js +24 -0
- package/dist/shared/cache.js.map +1 -0
- package/dist/shared/disclaimer.d.ts +1 -0
- package/dist/shared/disclaimer.js +3 -0
- package/dist/shared/disclaimer.js.map +1 -0
- package/dist/shared/errors.d.ts +8 -0
- package/dist/shared/errors.js +17 -0
- package/dist/shared/errors.js.map +1 -0
- package/dist/shared/food-safety-http-client.d.ts +9 -0
- package/dist/shared/food-safety-http-client.js +28 -0
- package/dist/shared/food-safety-http-client.js.map +1 -0
- package/dist/shared/format.d.ts +7 -0
- package/dist/shared/format.js +26 -0
- package/dist/shared/format.js.map +1 -0
- package/dist/shared/http-client.d.ts +17 -0
- package/dist/shared/http-client.js +47 -0
- package/dist/shared/http-client.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 zzjiho
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# kr-medicine-mcp
|
|
2
|
+
|
|
3
|
+
식품의약품안전처(MFDS) 공개 API를 **MCP 도구 10개**로 노출하는 서버입니다. 의약품 검색·상호작용·임부금기 검증과 식품 영양·알레르기·회수 정보를 Claude Desktop, Cursor, Claude Code 등 MCP 호환 클라이언트에서 바로 사용할 수 있습니다.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://modelcontextprotocol.io)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 왜 만들었나
|
|
13
|
+
|
|
14
|
+
LLM은 한국 약품명·성분·DUR(약물 안전사용) 정보를 잘 모릅니다. "이트라코나졸이랑 심바스타틴 같이 먹어도 되나"를 물으면 그럴듯하게 지어내기 쉬운데, 의료 영역에서 틀린 답은 사람을 다치게 할 수 있습니다.
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
사용자: "이트라코나졸이랑 심바스타틴 같이 먹어도 돼?"
|
|
18
|
+
|
|
19
|
+
LLM 단독: "둘 다 흔한 처방약입니다. 큰 문제는 없을 것으로 보이지만
|
|
20
|
+
의사 상담을 권장합니다." ← 환각
|
|
21
|
+
|
|
22
|
+
LLM + MCP: ⚠️ 병용금기 — 횡문근융해증 위험
|
|
23
|
+
출처: 식약처 DUR 품목정보
|
|
24
|
+
→ 의사·약사 상담 필수
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
이 서버는 모든 답을 식약처 공식 DB와 교차검증합니다. 진단·처방은 범위 밖이며, 조회된 사실만 전달하고 전문가 상담을 안내합니다.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 도구 10개
|
|
32
|
+
|
|
33
|
+
### 의약품 (7개)
|
|
34
|
+
|
|
35
|
+
| 도구 | 역할 | API |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `search_drug` | 약 이름/성분으로 검색 → 품목코드 획득 | e약은요 |
|
|
38
|
+
| `get_drug_detail` | 효능·용법·주의·상호작용·부작용 풀세트 | e약은요 |
|
|
39
|
+
| `find_alternatives` | 동일 주성분 다른 제품 | e약은요 |
|
|
40
|
+
| `check_interaction` | 두 약 병용금기 직접 검증 | DUR 품목정보 |
|
|
41
|
+
| `check_pregnancy_safety` | 임부금기 + 사유 | DUR 품목정보 |
|
|
42
|
+
| `check_age_caution` | 노인주의 + 특정연령대금기 | DUR 품목정보 |
|
|
43
|
+
| `identify_pill` | 모양·색상·각인으로 약 식별 | 의약품 낱알식별 |
|
|
44
|
+
|
|
45
|
+
### 식품 (3개)
|
|
46
|
+
|
|
47
|
+
| 도구 | 역할 | API |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `search_food_nutrition` | 식품 칼로리·영양성분 조회 | 식품영양성분 DB |
|
|
50
|
+
| `check_food_allergen` | 가공식품 원재료·알레르기 유발물질 확인 | 식품안전나라 |
|
|
51
|
+
| `get_food_recall` | 회수·판매중지 식품 조회 | 식품안전나라 |
|
|
52
|
+
|
|
53
|
+
> `search_food_nutrition` 은 `MFDS_API_KEY` 로 동작합니다. `check_food_allergen`·`get_food_recall` 은 별도의 `FOOD_SAFETY_API_KEY`(식품안전나라) 가 있을 때만 활성화되며, 없으면 이 두 도구만 안내 메시지를 반환하고 나머지는 정상 작동합니다.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 사용 예시
|
|
58
|
+
|
|
59
|
+
일상 대화에서 다음과 같이 물으면, 모델이 적절한 도구를 골라 식약처 DB를 조회한 뒤 답합니다.
|
|
60
|
+
|
|
61
|
+
### 약물 상호작용 — LLM이 가장 위험하게 틀리는 영역
|
|
62
|
+
```
|
|
63
|
+
"이트라코나졸이랑 심바스타틴 같이 먹어도 돼?"
|
|
64
|
+
→ check_interaction: 병용금기(횡문근융해증 위험) 탐지, 사유·출처 제시
|
|
65
|
+
|
|
66
|
+
"지금 와파린 먹는 중인데 아스피린 추가해도 되나?"
|
|
67
|
+
→ 기존 복용약 대비 신규 약 병용금기 교차검증
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 다제 복용 점검 (고령자)
|
|
71
|
+
```
|
|
72
|
+
"75세 어머니가 클로르페니라민, 디아제팜, 아미트립틸린을 같이 드세요. 괜찮을까요?"
|
|
73
|
+
→ check_age_caution + check_interaction:
|
|
74
|
+
노인주의 성분 + 약물 간 금기를 한 번에 점검
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 임신 중 복용
|
|
78
|
+
```
|
|
79
|
+
"임신 12주인데 감기약에 아미노필린 들어있어. 안전해?"
|
|
80
|
+
→ check_pregnancy_safety: 임부금기 등급과 사유 조회
|
|
81
|
+
(DB 미등재가 곧 '안전'은 아님을 명시)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 정체불명 알약 식별
|
|
85
|
+
```
|
|
86
|
+
"서랍에서 나온 흰색 둥근 알약, 한쪽 면에 'TYL 500' 각인됐어. 이거 뭐야?"
|
|
87
|
+
→ identify_pill: 모양·색상·각인으로 품목 특정
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 대체약 탐색 (품절·성분 동일)
|
|
91
|
+
```
|
|
92
|
+
"타이레놀이 약국에 없대. 같은 성분 다른 제품 알려줘"
|
|
93
|
+
→ find_alternatives: 동일 주성분 제품 목록
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 식품 안전
|
|
97
|
+
```
|
|
98
|
+
"우리 애가 땅콩 알레르기 있는데, 이 과자 먹여도 돼?"
|
|
99
|
+
→ check_food_allergen: 가공식품 원재료·알레르기 유발물질 확인
|
|
100
|
+
|
|
101
|
+
"OO 제품 회수됐는지 확인해줘"
|
|
102
|
+
→ get_food_recall: 회수·판매중지 처분 식품 조회
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
진단·처방 권고는 의료 안전상 일부러 거부하고, `의사·약사 상담`이 필요한 영역으로 안내합니다.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 설치
|
|
110
|
+
|
|
111
|
+
### 0단계: API 키 발급 (무료)
|
|
112
|
+
|
|
113
|
+
**(1) 공공데이터포털** — [data.go.kr](https://www.data.go.kr) 회원가입 후 다음 6개를 검색해서 활용신청 (모두 자동승인). 마이페이지의 **인증키(Decoding 버전)** 하나를 `MFDS_API_KEY` 로 사용합니다:
|
|
114
|
+
|
|
115
|
+
- 식품의약품안전처 의약품개요정보(e약은요)
|
|
116
|
+
- 식품의약품안전처 의약품 제품 허가정보
|
|
117
|
+
- 식품의약품안전처 의약품안전사용서비스(DUR) 품목정보
|
|
118
|
+
- 식품의약품안전처 의약품안전사용서비스(DUR) 성분정보
|
|
119
|
+
- 식품의약품안전처 의약품 낱알식별 정보
|
|
120
|
+
- 식품의약품안전처 식품영양성분 DB (`search_food_nutrition` 용)
|
|
121
|
+
|
|
122
|
+
**(2) 식품안전나라** (선택) — [foodsafetykorea.go.kr](https://www.foodsafetykorea.go.kr) 회원가입 → 활용신청(즉시 발급). `check_food_allergen`·`get_food_recall` 사용 시에만 필요하며 `FOOD_SAFETY_API_KEY` 로 사용합니다. 없으면 이 두 도구만 비활성화됩니다.
|
|
123
|
+
|
|
124
|
+
설치할 필요 없이 `npx` 로 바로 실행됩니다.
|
|
125
|
+
|
|
126
|
+
### 방법 1: Claude Desktop
|
|
127
|
+
|
|
128
|
+
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) 또는 `%APPDATA%\Claude\claude_desktop_config.json` (Windows) 편집:
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"mcpServers": {
|
|
133
|
+
"kr-medicine": {
|
|
134
|
+
"command": "npx",
|
|
135
|
+
"args": ["-y", "kr-medicine-mcp"],
|
|
136
|
+
"env": {
|
|
137
|
+
"MFDS_API_KEY": "발급받은_인증키_Decoding",
|
|
138
|
+
"FOOD_SAFETY_API_KEY": "식품안전나라_키 (선택)"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
저장 후 Claude Desktop 을 완전히 종료했다가 재실행합니다.
|
|
146
|
+
|
|
147
|
+
### 방법 2: Claude Code
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
claude mcp add kr-medicine \
|
|
151
|
+
--env MFDS_API_KEY=발급받은_인증키 \
|
|
152
|
+
--env FOOD_SAFETY_API_KEY=식품안전나라_키 \
|
|
153
|
+
-- npx -y kr-medicine-mcp
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 방법 3: 직접 빌드 (개발용)
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
git clone https://github.com/zzjiho/kr-medicine-mcp.git
|
|
160
|
+
cd kr-medicine-mcp
|
|
161
|
+
npm install
|
|
162
|
+
npm run build
|
|
163
|
+
|
|
164
|
+
cp .env.example .env
|
|
165
|
+
# .env 파일을 열어 MFDS_API_KEY=... 입력
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 개발
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm install
|
|
174
|
+
npm run build # tsc 컴파일
|
|
175
|
+
npm run watch # tsc --watch
|
|
176
|
+
bash test-all-tools.sh # 도구 종합 회귀 테스트
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
자세한 시나리오는 [TESTING.md](TESTING.md) 참고.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## 아키텍처
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
src/
|
|
187
|
+
├── core/ defineTool 헬퍼 + 미들웨어 합성 (캐시·면책·에러)
|
|
188
|
+
├── shared/ HTTP 클라이언트, TTL 캐시
|
|
189
|
+
├── domains/
|
|
190
|
+
│ ├── drug/ e약은요 (search · detail · alternatives)
|
|
191
|
+
│ ├── dur/ DUR 안전사용 (interaction · pregnancy · age-caution)
|
|
192
|
+
│ ├── pill/ 낱알식별 (identify)
|
|
193
|
+
│ └── food/ 식품 (nutrition · allergen · recall)
|
|
194
|
+
└── server.ts MCP stdio / HTTP 부팅 + DI 컨텍스트
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
도메인 기반 구조 + 미들웨어 자동 합성. 새 도구는 도메인 폴더에 `*.tool.ts` 파일 하나만 추가하면 등록·면책·캐싱·에러 처리가 자동으로 붙습니다.
|
|
198
|
+
|
|
199
|
+
stdio(로컬 클라이언트) 와 HTTP/SSE(원격 배포) 두 가지 트랜스포트를 지원합니다 (`MCP_MODE=http`).
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## ⚠️ 의료 면책
|
|
204
|
+
|
|
205
|
+
본 정보는 식약처 공식 데이터에 기반한 **참고용**입니다.
|
|
206
|
+
|
|
207
|
+
- 실제 복용 전 반드시 **의사·약사와 상담**하세요.
|
|
208
|
+
- 응급 상황은 **119**에 즉시 연락하세요.
|
|
209
|
+
- 진단·처방 등 의료 전문 영역은 본 도구의 범위 밖입니다.
|
|
210
|
+
- DB 미등재가 안전을 의미하지는 않습니다 — 등록되지 않은 위험이 있을 수 있습니다.
|
|
211
|
+
|
|
212
|
+
본 도구의 답변은 의료진의 전문적 판단을 대체할 수 없습니다.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## 라이선스
|
|
217
|
+
|
|
218
|
+
[MIT](LICENSE)
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 출처
|
|
223
|
+
|
|
224
|
+
- 식약처 의약품개요정보(e약은요) — 공공데이터포털
|
|
225
|
+
- 식약처 의약품 제품 허가정보 — 공공데이터포털
|
|
226
|
+
- 식약처 의약품안전사용서비스(DUR) 품목정보 — 공공데이터포털
|
|
227
|
+
- 식약처 의약품안전사용서비스(DUR) 성분정보 — 공공데이터포털
|
|
228
|
+
- 식약처 의약품 낱알식별 정보 — 공공데이터포털
|
|
229
|
+
- 식약처 식품영양성분 DB — 공공데이터포털
|
|
230
|
+
- 식약처 가공식품 원재료·알레르기 정보 — 식품안전나라
|
|
231
|
+
- 식약처 회수·판매중지 식품 정보 — 식품안전나라
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Cache } from "../shared/cache.js";
|
|
2
|
+
import type { DrugClient } from "../domains/drug/client.js";
|
|
3
|
+
import type { DurClient } from "../domains/dur/client.js";
|
|
4
|
+
import type { PillClient } from "../domains/pill/client.js";
|
|
5
|
+
import type { FoodNutritionClient, FoodSafetyClient } from "../domains/food/client.js";
|
|
6
|
+
/**
|
|
7
|
+
* 모든 도구가 공유하는 의존성 컨텍스트.
|
|
8
|
+
* 서버 부팅 시 1회 생성, 모든 도구 핸들러에 전달.
|
|
9
|
+
*/
|
|
10
|
+
export interface ToolContext {
|
|
11
|
+
readonly drug: DrugClient;
|
|
12
|
+
readonly dur: DurClient;
|
|
13
|
+
readonly pill: PillClient;
|
|
14
|
+
readonly food: FoodNutritionClient;
|
|
15
|
+
readonly foodSafety: FoodSafetyClient | null;
|
|
16
|
+
readonly cache: Cache;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../../src/core/context.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ToolDefinition } from "./tool.js";
|
|
2
|
+
export type ToolMiddleware = <T extends ToolDefinition>(tool: T) => T;
|
|
3
|
+
/**
|
|
4
|
+
* 미들웨어를 오른쪽부터 적용하여 도구를 감싼다.
|
|
5
|
+
* compose(a, b, c)(tool) === a(b(c(tool)))
|
|
6
|
+
*/
|
|
7
|
+
export declare function compose(...middlewares: ToolMiddleware[]): ToolMiddleware;
|
|
8
|
+
/**
|
|
9
|
+
* 모든 응답 끝에 의료 면책 문구 자동 첨부.
|
|
10
|
+
*/
|
|
11
|
+
export declare const withDisclaimer: ToolMiddleware;
|
|
12
|
+
/**
|
|
13
|
+
* 핸들러에서 throw된 에러를 MCP 에러 응답으로 변환.
|
|
14
|
+
*/
|
|
15
|
+
export declare const withErrorHandling: ToolMiddleware;
|
|
16
|
+
/**
|
|
17
|
+
* 도구 응답을 메모리 캐시 (입력별 키). TTL 만료 시 무효화.
|
|
18
|
+
*/
|
|
19
|
+
export declare const withCache: (ttlMs: number) => ToolMiddleware;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { MEDICAL_DISCLAIMER } from "../shared/disclaimer.js";
|
|
2
|
+
/**
|
|
3
|
+
* 미들웨어를 오른쪽부터 적용하여 도구를 감싼다.
|
|
4
|
+
* compose(a, b, c)(tool) === a(b(c(tool)))
|
|
5
|
+
*/
|
|
6
|
+
export function compose(...middlewares) {
|
|
7
|
+
return (tool) => middlewares.reduceRight((wrapped, mw) => mw(wrapped), tool);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 모든 응답 끝에 의료 면책 문구 자동 첨부.
|
|
11
|
+
*/
|
|
12
|
+
export const withDisclaimer = (tool) => {
|
|
13
|
+
const original = tool.handler;
|
|
14
|
+
return {
|
|
15
|
+
...tool,
|
|
16
|
+
handler: async (input, ctx) => {
|
|
17
|
+
const result = await original(input, ctx);
|
|
18
|
+
return appendText(result, "\n\n" + MEDICAL_DISCLAIMER);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* 핸들러에서 throw된 에러를 MCP 에러 응답으로 변환.
|
|
24
|
+
*/
|
|
25
|
+
export const withErrorHandling = (tool) => {
|
|
26
|
+
const original = tool.handler;
|
|
27
|
+
return {
|
|
28
|
+
...tool,
|
|
29
|
+
handler: async (input, ctx) => {
|
|
30
|
+
try {
|
|
31
|
+
return await original(input, ctx);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
35
|
+
return {
|
|
36
|
+
content: [{ type: "text", text: `❌ ${tool.name} 실행 중 오류: ${message}` }],
|
|
37
|
+
isError: true,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* 도구 응답을 메모리 캐시 (입력별 키). TTL 만료 시 무효화.
|
|
45
|
+
*/
|
|
46
|
+
export const withCache = (ttlMs) => {
|
|
47
|
+
return (tool) => {
|
|
48
|
+
const original = tool.handler;
|
|
49
|
+
return {
|
|
50
|
+
...tool,
|
|
51
|
+
handler: async (input, ctx) => {
|
|
52
|
+
const key = `${tool.name}:${stableStringify(input)}`;
|
|
53
|
+
const cached = ctx.cache.get(key);
|
|
54
|
+
if (cached !== undefined)
|
|
55
|
+
return cached;
|
|
56
|
+
const result = await original(input, ctx);
|
|
57
|
+
ctx.cache.set(key, result, ttlMs);
|
|
58
|
+
return result;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
function appendText(result, text) {
|
|
64
|
+
if (typeof result === "string")
|
|
65
|
+
return result + text;
|
|
66
|
+
if (result.isError)
|
|
67
|
+
return result; // 에러 응답에는 면책 첨부 안 함
|
|
68
|
+
const appended = {
|
|
69
|
+
...result,
|
|
70
|
+
content: [...result.content, { type: "text", text }],
|
|
71
|
+
};
|
|
72
|
+
return appended;
|
|
73
|
+
}
|
|
74
|
+
function stableStringify(value) {
|
|
75
|
+
if (value === null || typeof value !== "object")
|
|
76
|
+
return JSON.stringify(value);
|
|
77
|
+
if (Array.isArray(value))
|
|
78
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
79
|
+
const keys = Object.keys(value).sort();
|
|
80
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/core/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { compose, withCache, withDisclaimer, withErrorHandling } from "./middleware.js";
|
|
2
|
+
import allDomainTools from "../domains/index.js";
|
|
3
|
+
const ONE_HOUR = 60 * 60 * 1000;
|
|
4
|
+
/**
|
|
5
|
+
* 모든 도구에 공통 미들웨어를 적용한다.
|
|
6
|
+
* 적용 순서: 캐시 ← 면책 ← 에러처리 (밖에서 안으로)
|
|
7
|
+
*
|
|
8
|
+
* 즉 실행 시:
|
|
9
|
+
* 1) 에러처리: try/catch
|
|
10
|
+
* 2) 면책: 응답에 disclaimer 추가
|
|
11
|
+
* 3) 캐시: 입력 키로 응답 저장
|
|
12
|
+
*/
|
|
13
|
+
const wrapWithCommonMiddleware = compose(withErrorHandling, withDisclaimer, withCache(ONE_HOUR));
|
|
14
|
+
export function getRegisteredTools() {
|
|
15
|
+
return allDomainTools.map(wrapWithCommonMiddleware);
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/core/registry.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACvF,OAAO,cAAc,MAAM,qBAAqB,CAAA;AAEhD,MAAM,QAAQ,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAE/B;;;;;;;;GAQG;AACH,MAAM,wBAAwB,GAAG,OAAO,CACtC,iBAAiB,EACjB,cAAc,EACd,SAAS,CAAC,QAAQ,CAAC,CACpB,CAAA;AAED,MAAM,UAAU,kBAAkB;IAChC,OAAO,cAAc,CAAC,GAAG,CAAC,wBAAwB,CAAC,CAAA;AACrD,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { ToolContext } from "./context.js";
|
|
3
|
+
export interface ToolResponse {
|
|
4
|
+
content: Array<{
|
|
5
|
+
type: "text";
|
|
6
|
+
text: string;
|
|
7
|
+
}>;
|
|
8
|
+
isError?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type ToolHandlerResult = string | ToolResponse;
|
|
11
|
+
/**
|
|
12
|
+
* 등록 시 사용되는 도구 인터페이스.
|
|
13
|
+
*
|
|
14
|
+
* input은 의도적으로 `any` — 함수 파라미터 타입 변성(variance) 때문에
|
|
15
|
+
* 좁은 타입으로 정의된 핸들러를 일반 배열에 담을 수 없는 문제를 회피한다.
|
|
16
|
+
* 런타임에는 schema.parse()가 타입을 보장하므로 안전.
|
|
17
|
+
*
|
|
18
|
+
* 도구 작성자는 defineTool() 헬퍼를 통해 타입 안전하게 작성한다.
|
|
19
|
+
*/
|
|
20
|
+
export interface ToolDefinition {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly description: string;
|
|
23
|
+
readonly schema: z.ZodTypeAny;
|
|
24
|
+
readonly handler: (input: any, ctx: ToolContext) => Promise<ToolHandlerResult>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* 도구 정의 헬퍼. 작성 시점에는 schema에 따라 input 타입이 추론되어 안전.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* export default defineTool({
|
|
31
|
+
* name: "search_drug",
|
|
32
|
+
* description: "...",
|
|
33
|
+
* schema: z.object({ query: z.string() }),
|
|
34
|
+
* handler: async (input, ctx) => { input.query 자동 추론 ... }
|
|
35
|
+
* })
|
|
36
|
+
*/
|
|
37
|
+
export declare function defineTool<TSchema extends z.ZodTypeAny>(def: {
|
|
38
|
+
name: string;
|
|
39
|
+
description: string;
|
|
40
|
+
schema: TSchema;
|
|
41
|
+
handler: (input: z.infer<TSchema>, ctx: ToolContext) => Promise<ToolHandlerResult>;
|
|
42
|
+
}): ToolDefinition;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 도구 정의 헬퍼. 작성 시점에는 schema에 따라 input 타입이 추론되어 안전.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* export default defineTool({
|
|
6
|
+
* name: "search_drug",
|
|
7
|
+
* description: "...",
|
|
8
|
+
* schema: z.object({ query: z.string() }),
|
|
9
|
+
* handler: async (input, ctx) => { input.query 자동 추론 ... }
|
|
10
|
+
* })
|
|
11
|
+
*/
|
|
12
|
+
export function defineTool(def) {
|
|
13
|
+
return def;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool.js","sourceRoot":"","sources":["../../src/core/tool.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,UAAU,CAA+B,GAKxD;IACC,OAAO,GAAqB,CAAA;AAC9B,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineTool } from "../../core/tool.js";
|
|
3
|
+
import { truncate } from "../../shared/format.js";
|
|
4
|
+
const FindAlternativesSchema = z.object({
|
|
5
|
+
drugName: z
|
|
6
|
+
.string()
|
|
7
|
+
.min(1)
|
|
8
|
+
.describe("기준 의약품 이름 (예: '타이레놀'). 이 약과 동일 주성분의 다른 제품을 찾음."),
|
|
9
|
+
limit: z
|
|
10
|
+
.number()
|
|
11
|
+
.int()
|
|
12
|
+
.min(1)
|
|
13
|
+
.max(30)
|
|
14
|
+
.optional()
|
|
15
|
+
.default(10)
|
|
16
|
+
.describe("최대 결과 개수 (기본 10)"),
|
|
17
|
+
});
|
|
18
|
+
export default defineTool({
|
|
19
|
+
name: "find_alternatives",
|
|
20
|
+
description: "[대체약] 입력 의약품과 동일한 주성분을 가진 다른 제품들을 검색. " +
|
|
21
|
+
"사용 케이스: '이 약 단종됐다는데 같은 약 뭐 있어?', '저렴한 동일 성분 제네릭 있어?'. " +
|
|
22
|
+
"내부 동작: 1) 약 검색 → 주성분 추출 (괄호 안) → 2) 그 성분으로 재검색.",
|
|
23
|
+
schema: FindAlternativesSchema,
|
|
24
|
+
handler: async (input, ctx) => {
|
|
25
|
+
// 1) 기준 약 검색
|
|
26
|
+
const baseSearch = await ctx.drug.search(input.drugName, 5);
|
|
27
|
+
const baseDrug = baseSearch.items[0];
|
|
28
|
+
if (!baseDrug) {
|
|
29
|
+
return `'${input.drugName}' 검색 결과 없음. 정확한 약품명 확인 필요.`;
|
|
30
|
+
}
|
|
31
|
+
// 2) 주성분 추출
|
|
32
|
+
const ingredient = ctx.drug.extractMainIngredient(baseDrug.itemName);
|
|
33
|
+
if (!ingredient) {
|
|
34
|
+
return [
|
|
35
|
+
`'${baseDrug.itemName}' 의 주성분을 자동 추출하지 못했습니다.`,
|
|
36
|
+
`💡 search_drug 결과에서 약품명 끝 괄호 안의 성분을 확인 후 그 성분명으로 다시 시도하세요.`,
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
// 3) 주성분으로 재검색 → 동일 성분 다른 제품
|
|
40
|
+
const alts = await ctx.drug.search(ingredient, input.limit + 1);
|
|
41
|
+
const filtered = alts.items.filter((d) => d.itemSeq !== baseDrug.itemSeq).slice(0, input.limit);
|
|
42
|
+
return formatAlternatives(baseDrug, ingredient, filtered);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
function formatAlternatives(base, ingredient, alts) {
|
|
46
|
+
const header = [
|
|
47
|
+
`🔍 기준 약: ${base.itemName}`,
|
|
48
|
+
` • 주성분: ${ingredient}`,
|
|
49
|
+
"",
|
|
50
|
+
];
|
|
51
|
+
if (alts.length === 0) {
|
|
52
|
+
return header.concat([`동일 주성분 다른 제품 없음.`]).join("\n");
|
|
53
|
+
}
|
|
54
|
+
const lines = [...header, `📌 동일 주성분(${ingredient}) 다른 제품 ${alts.length}건:`, ""];
|
|
55
|
+
alts.forEach((d, i) => {
|
|
56
|
+
lines.push(`${i + 1}. ${d.itemName}`);
|
|
57
|
+
lines.push(` • 제조/수입사: ${d.entpName}`);
|
|
58
|
+
lines.push(` • 품목코드: ${d.itemSeq}`);
|
|
59
|
+
if (d.efcyQesitm)
|
|
60
|
+
lines.push(` • 효능: ${truncate(d.efcyQesitm, 80)}`);
|
|
61
|
+
lines.push("");
|
|
62
|
+
});
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=alternatives.tool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alternatives.tool.js","sourceRoot":"","sources":["../../../src/domains/drug/alternatives.tool.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { HttpClient } from "../../shared/http-client.js";
|
|
2
|
+
import type { DrugSearchResult, EasyDrugItem } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* 의약품 정보 조회 클라이언트.
|
|
5
|
+
*
|
|
6
|
+
* 사용 API:
|
|
7
|
+
* - DrbEasyDrugInfoService (e약은요): 일반의약품 효능/용법/주의/부작용
|
|
8
|
+
*/
|
|
9
|
+
export declare class DrugClient {
|
|
10
|
+
private readonly http;
|
|
11
|
+
constructor(http: HttpClient);
|
|
12
|
+
search(query: string, limit?: number): Promise<DrugSearchResult>;
|
|
13
|
+
getByItemSeq(itemSeq: string): Promise<EasyDrugItem | undefined>;
|
|
14
|
+
/**
|
|
15
|
+
* 단일 약품의 주성분명을 추출 — itemName 끝의 괄호 안 텍스트.
|
|
16
|
+
* 예: "타이레놀정500밀리그람(아세트아미노펜)" → "아세트아미노펜"
|
|
17
|
+
*/
|
|
18
|
+
extractMainIngredient(itemName: string): string | undefined;
|
|
19
|
+
private fetch;
|
|
20
|
+
private assertSuccess;
|
|
21
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { ApiError } from "../../shared/errors.js";
|
|
2
|
+
const E_DRUG_BASE = "https://apis.data.go.kr/1471000/DrbEasyDrugInfoService/getDrbEasyDrugList";
|
|
3
|
+
/**
|
|
4
|
+
* 의약품 정보 조회 클라이언트.
|
|
5
|
+
*
|
|
6
|
+
* 사용 API:
|
|
7
|
+
* - DrbEasyDrugInfoService (e약은요): 일반의약품 효능/용법/주의/부작용
|
|
8
|
+
*/
|
|
9
|
+
export class DrugClient {
|
|
10
|
+
http;
|
|
11
|
+
constructor(http) {
|
|
12
|
+
this.http = http;
|
|
13
|
+
}
|
|
14
|
+
async search(query, limit = 10) {
|
|
15
|
+
return this.fetch({ itemName: query, numOfRows: limit });
|
|
16
|
+
}
|
|
17
|
+
async getByItemSeq(itemSeq) {
|
|
18
|
+
const result = await this.fetch({ itemSeq, numOfRows: 1 });
|
|
19
|
+
return result.items[0];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* 단일 약품의 주성분명을 추출 — itemName 끝의 괄호 안 텍스트.
|
|
23
|
+
* 예: "타이레놀정500밀리그람(아세트아미노펜)" → "아세트아미노펜"
|
|
24
|
+
*/
|
|
25
|
+
extractMainIngredient(itemName) {
|
|
26
|
+
const match = itemName.match(/\(([^()]+)\)\s*$/);
|
|
27
|
+
return match?.[1]?.trim();
|
|
28
|
+
}
|
|
29
|
+
async fetch(params) {
|
|
30
|
+
const data = await this.http.getJson({
|
|
31
|
+
url: E_DRUG_BASE,
|
|
32
|
+
params: { ...params, pageNo: 1 },
|
|
33
|
+
});
|
|
34
|
+
this.assertSuccess(data);
|
|
35
|
+
return {
|
|
36
|
+
totalCount: data.body?.totalCount ?? 0,
|
|
37
|
+
items: data.body?.items ?? [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
assertSuccess(data) {
|
|
41
|
+
const code = data.header?.resultCode;
|
|
42
|
+
if (code !== undefined && code !== "00") {
|
|
43
|
+
throw new ApiError(`식약처 API 오류 (${code}): ${data.header?.resultMsg ?? "unknown"}`, undefined, E_DRUG_BASE);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/domains/drug/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAA;AAGjD,MAAM,WAAW,GAAG,2EAA2E,CAAA;AAE/F;;;;;GAKG;AACH,MAAM,OAAO,UAAU;IACQ;IAA7B,YAA6B,IAAgB;QAAhB,SAAI,GAAJ,IAAI,CAAY;IAAG,CAAC;IAEjD,KAAK,CAAC,MAAM,CAAC,KAAa,EAAE,KAAK,GAAG,EAAE;QACpC,OAAO,IAAI,CAAC,KAAK,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAA;IAC1D,CAAC;IAED,KAAK,CAAC,YAAY,CAAC,OAAe;QAChC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAA;QAC1D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;IACxB,CAAC;IAED;;;OAGG;IACH,qBAAqB,CAAC,QAAgB;QACpC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAA;QAChD,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAA;IAC3B,CAAC;IAEO,KAAK,CAAC,KAAK,CAAC,MAAuC;QACzD,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAsB;YACxD,GAAG,EAAE,WAAW;YAChB,MAAM,EAAE,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE;SACjC,CAAC,CAAA;QAEF,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAA;QAExB,OAAO;YACL,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,IAAI,CAAC;YACtC,KAAK,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,EAAE;SAC9B,CAAA;IACH,CAAC;IAEO,aAAa,CAAC,IAAyB;QAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,UAAU,CAAA;QACpC,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;YACxC,MAAM,IAAI,QAAQ,CAChB,eAAe,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,SAAS,IAAI,SAAS,EAAE,EAC9D,SAAS,EACT,WAAW,CACZ,CAAA;QACH,CAAC;IACH,CAAC;CACF"}
|