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 +0 -6
- package/build/lib/api-client.js +17 -17
- package/build/server/http-server.js +2 -2
- package/build/server/sse-server.js +9 -6
- package/build/tools/admin-rule.js +5 -4
- package/build/tools/annex.js +40 -10
- package/build/tools/batch-articles.js +1 -1
- package/build/tools/chains.js +61 -29
- package/build/tools/comparison.js +2 -1
- package/build/tools/english-law.js +3 -3
- package/build/tools/historical-law.js +3 -2
- package/build/tools/knowledge-base.js +2 -2
- package/build/tools/law-system-tree.js +4 -4
- package/build/tools/ordinance.js +2 -1
- package/build/tools/search.js +4 -2
- package/package.json +20 -1
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) => {
|
package/build/lib/api-client.js
CHANGED
|
@@ -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
|
}
|
package/build/tools/annex.js
CHANGED
|
@@ -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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 = `
|
|
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) {
|
package/build/tools/chains.js
CHANGED
|
@@ -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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
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
|
|
109
|
-
|
|
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.상하위법 || {};
|
package/build/tools/ordinance.js
CHANGED
|
@@ -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
|
}
|
package/build/tools/search.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
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"
|