law-mcp-server 0.1.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/src/mcp.ts ADDED
@@ -0,0 +1,87 @@
1
+ import readline from "node:readline";
2
+
3
+ type JsonRpcId = string | number | null;
4
+
5
+ type JsonRpcRequest = {
6
+ jsonrpc: "2.0";
7
+ method: string;
8
+ id?: JsonRpcId;
9
+ params?: unknown;
10
+ };
11
+
12
+ type JsonRpcError = {
13
+ code: number;
14
+ message: string;
15
+ };
16
+
17
+ type RequestHandler = (params: unknown) => Promise<unknown> | unknown;
18
+
19
+ export class StdioJsonRpcServer {
20
+ private handlers = new Map<string, RequestHandler>();
21
+
22
+ register(method: string, handler: RequestHandler) {
23
+ this.handlers.set(method, handler);
24
+ }
25
+
26
+ start() {
27
+ const rl = readline.createInterface({ input: process.stdin });
28
+ rl.on("line", async (line) => {
29
+ const message = this.parseMessage(line);
30
+ if (!message) return;
31
+
32
+ const { id, method, params } = message;
33
+ const handler = this.handlers.get(method);
34
+ if (!handler) {
35
+ if (id !== undefined) {
36
+ this.send({
37
+ jsonrpc: "2.0",
38
+ id,
39
+ error: { code: -32601, message: "Method not found" },
40
+ });
41
+ }
42
+ return;
43
+ }
44
+ try {
45
+ const result = await handler(params ?? {});
46
+ if (id !== undefined) {
47
+ this.send({ jsonrpc: "2.0", id, result });
48
+ }
49
+ } catch (error) {
50
+ if (id !== undefined) {
51
+ const messageText =
52
+ error instanceof Error ? error.message : String(error);
53
+ this.send({
54
+ jsonrpc: "2.0",
55
+ id,
56
+ error: { code: -32000, message: messageText },
57
+ });
58
+ }
59
+ }
60
+ });
61
+ }
62
+
63
+ private parseMessage(line: string): JsonRpcRequest | null {
64
+ try {
65
+ const parsed = JSON.parse(line) as JsonRpcRequest;
66
+ if (
67
+ !parsed ||
68
+ parsed.jsonrpc !== "2.0" ||
69
+ typeof parsed.method !== "string"
70
+ ) {
71
+ return null;
72
+ }
73
+ return parsed;
74
+ } catch (error) {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ private send(payload: {
80
+ jsonrpc: "2.0";
81
+ id: JsonRpcId;
82
+ result?: unknown;
83
+ error?: JsonRpcError;
84
+ }) {
85
+ process.stdout.write(JSON.stringify(payload) + "\n");
86
+ }
87
+ }
package/src/tools.ts ADDED
@@ -0,0 +1,162 @@
1
+ import { fetchLawData, searchLaws } from "./lawApi.js";
2
+ import { checkConsistency } from "./consistency.js";
3
+ import { ConsistencyOutput } from "./types.js";
4
+
5
+ type ToolContent = { type: string; text?: string; data?: unknown };
6
+ type ToolHandler = (input: Record<string, unknown>) => Promise<ToolContent[]>;
7
+
8
+ type Tool = {
9
+ name: string;
10
+ description: string;
11
+ inputSchema: Record<string, unknown>;
12
+ outputSchema?: Record<string, unknown>;
13
+ handler: ToolHandler;
14
+ };
15
+
16
+ const requireString = (value: unknown, field: string) => {
17
+ if (typeof value !== "string" || value.trim().length === 0) {
18
+ throw new Error(`Field ${field} is required`);
19
+ }
20
+ return value.trim();
21
+ };
22
+
23
+ const requireArrayOfStrings = (value: unknown, field: string) => {
24
+ if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
25
+ throw new Error(`Field ${field} must be an array of strings`);
26
+ }
27
+ return value as string[];
28
+ };
29
+
30
+ const fetchLaw: Tool = {
31
+ name: "fetch_law",
32
+ description: "Fetch a law by LawID and optional revision date",
33
+ inputSchema: {
34
+ type: "object",
35
+ properties: {
36
+ lawId: { type: "string" },
37
+ revisionDate: { type: "string" },
38
+ },
39
+ required: ["lawId"],
40
+ },
41
+ handler: async (input) => {
42
+ const lawId = requireString(input.lawId, "lawId");
43
+ const revisionDate =
44
+ typeof input.revisionDate === "string" ? input.revisionDate : undefined;
45
+ const data = await fetchLawData(lawId, revisionDate);
46
+ return [{ type: "json", data }];
47
+ },
48
+ };
49
+
50
+ const search: Tool = {
51
+ name: "search_laws",
52
+ description: "Search laws by keyword",
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ keyword: { type: "string" },
57
+ },
58
+ required: ["keyword"],
59
+ },
60
+ handler: async (input) => {
61
+ const keyword = requireString(input.keyword, "keyword");
62
+ const results = await searchLaws(keyword);
63
+ return [{ type: "json", data: results }];
64
+ },
65
+ };
66
+
67
+ const summarize: Tool = {
68
+ name: "summarize_law",
69
+ description: "Summarize a law or selected articles",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ lawId: { type: "string" },
74
+ articles: { type: "array", items: { type: "string" } },
75
+ },
76
+ required: ["lawId"],
77
+ },
78
+ handler: async (input) => {
79
+ const lawId = requireString(input.lawId, "lawId");
80
+ const articlesFilter = Array.isArray(input.articles)
81
+ ? requireArrayOfStrings(input.articles, "articles")
82
+ : undefined;
83
+ const data = await fetchLawData(lawId);
84
+ const articles = Array.isArray(data.LawBody?.MainProvision?.Article)
85
+ ? data.LawBody?.MainProvision?.Article
86
+ : data.LawBody?.MainProvision?.Article
87
+ ? [data.LawBody.MainProvision.Article]
88
+ : [];
89
+ const filtered = articlesFilter
90
+ ? articles.filter(
91
+ (article) =>
92
+ article.ArticleNumber &&
93
+ articlesFilter.includes(article.ArticleNumber)
94
+ )
95
+ : articles;
96
+ const body = filtered
97
+ .map((article) =>
98
+ `${article.ArticleNumber || ""} ${article.ArticleTitle || ""}`.trim()
99
+ )
100
+ .slice(0, 10)
101
+ .join("\n");
102
+ return [{ type: "text", text: body || "No articles available" }];
103
+ },
104
+ };
105
+
106
+ const listRevisions: Tool = {
107
+ name: "list_revisions",
108
+ description: "List known revisions for a law if provided by the API",
109
+ inputSchema: {
110
+ type: "object",
111
+ properties: {
112
+ lawId: { type: "string" },
113
+ },
114
+ required: ["lawId"],
115
+ },
116
+ handler: async (input) => {
117
+ const lawId = requireString(input.lawId, "lawId");
118
+ const data = await fetchLawData(lawId);
119
+ const revisions = Array.isArray((data as Record<string, unknown>).revisions)
120
+ ? ((data as Record<string, unknown>).revisions as string[])
121
+ : [];
122
+ return [{ type: "json", data: { lawId, revisions } }];
123
+ },
124
+ };
125
+
126
+ const check: Tool = {
127
+ name: "check_consistency",
128
+ description: "Check a document against one or more laws",
129
+ inputSchema: {
130
+ type: "object",
131
+ properties: {
132
+ documentText: { type: "string" },
133
+ lawIds: { type: "array", items: { type: "string" } },
134
+ articleHints: { type: "array", items: { type: "string" } },
135
+ strictness: { type: "string", enum: ["low", "medium", "high"] },
136
+ },
137
+ required: ["documentText"],
138
+ },
139
+ handler: async (input) => {
140
+ const documentText = requireString(input.documentText, "documentText");
141
+ const lawIds = Array.isArray(input.lawIds)
142
+ ? requireArrayOfStrings(input.lawIds, "lawIds")
143
+ : [];
144
+ if (!lawIds.length) {
145
+ throw new Error("At least one lawId is required for consistency checks");
146
+ }
147
+ const laws = await Promise.all(lawIds.map((lawId) => fetchLawData(lawId)));
148
+ const output: ConsistencyOutput = checkConsistency(documentText, laws);
149
+ return [{ type: "json", data: output }];
150
+ },
151
+ };
152
+
153
+ export const tools: Tool[] = [
154
+ fetchLaw,
155
+ search,
156
+ listRevisions,
157
+ check,
158
+ summarize,
159
+ ];
160
+
161
+ export const resolveTool = (name: string) =>
162
+ tools.find((tool) => tool.name === name);
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ export type LawArticleParagraph = {
2
+ ParagraphTitle?: string;
3
+ ParagraphSentence?: string | string[];
4
+ };
5
+
6
+ export type LawArticle = {
7
+ ArticleNumber?: string;
8
+ ArticleTitle?: string;
9
+ Paragraph?: LawArticleParagraph | LawArticleParagraph[];
10
+ };
11
+
12
+ export type LawBody = {
13
+ MainProvision?: {
14
+ Article?: LawArticle | LawArticle[];
15
+ };
16
+ };
17
+
18
+ export type LawData = {
19
+ LawID: string;
20
+ LawName?: string;
21
+ LawBody?: LawBody;
22
+ ApplProvision?: unknown;
23
+ SupplProvision?: unknown;
24
+ };
25
+
26
+ export type LawSearchItem = {
27
+ LawID: string;
28
+ LawName?: string;
29
+ PromulgationDate?: string;
30
+ };
31
+
32
+ export type LawSearchResponse = {
33
+ numberOfHits?: number;
34
+ referencelaw?: LawSearchItem | LawSearchItem[];
35
+ };
36
+
37
+ export type ToolResult = {
38
+ contentType: string;
39
+ data: unknown;
40
+ };
41
+
42
+ export type ConsistencyFinding = {
43
+ segment: string;
44
+ status: "aligned" | "potential_mismatch" | "not_found";
45
+ lawId?: string;
46
+ articleNumber?: string;
47
+ lawSnippet?: string;
48
+ score?: number;
49
+ };
50
+
51
+ export type ConsistencyOutput = {
52
+ findings: ConsistencyFinding[];
53
+ matchedLawIds: string[];
54
+ };
@@ -0,0 +1,75 @@
1
+ import assert from "node:assert";
2
+ import { MockAgent, setGlobalDispatcher } from "undici";
3
+ import { fetchLawData, searchLaws } from "../src/lawApi.js";
4
+ import { checkConsistency } from "../src/consistency.js";
5
+
6
+ const mockAgent = new MockAgent();
7
+ mockAgent.disableNetConnect();
8
+ const mockPool = mockAgent.get("https://laws.e-gov.go.jp");
9
+
10
+ mockPool
11
+ .intercept({ path: "/api/2/lawdata/TEST-LAW", method: "GET" })
12
+ .reply(200, {
13
+ LawID: "TEST-LAW",
14
+ LawName: "テスト法",
15
+ LawBody: {
16
+ MainProvision: {
17
+ Article: [
18
+ {
19
+ ArticleNumber: "第1条",
20
+ ArticleTitle: "(目的)",
21
+ Paragraph: {
22
+ ParagraphSentence: "この法律はテスト目的のために存在する。",
23
+ },
24
+ },
25
+ {
26
+ ArticleNumber: "第2条",
27
+ ArticleTitle: "(定義)",
28
+ Paragraph: { ParagraphSentence: "用語の定義を定める。" },
29
+ },
30
+ ],
31
+ },
32
+ },
33
+ });
34
+
35
+ mockPool
36
+ .intercept({
37
+ path: "/api/2/lawsearch/%E3%83%86%E3%82%B9%E3%83%88",
38
+ method: "GET",
39
+ })
40
+ .reply(200, {
41
+ numberOfHits: 1,
42
+ referencelaw: {
43
+ LawID: "TEST-LAW",
44
+ LawName: "テスト法",
45
+ PromulgationDate: "2024-01-01",
46
+ },
47
+ });
48
+
49
+ setGlobalDispatcher(mockAgent);
50
+
51
+ const run = async () => {
52
+ // fetchLawData returns structured law
53
+ const law = await fetchLawData("TEST-LAW");
54
+ assert.strictEqual(law.LawID, "TEST-LAW");
55
+ assert.ok(law.LawBody?.MainProvision?.Article);
56
+
57
+ // searchLaws returns the mocked hit
58
+ const search = await searchLaws("テスト");
59
+ assert.strictEqual(search.numberOfHits, 1);
60
+
61
+ // consistency check aligns a segment with 第1条
62
+ const doc = "この法律はテスト目的のために存在する。";
63
+ const result = checkConsistency(doc, [law]);
64
+ assert.strictEqual(result.findings.length, 1);
65
+ const finding = result.findings[0];
66
+ assert.notStrictEqual(finding.status, "not_found");
67
+ assert.strictEqual(finding.articleNumber, "第1条");
68
+
69
+ console.log("integration tests passed");
70
+ };
71
+
72
+ run().catch((error) => {
73
+ console.error(error);
74
+ process.exitCode = 1;
75
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "node",
6
+ "rootDir": ".",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src/**/*.ts", "test/**/*.ts"]
14
+ }