korean-law-mcp 2.1.1 → 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.
@@ -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
  // 키워드 확장
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "korean-law-mcp",
3
- "version": "2.1.1",
3
+ "version": "2.1.2",
4
4
  "description": "국가법령정보센터 API 기반 MCP 서버 - 한국 법령 조회·비교 도구",
5
5
  "type": "module",
6
6
  "main": "build/index.js",