korean-law-mcp 2.1.0 → 2.1.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.
package/build/index.js CHANGED
@@ -10,11 +10,6 @@ import { registerTools } from "./tool-registry.js";
10
10
  import { startHTTPServer } from "./server/http-server.js";
11
11
  // API 클라이언트 초기화
12
12
  const LAW_OC = process.env.LAW_OC || "";
13
- if (!LAW_OC) {
14
- console.error("⚠️ LAW_OC 환경변수 미설정. STDIO 모드에서는 API 호출이 실패합니다.");
15
- console.error(" API 키 발급: https://open.law.go.kr/LSO/openApi/guideResult.do");
16
- console.error(" HTTP 모드에서는 클라이언트가 헤더로 API 키를 제공할 수 있습니다.");
17
- }
18
13
  const apiClient = new LawApiClient({ apiKey: LAW_OC });
19
14
  // MCP 서버 생성
20
15
  const server = new Server({
@@ -41,7 +36,6 @@ async function main() {
41
36
  // STDIO 모드 (기본)
42
37
  const transport = new StdioServerTransport();
43
38
  await server.connect(transport);
44
- console.error("Korean Law MCP server running on stdio");
45
39
  }
46
40
  }
47
41
  main().catch((error) => {
@@ -70,13 +70,13 @@ export class LawApiClient {
70
70
  type: "JSON",
71
71
  });
72
72
  if (params.mst)
73
- apiParams.append("MST", params.mst);
73
+ apiParams.append("MST", String(params.mst));
74
74
  if (params.lawId)
75
- apiParams.append("ID", params.lawId);
75
+ apiParams.append("ID", String(params.lawId));
76
76
  if (params.jo)
77
- apiParams.append("JO", params.jo);
77
+ apiParams.append("JO", String(params.jo));
78
78
  if (params.efYd)
79
- apiParams.append("efYd", params.efYd);
79
+ apiParams.append("efYd", String(params.efYd));
80
80
  const url = `${LAW_API_BASE}/lawService.do?${apiParams.toString()}`;
81
81
  const response = await fetchWithRetry(url);
82
82
  this.throwIfError(response, "getLawText");
@@ -115,13 +115,13 @@ export class LawApiClient {
115
115
  type: "XML",
116
116
  });
117
117
  if (params.mst)
118
- apiParams.append("MST", params.mst);
118
+ apiParams.append("MST", String(params.mst));
119
119
  if (params.lawId)
120
- apiParams.append("ID", params.lawId);
120
+ apiParams.append("ID", String(params.lawId));
121
121
  if (params.ld)
122
- apiParams.append("LD", params.ld);
122
+ apiParams.append("LD", String(params.ld));
123
123
  if (params.ln)
124
- apiParams.append("LN", params.ln);
124
+ apiParams.append("LN", String(params.ln));
125
125
  const url = `${LAW_API_BASE}/lawService.do?${apiParams.toString()}`;
126
126
  const response = await fetchWithRetry(url);
127
127
  this.throwIfError(response, "compareOldNew");
@@ -138,9 +138,9 @@ export class LawApiClient {
138
138
  knd: params.knd || "2",
139
139
  });
140
140
  if (params.mst)
141
- apiParams.append("MST", params.mst);
141
+ apiParams.append("MST", String(params.mst));
142
142
  if (params.lawId)
143
- apiParams.append("ID", params.lawId);
143
+ apiParams.append("ID", String(params.lawId));
144
144
  const url = `${LAW_API_BASE}/lawService.do?${apiParams.toString()}`;
145
145
  const response = await fetchWithRetry(url);
146
146
  this.throwIfError(response, "getThreeTier");
@@ -273,17 +273,17 @@ export class LawApiClient {
273
273
  type: "XML",
274
274
  });
275
275
  if (params.lawId)
276
- apiParams.append("ID", params.lawId);
276
+ apiParams.append("ID", String(params.lawId));
277
277
  if (params.jo)
278
- apiParams.append("JO", params.jo);
278
+ apiParams.append("JO", String(params.jo));
279
279
  if (params.regDt)
280
- apiParams.append("regDt", params.regDt);
280
+ apiParams.append("regDt", String(params.regDt));
281
281
  if (params.fromRegDt)
282
- apiParams.append("fromRegDt", params.fromRegDt);
282
+ apiParams.append("fromRegDt", String(params.fromRegDt));
283
283
  if (params.toRegDt)
284
- apiParams.append("toRegDt", params.toRegDt);
284
+ apiParams.append("toRegDt", String(params.toRegDt));
285
285
  if (params.org)
286
- apiParams.append("org", params.org);
286
+ apiParams.append("org", String(params.org));
287
287
  if (params.page)
288
288
  apiParams.append("page", params.page.toString());
289
289
  const url = `${LAW_API_BASE}/lawSearch.do?${apiParams.toString()}`;
@@ -302,7 +302,7 @@ export class LawApiClient {
302
302
  });
303
303
  if (params.extraParams) {
304
304
  for (const [key, value] of Object.entries(params.extraParams)) {
305
- apiParams.append(key, value);
305
+ apiParams.append(key, String(value));
306
306
  }
307
307
  }
308
308
  const url = `${LAW_API_BASE}/${params.endpoint}?${apiParams.toString()}`;
@@ -27,7 +27,7 @@ export async function startHTTPServer(server, port) {
27
27
  deleteSession(sessionId);
28
28
  }
29
29
  }
30
- }, 5 * 60 * 1000);
30
+ }, 5 * 60 * 1000).unref();
31
31
  // Rate Limiting (RATE_LIMIT_RPM 환경변수, 기본: 60 req/min per IP)
32
32
  const rateLimitRpm = parseInt(process.env.RATE_LIMIT_RPM || "60", 10);
33
33
  const rateBuckets = new Map();
@@ -56,7 +56,7 @@ export async function startHTTPServer(server, port) {
56
56
  if (now >= bucket.resetAt)
57
57
  rateBuckets.delete(ip);
58
58
  }
59
- }, 5 * 60 * 1000);
59
+ }, 5 * 60 * 1000).unref();
60
60
  }
61
61
  // CORS 및 보안 헤더 설정
62
62
  const corsOrigin = process.env.CORS_ORIGIN || "*";
@@ -75,17 +75,20 @@ export async function startSSEServer(server, port) {
75
75
  }
76
76
  else if (!sessionId && isInitializeRequest(req.body)) {
77
77
  // 새 세션 초기화
78
+ // 세션 수 제한 — transport 생성 전에 체크하여 리소스 누수 방지
79
+ if (Object.keys(transports).length >= MAX_SESSIONS) {
80
+ res.status(503).json({
81
+ jsonrpc: "2.0",
82
+ error: { code: -32000, message: `Max sessions (${MAX_SESSIONS}) reached. Try again later.` },
83
+ id: null,
84
+ });
85
+ return;
86
+ }
78
87
  const eventStore = new InMemoryEventStore();
79
88
  transport = new StreamableHTTPServerTransport({
80
89
  sessionIdGenerator: () => randomUUID(),
81
90
  eventStore,
82
91
  onsessioninitialized: (newSessionId) => {
83
- // 세션 수 제한
84
- if (Object.keys(transports).length >= MAX_SESSIONS) {
85
- console.error(`Max sessions (${MAX_SESSIONS}) reached, rejecting new session`);
86
- return;
87
- }
88
- console.error(`Session initialized: ${newSessionId}`);
89
92
  transports[newSessionId] = { transport, lastAccess: Date.now() };
90
93
  }
91
94
  });
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { z } from "zod";
5
5
  import { DOMParser } from "@xmldom/xmldom";
6
+ import { truncateResponse } from "../lib/schemas.js";
6
7
  // search_admin_rule 스키마
7
8
  export const SearchAdminRuleSchema = z.object({
8
9
  query: z.string().describe("검색할 행정규칙명"),
@@ -61,7 +62,7 @@ export async function searchAdminRule(apiClient, input) {
61
62
  return {
62
63
  content: [{
63
64
  type: "text",
64
- text: resultText
65
+ text: truncateResponse(resultText)
65
66
  }]
66
67
  };
67
68
  }
@@ -115,7 +116,7 @@ export async function getAdminRule(apiClient, input) {
115
116
  return {
116
117
  content: [{
117
118
  type: "text",
118
- text: resultText
119
+ text: truncateResponse(resultText)
119
120
  }]
120
121
  };
121
122
  }
@@ -157,7 +158,7 @@ export async function getAdminRule(apiClient, input) {
157
158
  return {
158
159
  content: [{
159
160
  type: "text",
160
- text: resultText
161
+ text: truncateResponse(resultText)
161
162
  }]
162
163
  };
163
164
  }
@@ -197,7 +198,7 @@ export async function getAdminRule(apiClient, input) {
197
198
  return {
198
199
  content: [{
199
200
  type: "text",
200
- text: resultText
201
+ text: truncateResponse(resultText)
201
202
  }]
202
203
  };
203
204
  }
@@ -23,16 +23,22 @@ export async function getAnnexes(apiClient, input) {
23
23
  // 법제처 API는 결과 1건일 때 배열 대신 단일 객체를 반환하므로 정규화
24
24
  const toArray = (v) => v == null ? [] : Array.isArray(v) ? v : [v];
25
25
  const parseAnnexResponse = (jsonText) => {
26
- const json = JSON.parse(jsonText);
27
- const adminResult = json?.admRulBylSearch;
28
- const licResult = json?.licBylSearch;
29
- if (adminResult?.admbyl)
30
- return { list: toArray(adminResult.admbyl), type: "admin" };
31
- if (licResult?.ordinbyl)
32
- return { list: toArray(licResult.ordinbyl), type: "ordinance" };
33
- if (licResult?.licbyl)
34
- return { list: toArray(licResult.licbyl), type: "law" };
35
- return { list: [], type: "law" };
26
+ try {
27
+ const json = JSON.parse(jsonText);
28
+ const adminResult = json?.admRulBylSearch;
29
+ const licResult = json?.licBylSearch;
30
+ if (adminResult?.admbyl)
31
+ return { list: toArray(adminResult.admbyl), type: "admin" };
32
+ if (licResult?.ordinbyl)
33
+ return { list: toArray(licResult.ordinbyl), type: "ordinance" };
34
+ if (licResult?.licbyl)
35
+ return { list: toArray(licResult.licbyl), type: "law" };
36
+ return { list: [], type: "law" };
37
+ }
38
+ catch {
39
+ // JSON 파싱 실패 (HTML 에러 페이지 등) → 빈 배열 반환하여 fallback 진행
40
+ return { list: [], type: "law" };
41
+ }
36
42
  };
37
43
  // 1차: 원래 법령명 + knd 필터
38
44
  const result1 = parseAnnexResponse(await apiClient.getAnnexes({
@@ -64,6 +70,30 @@ export async function getAnnexes(apiClient, input) {
64
70
  lawType = result3.type;
65
71
  }
66
72
  }
73
+ // 4차: "규정" 타입은 licbyl과 admbyl 양쪽에 존재 가능 → admin fallback
74
+ if (annexList.length === 0 && /규정/.test(normalizedLawName)) {
75
+ try {
76
+ const adminText = await apiClient.fetchApi({
77
+ endpoint: "lawSearch.do",
78
+ target: "admbyl",
79
+ type: "JSON",
80
+ extraParams: {
81
+ query: normalizedLawName,
82
+ search: "2",
83
+ display: "100",
84
+ },
85
+ apiKey: input.apiKey,
86
+ });
87
+ const result4 = parseAnnexResponse(adminText);
88
+ if (result4.list.length > 0) {
89
+ annexList = result4.list;
90
+ lawType = "admin";
91
+ }
92
+ }
93
+ catch {
94
+ // admin fallback 실패 → 무시하고 진행
95
+ }
96
+ }
67
97
  if (annexList.length === 0) {
68
98
  return {
69
99
  content: [{ type: "text", text: `"${normalizedLawName}"에 대한 별표/서식이 없습니다.` }]
@@ -26,7 +26,7 @@ export const GetBatchArticlesSchema = z.object({
26
26
  * 단일 법령에서 조문 추출
27
27
  */
28
28
  async function fetchArticlesForLaw(apiClient, lawReq, efYd, apiKey) {
29
- const cacheKey = `lawtext:${lawReq.mst || lawReq.lawId}:full:${efYd || 'current'}`;
29
+ const cacheKey = `batch:${lawReq.mst || lawReq.lawId}:full:${efYd || 'current'}`;
30
30
  let fullLawData;
31
31
  const cached = lawCache.get(cacheKey);
32
32
  if (cached) {
@@ -34,36 +34,60 @@ handler, apiClient, input) {
34
34
  return { text: `오류: ${e instanceof Error ? e.message : String(e)}`, isError: true };
35
35
  }
36
36
  }
37
+ /** 법령명이 아닌 부가 키워드 제거 (법제처 lawSearch API는 법령명 검색이므로) */
38
+ const NON_LAW_NAME_RE = /\s*(과태료|절차|비용|처벌|기준|허가|신청|부과|근거|위반|방법|요건|조건|처분|수수료|신고|등록|면허|인가|승인|취소|정지|벌칙|벌금|과징금|이행강제금|시정명령|체계|구조|3단|판례|해석|개정|별표|시행령|시행규칙|서식|수입|수출|통관|반환|납부|감면|면제|제한|금지|의무|권리|자격|종류|기간|대상|범위|적용)\s*/g;
39
+ function stripNonLawKeywords(query) {
40
+ return query.replace(NON_LAW_NAME_RE, " ").trim();
41
+ }
42
+ /** XML에서 법령 정보 파싱 */
43
+ function parseLawXml(xmlText, max) {
44
+ const lawRegex = /<law[^>]*>([\s\S]*?)<\/law>/g;
45
+ const results = [];
46
+ let match;
47
+ while ((match = lawRegex.exec(xmlText)) !== null && results.length < max) {
48
+ const content = match[1];
49
+ const lawName = extractTag(content, "법령명한글");
50
+ if (!lawName)
51
+ continue; // 빈 법령명 제외
52
+ results.push({
53
+ lawName,
54
+ lawId: extractTag(content, "법령ID"),
55
+ mst: extractTag(content, "법령일련번호"),
56
+ lawType: extractTag(content, "법령구분명"),
57
+ });
58
+ }
59
+ return results;
60
+ }
37
61
  async function findLaws(apiClient, query, apiKey, max = 3) {
62
+ // 1차: 원본 쿼리로 검색
63
+ let results = [];
38
64
  try {
39
65
  const xmlText = await apiClient.searchLaw(query, apiKey);
40
- const lawRegex = /<law[^>]*>([\s\S]*?)<\/law>/g;
41
- const results = [];
42
- let match;
43
- while ((match = lawRegex.exec(xmlText)) !== null && results.length < max) {
44
- const content = match[1];
45
- results.push({
46
- lawName: extractTag(content, "법령명한글"),
47
- lawId: extractTag(content, "법령ID"),
48
- mst: extractTag(content, "법령일련번호"),
49
- lawType: extractTag(content, "법령구분명"),
50
- });
51
- }
52
- // 쿼리와 법령명 관련도 기반 정렬 (정확 매칭 > 부분 매칭 > 나머지)
53
- if (results.length > 1) {
54
- const queryWords = query.replace(/\s*(시행령|시행규칙|별표|판례|개정|체계|3단|구조|절차|비용|처벌|기준|허가|신청)\s*/g, " ")
55
- .trim().split(/\s+/).filter(w => w.length > 0);
56
- results.sort((a, b) => {
57
- const scoreA = scoreLawRelevance(a.lawName, query, queryWords);
58
- const scoreB = scoreLawRelevance(b.lawName, query, queryWords);
59
- return scoreB - scoreA;
60
- });
66
+ results = parseLawXml(xmlText, max);
67
+ }
68
+ catch { /* 2차 시도로 진행 */ }
69
+ // 2차: 결과 없으면 부가 키워드 제거 재시도
70
+ if (results.length === 0) {
71
+ const stripped = stripNonLawKeywords(query);
72
+ if (stripped && stripped !== query) {
73
+ try {
74
+ const xmlText = await apiClient.searchLaw(stripped, apiKey);
75
+ results = parseLawXml(xmlText, max);
76
+ }
77
+ catch { /* 빈 결과 반환 */ }
61
78
  }
62
- return results;
63
79
  }
64
- catch {
65
- return [];
80
+ // 쿼리와 법령명 관련도 기반 정렬 (정확 매칭 > 부분 매칭 > 나머지)
81
+ if (results.length > 1) {
82
+ const queryWords = query.replace(NON_LAW_NAME_RE, " ")
83
+ .trim().split(/\s+/).filter(w => w.length > 0);
84
+ results.sort((a, b) => {
85
+ const scoreA = scoreLawRelevance(a.lawName, query, queryWords);
86
+ const scoreB = scoreLawRelevance(b.lawName, query, queryWords);
87
+ return scoreB - scoreA;
88
+ });
66
89
  }
90
+ return results;
67
91
  }
68
92
  /** 쿼리 대비 법령명 관련도 점수 (높을수록 관련) */
69
93
  function scoreLawRelevance(lawName, query, queryWords) {
@@ -98,6 +122,13 @@ function detectExpansions(query) {
98
122
  exp.push("interpretation");
99
123
  return exp;
100
124
  }
125
+ /** 조례 쿼리에서 지역명·조례 키워드 제거 → 상위법 검색용 */
126
+ function stripOrdinanceKeywords(query) {
127
+ return query
128
+ .replace(/(?:서울|부산|대구|인천|광주|대전|울산|세종|경기|강원|충북|충남|전북|전남|경북|경남|제주)(?:시|도|특별시|광역시|특별자치시|특별자치도)?/g, "")
129
+ .replace(/\s*(조례|규칙|자치법규)\s*/g, " ")
130
+ .trim();
131
+ }
101
132
  function detectDomain(query) {
102
133
  if (/관세|수출|수입|통관|FTA|원산지/.test(query))
103
134
  return "customs";
@@ -334,9 +365,9 @@ export const chainOrdinanceCompareSchema = z.object({
334
365
  export async function chainOrdinanceCompare(apiClient, input) {
335
366
  try {
336
367
  const parts = [`═══ 조례 비교 연구: ${input.query} ═══`];
337
- // Step 1: 상위 법령 확인
338
- const parentQuery = input.parentLaw || input.query;
339
- const laws = await findLaws(apiClient, parentQuery, input.apiKey, 2);
368
+ // Step 1: 상위 법령 확인 (조례/지역명은 법령 검색에서 제거)
369
+ const parentQuery = input.parentLaw || stripOrdinanceKeywords(input.query);
370
+ const laws = parentQuery ? await findLaws(apiClient, parentQuery, input.apiKey, 2) : [];
340
371
  if (laws.length > 0) {
341
372
  const p = laws[0];
342
373
  parts.push(sec("상위 법령", `${p.lawName} (${p.lawType}) | MST: ${p.mst}`));
@@ -345,8 +376,9 @@ export async function chainOrdinanceCompare(apiClient, input) {
345
376
  if (!threeTier.isError)
346
377
  parts.push(sec("위임 체계 (법률·시행령·시행규칙)", threeTier.text));
347
378
  }
348
- // Step 2: 조례 검색 ( 지자체)
349
- const ordinances = await callTool(searchOrdinance, apiClient, { query: input.query, display: 20, apiKey: input.apiKey });
379
+ // Step 2: 조례 검색 — "조례"/"규칙" 제거 (이미 조례 DB에서 검색하므로)
380
+ const ordinanceQuery = input.query.replace(/\s*(조례|규칙|자치법규)\s*/g, " ").trim() || input.query;
381
+ const ordinances = await callTool(searchOrdinance, apiClient, { query: ordinanceQuery, display: 20, apiKey: input.apiKey });
350
382
  if (!ordinances.isError)
351
383
  parts.push(sec("전국 자치법규 검색 결과", ordinances.text));
352
384
  // 키워드 확장
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { z } from "zod";
5
5
  import { DOMParser } from "@xmldom/xmldom";
6
+ import { truncateResponse } from "../lib/schemas.js";
6
7
  export const CompareOldNewSchema = z.object({
7
8
  mst: z.string().optional().describe("법령일련번호"),
8
9
  lawId: z.string().optional().describe("법령ID"),
@@ -94,7 +95,7 @@ export async function compareOldNew(apiClient, input) {
94
95
  return {
95
96
  content: [{
96
97
  type: "text",
97
- text: resultText
98
+ text: truncateResponse(resultText)
98
99
  }]
99
100
  };
100
101
  }
@@ -94,11 +94,11 @@ export async function getEnglishLawText(apiClient, args) {
94
94
  }
95
95
  const extraParams = {};
96
96
  if (args.lawId)
97
- extraParams.ID = args.lawId;
97
+ extraParams.ID = String(args.lawId);
98
98
  if (args.mst)
99
- extraParams.MST = args.mst;
99
+ extraParams.MST = String(args.mst);
100
100
  if (args.lawName)
101
- extraParams.LM = args.lawName;
101
+ extraParams.LM = String(args.lawName);
102
102
  const responseText = await apiClient.fetchApi({
103
103
  endpoint: "lawService.do",
104
104
  target: "elaw",
@@ -105,8 +105,9 @@ export async function getHistoricalLaw(apiClient, args) {
105
105
  output += ` 제개정구분: ${basic.제개정구분명 || basic.제개정구분 || "N/A"}\n`;
106
106
  output += ` 소관부처: ${basic.소관부처명 || basic.소관부처 || "N/A"}\n\n`;
107
107
  // Extract articles
108
- const articles = law.조문 || [];
109
- if (Array.isArray(articles) && articles.length > 0) {
108
+ const rawArticles = law.조문;
109
+ const articles = rawArticles == null ? [] : Array.isArray(rawArticles) ? rawArticles : [rawArticles];
110
+ if (articles.length > 0) {
110
111
  if (args.jo) {
111
112
  // Filter to specific article
112
113
  const joCode = parseJoNumber(args.jo);
@@ -316,9 +316,9 @@ export async function getRelatedLaws(apiClient, args) {
316
316
  display: (args.display || 20).toString(),
317
317
  };
318
318
  if (args.lawId)
319
- extraParams.ID = args.lawId;
319
+ extraParams.ID = String(args.lawId);
320
320
  if (args.lawName)
321
- extraParams.query = args.lawName;
321
+ extraParams.query = String(args.lawName);
322
322
  let xmlText;
323
323
  try {
324
324
  xmlText = await apiClient.fetchApi({
@@ -14,11 +14,11 @@ export async function getLawSystemTree(apiClient, args) {
14
14
  }
15
15
  const extraParams = {};
16
16
  if (args.lawId)
17
- extraParams.ID = args.lawId;
17
+ extraParams.ID = String(args.lawId);
18
18
  if (args.mst)
19
- extraParams.MST = args.mst;
19
+ extraParams.MST = String(args.mst);
20
20
  if (args.lawName)
21
- extraParams.LM = args.lawName;
21
+ extraParams.LM = String(args.lawName);
22
22
  const responseText = await apiClient.fetchApi({
23
23
  endpoint: "lawService.do",
24
24
  target: "lsStmd",
@@ -48,7 +48,7 @@ export async function getLawSystemTree(apiClient, args) {
48
48
  output += ` 법령구분: ${lawType}\n`;
49
49
  output += ` 제개정: ${revision}\n`;
50
50
  output += ` 시행일자: ${formatDateDot(basicInfo.시행일자)}\n`;
51
- output += ` 공포일자: ${formatDateDot(basicInfo.공포일자)} (제${basicInfo.공포번호}호)\n\n`;
51
+ output += ` 공포일자: ${formatDateDot(basicInfo.공포일자)}${basicInfo.공포번호 ? ` (제${basicInfo.공포번호}호)` : ""}\n\n`;
52
52
  // Law hierarchy (상하위법)
53
53
  output += `📊 법령 체계:\n\n`;
54
54
  const hierarchy = tree.상하위법 || {};
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import { z } from "zod";
5
5
  import { cleanHtml } from "../lib/article-parser.js";
6
+ import { truncateResponse } from "../lib/schemas.js";
6
7
  export const GetOrdinanceSchema = z.object({
7
8
  ordinSeq: z.string().describe("자치법규 일련번호"),
8
9
  apiKey: z.string().optional().describe("API 키")
@@ -80,7 +81,7 @@ export async function getOrdinance(apiClient, input) {
80
81
  return {
81
82
  content: [{
82
83
  type: "text",
83
- text: resultText
84
+ text: truncateResponse(resultText)
84
85
  }]
85
86
  };
86
87
  }
@@ -4,6 +4,7 @@
4
4
  import { z } from "zod";
5
5
  import { DOMParser } from "@xmldom/xmldom";
6
6
  import { lawCache } from "../lib/cache.js";
7
+ import { truncateResponse } from "../lib/schemas.js";
7
8
  export const SearchLawSchema = z.object({
8
9
  query: z.string().describe("검색할 법령명 (예: '관세법', 'fta특례법', '화관법')"),
9
10
  display: z.number().optional().default(20).describe("최대 결과 개수"),
@@ -53,11 +54,12 @@ export async function searchLaw(apiClient, input) {
53
54
  }
54
55
  resultText += `\n💡 특정 조문을 조회하려면 get_law_text Tool을 사용하세요.`;
55
56
  // Cache the result (1 hour TTL)
56
- lawCache.set(cacheKey, resultText, 60 * 60 * 1000);
57
+ const truncated = truncateResponse(resultText);
58
+ lawCache.set(cacheKey, truncated, 60 * 60 * 1000);
57
59
  return {
58
60
  content: [{
59
61
  type: "text",
60
- text: resultText
62
+ text: truncated
61
63
  }]
62
64
  };
63
65
  }
package/package.json CHANGED
@@ -1,9 +1,28 @@
1
1
  {
2
2
  "name": "korean-law-mcp",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "국가법령정보센터 API 기반 MCP 서버 - 한국 법령 조회·비교 도구",
5
5
  "type": "module",
6
6
  "main": "build/index.js",
7
+ "types": "build/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./build/index.d.ts",
11
+ "import": "./build/index.js"
12
+ },
13
+ "./lib/*": {
14
+ "types": "./build/lib/*.d.ts",
15
+ "import": "./build/lib/*.js"
16
+ },
17
+ "./tools/*": {
18
+ "types": "./build/tools/*.d.ts",
19
+ "import": "./build/tools/*.js"
20
+ },
21
+ "./server/*": {
22
+ "types": "./build/server/*.d.ts",
23
+ "import": "./build/server/*.js"
24
+ }
25
+ },
7
26
  "bin": {
8
27
  "korean-law-mcp": "build/index.js",
9
28
  "korean-law": "build/cli.js"