korean-law-mcp 4.2.1 → 4.4.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/README.md +70 -19
- package/build/lib/errors.js +3 -1
- package/build/lib/law-search.js +24 -26
- package/build/lib/query-router.js +65 -8
- package/build/lib/tool-profiles.js +4 -0
- package/build/lib/xml-parser.d.ts +5 -0
- package/build/lib/xml-parser.js +7 -0
- package/build/server/http-server.js +44 -13
- package/build/tool-registry.d.ts +5 -0
- package/build/tool-registry.js +53 -16
- package/build/tools/applicable-law.d.ts +30 -0
- package/build/tools/applicable-law.js +248 -0
- package/build/tools/article-detail.js +2 -1
- package/build/tools/chains.js +0 -3
- package/build/tools/cite-check.d.ts +28 -0
- package/build/tools/cite-check.js +244 -0
- package/build/tools/law-text.js +4 -2
- package/build/tools/legal-analysis.d.ts +29 -0
- package/build/tools/legal-analysis.js +55 -0
- package/build/tools/legal-research.d.ts +54 -0
- package/build/tools/legal-research.js +105 -0
- package/build/tools/ordinance.js +2 -1
- package/build/tools/scenarios/index.js +12 -3
- package/build/tools/scenarios/time-travel.js +4 -3
- package/build/tools/verify-citations.js +4 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Korean Law MCP
|
|
2
2
|
|
|
3
|
-
**법제처 42개 API를
|
|
3
|
+
**법제처 42개 API를 9개 도구로.** 법령, 판례, 행정규칙, 자치법규, 조약, 해석례(국세청 포함) + **LLM 환각 방지 인용 검증** + **조문 영향 그래프** + **시점 비교 자동 diff** + **이럴 땐 이렇게 — 5단계 안내** + **판례 생사 확인(Citator)** + **행위시법 판단**을 AI 어시스턴트나 터미널에서 바로 사용.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/korean-law-mcp)
|
|
6
6
|
[](https://modelcontextprotocol.io)
|
|
@@ -14,6 +14,43 @@
|
|
|
14
14
|
|
|
15
15
|
---
|
|
16
16
|
|
|
17
|
+
## v4.4.0 — 노출 도구 통폐합 19개 → 9개 (컨텍스트 52% 감축)
|
|
18
|
+
|
|
19
|
+
MCP 클라이언트가 매 세션 읽는 도구 목록(ListTools)을 ~15.1KB → ~7.2KB로 줄였습니다.
|
|
20
|
+
|
|
21
|
+
- `chain_*` 8개 → **`legal_research`** 하나로 (`task` 파라미터: full_research·law_system·action_basis·dispute_prep·amendment_track·ordinance_compare·procedure_detail·document_review)
|
|
22
|
+
- 킬러피처 4개(`verify_citations`·`cite_check`·`applicable_law`·`impact_map`) → **`legal_analysis`** 하나로 (`mode` 파라미터)
|
|
23
|
+
- **하위호환**: 기존 도구명 직접 호출·`execute_tool` 경유 모두 그대로 동작. 광고 목록에서만 빠짐
|
|
24
|
+
|
|
25
|
+
## v4.3 — 판례 생사 확인 + 행위시법 판단
|
|
26
|
+
|
|
27
|
+
**"이 판례 아직 유효한가?" + "사건 시점엔 어떤 법이 적용되나?"** — 법률 실무에서 가장 위험한 두 실수를 잡는다.
|
|
28
|
+
|
|
29
|
+
### 1. `cite_check` — 판례 생사 확인 (한국형 Shepard's Citator)
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
"2007다27670 아직 유효해?"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
→ 그 사건번호를 **인용한 후속 판례를 본문검색으로 역추적** + 전원합의체 후속 판결 본문 정밀 스캔 → 변경·폐기 선언 감지:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
📊 판정: ❌ 변경·폐기 신호 감지 — 2018다248626(판례 변경 선언, 저촉 범위 변경)
|
|
39
|
+
맥락: "…2008년 전원합의체 판결은 이 판결의 견해와 배치되는 범위에서 변경하기로 한다…"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
판결문이 사건번호 대신 "(이하 '2008년 전원합의체 판결'이라 한다)" 별칭으로 변경 선언하는 관행까지 추적. 변경된 판례를 살아있는 것처럼 인용하는 사고를 차단한다. 무료 도구 중 유일.
|
|
43
|
+
|
|
44
|
+
### 2. `applicable_law` — 행위시법 판단 + 부칙 경과규정
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
"2023.5.10 당시 도로교통법 제44조"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
→ 기준일에 **시행 중이던 버전(MST) 특정** → 그 시점 조문 본문 → 현행과 비교 → **이후 개정 부칙의 적용례·경과조치 자동 발췌** + 행위시법(형법 §1)·제재처분 위반행위시법(행정기본법 §14③) 법리 안내. LLM이 현행법으로 오답하는 것을 구조적으로 방지.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
17
54
|
## v4.0 — 3개 킬러 기능 동시 추가
|
|
18
55
|
|
|
19
56
|
**조문 영향 그래프 + 시점 비교 + 단계별 안내.** 법무팀·연구자·실수요자가 매뉴얼로 며칠 걸리던 작업이 한 번에.
|
|
@@ -328,7 +365,7 @@ lexdiff에서 "산안기준규칙" 질의가 법제처 aiSearch의 키워드 부
|
|
|
328
365
|
**v3.0.2** — Unified Architecture + Setup Wizard
|
|
329
366
|
|
|
330
367
|
법제처 41개 API를 89개 MCP 도구로 구조화했던 v2.
|
|
331
|
-
v3는 같은 41개 API를 **14개 도구**로 재압축했습니다 (v3.2.2 이후 15개, v4.0
|
|
368
|
+
v3는 같은 41개 API를 **14개 도구**로 재압축했습니다 (v3.2.2 이후 15개, v4.3에서 19개, v4.4.0에서 통폐합으로 9개).
|
|
332
369
|
|
|
333
370
|
| | 법제처 원본 | v2 | v3 |
|
|
334
371
|
|---|:---:|:---:|:---:|
|
|
@@ -392,7 +429,7 @@ MCP 도구 설계에서 **도구 수 ≠ 기능 수**입니다.
|
|
|
392
429
|
|
|
393
430
|
대한민국에는 **1,600개 이상의 현행 법률**, **10,000개 이상의 행정규칙**, 그리고 대법원·헌법재판소·조세심판원·관세청까지 이어지는 방대한 판례 체계가 있습니다. 이 모든 게 [법제처](https://www.law.go.kr)라는 하나의 사이트에 있지만, 개발자 경험은 최악입니다.
|
|
394
431
|
|
|
395
|
-
이 프로젝트는 그 전체 법령 시스템을 **
|
|
432
|
+
이 프로젝트는 그 전체 법령 시스템을 **9개 도구**로 감싸서, AI 어시스턴트나 스크립트에서 바로 호출할 수 있게 만듭니다. 법제처를 수백 번 수동 검색하다 지친 공무원이 만들었습니다.
|
|
396
433
|
|
|
397
434
|
---
|
|
398
435
|
|
|
@@ -497,7 +534,7 @@ https://korean-law-mcp.fly.dev/mcp?oc=honggildong
|
|
|
497
534
|
|
|
498
535
|
> **참고**: 커넥터 URL을 수정하려면 삭제 후 다시 추가해야 합니다.
|
|
499
536
|
|
|
500
|
-
> v3부터 프로필 선택이 필요 없습니다.
|
|
537
|
+
> v3부터 프로필 선택이 필요 없습니다. 9개 도구가 42개 API 전체를 커버합니다.
|
|
501
538
|
> 기존에 `?profile=lite&oc=...` 주소를 넣으셨다면 **그대로 두셔도 됩니다** — 동일하게 작동합니다.
|
|
502
539
|
|
|
503
540
|
---
|
|
@@ -719,37 +756,51 @@ reg delete "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" /
|
|
|
719
756
|
|
|
720
757
|
---
|
|
721
758
|
|
|
722
|
-
## 도구 구조 (
|
|
759
|
+
## 도구 구조 (9개)
|
|
723
760
|
|
|
724
|
-
v4
|
|
761
|
+
v4.4.0은 9개 도구만 노출합니다 (컨텍스트 52% 감축). 기존 `chain_*` 8개는 `legal_research`의 `task`로, 킬러피처 4개는 `legal_analysis`의 `mode`로 통합. 나머지 전문 도구는 `discover_tools` → `execute_tool`로 접근하며, 기존 도구명 직접 호출도 하위호환으로 계속 동작합니다.
|
|
725
762
|
|
|
726
|
-
| 구분 | 도구 | 설명 |
|
|
727
|
-
|
|
728
|
-
|
|
|
729
|
-
| | `
|
|
730
|
-
| | `chain_action_basis` | 처분 근거 확인 (허가·인가·처분) | `penalty`: 처분·벌칙 기준 종합 / `action_plan`: 이럴 땐 이렇게, 5단계 안내 |
|
|
731
|
-
| | `chain_dispute_prep` | 쟁송 대비 (불복·소송·심판) | — |
|
|
732
|
-
| | `chain_amendment_track` | 개정 추적 (신구대조, 연혁) | `timeline`: 시계열 타임라인 / `time_travel`: 두 시점 자동 diff |
|
|
733
|
-
| | `chain_ordinance_compare` | 조례 비교 (상위법→전국 조례) | `compliance`: 상위법 적합성 검증 |
|
|
734
|
-
| | `chain_procedure_detail` | 절차·비용·서식 안내 | `manual`: 공무원 처리 매뉴얼 |
|
|
735
|
-
| | `chain_document_review` | 계약서·약관 리스크 분석 | — |
|
|
763
|
+
| 구분 | 도구 | 설명 |
|
|
764
|
+
|------|------|------|
|
|
765
|
+
| **리서치** (1) | `legal_research` | 다단계 법령 리서치 — `task` 8종 선택 (아래 표) |
|
|
766
|
+
| **정밀분석** (1) | `legal_analysis` | 검증·분석 — `mode` 4종 선택 (아래 표) |
|
|
736
767
|
| **법령** (3) | `search_law` | 법령 검색 → lawId, MST 획득 |
|
|
737
768
|
| | `get_law_text` | 조문 전문 조회 |
|
|
738
769
|
| | `get_annexes` | 별표/서식 조회 (금액표·요율표·별지서식) |
|
|
739
770
|
| **통합** (2) | `search_decisions` | **17개 도메인** 통합 검색 (판례·헌재·조세심판·공정위·노동위·관세·해석례·행심·개인정보위·권익위·소청심사·학칙·공사공단·공공기관·조약·영문법령) |
|
|
740
771
|
| | `get_decision_text` | **17개 도메인** 전문 조회 |
|
|
741
|
-
| **킬러** (2) | `verify_citations` | LLM 환각 방지 — 인용 조문 실존 여부 일괄 검증 (v3.5) |
|
|
742
|
-
| | `impact_map` | 조문 영향 그래프 — 인용 판례·해석·자치법규 역방향 탐색 + mermaid (v4.0) |
|
|
743
772
|
| **메타** (2) | `discover_tools` | 전문 도구 검색 (용어·별표·이력·비교 등) |
|
|
744
773
|
| | `execute_tool` | 전문 도구 프록시 실행 |
|
|
745
774
|
|
|
775
|
+
### `legal_research` task 8종 (구 chain_*)
|
|
776
|
+
|
|
777
|
+
| task | 설명 | 시나리오 확장 |
|
|
778
|
+
|------|------|-------------|
|
|
779
|
+
| `full_research` (기본) | 종합 리서치 (AI검색→법령→판례→해석) | `customs`: 관세·통관 종합 / `action_plan`: 이럴 땐 이렇게, 5단계 안내 |
|
|
780
|
+
| `law_system` | 법체계 분석 (3단비교, 위임구조) | `delegation`: 위임입법 감시 / `impact`: 영향도 분석 |
|
|
781
|
+
| `action_basis` | 처분 근거 확인 (허가·인가·처분) | `penalty`: 처분·벌칙 기준 종합 |
|
|
782
|
+
| `dispute_prep` | 쟁송 대비 (불복·소송·심판) | `domain`: tax/labor/privacy/competition |
|
|
783
|
+
| `amendment_track` | 개정 추적 (신구대조, 연혁) | `timeline`: 시계열 타임라인 / `time_travel`: 두 시점 자동 diff |
|
|
784
|
+
| `ordinance_compare` | 조례 비교 (상위법→전국 조례) | `compliance`: 상위법 적합성 검증 |
|
|
785
|
+
| `procedure_detail` | 절차·비용·서식 안내 | `manual`: 공무원 처리 매뉴얼 |
|
|
786
|
+
| `document_review` | 계약서·약관 리스크 분석 (`text` 필수) | — |
|
|
787
|
+
|
|
788
|
+
### `legal_analysis` mode 4종 (구 킬러피처)
|
|
789
|
+
|
|
790
|
+
| mode | 설명 | 필수 파라미터 |
|
|
791
|
+
|------|------|-------------|
|
|
792
|
+
| `verify_citations` | LLM 환각 방지 — 인용 조문 실존 여부 일괄 검증 (v3.5) | `text` |
|
|
793
|
+
| `cite_check` | 판례 생사 확인 — 후속 인용 역추적 + 변경·폐기 감지, 한국형 Citator (v4.3) | `caseNumber` |
|
|
794
|
+
| `applicable_law` | 행위시법 판단 — 시점 적용 버전 + 부칙 경과규정 발췌 (v4.3) | `lawName`, `date` |
|
|
795
|
+
| `impact_map` | 조문 영향 그래프 — 인용 판례·해석·자치법규 역방향 탐색 + mermaid (v4.0) | `lawName`, `jo` |
|
|
796
|
+
|
|
746
797
|
전체 도구 상세는 [docs/API.md](docs/API.md) 참조.
|
|
747
798
|
|
|
748
799
|
---
|
|
749
800
|
|
|
750
801
|
## 주요 특징
|
|
751
802
|
|
|
752
|
-
- **42개 API →
|
|
803
|
+
- **42개 API → 9개 도구** — 법령, 판례, 행정규칙, 자치법규, 헌재결정, 조세심판, 관세해석, 국세청 해석례, 조약, 학칙/공단/공공기관 규정, 법령용어
|
|
753
804
|
- **MCP + CLI** — Claude Desktop에서도, 터미널에서도 같은 도구 사용
|
|
754
805
|
- **법률 도메인 특화** — 약칭 자동 인식(`화관법` → `화학물질관리법`), 조문번호 변환(`제38조` ↔ `003800`), 3단 위임 구조 시각화
|
|
755
806
|
- **별표/별지서식 본문 추출** — HWPX·HWP·PDF·XLSX·DOCX 자동 변환 ([kordoc](https://github.com/chrisryugj/kordoc) 엔진)
|
package/build/lib/errors.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 통일된 에러 처리 모듈
|
|
3
3
|
*/
|
|
4
|
+
import { maskSensitiveUrl } from "./fetch-with-retry.js";
|
|
4
5
|
/**
|
|
5
6
|
* 에러 코드
|
|
6
7
|
*/
|
|
@@ -114,7 +115,8 @@ export function formatToolError(error, context) {
|
|
|
114
115
|
suggestions = [];
|
|
115
116
|
}
|
|
116
117
|
const lines = [];
|
|
117
|
-
|
|
118
|
+
// 최종 방어선 — 도구 코드가 URL 포함 에러를 직접 만들어도 API 키가 클라이언트로 새지 않게
|
|
119
|
+
lines.push(`[${code}] ${maskSensitiveUrl(msg)}`);
|
|
118
120
|
if (context) {
|
|
119
121
|
lines.push(`도구: ${context}`);
|
|
120
122
|
}
|
package/build/lib/law-search.js
CHANGED
|
@@ -64,45 +64,43 @@ export async function findLaws(apiClient, query, apiKey, max = 3, searchDisplay
|
|
|
64
64
|
if (cached)
|
|
65
65
|
return cached.slice(0, max);
|
|
66
66
|
const effectiveMax = Math.max(max, searchDisplay); // 정렬 대상 전체 수집
|
|
67
|
+
// 인프라 에러(타임아웃·5xx·파싱 실패)는 "법령 없음"과 구분해야 한다.
|
|
68
|
+
// 삼키면 법제처 장애 중 verify_citations가 실존 조문을 NOT_FOUND로 오판한다.
|
|
69
|
+
let lastInfraError;
|
|
70
|
+
const trySearch = async (q) => {
|
|
71
|
+
try {
|
|
72
|
+
const xmlText = await apiClient.searchLaw(q, apiKey, searchDisplay);
|
|
73
|
+
return parseLawXml(xmlText, effectiveMax);
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
if (e instanceof Error && /429|401|403|API 키/.test(e.message))
|
|
77
|
+
throw e;
|
|
78
|
+
lastInfraError = e;
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
};
|
|
67
82
|
// 1차: 원본 쿼리
|
|
68
|
-
let results =
|
|
69
|
-
try {
|
|
70
|
-
const xmlText = await apiClient.searchLaw(query, apiKey, searchDisplay);
|
|
71
|
-
results = parseLawXml(xmlText, effectiveMax);
|
|
72
|
-
}
|
|
73
|
-
catch (e) {
|
|
74
|
-
if (e instanceof Error && /429|401|403|API 키/.test(e.message))
|
|
75
|
-
throw e;
|
|
76
|
-
}
|
|
83
|
+
let results = await trySearch(query);
|
|
77
84
|
// 2차: 부가 키워드 제거
|
|
78
85
|
if (results.length === 0) {
|
|
79
86
|
const stripped = stripNonLawKeywords(query);
|
|
80
87
|
if (stripped && stripped !== query) {
|
|
81
|
-
|
|
82
|
-
const xmlText = await apiClient.searchLaw(stripped, apiKey, searchDisplay);
|
|
83
|
-
results = parseLawXml(xmlText, effectiveMax);
|
|
84
|
-
}
|
|
85
|
-
catch (e) {
|
|
86
|
-
if (e instanceof Error && /429|401|403|API 키/.test(e.message))
|
|
87
|
-
throw e;
|
|
88
|
-
}
|
|
88
|
+
results = await trySearch(stripped);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
// 3차: 법령명 패턴 직접 추출
|
|
92
92
|
if (results.length === 0) {
|
|
93
93
|
const lawNameMatch = query.match(/[가-힣]+(법|시행령|시행규칙|규칙|규정|령)(?:\s|$)/);
|
|
94
94
|
if (lawNameMatch) {
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const xmlText = await apiClient.searchLaw(extracted, apiKey, searchDisplay);
|
|
98
|
-
results = parseLawXml(xmlText, effectiveMax);
|
|
99
|
-
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
if (e instanceof Error && /429|401|403|API 키/.test(e.message))
|
|
102
|
-
throw e;
|
|
103
|
-
}
|
|
95
|
+
results = await trySearch(lawNameMatch[0].trim());
|
|
104
96
|
}
|
|
105
97
|
}
|
|
98
|
+
// 전 단계가 인프라 에러로만 끝났으면 "없음"이 아니라 "실패"로 전파
|
|
99
|
+
if (results.length === 0 && lastInfraError !== undefined) {
|
|
100
|
+
throw lastInfraError instanceof Error
|
|
101
|
+
? new Error(`법령 검색 실패 (법제처 API 오류 — 법령이 없다는 뜻이 아님): ${lastInfraError.message}`)
|
|
102
|
+
: lastInfraError;
|
|
103
|
+
}
|
|
106
104
|
// 관련도 정렬
|
|
107
105
|
if (results.length > 1) {
|
|
108
106
|
const queryWords = query.replace(NON_LAW_NAME_RE, " ")
|
|
@@ -29,15 +29,16 @@ function extractLawName(query) {
|
|
|
29
29
|
// 수식어: 단독 키워드만 제거 (법령명 일부인 경우 보존)
|
|
30
30
|
// "별표 1", "별표" 등 독립적 사용만 제거
|
|
31
31
|
.replace(/별표\s*\d*/g, "")
|
|
32
|
-
|
|
33
|
-
.replace(/(?:^|\s)(
|
|
34
|
-
.replace(/(?:^|\s)(
|
|
35
|
-
.replace(/(?:^|\s)(
|
|
36
|
-
.replace(/(?:^|\s)(
|
|
37
|
-
.replace(/(?:^|\s)(
|
|
32
|
+
// 뒤 경계는 lookahead(소비 안 함) — 소비하면 "개정 연혁"처럼 연속 키워드의 두 번째가 살아남음
|
|
33
|
+
.replace(/(?:^|\s)(판례|판결|사례|대법원|헌재|행정심판)(?=\s|$)/g, " ")
|
|
34
|
+
.replace(/(?:^|\s)(해석례?|유권해석|질의회신)(?=\s|$)/g, " ")
|
|
35
|
+
.replace(/(?:^|\s)(개정|이력|변경|연혁|신구대조)(?=\s|$)/g, " ")
|
|
36
|
+
.replace(/(?:^|\s)(3단비교|위임|인용|체계)(?=\s|$)/g, " ")
|
|
37
|
+
.replace(/(?:^|\s)(영문|영어|English)(?=\s|$)/gi, " ")
|
|
38
|
+
.replace(/(?:^|\s)(서식|양식|별지|신청서)(?=\s|$)/g, " ")
|
|
38
39
|
// 조례/규칙은 법령명 일부이므로 유지
|
|
39
40
|
// 동사형 수식어 제거
|
|
40
|
-
.replace(/(?:^|\s)(검색|조회|확인|알려줘|찾아줘|보여줘)(
|
|
41
|
+
.replace(/(?:^|\s)(검색|조회|확인|알려줘|찾아줘|보여줘)(?=\s|$)/g, " ")
|
|
41
42
|
// 정리
|
|
42
43
|
.replace(/\s+/g, " ")
|
|
43
44
|
.trim();
|
|
@@ -70,6 +71,11 @@ const routePatterns = [
|
|
|
70
71
|
if (/(?:파급|영향\s*그래프|impact|인용한\s*(?:모든|판례|판결|어디))/i.test(query)) {
|
|
71
72
|
return { _skip: true };
|
|
72
73
|
}
|
|
74
|
+
// applicable_law 양보: 기준일 + 시점 키워드 (예: "2023.5.10 당시 도로교통법 제44조")
|
|
75
|
+
if (/행위시법/.test(query) ||
|
|
76
|
+
(/\d{4}\s*[년.\-/]\s*\d{1,2}/.test(query) && /당시|시점|기준|에\s*적용/.test(query))) {
|
|
77
|
+
return { _skip: true };
|
|
78
|
+
}
|
|
73
79
|
const jo = extractArticleNumber(query);
|
|
74
80
|
const lawName = extractLawName(query);
|
|
75
81
|
return { _searchQuery: lawName, jo, _needsMst: true };
|
|
@@ -592,6 +598,55 @@ const routePatterns = [
|
|
|
592
598
|
reason: "시민 상황 키워드 → chain_full_research (action_plan 5단계 가이드)",
|
|
593
599
|
priority: 7,
|
|
594
600
|
},
|
|
601
|
+
// ── 29-0-3. 판례 생사 확인 (cite_check, v4.3) ──
|
|
602
|
+
// "2013다61381 아직 유효해?", "이 판례 변경됐어?", "2018두42559 인용 추적"
|
|
603
|
+
{
|
|
604
|
+
name: "cite_check",
|
|
605
|
+
patterns: [
|
|
606
|
+
/\d{2,4}\s*[가-힣]{1,5}\s*\d{1,7}.*?(?:유효|살아|변경|폐기|뒤집|생사|추적|아직|citator)/i,
|
|
607
|
+
/판례\s*(?:생사|유효성|변경\s*여부|폐기\s*여부|인용\s*추적)/,
|
|
608
|
+
/(?:변경|폐기)된?\s*판례(?:인지|냐|인가요?|\s*확인)/,
|
|
609
|
+
],
|
|
610
|
+
tool: "cite_check",
|
|
611
|
+
extract: (query) => {
|
|
612
|
+
const m = query.match(/(\d{2,4})\s*([가-힣]{1,5})\s*(\d{1,7})/);
|
|
613
|
+
if (!m)
|
|
614
|
+
return { _fallback: true, query };
|
|
615
|
+
return { caseNumber: `${m[1]}${m[2]}${m[3]}` };
|
|
616
|
+
},
|
|
617
|
+
reason: "사건번호 + 유효성 키워드 → cite_check (후속 인용 역추적 + 변경·폐기 감지)",
|
|
618
|
+
priority: 2,
|
|
619
|
+
},
|
|
620
|
+
// ── 29-0-4. 행위시법 판단 (applicable_law, v4.3) ──
|
|
621
|
+
// "2023년 5월 당시 도로교통법 제44조", "사건 시점에 적용되는 법", "행위시법"
|
|
622
|
+
{
|
|
623
|
+
name: "applicable_law",
|
|
624
|
+
patterns: [
|
|
625
|
+
/(\d{4})\s*[년\.\-\/]\s*(\d{1,2})\s*[월\.\-\/]?\s*(\d{1,2})?\s*일?\s*(?:당시|시점|기준|에\s*적용)/,
|
|
626
|
+
/(?:행위\s*시|사건\s*당시|계약\s*당시|위반\s*당시)\s*(?:의\s*)?(?:법|적용)/,
|
|
627
|
+
/행위시법|적용\s*법령\s*판단|당시\s*시행/,
|
|
628
|
+
],
|
|
629
|
+
tool: "applicable_law",
|
|
630
|
+
extract: (query) => {
|
|
631
|
+
const dm = query.match(/(\d{4})\s*[년\.\-\/]\s*(\d{1,2})(?:\s*[월\.\-\/]\s*(\d{1,2}))?/);
|
|
632
|
+
if (!dm)
|
|
633
|
+
return { _fallback: true, query };
|
|
634
|
+
const date = `${dm[1]}${dm[2].padStart(2, "0")}${(dm[3] || "1").padStart(2, "0")}`;
|
|
635
|
+
const jo = extractArticleNumber(query);
|
|
636
|
+
// 키워드 strip은 단어 경계 필수 — "근로기준법"의 "기준"을 떼면 법령명 파괴
|
|
637
|
+
const lawName = extractLawName(query.replace(/(\d{4})\s*[년\.\-\/]\s*\d{1,2}\s*[월\.\-\/]?\s*\d{0,2}\s*일?/g, " ")
|
|
638
|
+
.replace(/(?:^|\s)(당시|시점|기준일?|행위시법|사건|적용|시행)(?=\s|$)/g, " ")
|
|
639
|
+
.replace(/에\s*적용되?는?\s*법령?/g, " "));
|
|
640
|
+
if (!lawName)
|
|
641
|
+
return { _fallback: true, query };
|
|
642
|
+
const params = { lawName, date };
|
|
643
|
+
if (jo)
|
|
644
|
+
params.jo = jo;
|
|
645
|
+
return params;
|
|
646
|
+
},
|
|
647
|
+
reason: "기준일 + 법령명 → applicable_law (시점 적용 버전 + 부칙 경과규정)",
|
|
648
|
+
priority: 2,
|
|
649
|
+
},
|
|
595
650
|
// ── 29-1. 인용 검증 (citation validator) ──
|
|
596
651
|
{
|
|
597
652
|
name: "verify_citations",
|
|
@@ -709,8 +764,10 @@ export function routeQuery(query) {
|
|
|
709
764
|
// 자연어 날짜 조건 추출 (검색어에서 시간 표현 분리)
|
|
710
765
|
const dateParsed = parseDateRange(q);
|
|
711
766
|
const dateRange = dateParsed.range;
|
|
767
|
+
// 행위시법(applicable_law) 의도는 날짜 자체가 파라미터 — 날짜 제거 전 원문으로 매칭해야 함
|
|
768
|
+
const applicableLawHint = /행위시법|\d{4}\s*[년.\-/].{0,14}(?:당시|시점|기준|에\s*적용)/.test(q);
|
|
712
769
|
// 날짜 표현이 제거된 순수 검색어로 패턴 매칭
|
|
713
|
-
const routeInput = dateParsed.cleanQuery || q;
|
|
770
|
+
const routeInput = applicableLawHint ? q : (dateParsed.cleanQuery || q);
|
|
714
771
|
const result = _matchRoute(routeInput);
|
|
715
772
|
// 날짜 범위가 있으면 결과에 첨부
|
|
716
773
|
if (dateRange) {
|
|
@@ -35,6 +35,8 @@ export const TOOL_ALIASES = {
|
|
|
35
35
|
"해석례": ["법제처 해석", "유권해석", "질의회신"],
|
|
36
36
|
// 도구 의도 별칭
|
|
37
37
|
"인용검증": ["verify_citations", "조문 실존 확인", "환각 검증"],
|
|
38
|
+
"판례생사": ["cite_check", "판례 유효성", "판례 변경 여부", "인용 추적", "citator"],
|
|
39
|
+
"행위시법": ["applicable_law", "당시 법령", "적용 법령 판단", "경과조치", "부칙"],
|
|
38
40
|
"문서검토": ["analyze_document", "chain_document_review", "계약서 검토", "약관 검토"],
|
|
39
41
|
"처분기준": ["chain_action_basis", "과태료 기준", "과징금 기준", "영업정지 기간"],
|
|
40
42
|
"절차매뉴얼": ["chain_procedure_detail", "처리 절차", "신청 방법", "수수료"],
|
|
@@ -70,5 +72,7 @@ export const TOOL_CATEGORIES = {
|
|
|
70
72
|
"영문법령": ["search_english_law", "get_english_law_text"],
|
|
71
73
|
"용어": ["search_legal_terms", "get_legal_term_kb", "get_legal_term_detail", "get_daily_term", "get_daily_to_legal", "get_legal_to_daily", "get_term_articles", "get_related_laws"],
|
|
72
74
|
"문서분석": ["analyze_document"],
|
|
75
|
+
"판례생사": ["cite_check"],
|
|
76
|
+
"행위시법": ["applicable_law"],
|
|
73
77
|
"유틸리티": ["parse_jo_code", "get_law_abbreviations"],
|
|
74
78
|
};
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* 예: <strong class="tbl_tx_type">지방</strong>자치법 → 지방자치법
|
|
8
8
|
*/
|
|
9
9
|
export declare function stripHtml(text: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* 단일 객체 정규화 (Critical Rule 6) — API 응답의 배열 필드가 단일 객체로 올 수 있음.
|
|
12
|
+
* 기존 수동 패턴 `Array.isArray(x) ? x : x ? [x] : []`과 의미 동일 (falsy → []).
|
|
13
|
+
*/
|
|
14
|
+
export declare function toArray<T>(x: T | T[] | null | undefined): T[];
|
|
10
15
|
/**
|
|
11
16
|
* XML 태그에서 텍스트 추출 (CDATA 지원)
|
|
12
17
|
*/
|
package/build/lib/xml-parser.js
CHANGED
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
export function stripHtml(text) {
|
|
10
10
|
return text.replace(/<[^>]+>/g, "");
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* 단일 객체 정규화 (Critical Rule 6) — API 응답의 배열 필드가 단일 객체로 올 수 있음.
|
|
14
|
+
* 기존 수동 패턴 `Array.isArray(x) ? x : x ? [x] : []`과 의미 동일 (falsy → []).
|
|
15
|
+
*/
|
|
16
|
+
export function toArray(x) {
|
|
17
|
+
return Array.isArray(x) ? x : x ? [x] : [];
|
|
18
|
+
}
|
|
12
19
|
/**
|
|
13
20
|
* XML 태그에서 텍스트 추출 (CDATA 지원)
|
|
14
21
|
*/
|
|
@@ -9,6 +9,7 @@ import express from "express";
|
|
|
9
9
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
10
|
import { requestContext } from "../lib/session-state.js";
|
|
11
11
|
import { maskSensitiveUrl } from "../lib/fetch-with-retry.js";
|
|
12
|
+
import { TOOL_COUNTS } from "../tool-registry.js";
|
|
12
13
|
import { VERSION } from "../version.js";
|
|
13
14
|
/**
|
|
14
15
|
* 에러 메시지에서 민감 정보(API 키 포함 URL) scrub.
|
|
@@ -97,26 +98,49 @@ export async function startHTTPServer(createServer, port) {
|
|
|
97
98
|
health: "/health",
|
|
98
99
|
},
|
|
99
100
|
tools: {
|
|
100
|
-
exposed:
|
|
101
|
-
total:
|
|
102
|
-
description:
|
|
101
|
+
exposed: TOOL_COUNTS.exposed,
|
|
102
|
+
total: TOOL_COUNTS.total,
|
|
103
|
+
description: `V3_EXPOSED ${TOOL_COUNTS.exposed}개 직노출, 나머지 ${TOOL_COUNTS.total - TOOL_COUNTS.exposed}개는 execute_tool 경유`,
|
|
103
104
|
},
|
|
104
105
|
});
|
|
105
106
|
});
|
|
106
107
|
app.get("/health", (req, res) => {
|
|
107
108
|
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
108
109
|
});
|
|
110
|
+
// 서버 LAW_OC 폴백 사용량 전역 상한 — 키 없는 분산 요청이 서버 키의 법제처 quota를
|
|
111
|
+
// 소진시키는 것 방지 (IP당 limit만으로는 막을 수 없음). 0이면 폴백 비활성.
|
|
112
|
+
const fallbackRpm = parseInt(process.env.FALLBACK_RATE_LIMIT_RPM || "120", 10);
|
|
113
|
+
const fallbackBucket = { count: 0, resetAt: 0 };
|
|
114
|
+
function fallbackAllowed() {
|
|
115
|
+
if (fallbackRpm <= 0)
|
|
116
|
+
return false;
|
|
117
|
+
const now = Date.now();
|
|
118
|
+
if (now >= fallbackBucket.resetAt) {
|
|
119
|
+
fallbackBucket.count = 0;
|
|
120
|
+
fallbackBucket.resetAt = now + 60000;
|
|
121
|
+
}
|
|
122
|
+
return ++fallbackBucket.count <= fallbackRpm;
|
|
123
|
+
}
|
|
109
124
|
// POST /mcp - stateless 요청 처리
|
|
110
125
|
app.post("/mcp", async (req, res) => {
|
|
111
|
-
// Extract API key: URL query
|
|
112
|
-
|
|
113
|
-
const apiKey =
|
|
114
|
-
req.headers["apikey"] ||
|
|
126
|
+
// Extract API key: header > URL query
|
|
127
|
+
// 쿼리스트링 키는 프록시/엣지 액세스 로그에 평문으로 남으므로 헤더 사용 권장 (하위호환용 유지)
|
|
128
|
+
const apiKey = req.headers["apikey"] ||
|
|
115
129
|
req.headers["law_oc"] ||
|
|
116
130
|
req.headers["law-oc"] ||
|
|
117
131
|
req.headers["x-api-key"] ||
|
|
118
132
|
req.headers["authorization"]?.replace(/^Bearer\s+/i, "") ||
|
|
119
|
-
req.headers["x-law-oc"]
|
|
133
|
+
req.headers["x-law-oc"] ||
|
|
134
|
+
req.query.oc;
|
|
135
|
+
// 자체 키 없는 요청은 서버 LAW_OC로 폴백 — 전역 상한 적용
|
|
136
|
+
if (!apiKey && !fallbackAllowed()) {
|
|
137
|
+
res.status(429).json({
|
|
138
|
+
jsonrpc: "2.0",
|
|
139
|
+
error: { code: -32000, message: "Shared API quota exceeded. Provide your own key via 'apiKey' header (free: https://open.law.go.kr)." },
|
|
140
|
+
id: null,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
120
144
|
let server;
|
|
121
145
|
let transport;
|
|
122
146
|
try {
|
|
@@ -180,12 +204,19 @@ export async function startHTTPServer(createServer, port) {
|
|
|
180
204
|
console.error(`✓ MCP endpoint: http://0.0.0.0:${port}/mcp`);
|
|
181
205
|
console.error(`✓ Health check: http://0.0.0.0:${port}/health`);
|
|
182
206
|
});
|
|
183
|
-
// 종료 처리
|
|
184
|
-
|
|
207
|
+
// 종료 처리 — in-flight 요청 완료 대기 (최대 10초), 이후 강제 종료
|
|
208
|
+
function gracefulShutdown(signal) {
|
|
185
209
|
console.error(`${signal} received, shutting down server...`);
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
const forceExit = setTimeout(() => {
|
|
211
|
+
console.error("Shutdown timeout (10s) — forcing exit");
|
|
212
|
+
process.exit(1);
|
|
213
|
+
}, 10000);
|
|
214
|
+
forceExit.unref();
|
|
215
|
+
expressServer.close(() => {
|
|
216
|
+
clearTimeout(forceExit);
|
|
217
|
+
console.error("Server shutdown complete");
|
|
218
|
+
process.exit(0);
|
|
219
|
+
});
|
|
189
220
|
}
|
|
190
221
|
process.on("SIGINT", () => gracefulShutdown("SIGINT"));
|
|
191
222
|
process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
|
package/build/tool-registry.d.ts
CHANGED
|
@@ -9,4 +9,9 @@ import type { McpTool } from "./lib/types.js";
|
|
|
9
9
|
* 모든 MCP 도구 정의
|
|
10
10
|
*/
|
|
11
11
|
export declare const allTools: McpTool[];
|
|
12
|
+
/** 노출/전체 도구 수 — 헬스체크 등 표기용 파생값 (하드코딩 금지) */
|
|
13
|
+
export declare const TOOL_COUNTS: {
|
|
14
|
+
exposed: number;
|
|
15
|
+
total: number;
|
|
16
|
+
};
|
|
12
17
|
export declare function registerTools(server: Server, apiClient: LawApiClient): void;
|
package/build/tool-registry.js
CHANGED
|
@@ -53,6 +53,11 @@ import { getLinkedOrdinances, LinkedOrdinancesSchema, getLinkedOrdinanceArticles
|
|
|
53
53
|
import { analyzeDocument, AnalyzeDocumentSchema } from "./tools/document-analysis.js";
|
|
54
54
|
import { verifyCitations, VerifyCitationsSchema } from "./tools/verify-citations.js";
|
|
55
55
|
import { impactMap, ImpactMapSchema } from "./tools/impact-map.js";
|
|
56
|
+
import { citeCheck, CiteCheckSchema } from "./tools/cite-check.js";
|
|
57
|
+
import { applicableLaw, ApplicableLawSchema } from "./tools/applicable-law.js";
|
|
58
|
+
// 통합 진입점 (v4.4.0 — 노출 도구 수 축소용)
|
|
59
|
+
import { legalResearch, LegalResearchSchema } from "./tools/legal-research.js";
|
|
60
|
+
import { legalAnalysis, LegalAnalysisSchema } from "./tools/legal-analysis.js";
|
|
56
61
|
// Chain tool imports
|
|
57
62
|
import { chainLawSystem, chainLawSystemSchema, chainActionBasis, chainActionBasisSchema, chainDisputePrep, chainDisputePrepSchema, chainAmendmentTrack, chainAmendmentTrackSchema, chainOrdinanceCompare, chainOrdinanceCompareSchema, chainFullResearch, chainFullResearchSchema, chainProcedureDetail, chainProcedureDetailSchema, chainDocumentReview, chainDocumentReviewSchema, } from "./tools/chains.js";
|
|
58
63
|
/**
|
|
@@ -546,6 +551,21 @@ export const allTools = [
|
|
|
546
551
|
schema: GetArticleWithPrecedentsSchema,
|
|
547
552
|
handler: getArticleWithPrecedents
|
|
548
553
|
},
|
|
554
|
+
// === 통합 진입점 (v4.4.0) ===
|
|
555
|
+
// legal_research/legal_analysis가 아래 chain_*/킬러피처 12개를 대체 노출.
|
|
556
|
+
// 원본 도구는 allTools에 유지 — 직접 CallTool/execute_tool 하위호환.
|
|
557
|
+
{
|
|
558
|
+
name: "legal_research",
|
|
559
|
+
description: "[⛓리서치] 다단계 법령 리서치 통합 — 여러 API를 병렬로 엮는 복합 질문 전용. task: full_research=도메인·법령명 불명확한 자연어 질문 폴백(기본값, 예 '음주운전 처벌 기준') | law_system=법률·시행령·시행규칙 3단+위임+별표(예 '관세법 체계') | action_basis=처분·허가의 법적 근거+해석례+판례+행심(예 '영업정지 근거') | dispute_prep=불복·소송 준비, 판례+심판례+도메인 결정례(예 '과세처분 불복') | amendment_track=개정 이력+신구대조+연혁(예 '2023년 개정 뭐 바뀜') | ordinance_compare=조례 전국 비교+상위법 적합성(예 '서울시 주차 조례') | procedure_detail=절차·수수료·별표서식(예 '건축허가 절차') | document_review=계약서·약관 조항 리스크+근거법령(text 필수). 단일 조회로 답이 되면 search_law/get_law_text 쓸 것.",
|
|
560
|
+
schema: LegalResearchSchema,
|
|
561
|
+
handler: legalResearch
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
name: "legal_analysis",
|
|
565
|
+
description: "[정밀분석] 검증·분석 4종 통합. mode: verify_citations=텍스트 속 조문 인용('민법 제750조' 등)이 실존하는지 법제처 DB 교차검증, LLM 환각 방지(text 필수) | cite_check=판례 생사 확인 — 사건번호로 후속 인용 역추적+변경·폐기 감지, 한국형 Citator(caseNumber 필수) | applicable_law=사건 시점에 시행되던 법령 버전+그 시점 조문+부칙 경과조치, 행위시법 판단(lawName+date 필수, jo 선택) | impact_map=한 조문을 인용한 판례·헌재·해석례·행심·조례 역방향 그래프+mermaid(lawName+jo 필수)",
|
|
566
|
+
schema: LegalAnalysisSchema,
|
|
567
|
+
handler: legalAnalysis
|
|
568
|
+
},
|
|
549
569
|
// === 체인 도구 (다단계 자동 실행) ===
|
|
550
570
|
// 사용 원칙: 단일 조회(search_law/get_law_text)로 답이 되면 체인 쓰지 말 것.
|
|
551
571
|
// 체인은 "여러 API를 병렬로 엮어야 하는" 복합 질문 전용.
|
|
@@ -618,10 +638,24 @@ export const allTools = [
|
|
|
618
638
|
schema: ImpactMapSchema,
|
|
619
639
|
handler: impactMap
|
|
620
640
|
},
|
|
641
|
+
// === 판례 인용 추적 (v4.3 killer feature) ===
|
|
642
|
+
{
|
|
643
|
+
name: "cite_check",
|
|
644
|
+
description: "[판례생사] 한국형 Shepard's Citator — 사건번호(예: 2013다61381)로 ① 그 판례를 인용한 후속 판례 역추적(본문검색) ② 전원합의체 후속 판결의 변경·폐기 문구 정밀 스캔 ③ 계속인용/변경가능성 판정. '이 판례 아직 유효한가' 확인용. 변경·폐기된 판례 인용 사고 방지.",
|
|
645
|
+
schema: CiteCheckSchema,
|
|
646
|
+
handler: citeCheck
|
|
647
|
+
},
|
|
648
|
+
// === 행위시법 판단 (v4.3 killer feature) ===
|
|
649
|
+
{
|
|
650
|
+
name: "applicable_law",
|
|
651
|
+
description: "[행위시법] '사건 시점(예: 2023.5.10)에 적용되는 법은?' — 기준일에 시행 중이던 법령 버전(MST) 특정 + 그 시점 조문 본문 + 현행과 비교 + 이후 개정 부칙의 적용례·경과조치 자동 발췌 + 행위시법/처분시법 법리 안내. lawName + date 필수, jo 선택. LLM이 현행법으로 오답하는 것 방지.",
|
|
652
|
+
schema: ApplicableLawSchema,
|
|
653
|
+
handler: applicableLaw
|
|
654
|
+
},
|
|
621
655
|
// === 메타 도구 (lite 프로필용) ===
|
|
622
656
|
{
|
|
623
657
|
name: "discover_tools",
|
|
624
|
-
description: "[메타] 위
|
|
658
|
+
description: "[메타] 위 도구로 안 되는 경우. 전문도구(조세심판·관세·헌재·행심·공정위·개인정보위·노동위·학칙·조약·영문법령·용어 등 80+개) 카테고리 검색",
|
|
625
659
|
schema: DiscoverToolsSchema,
|
|
626
660
|
handler: discoverTools
|
|
627
661
|
},
|
|
@@ -652,7 +686,10 @@ function toMcpInputSchema(schema) {
|
|
|
652
686
|
// Zod v4: z.toJSONSchema()로 직접 변환 (zod-to-json-schema는 Zod v4 미지원)
|
|
653
687
|
const rawSchema = z.toJSONSchema(schema);
|
|
654
688
|
if (rawSchema?.type === "object" && rawSchema?.properties) {
|
|
689
|
+
// apiKey는 HTTP 헤더(session-state)로 전달되는 게 정식 경로 — 광고 스키마에서 숨김.
|
|
690
|
+
// Zod parse는 여전히 수용하므로 인자로 넘기는 기존 클라이언트도 동작.
|
|
655
691
|
const props = { ...rawSchema.properties };
|
|
692
|
+
delete props.apiKey;
|
|
656
693
|
const required = Array.isArray(rawSchema.required)
|
|
657
694
|
? rawSchema.required.filter((k) => k !== "apiKey")
|
|
658
695
|
: [];
|
|
@@ -666,39 +703,39 @@ function toMcpInputSchema(schema) {
|
|
|
666
703
|
return rawSchema;
|
|
667
704
|
}
|
|
668
705
|
/**
|
|
669
|
-
*
|
|
706
|
+
* v4.4.0 통합 프로필 — 9개 도구 노출, 나머지는 execute_tool로 접근
|
|
670
707
|
*
|
|
671
708
|
* 노출 기준:
|
|
672
709
|
* 1) 체인 도구가 fallback으로 자주 호출하는 종착 도구
|
|
673
710
|
* 2) discover_tools → execute_tool 왕복으로 평균 5초+ 손실 발생
|
|
674
711
|
* 3) 그 외는 execute_tool 경유 유지
|
|
675
712
|
*
|
|
713
|
+
* v4.4.0 통폐합: chain_* 8개 → legal_research(task), 킬러피처 4개
|
|
714
|
+
* (verify_citations/cite_check/applicable_law/impact_map) → legal_analysis(mode).
|
|
715
|
+
* 원본 12개는 allTools에 유지 — CallTool 직접 호출/execute_tool 하위호환.
|
|
716
|
+
*
|
|
676
717
|
* ⚠️ get_annexes 제거 금지:
|
|
677
718
|
* 헬스장 환불 케이스(trace ld-1775959823220, 79s)에서 별표 3의2를 가져오기 위해
|
|
678
719
|
* discover_tools × 2 + execute_tool 헛발질로 ~15초 손실. 직노출로 해결.
|
|
679
720
|
*/
|
|
680
721
|
const V3_EXPOSED = new Set([
|
|
681
|
-
"
|
|
682
|
-
"
|
|
683
|
-
"chain_procedure_detail", "chain_document_review",
|
|
722
|
+
"legal_research", // v4.4.0: chain_* 8개 통합 (task 파라미터)
|
|
723
|
+
"legal_analysis", // v4.4.0: verify_citations/cite_check/applicable_law/impact_map 통합 (mode 파라미터)
|
|
684
724
|
"search_law", "get_law_text",
|
|
685
725
|
"get_annexes",
|
|
686
726
|
"search_decisions", "get_decision_text",
|
|
687
727
|
"discover_tools", "execute_tool",
|
|
688
|
-
"verify_citations", // v3.5: LLM 환각 방지 인용 검증
|
|
689
|
-
"impact_map", // v4.0: 조문 영향 그래프 (역방향 탐색 + mermaid)
|
|
690
728
|
]);
|
|
691
729
|
// 이름 기반 O(1) 조회용 Map
|
|
692
|
-
|
|
730
|
+
// allTools는 정적 — 모듈 로드 시 1회만 구성 (HTTP 모드에서 요청마다 재구성 방지)
|
|
731
|
+
const toolMap = new Map(allTools.map(tool => [tool.name, tool]));
|
|
732
|
+
// 메타 도구가 전체 도구 목록 참조할 수 있도록 주입
|
|
733
|
+
setAllToolsRef(allTools);
|
|
734
|
+
// V3_EXPOSED만 노출 (나머지는 execute_tool 경유)
|
|
735
|
+
const exposedTools = allTools.filter(t => V3_EXPOSED.has(t.name));
|
|
736
|
+
/** 노출/전체 도구 수 — 헬스체크 등 표기용 파생값 (하드코딩 금지) */
|
|
737
|
+
export const TOOL_COUNTS = { exposed: exposedTools.length, total: allTools.length };
|
|
693
738
|
export function registerTools(server, apiClient) {
|
|
694
|
-
// Map 초기화
|
|
695
|
-
toolMap.clear();
|
|
696
|
-
for (const tool of allTools)
|
|
697
|
-
toolMap.set(tool.name, tool);
|
|
698
|
-
// 메타 도구가 전체 도구 목록 참조할 수 있도록 주입
|
|
699
|
-
setAllToolsRef(allTools);
|
|
700
|
-
// V3_EXPOSED 16개만 노출 (나머지는 execute_tool 경유)
|
|
701
|
-
const exposedTools = allTools.filter(t => V3_EXPOSED.has(t.name));
|
|
702
739
|
// ListTools 핸들러
|
|
703
740
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
704
741
|
tools: exposedTools.map(tool => ({
|