law-mcp-server 0.1.7 → 0.3.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/README.md +54 -8
- package/dist/src/cache.js +17 -0
- package/dist/src/config.js +6 -1
- package/dist/src/index.js +16 -9
- package/dist/src/lawApi.js +32 -24
- package/dist/src/tools.js +18 -34
- package/package.json +2 -3
package/README.md
CHANGED
|
@@ -16,13 +16,12 @@ This repository will host an MCP server that uses **法令API Version 2** (e-Gov
|
|
|
16
16
|
- `GET /lawsearch/{keyword}` – search laws by keyword.
|
|
17
17
|
- Response format: JSON (includes meta, LawName, Articles, etc.). Respect official rate limits; treat 429/503 as retryable with backoff.
|
|
18
18
|
|
|
19
|
-
## MCP Capabilities
|
|
19
|
+
## MCP Capabilities
|
|
20
20
|
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
24
|
-
- `
|
|
25
|
-
- `summarize_law` – Input: `lawId`, optional `articles` list. Output: concise bullet summary suitable for grounding model responses.
|
|
21
|
+
- `search_laws` – Input: `keyword` (string). Output: list of LawID, title, and promulgation date.
|
|
22
|
+
- `fetch_law` – Input: `lawId` (string), optional `revisionDate`. Output: normalized law JSON.
|
|
23
|
+
- `check_consistency` – Input: `documentText`, `lawIds` (required). Output: matched citations, conflicting passages, and similarity scores.
|
|
24
|
+
- `summarize_law` – Input: `lawId`, optional `articles` list. Output: concise article summary with paragraph text.
|
|
26
25
|
|
|
27
26
|
## Consistency Check Workflow
|
|
28
27
|
|
|
@@ -62,7 +61,7 @@ This repository will host an MCP server that uses **法令API Version 2** (e-Gov
|
|
|
62
61
|
- Build: `npm run build`.
|
|
63
62
|
- Copy `.env.example` to `.env` and adjust if needed.
|
|
64
63
|
- Run server over stdio (JSON-RPC): `npm start` (or `npm run dev` for ts-node).
|
|
65
|
-
- Configure via environment variables in `.env` (see Configuration section). The server registers tools `
|
|
64
|
+
- Configure via environment variables in `.env` (see Configuration section). The server registers tools `search_laws`, `fetch_law`, `check_consistency`, and `summarize_law`.
|
|
66
65
|
- Quality: `npm run lint` (ESLint) / `npm run format` (Prettier).
|
|
67
66
|
|
|
68
67
|
### Claude Desktop configuration
|
|
@@ -87,7 +86,54 @@ package, run `npm install && npm run build` and then `npm link` so the
|
|
|
87
86
|
## Usage Examples (conceptual)
|
|
88
87
|
|
|
89
88
|
- Search and fetch: “Search for 個人情報保護 and show the latest articles.” → calls `search_laws` then `fetch_law`.
|
|
90
|
-
- Consistency check:
|
|
89
|
+
- Consistency check: "Check this draft against 労働基準法 Articles 24 and 37; highlight mismatches." → calls `search_laws` to get LawID, then `check_consistency` with `lawIds=[...]`.
|
|
90
|
+
|
|
91
|
+
## Skills
|
|
92
|
+
|
|
93
|
+
This repository includes domain-specific skills that demonstrate effective usage patterns for law-mcp-server tools. Skills provide comprehensive guides on how to leverage the server's capabilities for specific use cases.
|
|
94
|
+
|
|
95
|
+
### Available Skills
|
|
96
|
+
|
|
97
|
+
#### Digital Marketing Law Skill (`skills/digital-marketing-law/`)
|
|
98
|
+
|
|
99
|
+
A comprehensive guide for using law-mcp-server to reference and verify compliance with laws related to digital marketing activities in Japan. This skill covers:
|
|
100
|
+
|
|
101
|
+
- **Display Regulations**: Misleading Representation Prevention Act (景品表示法), Specified Commercial Transactions Act (特商法), Consumer Contract Act
|
|
102
|
+
- **Personal Information & Tracking**: Personal Information Protection Act (個人情報保護法), Telecommunications Business Act (電気通信事業法), Specified Electronic Mail Act
|
|
103
|
+
- **Platform Regulations**: Digital Platform Transparency Act, Provider Liability Act
|
|
104
|
+
- **Industry-Specific Laws**: Pharmaceutical Affairs Act (薬機法), Financial Instruments and Exchange Act (金商法)
|
|
105
|
+
- **Intellectual Property**: Copyright Act, Trademark Act, Unfair Competition Prevention Act
|
|
106
|
+
- **Competition Law**: Antimonopoly Act (独占禁止法)
|
|
107
|
+
|
|
108
|
+
**Key Features**:
|
|
109
|
+
|
|
110
|
+
- Search patterns for formal names, abbreviations, and article numbers
|
|
111
|
+
- 5 practical workflows (privacy policy creation, ad review, email marketing, platform transactions, amendment tracking)
|
|
112
|
+
- Real-world use cases for JIAA/APTI activities, client proposals, and compliance checks
|
|
113
|
+
- Common Q&A (Cookie consent, influencer marketing, comparative advertising, AI-generated content, retargeting)
|
|
114
|
+
|
|
115
|
+
**Usage**:
|
|
116
|
+
|
|
117
|
+
1. Read the skill file: `skills/digital-marketing-law/digital-marketing-law-SKILL.md`
|
|
118
|
+
2. Reference the appropriate workflow for your task
|
|
119
|
+
3. Use the provided search keywords and tool sequences
|
|
120
|
+
4. Follow the best practices for law searches and consistency checks
|
|
121
|
+
|
|
122
|
+
### Using Skills with Claude
|
|
123
|
+
|
|
124
|
+
To enable Claude to use these skills effectively:
|
|
125
|
+
|
|
126
|
+
1. **With Claude Desktop**: Skills in this repository are automatically available when the law-mcp-server is configured
|
|
127
|
+
2. **With Claude API**: Include the skill content in your system prompts or as reference documentation
|
|
128
|
+
3. **Custom Integration**: Point Claude to the skills directory in your MCP server configuration
|
|
129
|
+
|
|
130
|
+
Skills enhance Claude's ability to:
|
|
131
|
+
|
|
132
|
+
- Choose the right tools for specific legal queries
|
|
133
|
+
- Use appropriate search keywords (formal names vs. abbreviations)
|
|
134
|
+
- Apply domain knowledge for effective law searches
|
|
135
|
+
- Structure multi-step legal compliance checks
|
|
136
|
+
- Provide context-aware recommendations
|
|
91
137
|
|
|
92
138
|
## Validation Plan (to implement)
|
|
93
139
|
|
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
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"
|
|
@@ -13,20 +14,17 @@ server.register("initialize", async (params) => {
|
|
|
13
14
|
serverInfo,
|
|
14
15
|
instructions: usageInstructions,
|
|
15
16
|
capabilities: {
|
|
16
|
-
tools: {
|
|
17
|
-
list: true,
|
|
18
|
-
call: true,
|
|
19
|
-
},
|
|
17
|
+
tools: {},
|
|
20
18
|
},
|
|
21
19
|
};
|
|
22
20
|
});
|
|
23
|
-
server.register("
|
|
21
|
+
server.register("notifications/initialized", async () => { });
|
|
22
|
+
server.register("ping", async () => ({}));
|
|
24
23
|
server.register("tools/list", async () => ({
|
|
25
24
|
tools: tools.map((tool) => ({
|
|
26
25
|
name: tool.name,
|
|
27
26
|
description: tool.description,
|
|
28
27
|
inputSchema: tool.inputSchema,
|
|
29
|
-
outputSchema: tool.outputSchema,
|
|
30
28
|
})),
|
|
31
29
|
}));
|
|
32
30
|
server.register("tools/call", async (params) => {
|
|
@@ -41,7 +39,16 @@ server.register("tools/call", async (params) => {
|
|
|
41
39
|
const args = payload.arguments && typeof payload.arguments === "object"
|
|
42
40
|
? payload.arguments
|
|
43
41
|
: {};
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
}
|
|
46
53
|
});
|
|
47
54
|
server.start();
|
package/dist/src/lawApi.js
CHANGED
|
@@ -10,23 +10,22 @@ class HttpError extends Error {
|
|
|
10
10
|
this.body = body;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
-
const
|
|
13
|
+
const request = async (url, context) => {
|
|
14
14
|
const controller = new AbortController();
|
|
15
|
-
const timer = setTimeout(() => controller.abort(),
|
|
15
|
+
const timer = setTimeout(() => controller.abort(), config.httpTimeoutMs);
|
|
16
|
+
let res;
|
|
16
17
|
try {
|
|
17
|
-
|
|
18
|
+
res = await fetch(url, {
|
|
19
|
+
headers: {
|
|
20
|
+
"User-Agent": config.userAgent,
|
|
21
|
+
Accept: "application/json",
|
|
22
|
+
},
|
|
23
|
+
signal: controller.signal,
|
|
24
|
+
});
|
|
18
25
|
}
|
|
19
26
|
finally {
|
|
20
27
|
clearTimeout(timer);
|
|
21
28
|
}
|
|
22
|
-
};
|
|
23
|
-
const request = async (url, context) => {
|
|
24
|
-
const res = await withTimeout(fetch(url, {
|
|
25
|
-
headers: {
|
|
26
|
-
"User-Agent": config.userAgent,
|
|
27
|
-
Accept: "application/json",
|
|
28
|
-
},
|
|
29
|
-
}), config.httpTimeoutMs);
|
|
30
29
|
if (!res.ok) {
|
|
31
30
|
const body = await res.text();
|
|
32
31
|
if (res.status === 404 && context?.lawId) {
|
|
@@ -53,28 +52,37 @@ export const fetchLawData = async (lawId, revisionDate) => {
|
|
|
53
52
|
return data;
|
|
54
53
|
};
|
|
55
54
|
export const searchLaws = async (keyword) => {
|
|
55
|
+
const cacheKey = `search:${keyword}`;
|
|
56
|
+
const cached = cache.get(cacheKey);
|
|
57
|
+
if (cached)
|
|
58
|
+
return cached;
|
|
56
59
|
const base = config.apiBase.endsWith("/")
|
|
57
60
|
? config.apiBase
|
|
58
61
|
: `${config.apiBase}/`;
|
|
59
62
|
const queryUrl = new URL(`lawsearch`, base);
|
|
60
63
|
queryUrl.searchParams.set("keyword", keyword);
|
|
64
|
+
const attempts = [];
|
|
61
65
|
try {
|
|
62
|
-
|
|
66
|
+
const data = await request(queryUrl.toString());
|
|
67
|
+
cache.set(cacheKey, data, config.cacheTtlSeconds);
|
|
68
|
+
return data;
|
|
63
69
|
}
|
|
64
70
|
catch (error) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.`);
|
|
76
85
|
}
|
|
77
|
-
throw err;
|
|
78
86
|
}
|
|
79
87
|
}
|
|
80
88
|
throw error;
|
package/dist/src/tools.js
CHANGED
|
@@ -1,6 +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
|
|
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.`;
|
|
4
4
|
const requireString = (value, field) => {
|
|
5
5
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
6
6
|
throw new Error(`Field ${field} is required`);
|
|
@@ -28,7 +28,7 @@ const fetchLaw = {
|
|
|
28
28
|
const lawId = requireString(input.lawId, "lawId");
|
|
29
29
|
const revisionDate = typeof input.revisionDate === "string" ? input.revisionDate : undefined;
|
|
30
30
|
const data = await fetchLawData(lawId, revisionDate);
|
|
31
|
-
return [{ type: "
|
|
31
|
+
return [{ type: "text", text: JSON.stringify(data, null, 2) }];
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
34
|
const search = {
|
|
@@ -44,7 +44,7 @@ const search = {
|
|
|
44
44
|
handler: async (input) => {
|
|
45
45
|
const keyword = requireString(input.keyword, "keyword");
|
|
46
46
|
const results = await searchLaws(keyword);
|
|
47
|
-
return [{ type: "
|
|
47
|
+
return [{ type: "text", text: JSON.stringify(results, null, 2) }];
|
|
48
48
|
},
|
|
49
49
|
};
|
|
50
50
|
const summarize = {
|
|
@@ -73,32 +73,24 @@ const summarize = {
|
|
|
73
73
|
? articles.filter((article) => article.ArticleNumber &&
|
|
74
74
|
articlesFilter.includes(article.ArticleNumber))
|
|
75
75
|
: articles;
|
|
76
|
+
const toArray = (v) => !v ? [] : Array.isArray(v) ? v : [v];
|
|
76
77
|
const body = filtered
|
|
77
|
-
.map((article) => `${article.ArticleNumber || ""} ${article.ArticleTitle || ""}`.trim())
|
|
78
78
|
.slice(0, 10)
|
|
79
|
-
.
|
|
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.ParagraphSentence);
|
|
84
|
+
return sentences.join("");
|
|
85
|
+
})
|
|
86
|
+
.filter(Boolean)
|
|
87
|
+
.join("\n");
|
|
88
|
+
return [heading, paragraphs].filter(Boolean).join("\n");
|
|
89
|
+
})
|
|
90
|
+
.join("\n\n");
|
|
80
91
|
return [{ type: "text", text: body || "No articles available" }];
|
|
81
92
|
},
|
|
82
93
|
};
|
|
83
|
-
const listRevisions = {
|
|
84
|
-
name: "list_revisions",
|
|
85
|
-
description: "List known revisions for a law (requires canonical LawID from search_laws).",
|
|
86
|
-
inputSchema: {
|
|
87
|
-
type: "object",
|
|
88
|
-
properties: {
|
|
89
|
-
lawId: { type: "string" },
|
|
90
|
-
},
|
|
91
|
-
required: ["lawId"],
|
|
92
|
-
},
|
|
93
|
-
handler: async (input) => {
|
|
94
|
-
const lawId = requireString(input.lawId, "lawId");
|
|
95
|
-
const data = await fetchLawData(lawId);
|
|
96
|
-
const revisions = Array.isArray(data.revisions)
|
|
97
|
-
? data.revisions
|
|
98
|
-
: [];
|
|
99
|
-
return [{ type: "json", data: { lawId, revisions } }];
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
94
|
const check = {
|
|
103
95
|
name: "check_consistency",
|
|
104
96
|
description: "Check a document against one or more laws (lawIds must be canonical LawIDs from search_laws).",
|
|
@@ -107,8 +99,6 @@ const check = {
|
|
|
107
99
|
properties: {
|
|
108
100
|
documentText: { type: "string" },
|
|
109
101
|
lawIds: { type: "array", items: { type: "string" } },
|
|
110
|
-
articleHints: { type: "array", items: { type: "string" } },
|
|
111
|
-
strictness: { type: "string", enum: ["low", "medium", "high"] },
|
|
112
102
|
},
|
|
113
103
|
required: ["documentText"],
|
|
114
104
|
},
|
|
@@ -122,14 +112,8 @@ const check = {
|
|
|
122
112
|
}
|
|
123
113
|
const laws = await Promise.all(lawIds.map((lawId) => fetchLawData(lawId)));
|
|
124
114
|
const output = checkConsistency(documentText, laws);
|
|
125
|
-
return [{ type: "
|
|
115
|
+
return [{ type: "text", text: JSON.stringify(output, null, 2) }];
|
|
126
116
|
},
|
|
127
117
|
};
|
|
128
|
-
export const tools = [
|
|
129
|
-
fetchLaw,
|
|
130
|
-
search,
|
|
131
|
-
listRevisions,
|
|
132
|
-
check,
|
|
133
|
-
summarize,
|
|
134
|
-
];
|
|
118
|
+
export const tools = [fetchLaw, search, check, summarize];
|
|
135
119
|
export const resolveTool = (name) => tools.find((tool) => tool.name === name);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "law-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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": {
|