Grants Search MCP Server
by Tar-ive
- grants-mcp
- src
#!/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 axios from 'axios';
const API_KEY = process.env.API_KEY;
// Update interface to match actual API response
interface Grant {
agency: string;
agency_code: string;
agency_name: string;
opportunity_id: number;
opportunity_number: string;
opportunity_title: string;
opportunity_status: string;
summary: {
award_ceiling?: number;
award_floor?: number;
post_date?: string;
close_date?: string;
summary_description?: string;
additional_info_url?: string;
agency_contact_description?: string;
agency_email_address?: string;
agency_phone_number?: string;
applicant_eligibility_description?: string;
};
category: string;
top_level_agency_name?: string;
}
interface GrantsAPIResponse {
data: Grant[];
pagination_info: {
total_records: number;
};
facet_counts: {
agency: Record<string, number>;
};
}
const server = new Server({
name: "exa",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
const formatGrantDetails = (grant: Grant) => {
return `
OPPORTUNITY DETAILS
------------------
Title: ${grant.opportunity_title}
Opportunity Number: ${grant.opportunity_number}
Agency: ${grant.agency_name} (${grant.agency_code})
Status: ${grant.opportunity_status}
FUNDING INFORMATION
------------------
Award Floor: ${grant.summary.award_floor ? `$${grant.summary.award_floor.toLocaleString()}` : 'Not specified'}
Award Ceiling: ${grant.summary.award_ceiling ? `$${grant.summary.award_ceiling.toLocaleString()}` : 'Not specified'}
Category: ${grant.category}
DATES AND DEADLINES
------------------
Posted Date: ${grant.summary.post_date || 'N/A'}
Close Date: ${grant.summary.close_date || 'N/A'}
CONTACT INFORMATION
------------------
Agency Contact: ${grant.summary.agency_contact_description || 'Not provided'}
Email: ${grant.summary.agency_email_address || 'Not provided'}
Phone: ${grant.summary.agency_phone_number || 'Not provided'}
ELIGIBILITY
------------------
${grant.summary.applicant_eligibility_description ?
grant.summary.applicant_eligibility_description.replace(/<[^>]*>/g, '').trim() :
'Eligibility information not provided'}
ADDITIONAL INFORMATION
------------------
More Details URL: ${grant.summary.additional_info_url || 'Not available'}
Description:
${grant.summary.summary_description ?
grant.summary.summary_description.replace(/<[^>]*>/g, '').trim() :
'No description available'}
==========================================================================
`;
};
const createSummary = (grants: Grant[], searchQuery: string, page: number = 1, grantsPerPage: number = 3) => {
const startIdx = (page - 1) * grantsPerPage;
const endIdx = startIdx + grantsPerPage;
const displayedGrants = grants.slice(startIdx, endIdx);
const totalPages = Math.ceil(grants.length / grantsPerPage);
return `Search Results for "${searchQuery}":
OVERVIEW
--------
Total Grants Found: ${grants.length}
Showing grants ${startIdx + 1} to ${Math.min(endIdx, grants.length)} of ${grants.length}
Page ${page} of ${totalPages}
DETAILED GRANT LISTINGS
----------------------
${displayedGrants.map(formatGrantDetails).join("\n")}
Note: Showing ${grantsPerPage} grants per page. Total grants available: ${grants.length}
`;
};
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "search-grants",
description: "Search for government grants based on keywords",
inputSchema: {
type: "object",
properties: {
query: {
type: "string",
description: "Search query for grants (e.g., 'Artificial intelligence', 'Climate change')"
},
page: {
type: "number",
description: "Page number for pagination (default: 1)"
},
grantsPerPage: {
type: "number",
description: "Number of grants per page (default: 3)"
}
},
required: ["query"]
}
}]
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "search-grants") {
try {
const args = request.params.arguments as { query?: string; page?: number; grantsPerPage?: number } | undefined;
const searchQuery = args?.query ? String(args.query).trim() : "Artificial intelligence";
const page = args?.page || 1;
const grantsPerPage = args?.grantsPerPage || 3;
console.error(`Debug: Starting search with query: ${searchQuery}, page: ${page}, grantsPerPage: ${grantsPerPage}`);
const url = 'https://api.simpler.grants.gov/v1/opportunities/search';
const searchData = {
filters: {
opportunity_status: {
one_of: ["forecasted", "posted"]
}
},
pagination: {
order_by: "opportunity_id",
page_offset: page,
page_size: grantsPerPage,
sort_direction: "descending"
},
query: searchQuery
};
const response = await axios.post<GrantsAPIResponse>(url, searchData, {
headers: {
'accept': 'application/json',
'X-Auth': API_KEY,
'Content-Type': 'application/json'
}
});
if (!response.data?.data) {
return {
content: [{
type: "text",
text: "No results found or invalid response format"
}]
};
}
const grants = response.data.data;
if (grants.length === 0) {
return {
content: [{
type: "text",
text: `No grants found matching "${searchQuery}"`
}]
};
}
const summaryText = createSummary(grants, searchQuery, page, grantsPerPage);
return {
content: [{
type: "text",
text: summaryText
}]
};
} catch (error) {
console.error('Debug: Error occurred:', error);
if (axios.isAxiosError(error)) {
console.error('Debug: Axios error response:', error.response?.data);
return {
content: [{
type: "text",
text: `API Error: ${error.response?.data?.message || error.message}`
}]
};
}
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : 'Unknown error occurred'}`
}]
};
}
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
async function run() {
try {
console.error('Debug: Starting server with API key:', API_KEY ? 'Present' : 'Missing');
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Grants Search MCP server running on stdio");
} catch (error) {
console.error('Debug: Server startup error:', error);
throw error;
}
}
run().catch((error) => {
console.error("Fatal error running server:", error);
process.exit(1);
});