korean-law-mcp 2.3.1 → 2.3.2

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.
Files changed (53) hide show
  1. package/README.md +126 -57
  2. package/build/lib/api-client.js +3 -22
  3. package/build/lib/article-parser.d.ts +15 -0
  4. package/build/lib/article-parser.js +51 -0
  5. package/build/lib/errors.d.ts +0 -15
  6. package/build/lib/errors.js +7 -40
  7. package/build/lib/fetch-with-retry.js +16 -4
  8. package/build/lib/schemas.d.ts +0 -8
  9. package/build/lib/schemas.js +0 -15
  10. package/build/lib/three-tier-parser.js +110 -115
  11. package/build/lib/types.d.ts +10 -7
  12. package/build/tool-registry.d.ts +0 -4
  13. package/build/tool-registry.js +7 -4
  14. package/build/tools/admin-appeals.js +6 -16
  15. package/build/tools/admin-rule.js +13 -24
  16. package/build/tools/advanced-search.js +3 -2
  17. package/build/tools/annex.js +8 -8
  18. package/build/tools/article-compare.js +5 -9
  19. package/build/tools/article-detail.js +5 -6
  20. package/build/tools/article-link-parser.js +1 -1
  21. package/build/tools/article-with-precedents.js +2 -3
  22. package/build/tools/autocomplete.js +4 -4
  23. package/build/tools/batch-articles.js +10 -44
  24. package/build/tools/chains.js +10 -0
  25. package/build/tools/committee-decisions.js +7 -11
  26. package/build/tools/comparison.js +5 -5
  27. package/build/tools/constitutional-decisions.js +7 -19
  28. package/build/tools/customs-interpretations.js +7 -7
  29. package/build/tools/english-law.js +4 -11
  30. package/build/tools/external-links.js +6 -6
  31. package/build/tools/historical-law.js +8 -14
  32. package/build/tools/interpretations.js +5 -15
  33. package/build/tools/kb-utils.js +2 -2
  34. package/build/tools/knowledge-base.js +30 -34
  35. package/build/tools/law-history.js +2 -1
  36. package/build/tools/law-linkage.d.ts +24 -4
  37. package/build/tools/law-statistics.js +2 -2
  38. package/build/tools/law-system-tree.js +6 -8
  39. package/build/tools/law-text.js +15 -62
  40. package/build/tools/law-tree.js +2 -2
  41. package/build/tools/legal-terms.js +2 -10
  42. package/build/tools/life-law.js +6 -13
  43. package/build/tools/meta-tools.js +8 -0
  44. package/build/tools/ordinance-search.js +2 -2
  45. package/build/tools/ordinance.js +4 -11
  46. package/build/tools/precedent-keywords.js +1 -1
  47. package/build/tools/precedent-summary.js +1 -1
  48. package/build/tools/precedents.js +11 -18
  49. package/build/tools/search.js +1 -1
  50. package/build/tools/similar-precedents.js +1 -1
  51. package/build/tools/tax-tribunal-decisions.js +9 -9
  52. package/build/tools/three-tier.js +7 -7
  53. package/package.json +1 -1
package/README.md CHANGED
@@ -14,23 +14,25 @@
14
14
 
15
15
  ---
16
16
 
17
- ## v2.3.1 변경사항
17
+ ## v2.3 변경사항
18
18
 
19
- - **URL 쿼리 API 키 지원** — 원격 MCP 접속 시 `?oc=your-key`로 API 키를 URL에 포함 가능. 매 요청마다 키를 전달할 필요 없이 세션 전체에 자동 적용. Claude.ai 커스텀 헤더 설정이 어려운 웹 클라이언트에서 유용.
20
- - **체인 도구 자동 전문 조회** — `chain_ordinance_compare`가 자치법규 검색 후 상위 1건 전문을 자동 조회하여 반환. 별도로 `get_ordinance`를 호출할 필요 없음.
21
- - **lite 프로필 도구 라우팅 개선** — 체인/메타 도구 description을 INPUT 의도 기반으로 재작성. 예시 포함으로 Claude 웹이 자치법규 검색·조회 등 올바른 도구를 선택하도록 개선.
22
- - **도구 힌트 execute_tool 경로 통일** — 비lite 도구 안내를 `execute_tool()` 호출 예시로 변경. Claude 웹이 존재하지 않는 도구를 직접 호출하는 문제 방지.
19
+ **원격 MCP 주소** (`your-key` 부분에 [법제처 Open API](https://open.law.go.kr/LSO/openApi/guideResult.do)에서 발급받은 본인 인증키(OC)를 넣으세요):
23
20
 
24
- <details>
25
- <summary>v2.3.0 변경사항</summary>
21
+ | 프로필 | URL | 도구 수 | 용도 |
22
+ |--------|-----|---------|------|
23
+ | lite | `https://korean-law-mcp.fly.dev/mcp?profile=lite&oc=your-key` | 14개 | Claude.ai 등 웹 클라이언트 (컨텍스트 87% 절감) |
24
+ | full | `https://korean-law-mcp.fly.dev/mcp?oc=your-key` | 89개 | Claude Desktop, Cursor 등 네이티브 클라이언트 |
26
25
 
27
- - **도구 프로필 (lite/full)** — 웹 클라이언트(Claude.ai 등)용 lite 프로필 도입. 89개 14개로 자동 축소하여 컨텍스트 소비 87% 절감. `/mcp?profile=lite`로 사용.
28
- - **체인 8개** + 핵심 직접 도구 4개 + 메타 도구 2개 = 14개로 동일 기능 커버
29
- - `discover_tools`: 의도/카테고리로 숨겨진 전문 도구 검색
30
- - `execute_tool`: discover로 찾은 도구를 프록시 실행
31
- - **kordoc 통합 파서** — 자체 HWP5/HWPX/PDF 파서 5개를 [kordoc](https://github.com/chrisryugj/kordoc) 통합 파서로 교체. 의존성 경량화.
26
+ 예시: 발급받은 인증키가 `honggildong`이면`https://korean-law-mcp.fly.dev/mcp?profile=lite&oc=honggildong`
32
27
 
33
- </details>
28
+ > lite는 체인 8개 + 핵심 4개 + 메타 2개로 동일 기능 커버. 특수 도구는 `discover_tools` → `execute_tool`로 접근.
29
+
30
+ - **도구 프로필 (lite/full)** — 89개 → 14개 자동 축소. 체인 도구가 내부에서 하위 도구를 직접 호출하므로 기능 손실 없음.
31
+ - **URL 쿼리 API 키** — `?oc=your-key`로 세션 전체에 API 키 자동 적용. 커스텀 헤더 설정이 어려운 웹 클라이언트에서 필수.
32
+ - **체인 자동 전문 조회** — `chain_ordinance_compare`가 자치법규 검색 후 상위 1건 전문을 자동 조회. 별도 `get_ordinance` 호출 불필요.
33
+ - **lite 도구 라우팅 개선** — 체인/메타 도구 description을 사용자 질문 의도 기반으로 재작성 + 예시 포함. Claude 웹이 "광진구 복무 조례" 같은 자치법규 질문에도 정확한 도구 선택.
34
+ - **도구 힌트 통일** — 비lite 도구 안내를 `execute_tool()` 호출 예시로 변경. 존재하지 않는 도구 직접 호출 문제 방지.
35
+ - **kordoc 통합 파서** — 자체 HWP5/HWPX/PDF 파서 5개를 [kordoc](https://github.com/chrisryugj/kordoc) 통합 파서로 교체. 의존성 경량화.
34
36
 
35
37
  <details>
36
38
  <summary>v2.2.0 변경사항</summary>
@@ -64,87 +66,154 @@
64
66
 
65
67
  ---
66
68
 
67
- ## 빠른 시작
69
+ ## 설치 및 사용법
68
70
 
69
- ### MCP 서버 (Claude Desktop / Cursor / Windsurf)
71
+ ### 0단계: API 발급 (무료, 1분)
70
72
 
71
- ```bash
72
- npm install -g korean-law-mcp
73
+ 모든 방법에 공통으로 필요한 **법제처 Open API 인증키(OC)**를 먼저 발급받으세요.
74
+
75
+ 1. [법제처 Open API 신청 페이지](https://open.law.go.kr/LSO/openApi/guideList.do)에 접속합니다.
76
+ 2. 회원가입 후 로그인합니다.
77
+ 3. **"Open API 사용 신청"** 버튼을 누릅니다.
78
+ 4. 신청서를 작성하면 **인증키(OC)**가 발급됩니다. (예: `honggildong`)
79
+ 5. 이 인증키를 아래 설정에서 사용합니다.
80
+
81
+ ---
82
+
83
+ ### 방법 1: Claude.ai 웹에서 바로 사용 (설치 없음, 가장 쉬움)
84
+
85
+ 아무것도 설치하지 않고, 주소 하나만 입력하면 됩니다. Claude Pro/Max/Team/Enterprise 요금제가 필요합니다 (Free는 커넥터 1개만 가능).
86
+
87
+ **커넥터 추가 방법:**
88
+
89
+ 1. [claude.ai](https://claude.ai)에 로그인합니다.
90
+ 2. 왼쪽 사이드바 하단의 **본인 이름**을 클릭합니다.
91
+ 3. **"설정"** (또는 Settings)을 선택합니다.
92
+ 4. **"커넥터"** (또는 Connectors) 메뉴로 들어갑니다.
93
+ 5. **"커스텀 커넥터"** 영역에서 **"커스텀 커넥터 추가"** 버튼을 클릭합니다.
94
+ 6. 아래 내용을 입력합니다:
95
+ - **이름**: `korean-law` (원하는 이름 아무거나 OK)
96
+ - **URL**: 아래 주소를 붙여넣으세요. `honggildong` 부분을 **0단계에서 발급받은 본인 인증키**로 바꾸세요:
97
+
98
+ ```
99
+ https://korean-law-mcp.fly.dev/mcp?profile=lite&oc=honggildong
73
100
  ```
74
101
 
75
- MCP 클라이언트 설정에 추가:
102
+ 7. **추가** 버튼을 누르면 등록 완료!
103
+
104
+ **도구 활성화 (중요!):**
105
+
106
+ 8. 추가한 커넥터의 **"구성"** (또는 Configure)을 클릭합니다.
107
+ 9. 도구 목록이 나오면, 모든 도구를 **"항상 사용"** (또는 Always allow)으로 설정합니다.
108
+ 10. 이렇게 하면 매번 승인할 필요 없이 AI가 바로 법령을 검색할 수 있습니다.
109
+
110
+ **사용하기:**
111
+
112
+ 11. 채팅 화면으로 돌아가서 "근로기준법 제74조 알려줘"라고 입력하면 끝!
113
+
114
+ > **참고**: 커넥터 URL을 수정하려면 삭제 후 다시 추가해야 합니다.
115
+
116
+ > **lite vs full 차이**: 위 주소는 lite 모드(14개 도구)입니다. 14개로도 89개 전체 기능을 사용할 수 있어요 — AI가 필요할 때 나머지 도구를 알아서 꺼내 씁니다. 모든 도구를 직접 보고 싶으면 주소에서 `profile=lite&`를 빼면 됩니다.
117
+
118
+ ---
119
+
120
+ ### 방법 2: AI 데스크톱 앱에서 사용 (설치 없음)
121
+
122
+ Claude Desktop, Cursor, Windsurf 같은 **데스크톱 앱**을 쓰고 있다면, 설정 파일에 아래 내용을 추가하세요.
123
+
124
+ **설정 파일 위치 찾기:**
125
+
126
+ | 앱 이름 | Windows | Mac |
127
+ |---------|---------|-----|
128
+ | Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` | `~/Library/Application Support/Claude/claude_desktop_config.json` |
129
+ | Cursor | 프로젝트 폴더 안 `.cursor/mcp.json` | 프로젝트 폴더 안 `.cursor/mcp.json` |
130
+ | Windsurf | 프로젝트 폴더 안 `.windsurf/mcp.json` | 프로젝트 폴더 안 `.windsurf/mcp.json` |
131
+
132
+ **설정 파일에 추가할 내용** (`honggildong`을 본인 인증키로 바꾸세요):
76
133
 
77
134
  ```json
78
135
  {
79
136
  "mcpServers": {
80
137
  "korean-law": {
81
- "command": "korean-law-mcp",
82
- "env": {
83
- "LAW_OC": "your-api-key"
84
- }
138
+ "url": "https://korean-law-mcp.fly.dev/mcp?oc=honggildong"
85
139
  }
86
140
  }
87
141
  }
88
142
  ```
89
143
 
90
- API 키는 [법제처 Open API](https://open.law.go.kr/LSO/openApi/guideResult.do)에서 무료 발급.
144
+ > 이미 다른 MCP 서버가 설정되어 있다면, `"mcpServers": { ... }` 안에 `"korean-law": { ... }` 부분만 추가하면 됩니다.
145
+
146
+ 저장 후 앱을 **재시작**하면 법령 도구가 활성화됩니다.
147
+
148
+ ---
149
+
150
+ ### 방법 3: 내 컴퓨터에 직접 설치 (오프라인 가능)
91
151
 
92
- | 클라이언트 | 설정 파일 |
93
- |-----------|----------|
94
- | Claude Desktop | `%APPDATA%\Claude\claude_desktop_config.json` (Win) / `~/Library/Application Support/Claude/claude_desktop_config.json` (Mac) |
95
- | Cursor | `.cursor/mcp.json` |
96
- | Windsurf | `.windsurf/mcp.json` |
97
- | Continue | `~/.continue/config.json` |
98
- | Zed | `~/.config/zed/settings.json` |
152
+ 인터넷 없이 쓰고 싶거나, 원격 서버를 거치지 않으려면 직접 설치할 수 있습니다.
99
153
 
100
- ### 원격 MCP (설치 없이 바로)
154
+ **사전 준비:** [Node.js](https://nodejs.org) 18 이상이 설치되어 있어야 합니다.
101
155
 
102
- API 키를 URL에 포함하여 바로 사용:
156
+ **1. 터미널(명령 프롬프트)을 열고 설치합니다:**
157
+
158
+ ```bash
159
+ npm install -g korean-law-mcp
160
+ ```
161
+
162
+ **2. AI 앱 설정 파일에 아래 내용을 추가합니다** (`honggildong`을 본인 인증키로 바꾸세요):
103
163
 
104
164
  ```json
105
165
  {
106
166
  "mcpServers": {
107
167
  "korean-law": {
108
- "url": "https://korean-law-mcp.fly.dev/mcp?oc=your-api-key"
168
+ "command": "korean-law-mcp",
169
+ "env": {
170
+ "LAW_OC": "honggildong"
171
+ }
109
172
  }
110
173
  }
111
174
  }
112
175
  ```
113
176
 
114
- **Claude.ai 클라이언트** — 컨텍스트 절약을 위해 lite 프로필 권장:
115
-
116
- ```
117
- https://korean-law-mcp.fly.dev/mcp?profile=lite&oc=your-api-key
118
- ```
119
-
120
- > lite 프로필은 체인 8개 + 핵심 4개 + 메타 2개 = **14개 도구**로 동일 기능 커버. 특수 도구가 필요하면 `discover_tools` → `execute_tool`로 접근.
121
-
122
- **API 키 전달 방법** (우선순위순):
177
+ **3. 앱을 재시작하면 완료!**
123
178
 
124
- | 방법 | 예시 | 설명 |
125
- |------|------|------|
126
- | URL 쿼리 | `?oc=your-key` | 웹 클라이언트에서 가장 간편. 세션 전체에 자동 적용 |
127
- | HTTP 헤더 | `apikey: your-key` | `law-oc`, `x-api-key`, `Authorization: Bearer` 등도 지원 |
128
- | 도구 파라미터 | `apiKey: "your-key"` | 개별 도구 호출 시 직접 전달 |
179
+ ---
129
180
 
130
- > API 키는 [법제처 Open API](https://open.law.go.kr/LSO/openApi/guideResult.do)에서 무료 발급.
181
+ ### 방법 4: 터미널(CLI)에서 직접 사용
131
182
 
132
- ### CLI
183
+ 개발자라면 터미널에서 직접 법령을 검색할 수 있습니다.
133
184
 
134
185
  ```bash
186
+ # 설치
135
187
  npm install -g korean-law-mcp
136
- export LAW_OC=your-api-key
137
-
138
- korean-law search_law --query "관세법"
139
- korean-law get_law_text --mst 160001 --jo "제38조"
140
- korean-law search_precedents --query "부당해고"
141
- korean-law list # 89개 전체 도구 목록
142
- korean-law list --category 판례 # 카테고리별 필터
143
- korean-law help search_law # 도구 도움말
188
+
189
+ # 인증키 설정 (honggildong을 본인 키로 바꾸세요)
190
+ export LAW_OC=honggildong # Mac/Linux
191
+ set LAW_OC=honggildong # Windows CMD
192
+ $env:LAW_OC="honggildong" # Windows PowerShell
193
+
194
+ # 사용 예시
195
+ korean-law "민법 제1조" # 자연어로 바로 조회
196
+ korean-law search_law --query "관세법" # 도구 직접 호출
197
+ korean-law list # 89개 전체 도구 목록
198
+ korean-law list --category 판례 # 카테고리별 필터
199
+ korean-law help search_law # 도구별 도움말
144
200
  ```
145
201
 
146
202
  ---
147
203
 
204
+ ### API 키 전달 방법 정리
205
+
206
+ 여러 방법으로 인증키를 전달할 수 있습니다. 위에서부터 우선 적용됩니다:
207
+
208
+ | 방법 | 사용법 | 언제 쓰나 |
209
+ |------|--------|-----------|
210
+ | URL에 포함 | 주소 끝에 `?oc=내키` | 웹 클라이언트에서 가장 간편 |
211
+ | HTTP 헤더 | `apikey: 내키` | 프로그래밍으로 연동할 때 |
212
+ | 환경변수 | `LAW_OC=내키` | 로컬 설치(방법 3, 4) |
213
+ | 도구 파라미터 | `apiKey: "내키"` | 특정 요청만 다른 키 쓸 때 |
214
+
215
+ ---
216
+
148
217
  ## 사용 예시
149
218
 
150
219
  ```
@@ -81,28 +81,9 @@ export class LawApiClient {
81
81
  const response = await fetchWithRetry(url);
82
82
  this.throwIfError(response, "getLawText");
83
83
  const text = await response.text();
84
- if (text.includes("<!DOCTYPE html") || text.includes("<html")) {
85
- let errorMsg = "법령을 찾을 수 없습니다.";
86
- if (params.jo) {
87
- errorMsg += "\n\n💡 개선 방법:";
88
- errorMsg += "\n 1. 전체 법령 조회 (조문 범위 확인):";
89
- if (params.mst) {
90
- errorMsg += `\n get_law_text(mst="${params.mst}")`;
91
- }
92
- else if (params.lawId) {
93
- errorMsg += `\n get_law_text(lawId="${params.lawId}")`;
94
- }
95
- errorMsg += "\n\n 2. 키워드 검색:";
96
- errorMsg += `\n search_all(query="관련 키워드")`;
97
- errorMsg += "\n\n 3. 법령 검색:";
98
- errorMsg += `\n search_law(query="법령명")`;
99
- errorMsg += "\n\n ℹ️ 일부 법령은 조문 수가 적습니다 (예: 약사법 시행령 제1~39조)";
100
- }
101
- else {
102
- errorMsg += " MST 또는 법령명을 확인해주세요.";
103
- }
104
- throw new Error(errorMsg);
105
- }
84
+ this.checkHtmlError(text, params.jo
85
+ ? `법령 조문(${params.jo})을 찾을 수 없습니다. MST/lawId와 조문번호를 확인해주세요.`
86
+ : "법령을 찾을 수 없습니다. MST 또는 법령명을 확인해주세요.");
106
87
  return text;
107
88
  }
108
89
  /**
@@ -5,5 +5,20 @@
5
5
  export declare function flattenContent(value: any): string;
6
6
  /** 항 배열에서 내용 추출 (재귀적으로 호/목 처리) */
7
7
  export declare function extractHangContent(hangInput: any[] | any): string;
8
+ /**
9
+ * 조문단위 객체를 텍스트로 포맷팅 (law-text, batch-articles, article-detail 공통)
10
+ * 조문 헤더(제X조 제목) + 본문 + 항/호/목을 결합하여 반환
11
+ */
12
+ export declare function formatArticleUnit(unit: {
13
+ 조문여부?: string;
14
+ 조문번호?: string;
15
+ 조문가지번호?: string;
16
+ 조문제목?: string;
17
+ 조문내용?: unknown;
18
+ 항?: unknown;
19
+ }): {
20
+ header: string;
21
+ body: string;
22
+ } | null;
8
23
  /** HTML 정리 - 엔티티 디코딩 순서 중요: &amp; 최후 처리 (이중 인코딩 방지) */
9
24
  export declare function cleanHtml(text: string): string;
@@ -61,6 +61,57 @@ export function extractHangContent(hangInput) {
61
61
  }
62
62
  return content;
63
63
  }
64
+ /**
65
+ * 조문단위 객체를 텍스트로 포맷팅 (law-text, batch-articles, article-detail 공통)
66
+ * 조문 헤더(제X조 제목) + 본문 + 항/호/목을 결합하여 반환
67
+ */
68
+ export function formatArticleUnit(unit) {
69
+ if (unit.조문여부 !== "조문")
70
+ return null;
71
+ const joNum = unit.조문번호 || "";
72
+ const joBranch = unit.조문가지번호 || "";
73
+ const joTitle = unit.조문제목 || "";
74
+ // 헤더
75
+ let header = "";
76
+ if (joNum) {
77
+ const displayNum = joBranch && joBranch !== "0" ? `제${joNum}조의${joBranch}` : `제${joNum}조`;
78
+ header = joTitle ? `${displayNum} ${joTitle}` : displayNum;
79
+ }
80
+ // 본문: 조문내용에서 헤더 패턴 제거
81
+ let mainContent = "";
82
+ if (unit.조문내용) {
83
+ const contentStr = flattenContent(unit.조문내용);
84
+ if (contentStr) {
85
+ const headerMatch = contentStr.match(/^(제\d+조(?:의\d+)?\s*(?:\([^)]+\))?)[\s\S]*/);
86
+ if (headerMatch) {
87
+ const bodyPart = contentStr.substring(headerMatch[1].length).trim();
88
+ mainContent = bodyPart || contentStr;
89
+ }
90
+ else {
91
+ mainContent = contentStr;
92
+ }
93
+ }
94
+ }
95
+ // 항/호/목
96
+ let paraContent = "";
97
+ if (unit.항) {
98
+ paraContent = extractHangContent(unit.항);
99
+ }
100
+ // 결합
101
+ let body = "";
102
+ if (mainContent) {
103
+ body = mainContent;
104
+ if (paraContent)
105
+ body += "\n" + paraContent;
106
+ }
107
+ else {
108
+ body = paraContent;
109
+ }
110
+ // HTML 정리
111
+ if (body)
112
+ body = cleanHtml(body);
113
+ return { header, body };
114
+ }
64
115
  /** HTML 정리 - 엔티티 디코딩 순서 중요: &amp; 최후 처리 (이중 인코딩 방지) */
65
116
  export function cleanHtml(text) {
66
117
  return text
@@ -21,9 +21,6 @@ export declare class LawApiError extends Error {
21
21
  code: ErrorCode;
22
22
  suggestions: string[];
23
23
  constructor(message: string, code: ErrorCode, suggestions?: string[]);
24
- /**
25
- * 사용자 친화적 포맷
26
- */
27
24
  format(): string;
28
25
  }
29
26
  /**
@@ -35,15 +32,3 @@ export declare class LawApiError extends Error {
35
32
  * 💡 제안: ...
36
33
  */
37
34
  export declare function formatToolError(error: unknown, context?: string): ToolResponse;
38
- /**
39
- * 법령 없음 에러
40
- */
41
- export declare function notFoundError(lawName: string, suggestions?: string[]): LawApiError;
42
- /**
43
- * API 에러
44
- */
45
- export declare function apiError(status: number, endpoint?: string): LawApiError;
46
- /**
47
- * 파라미터 검증 에러
48
- */
49
- export declare function invalidParamError(param: string, expected: string): LawApiError;
@@ -22,15 +22,12 @@ export class LawApiError extends Error {
22
22
  this.code = code;
23
23
  this.suggestions = suggestions;
24
24
  }
25
- /**
26
- * 사용자 친화적 포맷
27
- */
28
25
  format() {
29
- let result = `❌ ${this.message}`;
26
+ let result = `[ERROR] ${this.message}`;
30
27
  if (this.suggestions.length > 0) {
31
- result += "\n\n💡 개선 방법:";
28
+ result += "\n제안:";
32
29
  this.suggestions.forEach((s, i) => {
33
- result += `\n ${i + 1}. ${s}`;
30
+ result += `\n ${i + 1}. ${s}`;
34
31
  });
35
32
  }
36
33
  return result;
@@ -73,49 +70,19 @@ export function formatToolError(error, context) {
73
70
  msg = String(error);
74
71
  suggestions = [];
75
72
  }
76
- // 구조화된 텍스트 조립
77
73
  const lines = [];
78
- lines.push(`❌ [${code}] ${msg}`);
74
+ lines.push(`[${code}] ${msg}`);
79
75
  if (context) {
80
- lines.push(`🔧 도구: ${context}`);
76
+ lines.push(`도구: ${context}`);
81
77
  }
82
78
  if (suggestions.length > 0) {
83
- lines.push("💡 제안:");
79
+ lines.push("제안:");
84
80
  suggestions.forEach((s, i) => {
85
- lines.push(` ${i + 1}. ${s}`);
81
+ lines.push(` ${i + 1}. ${s}`);
86
82
  });
87
83
  }
88
- else {
89
- lines.push("💡 제안: (없음)");
90
- }
91
84
  return {
92
85
  content: [{ type: "text", text: lines.join("\n") }],
93
86
  isError: true,
94
87
  };
95
88
  }
96
- /**
97
- * 법령 없음 에러
98
- */
99
- export function notFoundError(lawName, suggestions) {
100
- return new LawApiError(`'${lawName}'을(를) 찾을 수 없습니다.`, ErrorCodes.NOT_FOUND, suggestions || [
101
- `search_law(query="${lawName}")로 법령 검색`,
102
- "법령명 철자 확인",
103
- ]);
104
- }
105
- /**
106
- * API 에러
107
- */
108
- export function apiError(status, endpoint) {
109
- const suggestions = status === 429
110
- ? ["잠시 후 다시 시도", "요청 빈도 줄이기"]
111
- : status >= 500
112
- ? ["법제처 API 상태 확인", "잠시 후 다시 시도"]
113
- : ["요청 파라미터 확인"];
114
- return new LawApiError(`API 오류 (${status})${endpoint ? ` - ${endpoint}` : ""}`, status === 429 ? ErrorCodes.RATE_LIMITED : ErrorCodes.API_ERROR, suggestions);
115
- }
116
- /**
117
- * 파라미터 검증 에러
118
- */
119
- export function invalidParamError(param, expected) {
120
- return new LawApiError(`잘못된 파라미터: ${param}`, ErrorCodes.INVALID_PARAM, [`${param}는 ${expected} 형식이어야 합니다.`]);
121
- }
@@ -28,8 +28,7 @@ export async function fetchWithRetry(url, options = {}) {
28
28
  }
29
29
  // Retryable error - check if we have retries left
30
30
  if (attempt < retries) {
31
- const baseDelay = retryDelay * Math.pow(2, attempt);
32
- const delay = baseDelay + Math.random() * baseDelay * 0.5;
31
+ const delay = getRetryDelay(response, retryDelay, attempt);
33
32
  await sleep(delay);
34
33
  continue;
35
34
  }
@@ -49,8 +48,7 @@ export async function fetchWithRetry(url, options = {}) {
49
48
  }
50
49
  // Retry on network errors
51
50
  if (attempt < retries) {
52
- const baseDelay = retryDelay * Math.pow(2, attempt);
53
- const delay = baseDelay + Math.random() * baseDelay * 0.5;
51
+ const delay = getRetryDelay(null, retryDelay, attempt);
54
52
  await sleep(delay);
55
53
  continue;
56
54
  }
@@ -58,6 +56,20 @@ export async function fetchWithRetry(url, options = {}) {
58
56
  }
59
57
  throw lastError || new Error("Request failed after retries");
60
58
  }
59
+ /** Retry-After 헤더 우선, 없으면 exponential backoff + jitter */
60
+ function getRetryDelay(response, retryDelay, attempt) {
61
+ if (response) {
62
+ const retryAfter = response.headers.get("Retry-After");
63
+ if (retryAfter) {
64
+ const seconds = Number(retryAfter);
65
+ if (!isNaN(seconds) && seconds > 0) {
66
+ return seconds * 1000;
67
+ }
68
+ }
69
+ }
70
+ const baseDelay = retryDelay * Math.pow(2, attempt);
71
+ return baseDelay + Math.random() * baseDelay * 0.5;
72
+ }
61
73
  function sleep(ms) {
62
74
  return new Promise((resolve) => setTimeout(resolve, ms));
63
75
  }
@@ -10,10 +10,6 @@ export declare const dateSchema: z.ZodString;
10
10
  * 선택적 날짜 스키마
11
11
  */
12
12
  export declare const optionalDateSchema: z.ZodOptional<z.ZodString>;
13
- /**
14
- * API 키 스키마
15
- */
16
- export declare const apiKeySchema: z.ZodOptional<z.ZodString>;
17
13
  /**
18
14
  * 페이지네이션 스키마
19
15
  */
@@ -21,10 +17,6 @@ export declare const paginationSchema: z.ZodObject<{
21
17
  display: z.ZodDefault<z.ZodNumber>;
22
18
  page: z.ZodDefault<z.ZodNumber>;
23
19
  }, z.core.$strip>;
24
- /**
25
- * 날짜 포맷터 (YYYYMMDD → "2024년 1월 1일")
26
- */
27
- export declare function formatDateKorean(dateStr: string | undefined | null): string;
28
20
  /**
29
21
  * 응답 크기 제한 (50KB)
30
22
  */
@@ -30,10 +30,6 @@ export const dateSchema = z
30
30
  * 선택적 날짜 스키마
31
31
  */
32
32
  export const optionalDateSchema = dateSchema.optional();
33
- /**
34
- * API 키 스키마
35
- */
36
- export const apiKeySchema = z.string().optional().describe("API 키 (생략시 환경변수 사용)");
37
33
  /**
38
34
  * 페이지네이션 스키마
39
35
  */
@@ -41,17 +37,6 @@ export const paginationSchema = z.object({
41
37
  display: z.number().min(1).max(100).default(20).describe("결과 수 (기본:20, 최대:100)"),
42
38
  page: z.number().min(1).default(1).describe("페이지 번호 (기본:1)"),
43
39
  });
44
- /**
45
- * 날짜 포맷터 (YYYYMMDD → "2024년 1월 1일")
46
- */
47
- export function formatDateKorean(dateStr) {
48
- if (!dateStr || dateStr.length < 8)
49
- return dateStr || "N/A";
50
- const y = dateStr.substring(0, 4);
51
- const m = parseInt(dateStr.substring(4, 6), 10);
52
- const d = parseInt(dateStr.substring(6, 8), 10);
53
- return `${y}년 ${m}월 ${d}일`;
54
- }
55
40
  /**
56
41
  * 응답 크기 제한 (50KB)
57
42
  */