law-mcp-server 0.1.7 → 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/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
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.6" };
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("ping", async () => ({ ok: true }));
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
- const result = await tool.handler(args);
45
- 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
+ }
46
53
  });
47
54
  server.start();
@@ -10,23 +10,22 @@ class HttpError extends Error {
10
10
  this.body = body;
11
11
  }
12
12
  }
13
- const withTimeout = async (promise, ms) => {
13
+ const request = async (url, context) => {
14
14
  const controller = new AbortController();
15
- const timer = setTimeout(() => controller.abort(), ms);
15
+ const timer = setTimeout(() => controller.abort(), config.httpTimeoutMs);
16
+ let res;
16
17
  try {
17
- 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
+ });
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
- return await request(queryUrl.toString());
66
+ const data = await request(queryUrl.toString());
67
+ cache.set(cacheKey, data, config.cacheTtlSeconds);
68
+ return data;
63
69
  }
64
70
  catch (error) {
65
- const is404 = error instanceof HttpError && error.status === 404;
66
- // Some deployments return 404 for no hits; normalize to empty result.
67
- if (is404) {
68
- const pathUrl = new URL(`lawsearch/${encodeURIComponent(keyword)}`, base);
69
- try {
70
- return await request(pathUrl.toString());
71
- }
72
- catch (err) {
73
- const path404 = err instanceof HttpError && err.status === 404;
74
- if (path404) {
75
- return { numberOfHits: 0, referencelaw: [] };
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, list_revisions, 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
+ 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: "json", data }];
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: "json", data: results }];
47
+ return [{ type: "text", text: JSON.stringify(results, null, 2) }];
48
48
  },
49
49
  };
50
50
  const summarize = {
@@ -73,32 +73,25 @@ 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
- .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");
80
92
  return [{ type: "text", text: body || "No articles available" }];
81
93
  },
82
94
  };
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
95
  const check = {
103
96
  name: "check_consistency",
104
97
  description: "Check a document against one or more laws (lawIds must be canonical LawIDs from search_laws).",
@@ -107,8 +100,6 @@ const check = {
107
100
  properties: {
108
101
  documentText: { type: "string" },
109
102
  lawIds: { type: "array", items: { type: "string" } },
110
- articleHints: { type: "array", items: { type: "string" } },
111
- strictness: { type: "string", enum: ["low", "medium", "high"] },
112
103
  },
113
104
  required: ["documentText"],
114
105
  },
@@ -122,13 +113,12 @@ const check = {
122
113
  }
123
114
  const laws = await Promise.all(lawIds.map((lawId) => fetchLawData(lawId)));
124
115
  const output = checkConsistency(documentText, laws);
125
- return [{ type: "json", data: output }];
116
+ return [{ type: "text", text: JSON.stringify(output, null, 2) }];
126
117
  },
127
118
  };
128
119
  export const tools = [
129
120
  fetchLaw,
130
121
  search,
131
- listRevisions,
132
122
  check,
133
123
  summarize,
134
124
  ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "law-mcp-server",
3
- "version": "0.1.7",
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": {