import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
// Define common search parameters for the Kokkai API
const kokkaiCommonSearchParams = {
startRecord: z
.number()
.int()
.min(1)
.optional()
.describe(
"Start position for retrieving search results (1-based). Default is 1."
),
nameOfHouse: z
.enum(["衆議院", "参議院", "両院", "両院協議会"])
.optional()
.describe(
"Name of the House (e.g., '衆議院' for House of Representatives, '参議院' for House of Councillors). '両院' and '両院協議会' yield the same results."
),
nameOfMeeting: z
.string()
.optional()
.describe(
"Name of the meeting (e.g., plenary session, committee). Partial match. Multiple terms separated by space for OR search."
),
any: z
.string()
.optional()
.describe(
"Keywords to search in speech content. Partial match. Multiple terms separated by space for AND search."
),
speaker: z
.string()
.optional()
.describe(
"Speaker's name. Partial match. Multiple names separated by space for OR search."
),
from: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format")
.optional()
.describe(
"Start date for meeting search (YYYY-MM-DD). Default '0000-01-01'."
),
until: z
.string()
.regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be in YYYY-MM-DD format")
.optional()
.describe(
"End date for meeting search (YYYY-MM-DD). Default '9999-12-31'."
),
supplementAndAppendix: z
.boolean()
.optional()
.describe(
"Limit search to supplements and appendices. Default false."
),
contentsAndIndex: z
.boolean()
.optional()
.describe("Limit search to table of contents and index. Default false."),
searchRange: z
.enum(["冒頭", "本文", "冒頭・本文"])
.optional()
.describe(
"Specifies where to search for keywords from 'any' parameter: '冒頭' (opening), '本文' (body), or '冒頭・本文' (both). Default '冒頭・本文'."
),
closing: z
.boolean()
.optional()
.describe("Limit search to meetings held during recess. Default false."),
speechNumber: z
.number()
.int()
.nonnegative()
.optional()
.describe("Exact match for speech number (e.g., 10)."),
speakerPosition: z
.string()
.optional()
.describe("Speaker's position/title. Partial match."),
speakerGroup: z
.string()
.optional()
.describe("Speaker's affiliated political group. Partial match."),
speakerRole: z
.enum(["証人", "参考人", "公述人"])
.optional()
.describe(
"Speaker's role: '証人' (witness), '参考人' (expert witness/referential speaker), '公述人' (public speaker)."
),
speechID: z
.string()
.optional()
.describe(
"Unique ID for a speech (format: issueID_paddedSpeechNumber, e.g., '100105254X00119470520_000'). Exact match."
),
issueID: z
.string()
.optional()
.describe(
"Unique ID for a meeting record (21 alphanumeric characters, e.g., '100105254X00119470520'). Exact match."
),
sessionFrom: z
.number()
.int()
.positive()
.optional()
.describe("Starting Diet session number (up to 3 digits)."),
sessionTo: z
.number()
.int()
.positive()
.optional()
.describe("Ending Diet session number (up to 3 digits)."),
issueFrom: z
.number()
.int()
.nonnegative()
.optional()
.describe(
"Starting issue number (up to 3 digits, 0 for TOC/index/appendix/supplement)."
),
issueTo: z
.number()
.int()
.nonnegative()
.optional()
.describe(
"Ending issue number (up to 3 digits, 0 for TOC/index/appendix/supplement)."
),
// recordPacking is handled internally, always 'json'
};
const KOKKAI_API_BASE_URL = "https://kokkai.ndl.go.jp/api";
export class KokkaiApiAgent extends McpAgent<Env> {
server = new McpServer({
name: "Kokkai API Agent",
version: "1.0.0",
description:
"Agent to interact with the National Diet Library's Kokkai Kaigiroku (Diet Minutes) Search System API.",
});
private async _callKokkaiApi(
endpoint: string,
params: Record<string, any>
): Promise<any> {
const queryParams = new URLSearchParams();
for (const key in params) {
if (params[key] !== undefined && params[key] !== null) {
queryParams.append(key, String(params[key]));
}
}
queryParams.append("recordPacking", "json"); // Always request JSON
const url = `${KOKKAI_API_BASE_URL}/${endpoint}?${queryParams.toString()}`;
console.log(`Calling Kokkai API: ${url}`);
try {
const response = await fetch(url, {
headers: {
"User-Agent": "MCP Agent Kokkai Client/1.0",
},
});
const responseData = (await response.json()) as any;
if (!response.ok) {
const errorMessage =
responseData.message || "Unknown API error";
const errorDetails = responseData.details
? responseData.details.join("; ")
: "";
console.error(
`Kokkai API Error: ${errorMessage} ${errorDetails}`,
responseData
);
return {
error: true,
status: response.status,
message: errorMessage,
details: errorDetails,
rawResponse: responseData,
};
}
return responseData;
} catch (error: any) {
console.error("Network or parsing error calling Kokkai API:", error);
return {
error: true,
message:
error.message || "Failed to fetch or parse API response.",
};
}
}
async init() {
this.server.tool(
"searchMeetingList",
"Searches for meeting summaries from the Kokkai API (会議単位簡易出力). Returns basic info like meeting name, date, ID, URL. Does not include speech text. Max 100 records.",
{
...kokkaiCommonSearchParams,
maximumRecords: z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe(
"Maximum number of records to retrieve (1-100) for meeting list. Default is 30."
),
},
async (params) => {
const result = await this._callKokkaiApi(
"meeting_list",
params
);
if (result.error) {
return {
content: [
{
type: "text",
text: `Error fetching meeting list: ${result.message} ${result.details || ""}`,
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
this.server.tool(
"searchMeetingsWithSpeechText",
"Searches for meetings and includes the full text of ALL speeches within those meetings from the Kokkai API (会議単位出力). Returns meeting info and all speech texts. Max 10 records.",
{
...kokkaiCommonSearchParams,
maximumRecords: z
.number()
.int()
.min(1)
.max(10)
.optional()
.describe(
"Maximum number of records to retrieve (1-10) for meetings with speech text. Default is 3."
),
},
async (params) => {
const result = await this._callKokkaiApi("meeting", params);
if (result.error) {
return {
content: [
{
type: "text",
text: `Error fetching meetings with speech text: ${result.message} ${result.details || ""}`,
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
this.server.tool(
"searchSpeeches",
"Searches for specific speeches from the Kokkai API (発言単位出力). Returns speech text and info about the meeting it belongs to. Max 100 records.",
{
...kokkaiCommonSearchParams,
maximumRecords: z
.number()
.int()
.min(1)
.max(100)
.optional()
.describe(
"Maximum number of records to retrieve (1-100) for speeches. Default is 30."
),
},
async (params) => {
const result = await this._callKokkaiApi("speech", params);
if (result.error) {
return {
content: [
{
type: "text",
text: `Error fetching speeches: ${result.message} ${result.details || ""}`,
},
],
};
}
return {
content: [
{
type: "text",
text: JSON.stringify(result, null, 2),
},
],
};
}
);
}
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === "/mcp") {
return KokkaiApiAgent.serve("/mcp").fetch(request, env, ctx);
}
return new Response("Not found", { status: 404 });
},
};