korean-law-mcp 4.4.2 → 4.4.3
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 +8 -0
- package/package.json +2 -2
- package/build/server/sse-server.d.ts +0 -5
- package/build/server/sse-server.js +0 -232
package/README.md
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
[](https://modelcontextprotocol.io)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
+
<a href="https://jocohunt.com/p/5lkkxp3x" target="_blank" rel="noopener" title="조코헌트 주간 1등 Top 1 위너">
|
|
10
|
+
<img
|
|
11
|
+
src="https://jocohunt.com/images/badges/weekly-light.svg"
|
|
12
|
+
alt="조코헌트 주간 1등 Top 1 위너"
|
|
13
|
+
style="width: 250px; height: auto;"
|
|
14
|
+
/>
|
|
15
|
+
</a>
|
|
16
|
+
|
|
9
17
|
> 법제처 Open API 기반 MCP 서버 + CLI. Claude Desktop, Cursor, Windsurf, Zed, Claude.ai 등에서 바로 사용 가능.
|
|
10
18
|
|
|
11
19
|
[English](./README-EN.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "korean-law-mcp",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.3",
|
|
4
4
|
"description": "법제처 42개 API → 9개 MCP 도구. 법령·판례·조례·조약 + 다단계 리서치(legal_research, 8 task) + 정밀분석(legal_analysis: 인용검증·판례생사·행위시법·영향그래프) + 시점 비교(time_travel) + 상황별 5단계 안내(action_plan) + 국세청 해석례(nts)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"express": "^5.2.1",
|
|
57
57
|
"kordoc": "^3.0.0",
|
|
58
58
|
"pdfjs-dist": "^5.5.207",
|
|
59
|
-
"zod": "^
|
|
59
|
+
"zod": "^4.0.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@types/express": "^5.0.6",
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Streamable HTTP 서버 - 리모트 배포용 (MCP 2025-03-26 스펙 준수)
|
|
3
|
-
*/
|
|
4
|
-
import express from "express";
|
|
5
|
-
import { randomUUID } from "node:crypto";
|
|
6
|
-
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
|
-
import { InMemoryEventStore } from "@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js";
|
|
8
|
-
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
-
import { sessionStore, setSessionApiKey, deleteSession } from "../lib/session-state.js";
|
|
10
|
-
import { VERSION } from "../version.js";
|
|
11
|
-
export async function startSSEServer(server, port) {
|
|
12
|
-
const app = express();
|
|
13
|
-
const transports = {};
|
|
14
|
-
// JSON 파싱 미들웨어 (크기 제한 명시)
|
|
15
|
-
app.use(express.json({ limit: "100kb" }));
|
|
16
|
-
// 유휴 세션 정리 (30분)
|
|
17
|
-
const SESSION_IDLE_TIMEOUT = 30 * 60 * 1000;
|
|
18
|
-
const MAX_SESSIONS = 100;
|
|
19
|
-
setInterval(() => {
|
|
20
|
-
const now = Date.now();
|
|
21
|
-
for (const sid of Object.keys(transports)) {
|
|
22
|
-
const session = transports[sid];
|
|
23
|
-
if (session.lastAccess && now - session.lastAccess > SESSION_IDLE_TIMEOUT) {
|
|
24
|
-
try {
|
|
25
|
-
session.transport.close();
|
|
26
|
-
}
|
|
27
|
-
catch { /* ignore */ }
|
|
28
|
-
delete transports[sid];
|
|
29
|
-
deleteSession(sid);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}, 5 * 60 * 1000).unref();
|
|
33
|
-
// CORS 및 보안 헤더 설정
|
|
34
|
-
const corsOrigin = process.env.CORS_ORIGIN || "*";
|
|
35
|
-
app.use((req, res, next) => {
|
|
36
|
-
res.header("Access-Control-Allow-Origin", corsOrigin);
|
|
37
|
-
res.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS, HEAD");
|
|
38
|
-
res.header("Access-Control-Allow-Headers", "Content-Type, Accept, Authorization, Mcp-Protocol-Version, mcp-protocol-version, Mcp-Session-Id, mcp-session-id, Last-Event-ID, last-event-id, Traceparent, Tracestate");
|
|
39
|
-
res.header("Access-Control-Expose-Headers", "Mcp-Session-Id, Content-Type, Mcp-Protocol-Version, Traceparent, Tracestate");
|
|
40
|
-
res.header("Access-Control-Max-Age", "86400");
|
|
41
|
-
// Security headers (http-server.ts와 동일)
|
|
42
|
-
res.header("X-Content-Type-Options", "nosniff");
|
|
43
|
-
res.header("X-Frame-Options", "DENY");
|
|
44
|
-
res.header("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
45
|
-
res.header("Mcp-Protocol-Version", "2025-03-26");
|
|
46
|
-
if (req.method === "OPTIONS") {
|
|
47
|
-
return res.sendStatus(200);
|
|
48
|
-
}
|
|
49
|
-
next();
|
|
50
|
-
});
|
|
51
|
-
// 헬스체크 엔드포인트
|
|
52
|
-
app.get("/", (req, res) => {
|
|
53
|
-
res.json({
|
|
54
|
-
name: "Korean Law MCP Server",
|
|
55
|
-
version: VERSION,
|
|
56
|
-
status: "running",
|
|
57
|
-
protocol: "streamable-http",
|
|
58
|
-
endpoints: {
|
|
59
|
-
mcp: "/mcp",
|
|
60
|
-
health: "/health"
|
|
61
|
-
}
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
app.get("/health", (req, res) => {
|
|
65
|
-
res.json({ status: "ok", timestamp: new Date().toISOString() });
|
|
66
|
-
});
|
|
67
|
-
// MCP POST 엔드포인트 (초기화 및 요청 처리)
|
|
68
|
-
app.post("/mcp", async (req, res) => {
|
|
69
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
70
|
-
// API 키 추출 (http-server.ts와 동일 패턴)
|
|
71
|
-
const apiKeyFromHeader = req.headers["apikey"] ||
|
|
72
|
-
req.headers["law_oc"] ||
|
|
73
|
-
req.headers["law-oc"] ||
|
|
74
|
-
req.headers["LAW_OC"] ||
|
|
75
|
-
req.headers["x-api-key"] ||
|
|
76
|
-
req.headers["authorization"]?.replace(/^Bearer\s+/i, "") ||
|
|
77
|
-
req.headers["x-law-oc"];
|
|
78
|
-
if (sessionId) {
|
|
79
|
-
console.error(`Received MCP request for session: ${sessionId.slice(0, 8)}...`);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
console.error("New MCP request (no session ID)");
|
|
83
|
-
}
|
|
84
|
-
try {
|
|
85
|
-
let transport;
|
|
86
|
-
if (sessionId && transports[sessionId]) {
|
|
87
|
-
// 기존 세션 재사용 + 접근 시각 갱신
|
|
88
|
-
;
|
|
89
|
-
transports[sessionId].lastAccess = Date.now();
|
|
90
|
-
transport = transports[sessionId].transport;
|
|
91
|
-
// API 키 업데이트 (헤더에서 제공된 경우)
|
|
92
|
-
if (apiKeyFromHeader) {
|
|
93
|
-
setSessionApiKey(sessionId, apiKeyFromHeader);
|
|
94
|
-
}
|
|
95
|
-
// AsyncLocalStorage로 세션 ID 격리 (동시 요청 안전)
|
|
96
|
-
await sessionStore.run(sessionId, async () => {
|
|
97
|
-
await transport.handleRequest(req, res, req.body);
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
else if (!sessionId && isInitializeRequest(req.body)) {
|
|
102
|
-
// 새 세션 초기화
|
|
103
|
-
// 세션 수 제한 — transport 생성 전에 체크하여 리소스 누수 방지
|
|
104
|
-
if (Object.keys(transports).length >= MAX_SESSIONS) {
|
|
105
|
-
res.status(503).json({
|
|
106
|
-
jsonrpc: "2.0",
|
|
107
|
-
error: { code: -32000, message: `Max sessions (${MAX_SESSIONS}) reached. Try again later.` },
|
|
108
|
-
id: null,
|
|
109
|
-
});
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
const eventStore = new InMemoryEventStore();
|
|
113
|
-
transport = new StreamableHTTPServerTransport({
|
|
114
|
-
sessionIdGenerator: () => randomUUID(),
|
|
115
|
-
eventStore,
|
|
116
|
-
onsessioninitialized: (newSessionId) => {
|
|
117
|
-
transports[newSessionId] = { transport, lastAccess: Date.now() };
|
|
118
|
-
if (apiKeyFromHeader) {
|
|
119
|
-
setSessionApiKey(newSessionId, apiKeyFromHeader);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
// 세션 종료 시 정리
|
|
124
|
-
transport.onclose = () => {
|
|
125
|
-
const sid = transport.sessionId;
|
|
126
|
-
if (sid && transports[sid]) {
|
|
127
|
-
console.error(`Transport closed for session ${sid.slice(0, 8)}...`);
|
|
128
|
-
delete transports[sid];
|
|
129
|
-
deleteSession(sid);
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
// 서버 연결
|
|
133
|
-
await server.connect(transport);
|
|
134
|
-
await transport.handleRequest(req, res, req.body);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
// 잘못된 요청
|
|
139
|
-
res.status(400).json({
|
|
140
|
-
jsonrpc: "2.0",
|
|
141
|
-
error: {
|
|
142
|
-
code: -32000,
|
|
143
|
-
message: "Invalid request: missing session ID or not an initialization request"
|
|
144
|
-
},
|
|
145
|
-
id: null
|
|
146
|
-
});
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
catch (error) {
|
|
151
|
-
console.error("Error handling MCP POST request:", error);
|
|
152
|
-
if (!res.headersSent) {
|
|
153
|
-
res.status(500).json({
|
|
154
|
-
jsonrpc: "2.0",
|
|
155
|
-
error: {
|
|
156
|
-
code: -32603,
|
|
157
|
-
message: "Internal server error"
|
|
158
|
-
},
|
|
159
|
-
id: null
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
// MCP GET 엔드포인트 (SSE 스트림)
|
|
165
|
-
app.get("/mcp", async (req, res) => {
|
|
166
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
167
|
-
if (!sessionId || !transports[sessionId]) {
|
|
168
|
-
res.status(400).send("Invalid or missing session ID");
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
const lastEventId = req.headers["last-event-id"];
|
|
172
|
-
if (lastEventId) {
|
|
173
|
-
console.error(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
console.error(`Establishing SSE stream for session ${sessionId.slice(0, 8)}...`);
|
|
177
|
-
}
|
|
178
|
-
try {
|
|
179
|
-
const transport = transports[sessionId].transport;
|
|
180
|
-
await transport.handleRequest(req, res);
|
|
181
|
-
}
|
|
182
|
-
catch (error) {
|
|
183
|
-
console.error("[GET /mcp] Error:", error);
|
|
184
|
-
if (!res.headersSent) {
|
|
185
|
-
res.status(500).send("Internal server error");
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
// MCP DELETE 엔드포인트 (세션 종료)
|
|
190
|
-
app.delete("/mcp", async (req, res) => {
|
|
191
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
192
|
-
if (!sessionId || !transports[sessionId]) {
|
|
193
|
-
res.status(400).send("Invalid or missing session ID");
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
console.error(`Session termination request for ${sessionId.slice(0, 8)}...`);
|
|
197
|
-
try {
|
|
198
|
-
const transport = transports[sessionId].transport;
|
|
199
|
-
await transport.handleRequest(req, res);
|
|
200
|
-
delete transports[sessionId];
|
|
201
|
-
deleteSession(sessionId);
|
|
202
|
-
console.error(`Session removed: ${sessionId.slice(0, 8)}...`);
|
|
203
|
-
}
|
|
204
|
-
catch (error) {
|
|
205
|
-
console.error("Error handling session termination:", error);
|
|
206
|
-
if (!res.headersSent) {
|
|
207
|
-
res.status(500).send("Error processing session termination");
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
// 서버 시작
|
|
212
|
-
app.listen(port, "0.0.0.0", () => {
|
|
213
|
-
console.error(`✓ Korean Law MCP server (Streamable HTTP) listening on port ${port}`);
|
|
214
|
-
console.error(`✓ MCP endpoint: http://0.0.0.0:${port}/mcp`);
|
|
215
|
-
console.error(`✓ Health check: http://0.0.0.0:${port}/health`);
|
|
216
|
-
});
|
|
217
|
-
// 종료 처리
|
|
218
|
-
process.on("SIGINT", async () => {
|
|
219
|
-
console.error("Shutting down server...");
|
|
220
|
-
for (const sessionId in transports) {
|
|
221
|
-
try {
|
|
222
|
-
await transports[sessionId].transport.close();
|
|
223
|
-
delete transports[sessionId];
|
|
224
|
-
deleteSession(sessionId);
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
console.error(`Error closing transport ${sessionId}:`, error);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
process.exit(0);
|
|
231
|
-
});
|
|
232
|
-
}
|