law-mcp-server 0.1.6 → 0.2.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/bin/law-mcp-server.js +0 -0
- package/dist/src/cache.js +17 -0
- package/dist/src/config.js +6 -1
- package/dist/src/index.js +18 -10
- package/dist/src/lawApi.js +43 -23
- package/dist/src/tools.js +21 -30
- package/package.json +2 -3
package/bin/law-mcp-server.js
CHANGED
|
File without changes
|
package/dist/src/cache.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const MAX_ENTRIES = 500;
|
|
1
2
|
export class MemoryCache {
|
|
2
3
|
store = new Map();
|
|
3
4
|
get(key) {
|
|
@@ -11,8 +12,24 @@ export class MemoryCache {
|
|
|
11
12
|
return entry.value;
|
|
12
13
|
}
|
|
13
14
|
set(key, value, ttlSeconds) {
|
|
15
|
+
if (this.store.size >= MAX_ENTRIES) {
|
|
16
|
+
this.evictExpired();
|
|
17
|
+
}
|
|
18
|
+
if (this.store.size >= MAX_ENTRIES) {
|
|
19
|
+
const oldest = this.store.keys().next().value;
|
|
20
|
+
if (oldest !== undefined)
|
|
21
|
+
this.store.delete(oldest);
|
|
22
|
+
}
|
|
14
23
|
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
15
24
|
this.store.set(key, { value, expiresAt });
|
|
16
25
|
}
|
|
26
|
+
evictExpired() {
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
for (const [key, entry] of this.store) {
|
|
29
|
+
if (now > entry.expiresAt) {
|
|
30
|
+
this.store.delete(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
17
34
|
}
|
|
18
35
|
export const cache = new MemoryCache();
|
package/dist/src/config.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const pkg = require("../../package.json");
|
|
4
|
+
export const name = pkg.name;
|
|
5
|
+
export const version = pkg.version;
|
|
1
6
|
const toNumber = (value, fallback) => {
|
|
2
7
|
const parsed = Number(value);
|
|
3
8
|
return Number.isFinite(parsed) ? parsed : fallback;
|
|
@@ -6,5 +11,5 @@ export const config = {
|
|
|
6
11
|
apiBase: process.env.LAW_API_BASE?.trim() || "https://laws.e-gov.go.jp/api/2/",
|
|
7
12
|
httpTimeoutMs: toNumber(process.env.HTTP_TIMEOUT_MS, 15000),
|
|
8
13
|
cacheTtlSeconds: toNumber(process.env.CACHE_TTL_SECONDS, 900),
|
|
9
|
-
userAgent:
|
|
14
|
+
userAgent: `${pkg.name}/${pkg.version}`,
|
|
10
15
|
};
|
package/dist/src/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { StdioJsonRpcServer } from "./mcp.js";
|
|
3
|
-
import { tools, resolveTool } from "./tools.js";
|
|
3
|
+
import { tools, resolveTool, usageInstructions } from "./tools.js";
|
|
4
|
+
import { name, version } from "./config.js";
|
|
4
5
|
const server = new StdioJsonRpcServer();
|
|
5
|
-
const serverInfo = { name
|
|
6
|
+
const serverInfo = { name, version };
|
|
6
7
|
server.register("initialize", async (params) => {
|
|
7
8
|
const payload = (params ?? {});
|
|
8
9
|
const protocolVersion = typeof payload.protocolVersion === "string"
|
|
@@ -11,21 +12,19 @@ server.register("initialize", async (params) => {
|
|
|
11
12
|
return {
|
|
12
13
|
protocolVersion,
|
|
13
14
|
serverInfo,
|
|
15
|
+
instructions: usageInstructions,
|
|
14
16
|
capabilities: {
|
|
15
|
-
tools: {
|
|
16
|
-
list: true,
|
|
17
|
-
call: true,
|
|
18
|
-
},
|
|
17
|
+
tools: {},
|
|
19
18
|
},
|
|
20
19
|
};
|
|
21
20
|
});
|
|
22
|
-
server.register("
|
|
21
|
+
server.register("notifications/initialized", async () => { });
|
|
22
|
+
server.register("ping", async () => ({}));
|
|
23
23
|
server.register("tools/list", async () => ({
|
|
24
24
|
tools: tools.map((tool) => ({
|
|
25
25
|
name: tool.name,
|
|
26
26
|
description: tool.description,
|
|
27
27
|
inputSchema: tool.inputSchema,
|
|
28
|
-
outputSchema: tool.outputSchema,
|
|
29
28
|
})),
|
|
30
29
|
}));
|
|
31
30
|
server.register("tools/call", async (params) => {
|
|
@@ -40,7 +39,16 @@ server.register("tools/call", async (params) => {
|
|
|
40
39
|
const args = payload.arguments && typeof payload.arguments === "object"
|
|
41
40
|
? payload.arguments
|
|
42
41
|
: {};
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
try {
|
|
43
|
+
const result = await tool.handler(args);
|
|
44
|
+
return { content: result };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
48
|
+
return {
|
|
49
|
+
content: [{ type: "text", text: message }],
|
|
50
|
+
isError: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
45
53
|
});
|
|
46
54
|
server.start();
|
package/dist/src/lawApi.js
CHANGED
|
@@ -1,29 +1,37 @@
|
|
|
1
1
|
import { fetch } from "undici";
|
|
2
2
|
import { cache } from "./cache.js";
|
|
3
3
|
import { config } from "./config.js";
|
|
4
|
-
|
|
4
|
+
class HttpError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
body;
|
|
7
|
+
constructor(status, body) {
|
|
8
|
+
super(`Request failed ${status}: ${body}`);
|
|
9
|
+
this.status = status;
|
|
10
|
+
this.body = body;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const request = async (url, context) => {
|
|
5
14
|
const controller = new AbortController();
|
|
6
|
-
const timer = setTimeout(() => controller.abort(),
|
|
15
|
+
const timer = setTimeout(() => controller.abort(), config.httpTimeoutMs);
|
|
16
|
+
let res;
|
|
7
17
|
try {
|
|
8
|
-
|
|
18
|
+
res = await fetch(url, {
|
|
19
|
+
headers: {
|
|
20
|
+
"User-Agent": config.userAgent,
|
|
21
|
+
Accept: "application/json",
|
|
22
|
+
},
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
});
|
|
9
25
|
}
|
|
10
26
|
finally {
|
|
11
27
|
clearTimeout(timer);
|
|
12
28
|
}
|
|
13
|
-
};
|
|
14
|
-
const request = async (url, context) => {
|
|
15
|
-
const res = await withTimeout(fetch(url, {
|
|
16
|
-
headers: {
|
|
17
|
-
"User-Agent": config.userAgent,
|
|
18
|
-
Accept: "application/json",
|
|
19
|
-
},
|
|
20
|
-
}), config.httpTimeoutMs);
|
|
21
29
|
if (!res.ok) {
|
|
22
30
|
const body = await res.text();
|
|
23
31
|
if (res.status === 404 && context?.lawId) {
|
|
24
32
|
throw new Error(`Law not found for lawId "${context.lawId}". Use the official LawID (e.g., 平成十五年法律第五十七号 => H15HO57). Upstream: ${body}`);
|
|
25
33
|
}
|
|
26
|
-
throw new
|
|
34
|
+
throw new HttpError(res.status, body);
|
|
27
35
|
}
|
|
28
36
|
const data = (await res.json());
|
|
29
37
|
return data;
|
|
@@ -44,25 +52,37 @@ export const fetchLawData = async (lawId, revisionDate) => {
|
|
|
44
52
|
return data;
|
|
45
53
|
};
|
|
46
54
|
export const searchLaws = async (keyword) => {
|
|
55
|
+
const cacheKey = `search:${keyword}`;
|
|
56
|
+
const cached = cache.get(cacheKey);
|
|
57
|
+
if (cached)
|
|
58
|
+
return cached;
|
|
47
59
|
const base = config.apiBase.endsWith("/")
|
|
48
60
|
? config.apiBase
|
|
49
61
|
: `${config.apiBase}/`;
|
|
50
62
|
const queryUrl = new URL(`lawsearch`, base);
|
|
51
63
|
queryUrl.searchParams.set("keyword", keyword);
|
|
64
|
+
const attempts = [];
|
|
52
65
|
try {
|
|
53
|
-
|
|
66
|
+
const data = await request(queryUrl.toString());
|
|
67
|
+
cache.set(cacheKey, data, config.cacheTtlSeconds);
|
|
68
|
+
return data;
|
|
54
69
|
}
|
|
55
70
|
catch (error) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
if (error instanceof HttpError) {
|
|
72
|
+
attempts.push(`query style (${queryUrl.toString()}): ${error.status} ${error.body}`);
|
|
73
|
+
if (error.status === 404) {
|
|
74
|
+
const pathUrl = new URL(`lawsearch/${encodeURIComponent(keyword)}`, base);
|
|
75
|
+
try {
|
|
76
|
+
const data = await request(pathUrl.toString());
|
|
77
|
+
cache.set(cacheKey, data, config.cacheTtlSeconds);
|
|
78
|
+
return data;
|
|
79
|
+
}
|
|
80
|
+
catch (err) {
|
|
81
|
+
if (err instanceof HttpError) {
|
|
82
|
+
attempts.push(`path style (${pathUrl.toString()}): ${err.status} ${err.body}`);
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Law search failed for keyword "${keyword}". Attempts: ${attempts.join(" | ")}. Ensure LAW_API_BASE is reachable and keyword is valid.`);
|
|
85
|
+
}
|
|
66
86
|
}
|
|
67
87
|
}
|
|
68
88
|
throw error;
|
package/dist/src/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fetchLawData, searchLaws } from "./lawApi.js";
|
|
2
2
|
import { checkConsistency } from "./consistency.js";
|
|
3
|
+
export const usageInstructions = `Usage guidelines:\n\n- To find laws by Japanese name/keyword, call search_laws first. It returns canonical e-Gov LawID values (e.g., 個人情報保護法 -> H15HO57).\n- Always pass the canonical LawID to fetch_law and check_consistency (lawIds). Do not pass the Japanese title string.\n- fetch_law accepts optional revisionDate when you need a specific revision.\n- check_consistency requires at least one LawID in lawIds. Use search_laws to discover the IDs before calling.\n- summarize_law can take an optional articles array of article numbers to limit the summary.`;
|
|
3
4
|
const requireString = (value, field) => {
|
|
4
5
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
5
6
|
throw new Error(`Field ${field} is required`);
|
|
@@ -14,7 +15,7 @@ const requireArrayOfStrings = (value, field) => {
|
|
|
14
15
|
};
|
|
15
16
|
const fetchLaw = {
|
|
16
17
|
name: "fetch_law",
|
|
17
|
-
description: "Fetch a law by LawID and optional revision date",
|
|
18
|
+
description: "Fetch a law by canonical e-Gov LawID (e.g., H15HO57) and optional revision date. Use search_laws to look up the LawID first.",
|
|
18
19
|
inputSchema: {
|
|
19
20
|
type: "object",
|
|
20
21
|
properties: {
|
|
@@ -27,12 +28,12 @@ const fetchLaw = {
|
|
|
27
28
|
const lawId = requireString(input.lawId, "lawId");
|
|
28
29
|
const revisionDate = typeof input.revisionDate === "string" ? input.revisionDate : undefined;
|
|
29
30
|
const data = await fetchLawData(lawId, revisionDate);
|
|
30
|
-
return [{ type: "
|
|
31
|
+
return [{ type: "text", text: JSON.stringify(data, null, 2) }];
|
|
31
32
|
},
|
|
32
33
|
};
|
|
33
34
|
const search = {
|
|
34
35
|
name: "search_laws",
|
|
35
|
-
description: "Search laws by keyword",
|
|
36
|
+
description: "Search laws by Japanese keyword/name and return canonical LawID values for use with other tools.",
|
|
36
37
|
inputSchema: {
|
|
37
38
|
type: "object",
|
|
38
39
|
properties: {
|
|
@@ -43,7 +44,7 @@ const search = {
|
|
|
43
44
|
handler: async (input) => {
|
|
44
45
|
const keyword = requireString(input.keyword, "keyword");
|
|
45
46
|
const results = await searchLaws(keyword);
|
|
46
|
-
return [{ type: "
|
|
47
|
+
return [{ type: "text", text: JSON.stringify(results, null, 2) }];
|
|
47
48
|
},
|
|
48
49
|
};
|
|
49
50
|
const summarize = {
|
|
@@ -72,42 +73,33 @@ const summarize = {
|
|
|
72
73
|
? articles.filter((article) => article.ArticleNumber &&
|
|
73
74
|
articlesFilter.includes(article.ArticleNumber))
|
|
74
75
|
: articles;
|
|
76
|
+
const toArray = (v) => !v ? [] : Array.isArray(v) ? v : [v];
|
|
75
77
|
const body = filtered
|
|
76
|
-
.map((article) => `${article.ArticleNumber || ""} ${article.ArticleTitle || ""}`.trim())
|
|
77
78
|
.slice(0, 10)
|
|
78
|
-
.
|
|
79
|
+
.map((article) => {
|
|
80
|
+
const heading = `${article.ArticleNumber || ""} ${article.ArticleTitle || ""}`.trim();
|
|
81
|
+
const paragraphs = toArray(article.Paragraph)
|
|
82
|
+
.map((p) => {
|
|
83
|
+
const sentences = toArray(p
|
|
84
|
+
.ParagraphSentence);
|
|
85
|
+
return sentences.join("");
|
|
86
|
+
})
|
|
87
|
+
.filter(Boolean)
|
|
88
|
+
.join("\n");
|
|
89
|
+
return [heading, paragraphs].filter(Boolean).join("\n");
|
|
90
|
+
})
|
|
91
|
+
.join("\n\n");
|
|
79
92
|
return [{ type: "text", text: body || "No articles available" }];
|
|
80
93
|
},
|
|
81
94
|
};
|
|
82
|
-
const listRevisions = {
|
|
83
|
-
name: "list_revisions",
|
|
84
|
-
description: "List known revisions for a law if provided by the API",
|
|
85
|
-
inputSchema: {
|
|
86
|
-
type: "object",
|
|
87
|
-
properties: {
|
|
88
|
-
lawId: { type: "string" },
|
|
89
|
-
},
|
|
90
|
-
required: ["lawId"],
|
|
91
|
-
},
|
|
92
|
-
handler: async (input) => {
|
|
93
|
-
const lawId = requireString(input.lawId, "lawId");
|
|
94
|
-
const data = await fetchLawData(lawId);
|
|
95
|
-
const revisions = Array.isArray(data.revisions)
|
|
96
|
-
? data.revisions
|
|
97
|
-
: [];
|
|
98
|
-
return [{ type: "json", data: { lawId, revisions } }];
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
95
|
const check = {
|
|
102
96
|
name: "check_consistency",
|
|
103
|
-
description: "Check a document against one or more laws",
|
|
97
|
+
description: "Check a document against one or more laws (lawIds must be canonical LawIDs from search_laws).",
|
|
104
98
|
inputSchema: {
|
|
105
99
|
type: "object",
|
|
106
100
|
properties: {
|
|
107
101
|
documentText: { type: "string" },
|
|
108
102
|
lawIds: { type: "array", items: { type: "string" } },
|
|
109
|
-
articleHints: { type: "array", items: { type: "string" } },
|
|
110
|
-
strictness: { type: "string", enum: ["low", "medium", "high"] },
|
|
111
103
|
},
|
|
112
104
|
required: ["documentText"],
|
|
113
105
|
},
|
|
@@ -121,13 +113,12 @@ const check = {
|
|
|
121
113
|
}
|
|
122
114
|
const laws = await Promise.all(lawIds.map((lawId) => fetchLawData(lawId)));
|
|
123
115
|
const output = checkConsistency(documentText, laws);
|
|
124
|
-
return [{ type: "
|
|
116
|
+
return [{ type: "text", text: JSON.stringify(output, null, 2) }];
|
|
125
117
|
},
|
|
126
118
|
};
|
|
127
119
|
export const tools = [
|
|
128
120
|
fetchLaw,
|
|
129
121
|
search,
|
|
130
|
-
listRevisions,
|
|
131
122
|
check,
|
|
132
123
|
summarize,
|
|
133
124
|
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "law-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for e-Gov law API consistency checks",
|
|
6
6
|
"files": [
|
|
@@ -16,14 +16,13 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsc",
|
|
18
18
|
"prepare": "npm run build",
|
|
19
|
-
"start": "node dist/index.js",
|
|
19
|
+
"start": "node dist/src/index.js",
|
|
20
20
|
"dev": "ts-node src/index.ts",
|
|
21
21
|
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
22
22
|
"format": "prettier --write .",
|
|
23
23
|
"test": "npm run build && node dist/test/integration.test.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"stripe": "^20.3.1",
|
|
27
26
|
"undici": "^6.13.0"
|
|
28
27
|
},
|
|
29
28
|
"devDependencies": {
|