korean-law-mcp 3.2.1 → 3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Korean Law MCP
2
2
 
3
- **법제처 41개 API를 14개 도구로.** 법령, 판례, 행정규칙, 자치법규, 조약, 해석례를 AI 어시스턴트나 터미널에서 바로 사용.
3
+ **법제처 41개 API를 15개 도구로.** 법령, 판례, 행정규칙, 자치법규, 조약, 해석례를 AI 어시스턴트나 터미널에서 바로 사용.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/korean-law-mcp.svg)](https://www.npmjs.com/package/korean-law-mcp)
6
6
  [![MCP 1.27](https://img.shields.io/badge/MCP-1.27-blue)](https://modelcontextprotocol.io)
@@ -81,7 +81,47 @@
81
81
  > 모든 결과 끝에 **"이어서 할 수 있는 조회"**가 제안됩니다. 복사해서 바로 이어가세요.
82
82
 
83
83
  <details>
84
- <summary>v3.2.1 변경 이력</summary>
84
+ <summary>v3.2.1~v3.4.0 변경 이력</summary>
85
+
86
+ **v3.4.0** — 판례 응답 토큰 평균 74% 감축 + `get_decision_text`에 `full` 파라미터 추가
87
+
88
+ 법령 RAG 관점에서 판례 응답 구조를 재해석: 판시사항·판결요지·주문은 규범 재사용의 핵심이라 full 유지, "이유" 전문은 사안별 사실관계 나열이라 LLM이 대부분 소비만 하고 버림. 이 비대칭을 활용해 판례/헌재/행심(`precedent`/`constitutional`/`admin_appeal`) 3개 도메인에 **계단식 축약 + structured ref densify** 적용. `lib/decision-compact.ts` 신규:
89
+
90
+ - **`compactBody`** — 전문/이유 섹션을 앞 800자 + 중략 마커 + 뒤 400자로 축약. 판결 종결어미(`~다.`, `~라 할 것이다.`)와 문장 경계 가드 내장. `minSave` 가드로 짧은 본문(1300자 이하)은 skip
91
+ - **`densifyLawRefs`** — 참조조문의 괄호 설명 제거 (`제390조(채무불이행과 손해배상)` → `제390조`). 평균 40~55% 절감
92
+ - **`densifyPrecedentRefs`** — 참조판례의 "선고"/"판결" 제거 + 날짜 공백 압축 (`2020. 3. 26. 선고 2018두56077 판결` → `2020.3.26. 2018두56077`)
93
+ - **`stripRepeatedSummary`** — 법제처 API가 판시/요지를 본문 앞쪽에 또 섞어 보내는 케이스 탐지·제거
94
+
95
+ `get_decision_text`에 `full?: boolean` 파라미터 추가. 미지정(기본)=축약, `true`=전문. 응답 중간의 `⋯ 중략 N자 (full=true로 전문 조회) ⋯` 마커가 재호출 힌트 역할.
96
+
97
+ **실측 (실제 법제처 API, 고정 ID 8건)**:
98
+
99
+ | 도메인 | Before avg | After avg | 절감 |
100
+ |---|---:|---:|---:|
101
+ | 판례 | 5,230 chars | 3,049 chars | **-42%** |
102
+ | 헌재 | 8,368 chars | 1,703 chars | **-80%** |
103
+ | 행심 | 8,429 chars | 1,491 chars | **-82%** |
104
+ | **종합** | **7,606 chars (1,901 tok)** | **1,960 chars (490 tok)** | **-74%** |
105
+
106
+ 긴 결정례(15,000자↑)에서 **80~89%** 절감이 가장 두드러짐. 짧은 본문은 `minSave` 가드로 원본 유지. 품질 손실 없음 (판시·요지·주문은 항상 full).
107
+
108
+ 부가로 **ListTools 페이로드도 -14%** (9,671 → 8,296 bytes, 344 토큰↓): `chain_*` 8개 description 간결화, `search_decisions`/`get_decision_text` 필드 describe에서 17 도메인 이중 기재 제거.
109
+
110
+ **v3.3.1** — 법령 약칭 사전 대폭 확장 (11 → 52개, +41)
111
+
112
+ lexdiff에서 "산안기준규칙" 질의가 법제처 aiSearch의 키워드 부분매칭으로 **국가표준기본법**으로 환각되던 사례가 발견돼 `resolveLawAlias`의 `LAW_ALIAS_ENTRIES`를 대폭 보강. 다빈도 노무/안전(산안법·중처법·근기법 등), 개인정보/정보통신(개보법·정보통신망법), 청렴/이해충돌(청탁금지법·이해충돌방지법), 공공계약(국가계약법·지방계약법), 부동산/임대차(주임법·상임법·부거법), 공정거래(공정거래법·하도급법·약관법·표시광고법·가맹사업법), 금융(자본시장법·특금법·전금법), 도시계획(국토계획법·도정법), 환경(감염병예방법·대기환경법), 운수(여객운수법·화물운수법), 민·형사 절차(민소법·형소법·민집법), 사회보험(국건법·산재보험법·고보법), 통신(전기통신사업법) 커버. `api-client.ts`/`law-parser.ts`가 이미 `resolveLawAlias`를 사용 중이라 **데이터 추가만으로 기존 검색 경로가 자동 혜택**. 신규 41개 + 회귀 4개 포함 **45/45 테스트 통과**.
113
+
114
+ **v3.3.0** — HTTP stateless 모드 전환 + kordoc 2.3.0
115
+
116
+ 원격 서버(`korean-law-mcp.fly.dev`)가 주기적으로 OOM kill로 재시작되면서 기존 세션 ID가 무효화되던 문제를 근본 해결. MCP 공식 stateless 패턴(`sessionIdGenerator: undefined`)으로 전환하여 매 요청마다 fresh `Server + Transport`를 생성, 요청 종료 시 즉시 해제. in-memory 세션 Map·InMemoryEventStore·idle cleanup 전부 제거로 누수 원인 소거. 재시작·스케일아웃·배포 모두 무손실. `GET /mcp`·`DELETE /mcp`는 공식 예제와 동일하게 `405`. API 키는 `AsyncLocalStorage`로 요청 단위 격리 (race condition 방지).
117
+
118
+ - **HTTP stateless 전환** — [src/server/http-server.ts](src/server/http-server.ts) (참고: `@modelcontextprotocol/sdk/examples/server/simpleStatelessStreamableHttp.js`)
119
+ - **kordoc 2.2.5 → 2.3.0** — 별표/서식 파싱 엔진 업데이트
120
+ - **세션 관리 코드 완전 제거** — `sessions` Map, `MAX_SESSIONS`, idle cleanup `setInterval`, `InMemoryEventStore`, POST/GET/DELETE 분기 로직 삭제 (v3.2.3의 LRU eviction 접근을 대체)
121
+
122
+ **v3.2.3** — HTTP 세션 안정성 중간 개선. `MAX_SESSIONS` 100→500 + LRU eviction. _v3.3.0의 stateless 전환으로 대체됨._
123
+
124
+ **v3.2.2** — 별표/서식 조회 도구(`get_annexes`)를 기본 노출 도구에 추가. **노출 도구 수 14 → 15개**. 환불·감경 키워드 질의 시 별표 자동 조회 로직 추가.
85
125
 
86
126
  **v3.2.1** — kordoc 2.2.5 업데이트.
87
127
 
@@ -90,7 +130,7 @@
90
130
  <details>
91
131
  <summary>개발자용: 시나리오 기술 상세</summary>
92
132
 
93
- 기존 8개 체인 도구에 `scenario` 파라미터가 추가되었습니다. 도구 수는 14개로 동일합니다.
133
+ 기존 8개 체인 도구에 `scenario` 파라미터가 추가되었습니다. (노출 도구 수는 v3.2.2에서 `get_annexes` 노출 추가로 14 → 15개)
94
134
 
95
135
  | scenario | 호스트 체인 | 추가 조회 |
96
136
  |---------|-----------|----------|
@@ -146,11 +186,11 @@
146
186
  **v3.0.2** — Unified Architecture + Setup Wizard
147
187
 
148
188
  법제처 41개 API를 89개 MCP 도구로 구조화했던 v2.
149
- v3는 같은 41개 API를 **14개 도구**로 재압축했습니다.
189
+ v3는 같은 41개 API를 **15개 도구**로 재압축했습니다.
150
190
 
151
191
  | | 법제처 원본 | v2 | v3 |
152
192
  |---|:---:|:---:|:---:|
153
- | API/도구 수 | 41 | 89 | **14** |
193
+ | API/도구 수 | 41 | 89 | **15** |
154
194
  | AI 컨텍스트 비용 | - | ~110 KB | **~20 KB** |
155
195
  | 기능 커버리지 | - | 100% | **100%** |
156
196
  | 프로필 관리 | - | lite/full 분리 | **단일 (불필요)** |
@@ -210,7 +250,7 @@ MCP 도구 설계에서 **도구 수 ≠ 기능 수**입니다.
210
250
 
211
251
  대한민국에는 **1,600개 이상의 현행 법률**, **10,000개 이상의 행정규칙**, 그리고 대법원·헌법재판소·조세심판원·관세청까지 이어지는 방대한 판례 체계가 있습니다. 이 모든 게 [법제처](https://www.law.go.kr)라는 하나의 사이트에 있지만, 개발자 경험은 최악입니다.
212
252
 
213
- 이 프로젝트는 그 전체 법령 시스템을 **14개 도구**로 감싸서, AI 어시스턴트나 스크립트에서 바로 호출할 수 있게 만듭니다. 법제처를 백 번째 수동 검색하다 지친 공무원이 만들었습니다.
253
+ 이 프로젝트는 그 전체 법령 시스템을 **15개 도구**로 감싸서, AI 어시스턴트나 스크립트에서 바로 호출할 수 있게 만듭니다. 법제처를 백 번째 수동 검색하다 지친 공무원이 만들었습니다.
214
254
 
215
255
  ---
216
256
 
@@ -261,7 +301,7 @@ https://korean-law-mcp.fly.dev/mcp?oc=honggildong
261
301
 
262
302
  > **참고**: 커넥터 URL을 수정하려면 삭제 후 다시 추가해야 합니다.
263
303
 
264
- > v3부터 프로필 선택이 필요 없습니다. 14개 도구가 41개 API 전체를 커버합니다.
304
+ > v3부터 프로필 선택이 필요 없습니다. 15개 도구가 41개 API 전체를 커버합니다.
265
305
  > 기존에 `?profile=lite&oc=...` 주소를 넣으셨다면 **그대로 두셔도 됩니다** — 동일하게 작동합니다.
266
306
 
267
307
  ---
@@ -391,9 +431,9 @@ korean-law help search_law # 도구별 도움말
391
431
 
392
432
  ---
393
433
 
394
- ## 도구 구조 (14개)
434
+ ## 도구 구조 (15개)
395
435
 
396
- v3는 14개 도구만 노출합니다. 나머지 전문 도구는 `discover_tools` → `execute_tool`로 접근.
436
+ v3는 15개 도구만 노출합니다. 나머지 전문 도구는 `discover_tools` → `execute_tool`로 접근.
397
437
 
398
438
  | 구분 | 도구 | 설명 | 시나리오 확장 |
399
439
  |------|------|------|-------------|
@@ -405,8 +445,9 @@ v3는 14개 도구만 노출합니다. 나머지 전문 도구는 `discover_tool
405
445
  | | `chain_ordinance_compare` | 조례 비교 (상위법→전국 조례) | `compliance`: 상위법 적합성 검증 |
406
446
  | | `chain_procedure_detail` | 절차·비용·서식 안내 | `manual`: 공무원 처리 매뉴얼 |
407
447
  | | `chain_document_review` | 계약서·약관 리스크 분석 | — |
408
- | **법령** (2) | `search_law` | 법령 검색 → lawId, MST 획득 |
448
+ | **법령** (3) | `search_law` | 법령 검색 → lawId, MST 획득 |
409
449
  | | `get_law_text` | 조문 전문 조회 |
450
+ | | `get_annexes` | 별표/서식 조회 (금액표·요율표·별지서식) |
410
451
  | **통합** (2) | `search_decisions` | **17개 도메인** 통합 검색 (판례·헌재·조세심판·공정위·노동위·관세·해석례·행심·개인정보위·권익위·소청심사·학칙·공사공단·공공기관·조약·영문법령) |
411
452
  | | `get_decision_text` | **17개 도메인** 전문 조회 |
412
453
  | **메타** (2) | `discover_tools` | 전문 도구 검색 (용어·별표·이력·비교 등) |
@@ -418,7 +459,7 @@ v3는 14개 도구만 노출합니다. 나머지 전문 도구는 `discover_tool
418
459
 
419
460
  ## 주요 특징
420
461
 
421
- - **41개 API → 14개 도구** — 법령, 판례, 행정규칙, 자치법규, 헌재결정, 조세심판, 관세해석, 조약, 학칙/공단/공공기관 규정, 법령용어
462
+ - **41개 API → 15개 도구** — 법령, 판례, 행정규칙, 자치법규, 헌재결정, 조세심판, 관세해석, 조약, 학칙/공단/공공기관 규정, 법령용어
422
463
  - **MCP + CLI** — Claude Desktop에서도, 터미널에서도 같은 도구 사용
423
464
  - **법률 도메인 특화** — 약칭 자동 인식(`화관법` → `화학물질관리법`), 조문번호 변환(`제38조` ↔ `003800`), 3단 위임 구조 시각화
424
465
  - **별표/별지서식 본문 추출** — HWPX·HWP·PDF·XLSX·DOCX 자동 변환 ([kordoc](https://github.com/chrisryugj/kordoc) 엔진)
@@ -9,7 +9,7 @@ export declare class LawApiClient {
9
9
  /**
10
10
  * API 키 결정 순서:
11
11
  * 1. 요청별 override 키
12
- * 2. 현재 세션의 API 키 (HTTP 모드)
12
+ * 2. 현재 요청 컨텍스트의 API 키 (HTTP stateless 모드)
13
13
  * 3. 환경변수 LAW_OC
14
14
  * 4. 생성자에서 받은 기본 키
15
15
  */
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { normalizeLawSearchText, resolveLawAlias } from "./search-normalizer.js";
5
5
  import { fetchWithRetry } from "./fetch-with-retry.js";
6
- import { sessionStore, getSessionApiKey } from "./session-state.js";
6
+ import { requestContext } from "./session-state.js";
7
7
  const LAW_API_BASE = "https://www.law.go.kr/DRF";
8
8
  export class LawApiClient {
9
9
  constructor(config) {
@@ -12,14 +12,13 @@ export class LawApiClient {
12
12
  /**
13
13
  * API 키 결정 순서:
14
14
  * 1. 요청별 override 키
15
- * 2. 현재 세션의 API 키 (HTTP 모드)
15
+ * 2. 현재 요청 컨텍스트의 API 키 (HTTP stateless 모드)
16
16
  * 3. 환경변수 LAW_OC
17
17
  * 4. 생성자에서 받은 기본 키
18
18
  */
19
19
  getApiKey(overrideKey) {
20
- const currentSessionId = sessionStore.getStore();
21
- const sessionApiKey = currentSessionId ? getSessionApiKey(currentSessionId) : undefined;
22
- const key = overrideKey || sessionApiKey || process.env.LAW_OC || process.env.KOREAN_LAW_API_KEY || this.defaultApiKey;
20
+ const ctxApiKey = requestContext.getStore()?.apiKey;
21
+ const key = overrideKey || ctxApiKey || process.env.LAW_OC || process.env.KOREAN_LAW_API_KEY || this.defaultApiKey;
23
22
  if (!key) {
24
23
  throw new Error("API 키가 필요합니다. 법제처(https://open.law.go.kr/LSO/openApi/guideResult.do)에서 발급받으세요.");
25
24
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Decision Compact — 판례/헌재/행심 응답 토큰 최적화 유틸
3
+ *
4
+ * B. compactBody: 본문("이유"/"전문")을 "앞 800자 + 중략 + 뒤 400자"로 계단식 축약.
5
+ * 판시사항/판결요지/주문은 이미 별도 필드로 분리 반환되므로 본문에만 적용.
6
+ * 문장 경계(마침표·판례 특유 종결어미)를 존중하여 중간 절단 방지.
7
+ *
8
+ * C. densifyLawRefs / densifyPrecedentRefs: 참조조문·참조판례의 서지 잉여 제거.
9
+ * 괄호 안 조문명, "선고"/"판결" 군더더기, 날짜 공백 압축. 구조 유지 + 압축.
10
+ *
11
+ * 안전성: 모든 함수는 매칭 실패 / 빈 입력 / 짧은 입력 시 **원본 그대로 반환**.
12
+ */
13
+ export interface CompactOptions {
14
+ /** true 시 축약 비활성 → 원본 반환 */
15
+ full?: boolean;
16
+ /** 앞쪽 보존 길이 (기본 800자) */
17
+ headSize?: number;
18
+ /** 뒤쪽 보존 길이 (기본 400자) */
19
+ tailSize?: number;
20
+ /** head+tail+minSave 이하면 축약 안 함 (기본 500자) */
21
+ minSave?: number;
22
+ }
23
+ /**
24
+ * 본문 계단식 축약
25
+ * - 앞 800자 + 중략 마커 + 뒤 400자
26
+ * - 문장 경계 가드: 마침표/판례 종결어미/빈 줄에서 끊기
27
+ * - head 기준 50% 이상 위치에 경계 없으면 원시 슬라이스 사용 (fallback)
28
+ */
29
+ export declare function compactBody(text: string, opts?: CompactOptions): string;
30
+ /**
31
+ * 참조조문 densify
32
+ *
33
+ * 압축 전략:
34
+ * 1) 조문명 괄호 설명 제거: "제390조(채무불이행과 손해배상)" → "제390조"
35
+ * 2) 연속 공백/구분자 정리
36
+ *
37
+ * 조문명 괄호는 평균 15~30자 × 참조조문 5~10개 = 150~300자 절감 (40%).
38
+ * 법령명 자체는 건드리지 않음 — LLM이 후속 도구 호출 시 파싱 필요.
39
+ */
40
+ export declare function densifyLawRefs(text: string): string;
41
+ /**
42
+ * 참조판례 densify
43
+ *
44
+ * 압축 전략:
45
+ * 1) "선고" 생략: "대법원 2020. 3. 26. 선고 2018두56077 판결" → "대법원 2020.3.26. 2018두56077"
46
+ * 2) 날짜 공백 압축: "2020. 3. 26." → "2020.3.26."
47
+ * 3) "판결" 접미어 제거
48
+ * 4) 구분자 정리
49
+ */
50
+ export declare function densifyPrecedentRefs(text: string): string;
51
+ /**
52
+ * 본문에서 판시사항/판결요지가 반복 등장하면 제거
53
+ *
54
+ * 법제처 API 판례 응답은 `판시사항`, `판결요지`가 별도 필드로 나오지만
55
+ * `판례내용`(전문) 앞쪽에 같은 내용이 반복되는 케이스가 많다.
56
+ * 이미 상단에 렌더된 부분을 본문에서 제거하여 LLM 중복 소비 방지.
57
+ */
58
+ export declare function stripRepeatedSummary(body: string, summaries: Array<string | undefined>): string;
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Decision Compact — 판례/헌재/행심 응답 토큰 최적화 유틸
3
+ *
4
+ * B. compactBody: 본문("이유"/"전문")을 "앞 800자 + 중략 + 뒤 400자"로 계단식 축약.
5
+ * 판시사항/판결요지/주문은 이미 별도 필드로 분리 반환되므로 본문에만 적용.
6
+ * 문장 경계(마침표·판례 특유 종결어미)를 존중하여 중간 절단 방지.
7
+ *
8
+ * C. densifyLawRefs / densifyPrecedentRefs: 참조조문·참조판례의 서지 잉여 제거.
9
+ * 괄호 안 조문명, "선고"/"판결" 군더더기, 날짜 공백 압축. 구조 유지 + 압축.
10
+ *
11
+ * 안전성: 모든 함수는 매칭 실패 / 빈 입력 / 짧은 입력 시 **원본 그대로 반환**.
12
+ */
13
+ /**
14
+ * 본문 계단식 축약
15
+ * - 앞 800자 + 중략 마커 + 뒤 400자
16
+ * - 문장 경계 가드: 마침표/판례 종결어미/빈 줄에서 끊기
17
+ * - head 기준 50% 이상 위치에 경계 없으면 원시 슬라이스 사용 (fallback)
18
+ */
19
+ export function compactBody(text, opts = {}) {
20
+ if (opts.full || !text)
21
+ return text;
22
+ const HEAD = opts.headSize ?? 800;
23
+ const TAIL = opts.tailSize ?? 400;
24
+ const MIN_SAVE = opts.minSave ?? 500;
25
+ // 짧으면 축약 이득 없음 → 원본
26
+ if (text.length <= HEAD + TAIL + MIN_SAVE)
27
+ return text;
28
+ // HEAD — 앞에서 HEAD자까지 중 문장 끝에서 자르기
29
+ const headRaw = text.slice(0, HEAD);
30
+ const headBoundaries = [
31
+ headRaw.lastIndexOf("다.\n"),
32
+ headRaw.lastIndexOf("라.\n"),
33
+ headRaw.lastIndexOf("다. "),
34
+ headRaw.lastIndexOf("라. "),
35
+ headRaw.lastIndexOf(".\n\n"),
36
+ headRaw.lastIndexOf("\n\n"),
37
+ headRaw.lastIndexOf(". "),
38
+ ];
39
+ const headCutCandidate = Math.max(...headBoundaries);
40
+ const headCut = headCutCandidate > HEAD * 0.5 ? headCutCandidate + 2 : HEAD;
41
+ const head = text.slice(0, headCut).trimEnd();
42
+ // TAIL — 뒤에서 TAIL자 범위 중 문장 시작에서 자르기
43
+ const tailStart = text.length - TAIL;
44
+ const tailRaw = text.slice(tailStart);
45
+ const tailBoundaryIdx = [
46
+ tailRaw.indexOf("\n\n"),
47
+ tailRaw.indexOf(". "),
48
+ tailRaw.indexOf("다.\n"),
49
+ tailRaw.indexOf("다. "),
50
+ ]
51
+ .filter((i) => i >= 0)
52
+ .sort((a, b) => a - b)[0];
53
+ const tailFrom = tailBoundaryIdx !== undefined && tailBoundaryIdx < TAIL * 0.5
54
+ ? tailStart + tailBoundaryIdx + 2
55
+ : tailStart;
56
+ const tail = text.slice(tailFrom).trimStart();
57
+ const omitted = text.length - head.length - tail.length;
58
+ if (omitted < MIN_SAVE)
59
+ return text; // 실질 절감 미달 시 원본
60
+ return `${head}\n\n⋯ 중략 ${omitted.toLocaleString()}자 (full=true로 전문 조회) ⋯\n\n${tail}`;
61
+ }
62
+ /**
63
+ * 참조조문 densify
64
+ *
65
+ * 압축 전략:
66
+ * 1) 조문명 괄호 설명 제거: "제390조(채무불이행과 손해배상)" → "제390조"
67
+ * 2) 연속 공백/구분자 정리
68
+ *
69
+ * 조문명 괄호는 평균 15~30자 × 참조조문 5~10개 = 150~300자 절감 (40%).
70
+ * 법령명 자체는 건드리지 않음 — LLM이 후속 도구 호출 시 파싱 필요.
71
+ */
72
+ export function densifyLawRefs(text) {
73
+ if (!text)
74
+ return text;
75
+ const original = text;
76
+ // 1) 조문 뒤 괄호 설명 제거
77
+ // "제390조(채무불이행과 손해배상)" → "제390조"
78
+ // "제1항(적용범위)" → "제1항"
79
+ // 길이 3~40자 괄호만 타겟 (짧은 건 의미 있을 수 있음)
80
+ let compact = text.replace(/(제\d+조(?:의\d+)?|제\d+항|제\d+호)\s*\([^)]{3,40}\)/g, "$1");
81
+ // 2) 공백/구분자 정리
82
+ compact = compact
83
+ .replace(/\s*,\s*/g, ", ")
84
+ .replace(/\s*\/\s*/g, " / ")
85
+ .replace(/[ \t]{2,}/g, " ")
86
+ .replace(/\n{3,}/g, "\n\n")
87
+ .trim();
88
+ // 실질 절감 없으면 원본
89
+ if (compact.length >= original.length * 0.95)
90
+ return original;
91
+ return compact;
92
+ }
93
+ /**
94
+ * 참조판례 densify
95
+ *
96
+ * 압축 전략:
97
+ * 1) "선고" 생략: "대법원 2020. 3. 26. 선고 2018두56077 판결" → "대법원 2020.3.26. 2018두56077"
98
+ * 2) 날짜 공백 압축: "2020. 3. 26." → "2020.3.26."
99
+ * 3) "판결" 접미어 제거
100
+ * 4) 구분자 정리
101
+ */
102
+ export function densifyPrecedentRefs(text) {
103
+ if (!text)
104
+ return text;
105
+ const original = text;
106
+ let compact = text
107
+ // "선고" 앞뒤 공백 포함 제거
108
+ .replace(/\s*선고\s*/g, " ")
109
+ // "판결" 접미어 (공백 포함) 제거
110
+ .replace(/\s*판결(?=[\s,/;]|$)/g, "")
111
+ // 날짜 공백 압축: "2020. 3. 26." → "2020.3.26."
112
+ .replace(/(\d{4})\.\s*(\d{1,2})\.\s*(\d{1,2})\./g, "$1.$2.$3.")
113
+ // "[동지] ", "[공보 생략]" 같은 부가표기 제거
114
+ .replace(/\s*\[[^\]]{2,15}\]\s*/g, " ")
115
+ // 연속 공백
116
+ .replace(/[ \t]{2,}/g, " ")
117
+ .replace(/\s*,\s*/g, ", ")
118
+ .trim();
119
+ if (compact.length >= original.length * 0.95)
120
+ return original;
121
+ return compact;
122
+ }
123
+ /**
124
+ * 본문에서 판시사항/판결요지가 반복 등장하면 제거
125
+ *
126
+ * 법제처 API 판례 응답은 `판시사항`, `판결요지`가 별도 필드로 나오지만
127
+ * `판례내용`(전문) 앞쪽에 같은 내용이 반복되는 케이스가 많다.
128
+ * 이미 상단에 렌더된 부분을 본문에서 제거하여 LLM 중복 소비 방지.
129
+ */
130
+ export function stripRepeatedSummary(body, summaries) {
131
+ if (!body)
132
+ return body;
133
+ let result = body;
134
+ for (const s of summaries) {
135
+ if (!s || s.length < 20)
136
+ continue;
137
+ const head = s.trim().slice(0, Math.min(80, s.length));
138
+ if (head.length < 20)
139
+ continue;
140
+ // 본문 앞 25% 안에서 동일 구간 탐지
141
+ const zone = result.slice(0, Math.floor(result.length * 0.25));
142
+ const idx = zone.indexOf(head);
143
+ if (idx >= 0) {
144
+ // 매칭 구간 ~ 요약 전체 길이만큼 제거 (±20% tolerance)
145
+ const end = Math.min(idx + s.length + 50, result.length);
146
+ result = result.slice(0, idx) + result.slice(end);
147
+ }
148
+ }
149
+ return result;
150
+ }
@@ -73,6 +73,193 @@ const LAW_ALIAS_ENTRIES = [
73
73
  canonical: "지방공무원 보수규정",
74
74
  aliases: ["지방공무원보수규정", "지공보수규정"],
75
75
  },
76
+ // ── 다빈도 노무/안전 ──
77
+ {
78
+ canonical: "산업안전보건법",
79
+ aliases: ["산안법"],
80
+ alternatives: ["산업안전보건법 시행령", "산업안전보건법 시행규칙", "산업안전보건기준에 관한 규칙"],
81
+ },
82
+ {
83
+ canonical: "산업안전보건기준에 관한 규칙",
84
+ aliases: ["산안기준규칙", "안전보건규칙", "산업안전보건규칙", "산안규칙", "안전보건기준규칙"],
85
+ alternatives: ["산업안전보건법", "산업안전보건법 시행령"],
86
+ },
87
+ {
88
+ canonical: "중대재해 처벌 등에 관한 법률",
89
+ aliases: ["중대재해처벌법", "중처법", "중대재해법"],
90
+ alternatives: ["산업안전보건법"],
91
+ },
92
+ {
93
+ canonical: "근로기준법",
94
+ aliases: ["근기법", "근로법"],
95
+ },
96
+ {
97
+ canonical: "남녀고용평등과 일ㆍ가정 양립 지원에 관한 법률",
98
+ aliases: ["남녀고용평등법", "고평법"],
99
+ },
100
+ // ── 개인정보/정보통신 ──
101
+ {
102
+ canonical: "개인정보 보호법",
103
+ aliases: ["개보법", "개인정보법", "개인정보보호법"],
104
+ },
105
+ {
106
+ canonical: "정보통신망 이용촉진 및 정보보호 등에 관한 법률",
107
+ aliases: ["정보통신망법", "정통망법"],
108
+ },
109
+ // ── 청렴/이해충돌 ──
110
+ {
111
+ canonical: "부정청탁 및 금품등 수수의 금지에 관한 법률",
112
+ aliases: ["청탁금지법", "김영란법"],
113
+ },
114
+ {
115
+ canonical: "공직자의 이해충돌 방지법",
116
+ aliases: ["이해충돌방지법", "공직자이해충돌방지법"],
117
+ },
118
+ // ── 공공계약/공공기관 ──
119
+ {
120
+ canonical: "국가를 당사자로 하는 계약에 관한 법률",
121
+ aliases: ["국가계약법"],
122
+ alternatives: ["국가를 당사자로 하는 계약에 관한 법률 시행령"],
123
+ },
124
+ {
125
+ canonical: "지방자치단체를 당사자로 하는 계약에 관한 법률",
126
+ aliases: ["지방계약법"],
127
+ alternatives: ["지방자치단체를 당사자로 하는 계약에 관한 법률 시행령"],
128
+ },
129
+ {
130
+ canonical: "공공기관의 정보공개에 관한 법률",
131
+ aliases: ["정보공개법"],
132
+ },
133
+ // ── 부동산/주택 ──
134
+ {
135
+ canonical: "부동산 거래신고 등에 관한 법률",
136
+ aliases: ["부동산거래신고법", "부거법"],
137
+ },
138
+ {
139
+ canonical: "주택임대차보호법",
140
+ aliases: ["주임법"],
141
+ },
142
+ {
143
+ canonical: "상가건물 임대차보호법",
144
+ aliases: ["상임법", "상가임대차법"],
145
+ },
146
+ // ── 소방/건축 ──
147
+ {
148
+ canonical: "소방시설 설치 및 관리에 관한 법률",
149
+ aliases: ["소방시설법"],
150
+ },
151
+ // ── 세법 ──
152
+ {
153
+ canonical: "국세기본법",
154
+ aliases: ["국기법"],
155
+ },
156
+ {
157
+ canonical: "부가가치세법",
158
+ aliases: ["부가세법"],
159
+ },
160
+ // ── 공정거래/소비자 ──
161
+ {
162
+ canonical: "독점규제 및 공정거래에 관한 법률",
163
+ aliases: ["공정거래법", "공거법", "독점규제법"],
164
+ alternatives: ["독점규제 및 공정거래에 관한 법률 시행령"],
165
+ },
166
+ {
167
+ canonical: "하도급거래 공정화에 관한 법률",
168
+ aliases: ["하도급법"],
169
+ },
170
+ {
171
+ canonical: "약관의 규제에 관한 법률",
172
+ aliases: ["약관법", "약관규제법"],
173
+ },
174
+ {
175
+ canonical: "표시ㆍ광고의 공정화에 관한 법률",
176
+ aliases: ["표시광고법"],
177
+ },
178
+ {
179
+ canonical: "가맹사업거래의 공정화에 관한 법률",
180
+ aliases: ["가맹사업법", "가맹법"],
181
+ },
182
+ {
183
+ canonical: "전자상거래 등에서의 소비자보호에 관한 법률",
184
+ aliases: ["전자상거래법", "전상법"],
185
+ },
186
+ {
187
+ canonical: "신용정보의 이용 및 보호에 관한 법률",
188
+ aliases: ["신용정보법", "신정법"],
189
+ },
190
+ // ── 금융 ──
191
+ {
192
+ canonical: "자본시장과 금융투자업에 관한 법률",
193
+ aliases: ["자본시장법", "자시법"],
194
+ alternatives: ["자본시장과 금융투자업에 관한 법률 시행령"],
195
+ },
196
+ {
197
+ canonical: "특정 금융거래정보의 보고 및 이용 등에 관한 법률",
198
+ aliases: ["특정금융정보법", "특금법"],
199
+ },
200
+ {
201
+ canonical: "전자금융거래법",
202
+ aliases: ["전금법"],
203
+ },
204
+ // ── 부동산/도시 ──
205
+ {
206
+ canonical: "국토의 계획 및 이용에 관한 법률",
207
+ aliases: ["국토계획법", "국계법", "국토이용법"],
208
+ alternatives: ["국토의 계획 및 이용에 관한 법률 시행령"],
209
+ },
210
+ {
211
+ canonical: "도시 및 주거환경정비법",
212
+ aliases: ["도시정비법", "도정법"],
213
+ },
214
+ // ── 환경/보건 ──
215
+ {
216
+ canonical: "감염병의 예방 및 관리에 관한 법률",
217
+ aliases: ["감염병예방법", "감염병법"],
218
+ },
219
+ {
220
+ canonical: "대기환경보전법",
221
+ aliases: ["대기환경법", "대기법"],
222
+ },
223
+ // ── 교통/운수 ──
224
+ {
225
+ canonical: "여객자동차 운수사업법",
226
+ aliases: ["여객운수법", "여객자동차법"],
227
+ },
228
+ {
229
+ canonical: "화물자동차 운수사업법",
230
+ aliases: ["화물운수법", "화운법"],
231
+ },
232
+ // ── 민·형사 절차 ──
233
+ {
234
+ canonical: "민사소송법",
235
+ aliases: ["민소법"],
236
+ },
237
+ {
238
+ canonical: "형사소송법",
239
+ aliases: ["형소법"],
240
+ },
241
+ {
242
+ canonical: "민사집행법",
243
+ aliases: ["민집법"],
244
+ },
245
+ // ── 사회보험/복지 ──
246
+ {
247
+ canonical: "국민건강보험법",
248
+ aliases: ["국건법", "건보법"],
249
+ },
250
+ {
251
+ canonical: "산업재해보상보험법",
252
+ aliases: ["산재보험법", "산재법"],
253
+ },
254
+ {
255
+ canonical: "고용보험법",
256
+ aliases: ["고보법"],
257
+ },
258
+ // ── 통신 ──
259
+ {
260
+ canonical: "전기통신사업법",
261
+ aliases: ["전기통신법", "전사법"],
262
+ },
76
263
  ];
77
264
  const aliasLookup = new Map();
78
265
  for (const entry of LAW_ALIAS_ENTRIES) {
@@ -1,8 +1,11 @@
1
1
  /**
2
- * 세션 상태 관리 (AsyncLocalStorage 기반 - 동시 요청 안전)
2
+ * 요청별 컨텍스트 (AsyncLocalStorage 기반 - stateless 모드)
3
+ *
4
+ * HTTP stateless 전환 후: 세션 Map 없음. 매 요청마다 ALS에 API 키를 주입하고
5
+ * api-client가 getStore()로 조회. 요청 끝나면 자동 소멸.
3
6
  */
4
7
  import { AsyncLocalStorage } from "node:async_hooks";
5
- export declare const sessionStore: AsyncLocalStorage<string | undefined>;
6
- export declare function setSessionApiKey(sessionId: string, apiKey: string): void;
7
- export declare function getSessionApiKey(sessionId: string): string | undefined;
8
- export declare function deleteSession(sessionId: string): void;
8
+ export interface RequestContext {
9
+ apiKey?: string;
10
+ }
11
+ export declare const requestContext: AsyncLocalStorage<RequestContext>;
@@ -1,24 +1,8 @@
1
1
  /**
2
- * 세션 상태 관리 (AsyncLocalStorage 기반 - 동시 요청 안전)
2
+ * 요청별 컨텍스트 (AsyncLocalStorage 기반 - stateless 모드)
3
+ *
4
+ * HTTP stateless 전환 후: 세션 Map 없음. 매 요청마다 ALS에 API 키를 주입하고
5
+ * api-client가 getStore()로 조회. 요청 끝나면 자동 소멸.
3
6
  */
4
7
  import { AsyncLocalStorage } from "node:async_hooks";
5
- // 요청별 세션 ID 격리 (레이스 컨디션 방지)
6
- export const sessionStore = new AsyncLocalStorage();
7
- // 세션별 API 키 맵
8
- const MAX_SESSIONS = 100;
9
- const sessionApiKeys = new Map();
10
- export function setSessionApiKey(sessionId, apiKey) {
11
- // 메모리 누수 방지: 최대 세션 수 초과 시 가장 오래된 세션 삭제
12
- if (sessionApiKeys.size >= MAX_SESSIONS && !sessionApiKeys.has(sessionId)) {
13
- const oldest = sessionApiKeys.keys().next().value;
14
- if (oldest)
15
- sessionApiKeys.delete(oldest);
16
- }
17
- sessionApiKeys.set(sessionId, apiKey);
18
- }
19
- export function getSessionApiKey(sessionId) {
20
- return sessionApiKeys.get(sessionId);
21
- }
22
- export function deleteSession(sessionId) {
23
- sessionApiKeys.delete(sessionId);
24
- }
8
+ export const requestContext = new AsyncLocalStorage();
@@ -1,5 +1,9 @@
1
1
  /**
2
- * Streamable HTTP 서버 - 리모트 배포용 (MCP 표준)
2
+ * Streamable HTTP 서버 - stateless 모드 (MCP 공식 패턴)
3
+ *
4
+ * 매 POST 요청마다 fresh Server + Transport 생성, 요청 종료 시 즉시 정리.
5
+ * 세션 Map/EventStore/idle cleanup 없음 → 재시작/스케일아웃/OOM 내성.
6
+ * 참고: @modelcontextprotocol/sdk/examples/server/simpleStatelessStreamableHttp.js
3
7
  */
4
8
  import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
9
  import { type ToolProfile } from "../lib/tool-profiles.js";