jgrants-mcp
Version:
日本の補助金情報を検索するためのMCP (Model Context Protocol) サーバー
297 lines (296 loc) • 12.2 kB
JavaScript
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
const API_BASE_URL = "https://api.jgrants-portal.go.jp/exp/v1/public";
const ListSubsidiesSchema = z.object({
keyword: z.string().default("補助金"),
});
const GetSubsidyDetailSchema = z.object({
subsidy_id: z.string(),
});
const DownloadAttachmentSchema = z.object({
subsidy_id: z.string(),
category: z.enum([
"application_guidelines",
"outline_of_grant",
"application_form",
]),
index: z.number().int().min(0),
});
const server = new Server({
name: "jgrants-mcp",
version: "0.1.0",
}, {
capabilities: {
tools: {},
},
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_subsidies",
description: "Search for subsidies currently accepting applications using the specified keyword. Default is '補助金' (subsidy).",
inputSchema: {
type: "object",
properties: {
keyword: {
type: "string",
description: "Search keyword",
default: "補助金",
},
},
},
},
{
name: "get_subsidy_detail",
description: "Get detailed information about a specific subsidy. Use the subsidy ID, not the title.",
inputSchema: {
type: "object",
properties: {
subsidy_id: {
type: "string",
description: "Subsidy ID",
},
},
required: ["subsidy_id"],
},
},
{
name: "download_attachment",
description: "Download a subsidy's attachment document. Returns the file data in base64 encoding along with metadata. First call get_subsidy_detail to see the attachments array for each category (application_guidelines, outline_of_grant, application_form), then use the 'index' from attachments[n].index to download the specific file.",
inputSchema: {
type: "object",
properties: {
subsidy_id: {
type: "string",
description: "Subsidy ID",
},
category: {
type: "string",
enum: [
"application_guidelines",
"outline_of_grant",
"application_form",
],
description: "Attachment category",
},
index: {
type: "integer",
description: "Attachment index (starts from 0)",
minimum: 0,
},
},
required: ["subsidy_id", "category", "index"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
switch (request.params.name) {
case "list_subsidies": {
const args = ListSubsidiesSchema.parse(request.params.arguments ?? {});
const params = new URLSearchParams({
keyword: args.keyword,
sort: "acceptance_end_datetime",
order: "ASC",
acceptance: "1",
});
try {
const response = await fetch(`${API_BASE_URL}/subsidies?${params}`);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`API error: ${response.status} - ${errorBody}`);
}
const data = (await response.json());
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
},
],
structuredContent: data,
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error occurred: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
}
case "get_subsidy_detail": {
const args = GetSubsidyDetailSchema.parse(request.params.arguments);
try {
const response = await fetch(`${API_BASE_URL}/subsidies/id/${args.subsidy_id}`);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`API error: ${response.status} - ${errorBody}`);
}
const data = (await response.json());
if (!data.result || data.result.length === 0) {
return {
content: [
{
type: "text",
text: `Subsidy with ID ${args.subsidy_id} was not found.`,
},
],
};
}
const subsidyInfo = data.result[0];
const calculateBase64Size = (base64) => {
const cleanBase64 = base64.replace(/\s+/g, "");
const paddingCount = cleanBase64.endsWith("==")
? 2
: cleanBase64.endsWith("=")
? 1
: 0;
return Math.floor((cleanBase64.length * 3) / 4) - paddingCount;
};
const buildAttachmentList = (attachments) => attachments?.map((attachment, index) => ({
index,
name: attachment.name,
sizeBytes: calculateBase64Size(attachment.data),
})) ?? [];
const applicationGuidelinesAttachments = buildAttachmentList(subsidyInfo.application_guidelines);
const outlineOfGrantAttachments = buildAttachmentList(subsidyInfo.outline_of_grant);
const applicationFormAttachments = buildAttachmentList(subsidyInfo.application_form);
const subsidySummary = {
...subsidyInfo,
application_guidelines: {
count: applicationGuidelinesAttachments.length,
hasAttachments: applicationGuidelinesAttachments.length > 0,
attachments: applicationGuidelinesAttachments,
},
outline_of_grant: {
count: outlineOfGrantAttachments.length,
hasAttachments: outlineOfGrantAttachments.length > 0,
attachments: outlineOfGrantAttachments,
},
application_form: {
count: applicationFormAttachments.length,
hasAttachments: applicationFormAttachments.length > 0,
attachments: applicationFormAttachments,
},
};
return {
content: [
{
type: "text",
text: JSON.stringify(subsidySummary, null, 2),
},
],
structuredContent: subsidySummary,
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error occurred: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
}
case "download_attachment": {
const args = DownloadAttachmentSchema.parse(request.params.arguments);
try {
const response = await fetch(`${API_BASE_URL}/subsidies/id/${args.subsidy_id}`);
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`API error: ${response.status} - ${errorBody}`);
}
const data = (await response.json());
if (!data.result || data.result.length === 0) {
return {
content: [
{
type: "text",
text: `Subsidy with ID ${args.subsidy_id} was not found.`,
},
],
};
}
const subsidyInfo = data.result[0];
if (!subsidyInfo[args.category] ||
!Array.isArray(subsidyInfo[args.category])) {
return {
content: [
{
type: "text",
text: `Attachment category '${args.category}' does not exist.`,
},
],
};
}
const attachments = subsidyInfo[args.category];
if (args.index < 0 || args.index >= attachments.length) {
return {
content: [
{
type: "text",
text: `Attachment index ${args.index} is invalid.`,
},
],
};
}
const attachment = attachments[args.index];
const actualByteSize = Buffer.from(attachment.data, "base64").length;
const output = {
subsidy_id: subsidyInfo.id,
subsidy_name: subsidyInfo.name,
category: args.category,
index: args.index,
file_name: attachment.name,
data: attachment.data,
data_size_bytes: actualByteSize,
encoding: "base64",
};
return {
content: [
{
type: "text",
text: JSON.stringify({
...output,
data: `[Base64 data: ${actualByteSize} bytes]`,
}, null, 2),
},
],
structuredContent: output,
};
}
catch (error) {
return {
content: [
{
type: "text",
text: `Error occurred: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
};
}
}
default:
throw new Error("Unknown tool");
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("jGrants MCP server started");
}
main().catch((error) => {
console.error("Fatal error:", error);
process.exit(1);
});