Skip to main content
Glama

jgrants-mcp

index.ts10.9 kB
#!/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"; import { SubsidyListResponse, SubsidyDetailResponse, SubsidyDetailSummary, } from "./types.js"; const API_BASE_URL = "https://api.jgrants-portal.go.jp/exp/v1/public"; // Tool schema definitions 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), }); // Initialize MCP server const server = new Server( { name: "jgrants-mcp", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); // List available 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"], }, }, ], }; }); // Execute tools 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()) as SubsidyListResponse; 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()) as SubsidyDetailResponse; // Check result 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]; // 添付ファイルは軽量メタ情報(index, name, サイズ等)のみ返し、実データは download_attachment で取得 // Base64 文字列長から概算バイトサイズを計算(デコードコストを回避) const calculateBase64Size = (base64: string): number => { // 改行などの空白文字を除去(API が 76 文字ごとの改行付きで返すケースに対応) 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?: Array<{ name: string; data: string }> ) => 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: SubsidyDetailSummary = { ...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()) as SubsidyDetailResponse; 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" as const, }; 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"); } }); // Main function async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("jGrants MCP server started"); } // Error handling main().catch((error) => { console.error("Fatal error:", error); process.exit(1); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tachibanayu24/jgrants-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server