law-mcp-server 0.1.1 → 0.1.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 +4 -0
- package/bin/law-mcp-server.js +9 -1
- package/dist/src/index.js +17 -0
- package/package.json +14 -6
- package/.env.example +0 -8
- package/.eslintignore +0 -2
- package/.eslintrc.cjs +0 -19
- package/.prettierignore +0 -2
- package/.prettierrc +0 -5
- package/src/cache.ts +0 -25
- package/src/config.ts +0 -19
- package/src/consistency.ts +0 -128
- package/src/index.ts +0 -36
- package/src/lawApi.ts +0 -62
- package/src/mcp.ts +0 -87
- package/src/tools.ts +0 -162
- package/src/types.ts +0 -54
- package/test/integration.test.ts +0 -75
- package/tsconfig.json +0 -14
package/README.md
CHANGED
|
@@ -80,6 +80,10 @@ This repository will host an MCP server that uses **法令API Version 2** (e-Gov
|
|
|
80
80
|
}
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
If you are installing from a local clone instead of the published
|
|
84
|
+
package, run `npm install && npm run build` and then `npm link` so the
|
|
85
|
+
`law-mcp-server` command is available on your `PATH` for Claude Desktop.
|
|
86
|
+
|
|
83
87
|
## Usage Examples (conceptual)
|
|
84
88
|
|
|
85
89
|
- Search and fetch: “Search for 個人情報保護 and show the latest articles.” → calls `search_laws` then `fetch_law`.
|
package/bin/law-mcp-server.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const entry = new URL("../dist/src/index.js", import.meta.url);
|
|
4
|
+
|
|
5
|
+
import(entry.href).catch((error) => {
|
|
6
|
+
console.error(
|
|
7
|
+
"Failed to start law-mcp-server. Run `npm run build` to generate dist/ before launching."
|
|
8
|
+
);
|
|
9
|
+
console.error(error);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
package/dist/src/index.js
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
import { StdioJsonRpcServer } from "./mcp.js";
|
|
3
3
|
import { tools, resolveTool } from "./tools.js";
|
|
4
4
|
const server = new StdioJsonRpcServer();
|
|
5
|
+
const serverInfo = { name: "law-mcp-server", version: "0.1.2" };
|
|
6
|
+
server.register("initialize", async (params) => {
|
|
7
|
+
const payload = (params ?? {});
|
|
8
|
+
const protocolVersion = typeof payload.protocolVersion === "string"
|
|
9
|
+
? payload.protocolVersion
|
|
10
|
+
: "2024-06-17";
|
|
11
|
+
return {
|
|
12
|
+
protocolVersion,
|
|
13
|
+
serverInfo,
|
|
14
|
+
capabilities: {
|
|
15
|
+
tools: {
|
|
16
|
+
list: true,
|
|
17
|
+
call: true,
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
5
22
|
server.register("ping", async () => ({ ok: true }));
|
|
6
23
|
server.register("tools/list", async () => ({
|
|
7
24
|
tools: tools.map((tool) => ({
|
package/package.json
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "law-mcp-server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP server for e-Gov law API consistency checks",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist",
|
|
8
|
+
"bin",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
6
12
|
"bin": {
|
|
7
13
|
"law-mcp-server": "bin/law-mcp-server.js"
|
|
8
14
|
},
|
|
9
|
-
"main": "dist/index.js",
|
|
15
|
+
"main": "dist/src/index.js",
|
|
10
16
|
"scripts": {
|
|
11
17
|
"build": "tsc",
|
|
18
|
+
"prepare": "npm run build",
|
|
12
19
|
"start": "node dist/index.js",
|
|
13
20
|
"dev": "ts-node src/index.ts",
|
|
14
21
|
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
|
@@ -16,16 +23,17 @@
|
|
|
16
23
|
"test": "npm run build && node dist/test/integration.test.js"
|
|
17
24
|
},
|
|
18
25
|
"dependencies": {
|
|
26
|
+
"stripe": "^20.3.1",
|
|
19
27
|
"undici": "^6.13.0"
|
|
20
28
|
},
|
|
21
29
|
"devDependencies": {
|
|
22
|
-
"ts-node": "^10.9.2",
|
|
23
|
-
"typescript": "5.3.3",
|
|
24
30
|
"@types/node": "^20.11.0",
|
|
25
|
-
"eslint": "^8.57.0",
|
|
26
31
|
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
27
32
|
"@typescript-eslint/parser": "^6.21.0",
|
|
33
|
+
"eslint": "^8.57.0",
|
|
28
34
|
"eslint-config-prettier": "^9.1.0",
|
|
29
|
-
"prettier": "^3.2.5"
|
|
35
|
+
"prettier": "^3.2.5",
|
|
36
|
+
"ts-node": "^10.9.2",
|
|
37
|
+
"typescript": "5.3.3"
|
|
30
38
|
}
|
|
31
39
|
}
|
package/.env.example
DELETED
package/.eslintignore
DELETED
package/.eslintrc.cjs
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
env: {
|
|
3
|
-
es2022: true,
|
|
4
|
-
node: true,
|
|
5
|
-
},
|
|
6
|
-
parser: "@typescript-eslint/parser",
|
|
7
|
-
parserOptions: {
|
|
8
|
-
sourceType: "module",
|
|
9
|
-
ecmaVersion: "latest",
|
|
10
|
-
},
|
|
11
|
-
plugins: ["@typescript-eslint"],
|
|
12
|
-
extends: [
|
|
13
|
-
"eslint:recommended",
|
|
14
|
-
"plugin:@typescript-eslint/recommended",
|
|
15
|
-
"prettier",
|
|
16
|
-
],
|
|
17
|
-
ignorePatterns: ["dist/**"],
|
|
18
|
-
rules: {},
|
|
19
|
-
};
|
package/.prettierignore
DELETED
package/.prettierrc
DELETED
package/src/cache.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
type Entry<T> = {
|
|
2
|
-
value: T;
|
|
3
|
-
expiresAt: number;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export class MemoryCache<T> {
|
|
7
|
-
private store = new Map<string, Entry<T>>();
|
|
8
|
-
|
|
9
|
-
get(key: string): T | undefined {
|
|
10
|
-
const entry = this.store.get(key);
|
|
11
|
-
if (!entry) return undefined;
|
|
12
|
-
if (Date.now() > entry.expiresAt) {
|
|
13
|
-
this.store.delete(key);
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
return entry.value;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
set(key: string, value: T, ttlSeconds: number) {
|
|
20
|
-
const expiresAt = Date.now() + ttlSeconds * 1000;
|
|
21
|
-
this.store.set(key, { value, expiresAt });
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export const cache = new MemoryCache<unknown>();
|
package/src/config.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export type AppConfig = {
|
|
2
|
-
apiBase: string;
|
|
3
|
-
httpTimeoutMs: number;
|
|
4
|
-
cacheTtlSeconds: number;
|
|
5
|
-
userAgent: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
const toNumber = (value: string | undefined, fallback: number) => {
|
|
9
|
-
const parsed = Number(value);
|
|
10
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const config: AppConfig = {
|
|
14
|
-
apiBase:
|
|
15
|
-
process.env.LAW_API_BASE?.trim() || "https://laws.e-gov.go.jp/api/2/",
|
|
16
|
-
httpTimeoutMs: toNumber(process.env.HTTP_TIMEOUT_MS, 15000),
|
|
17
|
-
cacheTtlSeconds: toNumber(process.env.CACHE_TTL_SECONDS, 900),
|
|
18
|
-
userAgent: "law-mcp-server/0.1.0",
|
|
19
|
-
};
|
package/src/consistency.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LawArticle,
|
|
3
|
-
LawData,
|
|
4
|
-
ConsistencyFinding,
|
|
5
|
-
ConsistencyOutput,
|
|
6
|
-
} from "./types.js";
|
|
7
|
-
|
|
8
|
-
type FlattenedArticle = {
|
|
9
|
-
lawId: string;
|
|
10
|
-
articleNumber?: string;
|
|
11
|
-
text: string;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const toArray = <T>(value: T | T[] | undefined): T[] => {
|
|
15
|
-
if (!value) return [];
|
|
16
|
-
return Array.isArray(value) ? value : [value];
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const paragraphText = (paragraph: unknown): string => {
|
|
20
|
-
if (!paragraph || typeof paragraph !== "object") return "";
|
|
21
|
-
const p = paragraph as { ParagraphSentence?: string | string[] };
|
|
22
|
-
const sentences = toArray(p.ParagraphSentence);
|
|
23
|
-
return sentences.join("\n");
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const articleText = (article: LawArticle): string => {
|
|
27
|
-
const paragraphs = toArray(article.Paragraph);
|
|
28
|
-
const body = paragraphs.map(paragraphText).filter(Boolean).join("\n");
|
|
29
|
-
const title = article.ArticleTitle || "";
|
|
30
|
-
return [title, body].filter(Boolean).join("\n");
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const flattenArticles = (law: LawData): FlattenedArticle[] => {
|
|
34
|
-
const articles = toArray(law.LawBody?.MainProvision?.Article);
|
|
35
|
-
return articles.map((article) => ({
|
|
36
|
-
lawId: law.LawID,
|
|
37
|
-
articleNumber: article.ArticleNumber,
|
|
38
|
-
text: articleText(article),
|
|
39
|
-
}));
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const similarityScore = (a: string, b: string): number => {
|
|
43
|
-
const tokenize = (value: string) =>
|
|
44
|
-
value
|
|
45
|
-
.replace(/[\s、。,..\n\t]+/g, " ")
|
|
46
|
-
.split(" ")
|
|
47
|
-
.map((v) => v.trim())
|
|
48
|
-
.filter(Boolean);
|
|
49
|
-
const tokensA = tokenize(a.toLowerCase());
|
|
50
|
-
const tokensB = tokenize(b.toLowerCase());
|
|
51
|
-
if (!tokensA.length || !tokensB.length) return 0;
|
|
52
|
-
const setB = new Set(tokensB);
|
|
53
|
-
const intersection = tokensA.filter((token) => setB.has(token)).length;
|
|
54
|
-
const union = new Set([...tokensA, ...tokensB]).size;
|
|
55
|
-
return intersection / union;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
const extractArticleHint = (segment: string): string | undefined => {
|
|
59
|
-
const match = segment.match(
|
|
60
|
-
/第\s*([0-90-9一二三四五六七八九十百千]+)\s*条/
|
|
61
|
-
);
|
|
62
|
-
return match ? match[1] : undefined;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const bestArticleMatch = (
|
|
66
|
-
segment: string,
|
|
67
|
-
articles: FlattenedArticle[]
|
|
68
|
-
): FlattenedArticle | undefined => {
|
|
69
|
-
const hint = extractArticleHint(segment);
|
|
70
|
-
if (hint) {
|
|
71
|
-
const exact = articles.find(
|
|
72
|
-
(a) => a.articleNumber && a.articleNumber.includes(hint)
|
|
73
|
-
);
|
|
74
|
-
if (exact) return exact;
|
|
75
|
-
}
|
|
76
|
-
let best: FlattenedArticle | undefined;
|
|
77
|
-
let bestScore = 0;
|
|
78
|
-
for (const article of articles) {
|
|
79
|
-
const score = similarityScore(segment, article.text);
|
|
80
|
-
if (score > bestScore) {
|
|
81
|
-
best = article;
|
|
82
|
-
bestScore = score;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return best;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
export const checkConsistency = (
|
|
89
|
-
documentText: string,
|
|
90
|
-
laws: LawData[]
|
|
91
|
-
): ConsistencyOutput => {
|
|
92
|
-
const segments = documentText
|
|
93
|
-
.split(/\n+/)
|
|
94
|
-
.map((s) => s.trim())
|
|
95
|
-
.filter(Boolean);
|
|
96
|
-
|
|
97
|
-
const flattened = laws
|
|
98
|
-
.flatMap(flattenArticles)
|
|
99
|
-
.filter((a) => a.text.trim().length > 0);
|
|
100
|
-
|
|
101
|
-
const findings: ConsistencyFinding[] = segments.map((segment) => {
|
|
102
|
-
const match = bestArticleMatch(segment, flattened);
|
|
103
|
-
if (!match) {
|
|
104
|
-
return { segment, status: "not_found" };
|
|
105
|
-
}
|
|
106
|
-
const score = similarityScore(segment, match.text);
|
|
107
|
-
const status =
|
|
108
|
-
score >= 0.6
|
|
109
|
-
? "aligned"
|
|
110
|
-
: score >= 0.25
|
|
111
|
-
? "potential_mismatch"
|
|
112
|
-
: "not_found";
|
|
113
|
-
return {
|
|
114
|
-
segment,
|
|
115
|
-
status,
|
|
116
|
-
lawId: match.lawId,
|
|
117
|
-
articleNumber: match.articleNumber,
|
|
118
|
-
lawSnippet: match.text.slice(0, 400),
|
|
119
|
-
score: Number(score.toFixed(3)),
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
const matchedLawIds = Array.from(
|
|
124
|
-
new Set(findings.map((f) => f.lawId).filter(Boolean))
|
|
125
|
-
) as string[];
|
|
126
|
-
|
|
127
|
-
return { findings, matchedLawIds };
|
|
128
|
-
};
|
package/src/index.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { StdioJsonRpcServer } from "./mcp.js";
|
|
4
|
-
import { tools, resolveTool } from "./tools.js";
|
|
5
|
-
|
|
6
|
-
const server = new StdioJsonRpcServer();
|
|
7
|
-
|
|
8
|
-
server.register("ping", async () => ({ ok: true }));
|
|
9
|
-
|
|
10
|
-
server.register("tools/list", async () => ({
|
|
11
|
-
tools: tools.map((tool) => ({
|
|
12
|
-
name: tool.name,
|
|
13
|
-
description: tool.description,
|
|
14
|
-
inputSchema: tool.inputSchema,
|
|
15
|
-
outputSchema: tool.outputSchema,
|
|
16
|
-
})),
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
server.register("tools/call", async (params) => {
|
|
20
|
-
const payload = (params ?? {}) as { name?: unknown; arguments?: unknown };
|
|
21
|
-
if (typeof payload.name !== "string") {
|
|
22
|
-
throw new Error("Tool name is required and must be a string");
|
|
23
|
-
}
|
|
24
|
-
const tool = resolveTool(payload.name);
|
|
25
|
-
if (!tool) {
|
|
26
|
-
throw new Error(`Tool ${payload.name} is not available`);
|
|
27
|
-
}
|
|
28
|
-
const args =
|
|
29
|
-
payload.arguments && typeof payload.arguments === "object"
|
|
30
|
-
? (payload.arguments as Record<string, unknown>)
|
|
31
|
-
: {};
|
|
32
|
-
const result = await tool.handler(args);
|
|
33
|
-
return { content: result };
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
server.start();
|
package/src/lawApi.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { fetch } from "undici";
|
|
2
|
-
import { cache } from "./cache.js";
|
|
3
|
-
import { config } from "./config.js";
|
|
4
|
-
import { LawData, LawSearchResponse } from "./types.js";
|
|
5
|
-
|
|
6
|
-
const withTimeout = async <T>(promise: Promise<T>, ms: number) => {
|
|
7
|
-
const controller = new AbortController();
|
|
8
|
-
const timer = setTimeout(() => controller.abort(), ms);
|
|
9
|
-
try {
|
|
10
|
-
return await promise;
|
|
11
|
-
} finally {
|
|
12
|
-
clearTimeout(timer);
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const request = async <T>(url: string) => {
|
|
17
|
-
const res = await withTimeout(
|
|
18
|
-
fetch(url, {
|
|
19
|
-
headers: {
|
|
20
|
-
"User-Agent": config.userAgent,
|
|
21
|
-
Accept: "application/json",
|
|
22
|
-
},
|
|
23
|
-
}),
|
|
24
|
-
config.httpTimeoutMs
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
if (!res.ok) {
|
|
28
|
-
const body = await res.text();
|
|
29
|
-
throw new Error(`Request failed ${res.status}: ${body}`);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const data = (await res.json()) as T;
|
|
33
|
-
return data;
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export const fetchLawData = async (
|
|
37
|
-
lawId: string,
|
|
38
|
-
revisionDate?: string
|
|
39
|
-
): Promise<LawData> => {
|
|
40
|
-
const cacheKey = `law:${lawId}:${revisionDate || "latest"}`;
|
|
41
|
-
const cached = cache.get(cacheKey) as LawData | undefined;
|
|
42
|
-
if (cached) return cached;
|
|
43
|
-
|
|
44
|
-
const base = config.apiBase.endsWith("/")
|
|
45
|
-
? config.apiBase
|
|
46
|
-
: `${config.apiBase}/`;
|
|
47
|
-
const url = new URL(`lawdata/${encodeURIComponent(lawId)}`, base);
|
|
48
|
-
if (revisionDate) url.searchParams.set("revision", revisionDate);
|
|
49
|
-
const data = await request<LawData>(url.toString());
|
|
50
|
-
cache.set(cacheKey, data, config.cacheTtlSeconds);
|
|
51
|
-
return data;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
export const searchLaws = async (
|
|
55
|
-
keyword: string
|
|
56
|
-
): Promise<LawSearchResponse> => {
|
|
57
|
-
const base = config.apiBase.endsWith("/")
|
|
58
|
-
? config.apiBase
|
|
59
|
-
: `${config.apiBase}/`;
|
|
60
|
-
const url = new URL(`lawsearch/${encodeURIComponent(keyword)}`, base);
|
|
61
|
-
return request<LawSearchResponse>(url.toString());
|
|
62
|
-
};
|
package/src/mcp.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
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
|
-
};
|
package/test/integration.test.ts
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|