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.
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();
@@ -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: "law-mcp-server/0.1.0",
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: "law-mcp-server", version: "0.1.5" };
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("ping", async () => ({ ok: true }));
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
- const result = await tool.handler(args);
44
- return { content: result };
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();
@@ -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
- const withTimeout = async (promise, ms) => {
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(), ms);
15
+ const timer = setTimeout(() => controller.abort(), config.httpTimeoutMs);
16
+ let res;
7
17
  try {
8
- return await promise;
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 Error(`Request failed ${res.status}: ${body}`);
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
- return await request(queryUrl.toString());
66
+ const data = await request(queryUrl.toString());
67
+ cache.set(cacheKey, data, config.cacheTtlSeconds);
68
+ return data;
54
69
  }
55
70
  catch (error) {
56
- const message = error instanceof Error ? error.message : String(error);
57
- if (message.includes("404")) {
58
- // Some deployments expect the keyword in the path; retry once
59
- const pathUrl = new URL(`lawsearch/${encodeURIComponent(keyword)}`, base);
60
- try {
61
- return await request(pathUrl.toString());
62
- }
63
- catch (err) {
64
- const msg = err instanceof Error ? err.message : String(err);
65
- throw new Error(`Law search failed for keyword "${keyword}". Tried query and path styles. Upstream: ${msg}`);
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: "json", data }];
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: "json", data: results }];
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
- .join("\n");
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: "json", data: output }];
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.1.6",
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": {