import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
const SBA_API_URL = "https://api.sba.gov";
const SBIR_API_URL = "https://api.www.sbir.gov/public/api";
let sizeStandardsCache: any[] | null = null;
async function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function sbirRequest(endpoint: string, params: Record<string, string> = {}, retries = 3): Promise<any> {
const searchParams = new URLSearchParams(params);
const url = `${SBIR_API_URL}/${endpoint}?${searchParams.toString()}`;
for (let attempt = 0; attempt <= retries; attempt++) {
const response = await fetch(url, {
headers: { Accept: "application/json" },
});
if (response.ok) {
return response.json();
}
if (response.status === 429 && attempt < retries) {
// Exponential backoff: 2s, 4s, 8s
const waitTime = Math.pow(2, attempt + 1) * 1000;
await sleep(waitTime);
continue;
}
if (response.status === 429) {
throw new Error(`SBIR API rate limited. The API has strict rate limits - please wait a few minutes and try again.`);
}
throw new Error(`SBIR API error: ${response.status} - ${response.statusText}`);
}
throw new Error("SBIR API request failed after retries");
}
async function fetchSizeStandards(): Promise<any[]> {
// Use cache to avoid repeated fetches
if (sizeStandardsCache) {
return sizeStandardsCache;
}
// Fetch the NAICS size standards JSON from SBA API
const url = `${SBA_API_URL}/naics/naics.json`;
const response = await fetch(url, {
headers: { Accept: "application/json" },
});
if (!response.ok) {
throw new Error(`SBA API error: ${response.status} - ${response.statusText}`);
}
const data = await response.json();
// The JSON is an array of NAICS entries
sizeStandardsCache = Array.isArray(data) ? data : [];
return sizeStandardsCache;
}
const server = new Server(
{ name: "sba", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_size_standards",
description: "Get SBA small business size standards by NAICS code. Returns revenue/employee thresholds that define 'small business' for federal contracting.",
inputSchema: {
type: "object",
properties: {
naicsCode: {
type: "string",
description: "NAICS code to look up (e.g., '541511' for custom computer programming). Can be partial for broader search."
},
industry: {
type: "string",
description: "Industry keyword to search (e.g., 'software', 'construction', 'restaurant')"
},
},
required: [],
},
},
{
name: "check_size_standard",
description: "Check if a business qualifies as 'small' under SBA standards for a specific NAICS code.",
inputSchema: {
type: "object",
properties: {
naicsCode: {
type: "string",
description: "NAICS code for the industry"
},
annualRevenue: {
type: "number",
description: "Annual revenue in millions of dollars (e.g., 15.5 for $15.5M)"
},
employeeCount: {
type: "number",
description: "Number of employees"
},
},
required: ["naicsCode"],
},
},
{
name: "search_sbir_firms",
description: "Search SBIR/STTR firms (small businesses receiving federal R&D funding). Find innovative companies by keyword, name, or location.",
inputSchema: {
type: "object",
properties: {
keyword: {
type: "string",
description: "Search keyword (searches company name and other fields)"
},
name: {
type: "string",
description: "Company name to search"
},
state: {
type: "string",
description: "State abbreviation (e.g., 'CA', 'TX', 'NY')"
},
womanOwned: {
type: "boolean",
description: "Filter for woman-owned businesses"
},
hubzone: {
type: "boolean",
description: "Filter for HUBZone businesses"
},
limit: {
type: "number",
description: "Max results (default 50, max 5000)"
},
},
required: [],
},
},
{
name: "search_sbir_awards",
description: "Search SBIR/STTR awards (federal R&D contracts to small businesses). Find by firm, agency, topic, or year.",
inputSchema: {
type: "object",
properties: {
firm: {
type: "string",
description: "Company/firm name"
},
keyword: {
type: "string",
description: "Search keyword in award title/abstract"
},
agency: {
type: "string",
description: "Funding agency (e.g., 'DOD', 'NASA', 'NIH', 'NSF', 'DOE')"
},
year: {
type: "string",
description: "Award year (e.g., '2024')"
},
state: {
type: "string",
description: "State abbreviation"
},
phase: {
type: "string",
description: "SBIR phase: '1' (feasibility), '2' (development), '3' (commercialization)"
},
limit: {
type: "number",
description: "Max results (default 50, max 5000)"
},
},
required: [],
},
},
{
name: "get_sbir_firm_details",
description: "Get detailed information about a specific SBIR firm by name",
inputSchema: {
type: "object",
properties: {
name: {
type: "string",
description: "Exact company name"
},
},
required: ["name"],
},
},
{
name: "sbir_stats",
description: "Get SBIR/STTR statistics - count of firms or awards by criteria",
inputSchema: {
type: "object",
properties: {
type: {
type: "string",
description: "'firms' or 'awards'"
},
state: {
type: "string",
description: "State abbreviation to filter"
},
agency: {
type: "string",
description: "Agency to filter (for awards)"
},
year: {
type: "string",
description: "Year to filter (for awards)"
},
},
required: ["type"],
},
},
],
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "get_size_standards": {
const { naicsCode, industry } = args as any;
const records = await fetchSizeStandards();
let filtered = records;
if (naicsCode) {
filtered = filtered.filter((r: any) =>
r.id?.toString().startsWith(naicsCode)
);
}
if (industry) {
const searchTerm = industry.toLowerCase();
filtered = filtered.filter((r: any) =>
r.description?.toLowerCase().includes(searchTerm)
);
}
// Limit results
filtered = filtered.slice(0, 50);
const standards = filtered.map((r: any) => {
const hasEmployeeLimit = r.employeeCountLimit !== null && r.employeeCountLimit !== undefined;
const hasRevenueLimit = r.revenueLimit !== null && r.revenueLimit !== undefined;
let sizeStandard = "";
let sizeType = "";
if (hasEmployeeLimit) {
sizeStandard = `${r.employeeCountLimit} employees`;
sizeType = "employees";
} else if (hasRevenueLimit) {
sizeStandard = `$${r.revenueLimit}M`;
sizeType = "revenue (millions)";
}
return {
naicsCode: r.id,
industry: r.description,
sizeStandard,
sizeType,
sector: r.sectorDescription,
};
});
return {
content: [{
type: "text",
text: JSON.stringify({
count: standards.length,
note: "Size standard determines max revenue (in millions) or employee count to qualify as 'small business'",
standards,
}, null, 2),
}],
};
}
case "check_size_standard": {
const { naicsCode, annualRevenue, employeeCount } = args as any;
const records = await fetchSizeStandards();
const match = records.find((r: any) => r.id?.toString() === naicsCode);
if (!match) {
return {
content: [{
type: "text",
text: JSON.stringify({
error: `NAICS code ${naicsCode} not found`,
suggestion: "Use get_size_standards with a partial code or industry keyword to find valid codes",
}, null, 2),
}],
};
}
const hasEmployeeLimit = match.employeeCountLimit !== null && match.employeeCountLimit !== undefined;
const hasRevenueLimit = match.revenueLimit !== null && match.revenueLimit !== undefined;
let sizeStandard = "";
let sizeType = "";
let threshold = 0;
if (hasEmployeeLimit) {
threshold = match.employeeCountLimit;
sizeStandard = `${threshold} employees`;
sizeType = "employees";
} else if (hasRevenueLimit) {
threshold = match.revenueLimit;
sizeStandard = `$${threshold}M`;
sizeType = "revenue (millions)";
}
let qualifies: boolean | null = null;
let reason = "";
if (hasEmployeeLimit && employeeCount !== undefined) {
qualifies = employeeCount <= threshold;
reason = `${employeeCount} employees vs ${threshold} employee threshold`;
} else if (hasRevenueLimit && annualRevenue !== undefined) {
qualifies = annualRevenue <= threshold;
reason = `$${annualRevenue}M revenue vs $${threshold}M threshold`;
} else {
reason = hasEmployeeLimit
? `Provide employeeCount to check (threshold: ${threshold} employees)`
: `Provide annualRevenue to check (threshold: $${threshold}M)`;
}
return {
content: [{
type: "text",
text: JSON.stringify({
naicsCode,
industry: match.description,
sizeStandard,
sizeType,
qualifiesAsSmall: qualifies,
reason,
}, null, 2),
}],
};
}
case "search_sbir_firms": {
const { keyword, name: firmName, state, womanOwned, hubzone, limit = 50 } = args as any;
const params: Record<string, string> = {
rows: String(Math.min(limit, 5000)),
};
if (keyword) params.keyword = keyword;
if (firmName) params.name = firmName;
const data = await sbirRequest("firm", params);
let firms = Array.isArray(data) ? data : [];
// Apply filters not supported by API
if (state) {
firms = firms.filter((f: any) => f.state?.toUpperCase() === state.toUpperCase());
}
if (womanOwned === true) {
firms = firms.filter((f: any) => f.woman_owned === "Y" || f.woman_owned === true);
}
if (hubzone === true) {
firms = firms.filter((f: any) => f.hubzone_owned === "Y" || f.hubzone_owned === true);
}
const results = firms.slice(0, limit).map((f: any) => ({
name: f.company_name,
city: f.city,
state: f.state,
zip: f.zip,
website: f.company_url,
awards: f.number_awards,
womanOwned: f.woman_owned === "Y",
hubzone: f.hubzone_owned === "Y",
disadvantaged: f.socially_economically_disadvantaged === "Y",
sbirUrl: f.sbir_url,
}));
return {
content: [{
type: "text",
text: JSON.stringify({
total: results.length,
firms: results,
}, null, 2),
}],
};
}
case "search_sbir_awards": {
const { firm, keyword, agency, year, state, phase, limit = 50 } = args as any;
const params: Record<string, string> = {
rows: String(Math.min(limit, 5000)),
};
if (firm) params.firm = firm;
if (keyword) params.keyword = keyword;
if (agency) params.agency = agency;
if (year) params.year = year;
const data = await sbirRequest("awards", params);
let awards = Array.isArray(data) ? data : [];
// Apply filters
if (state) {
awards = awards.filter((a: any) => a.state?.toUpperCase() === state.toUpperCase());
}
if (phase) {
awards = awards.filter((a: any) => a.phase?.toString() === phase);
}
const results = awards.slice(0, limit).map((a: any) => ({
firm: a.firm,
title: a.award_title,
agency: a.agency,
branch: a.branch,
phase: a.phase,
program: a.program,
year: a.award_year,
amount: a.award_amount,
city: a.city,
state: a.state,
abstract: a.abstract?.substring(0, 300) + (a.abstract?.length > 300 ? "..." : ""),
piName: a.pi_name,
keywords: a.research_area_keywords,
}));
return {
content: [{
type: "text",
text: JSON.stringify({
total: results.length,
awards: results,
}, null, 2),
}],
};
}
case "get_sbir_firm_details": {
const { name: firmName } = args as any;
const data = await sbirRequest("firm", { name: firmName, rows: "10" });
const firms = Array.isArray(data) ? data : [];
const firm = firms.find((f: any) =>
f.company_name?.toLowerCase() === firmName.toLowerCase()
) || firms[0];
if (!firm) {
return {
content: [{
type: "text",
text: JSON.stringify({ error: `Firm "${firmName}" not found` }, null, 2),
}],
};
}
// Also get their awards
const awards = await sbirRequest("awards", { firm: firm.company_name, rows: "100" });
return {
content: [{
type: "text",
text: JSON.stringify({
firm: {
name: firm.company_name,
address: [firm.address1, firm.address2].filter(Boolean).join(", "),
city: firm.city,
state: firm.state,
zip: firm.zip,
website: firm.company_url,
totalAwards: firm.number_awards,
womanOwned: firm.woman_owned === "Y",
hubzone: firm.hubzone_owned === "Y",
disadvantaged: firm.socially_economically_disadvantaged === "Y",
uei: firm.uei,
duns: firm.duns,
sbirUrl: firm.sbir_url,
},
recentAwards: (Array.isArray(awards) ? awards : []).slice(0, 10).map((a: any) => ({
title: a.award_title,
agency: a.agency,
phase: a.phase,
year: a.award_year,
amount: a.award_amount,
})),
}, null, 2),
}],
};
}
case "sbir_stats": {
const { type, state, agency, year } = args as any;
if (type === "firms") {
const params: Record<string, string> = { rows: "5000" };
const data = await sbirRequest("firm", params);
let firms = Array.isArray(data) ? data : [];
if (state) {
firms = firms.filter((f: any) => f.state?.toUpperCase() === state.toUpperCase());
}
const stats = {
totalFirms: firms.length,
womanOwned: firms.filter((f: any) => f.woman_owned === "Y").length,
hubzone: firms.filter((f: any) => f.hubzone_owned === "Y").length,
disadvantaged: firms.filter((f: any) => f.socially_economically_disadvantaged === "Y").length,
};
// Top states if not filtered
if (!state) {
const byState: Record<string, number> = {};
firms.forEach((f: any) => {
if (f.state) byState[f.state] = (byState[f.state] || 0) + 1;
});
const topStates = Object.entries(byState)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([st, count]) => ({ state: st, count }));
(stats as any).topStates = topStates;
}
return {
content: [{
type: "text",
text: JSON.stringify(stats, null, 2),
}],
};
} else if (type === "awards") {
const params: Record<string, string> = { rows: "5000" };
if (agency) params.agency = agency;
if (year) params.year = year;
const data = await sbirRequest("awards", params);
let awards = Array.isArray(data) ? data : [];
if (state) {
awards = awards.filter((a: any) => a.state?.toUpperCase() === state.toUpperCase());
}
const totalAmount = awards.reduce((sum: number, a: any) => {
const amt = parseFloat(a.award_amount?.toString().replace(/[^0-9.]/g, '') || "0");
return sum + amt;
}, 0);
const stats = {
totalAwards: awards.length,
totalAmount: `$${(totalAmount / 1000000).toFixed(1)}M`,
byPhase: {
phase1: awards.filter((a: any) => a.phase === "1" || a.phase === 1).length,
phase2: awards.filter((a: any) => a.phase === "2" || a.phase === 2).length,
},
};
// Top agencies if not filtered
if (!agency) {
const byAgency: Record<string, number> = {};
awards.forEach((a: any) => {
if (a.agency) byAgency[a.agency] = (byAgency[a.agency] || 0) + 1;
});
const topAgencies = Object.entries(byAgency)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([ag, count]) => ({ agency: ag, count }));
(stats as any).topAgencies = topAgencies;
}
return {
content: [{
type: "text",
text: JSON.stringify(stats, null, 2),
}],
};
}
return {
content: [{
type: "text",
text: JSON.stringify({ error: "type must be 'firms' or 'awards'" }, null, 2),
}],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
return {
content: [{ type: "text", text: `Error: ${error.message}` }],
isError: true,
};
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("SBA MCP server running");
}
main().catch(console.error);