Skip to main content
Glama
us-legal-apis.ts30.5 kB
// US Legal APIs Integration // Comprehensive integration with US government legal data sources import axios from "axios"; // Relevance scoring utilities function calculateRelevanceScore(text: string, query: string): number { if (!text || !query) return 0; const lowerText = text.toLowerCase(); const queryTerms = query .toLowerCase() .split(/\s+/) .filter((term) => term.length > 2); if (queryTerms.length === 0) return 0; let score = 0; const exactMatch = lowerText.includes(query.toLowerCase()); if (exactMatch) score += 10; // Count how many query terms appear let matchedTerms = 0; for (const term of queryTerms) { if (lowerText.includes(term)) { matchedTerms++; // Bonus for term appearing multiple times const occurrences = (lowerText.match(new RegExp(term, "g")) || []).length; score += Math.min(occurrences, 3); } } // Bonus for all terms matching if (matchedTerms === queryTerms.length) score += 5; // Bonus for title/short title match (handled in specific scoring functions) return score; } function scoreBillRelevance(bill: any, query: string): number { let score = 0; const queryLower = query.toLowerCase(); const queryTerms = queryLower.split(/\s+/).filter((term) => term.length > 2); // High weight for title matches if (bill.title) { const titleScore = calculateRelevanceScore(bill.title, query); score += titleScore * 4; // Increased from 3 } if (bill.shortTitle) { const shortTitleScore = calculateRelevanceScore(bill.shortTitle, query); score += shortTitleScore * 4; // Increased from 3 } // Medium weight for summary if (bill.summary?.text) { score += calculateRelevanceScore(bill.summary.text, query) * 2; } // Check latest action text if (bill.latestAction?.text) { const actionScore = calculateRelevanceScore(bill.latestAction.text, query); score += actionScore * 1.5; } // Enhanced subject/topic matching - check each query term individually if (bill.subjects && queryTerms.length > 0) { for (const subject of bill.subjects) { const subjectName = ( typeof subject === "string" ? subject : subject.name || "" ).toLowerCase(); // Check if any query term matches the subject for (const term of queryTerms) { if (subjectName.includes(term)) { score += 20; // Increased from 15 break; // Only count once per subject } } } } return score; } function scoreDocumentRelevance(doc: any, query: string): number { let score = 0; // High weight for title if (doc.title) { score += calculateRelevanceScore(doc.title, query) * 3; } // Medium weight for abstract if (doc.abstract) { score += calculateRelevanceScore(doc.abstract, query) * 2; } // Check agency names for relevance by matching query terms against agency names if (doc.agency_names && Array.isArray(doc.agency_names)) { const queryTerms = query .toLowerCase() .split(/\s+/) .filter((term) => term.length > 2); for (const agency of doc.agency_names) { const agencyLower = agency.toLowerCase(); // Check if any query term appears in the agency name const agencyMatch = queryTerms.some((term) => agencyLower.includes(term)); if (agencyMatch) { score += 10; } } } return score; } // API Base URLs export const API_ENDPOINTS = { CONGRESS: "https://api.congress.gov/v3", FEDERAL_REGISTER: "https://www.federalregister.gov/api/v1", US_CODE: "https://uscode.house.gov/api", REGULATIONS_GOV: "https://api.regulations.gov/v4", GPO: "https://api.govinfo.gov", COURT_LISTENER: "https://www.courtlistener.com/api/rest/v3", } as const; // Types for US Legal Data export interface CongressBill { congress: number; type: string; number: number; title: string; shortTitle?: string; summary?: string; url: string; introducedDate: string; latestAction?: { actionDate: string; text: string; }; subjects?: string[]; sponsors?: Array<{ bioguideId: string; firstName: string; lastName: string; party: string; state: string; }>; } export interface FederalRegisterDocument { document_number: string; title: string; abstract?: string; publication_date: string; effective_date?: string; agency_names: string[]; document_type: string; pdf_url: string; html_url: string; json_url: string; sections?: Array<{ title: string; content: string; }>; } export interface USCodeSection { title: number; section: string; text: string; url: string; last_updated: string; source: string; } export interface RegulationComment { id: string; comment: string; posted_date: string; agency_id: string; document_id: string; submitter_name?: string; organization?: string; } export interface CourtOpinion { id: number; case_name: string; case_name_full?: string; date_filed: string; date_modified?: string; court: string; court_id: number; jurisdiction: string; citation?: string; citation_count?: number; precedential_status: string; url: string; absolute_url: string; download_url?: string; plain_text?: string; html?: string; html_lawbox?: string; html_columbia?: string; html_anon_2020?: string; judges?: string[]; docket?: string; docket_number?: string; slug?: string; } export interface CongressVote { rollNumber: number; url: string; voteDate: string; voteQuestion: string; voteResult: string; voteTitle: string; voteType: string; chamber: string; congress: number; session: number; members?: Array<{ member: { bioguideId: string; firstName: string; lastName: string; party: string; state: string; }; votePosition: string; }>; } export interface Committee { systemCode: string; name: string; url: string; chamber?: string; committeeType?: string; subcommittees?: Array<{ systemCode: string; name: string; url: string; }>; } // Congress.gov API Functions export class CongressAPI { private apiKey: string; constructor(apiKey?: string) { this.apiKey = apiKey || process.env.CONGRESS_API_KEY || ""; } async searchBills( query: string, congress?: number, limit: number = 20, ): Promise<CongressBill[]> { try { // Get more results than needed for filtering (increased for better relevance) const fetchLimit = Math.min(limit * 5, 250); const params = new URLSearchParams({ q: query, limit: fetchLimit.toString(), format: "json", ...(this.apiKey && { api_key: this.apiKey }), ...(congress && { congress: congress.toString() }), }); const response = await axios.get( `${API_ENDPOINTS.CONGRESS}/bill?${params}`, ); const bills = (response.data.bills || []).map((bill: any) => ({ congress: bill.congress, type: bill.type, number: bill.number, title: bill.title, shortTitle: bill.shortTitle, summary: bill.summary?.text, url: bill.url, introducedDate: bill.introducedDate, latestAction: bill.latestAction ? { actionDate: bill.latestAction.actionDate, text: bill.latestAction.text, } : undefined, subjects: bill.subjects?.map((s: any) => s.name || s), sponsors: bill.sponsors?.map((s: any) => ({ bioguideId: s.bioguideId, firstName: s.firstName, lastName: s.lastName, party: s.party, state: s.state, })), })); // Score and sort by relevance const scoredBills = bills.map((bill: any) => ({ ...bill, relevanceScore: scoreBillRelevance(bill, query), })); // Sort by relevance (highest first) scoredBills.sort((a: any, b: any) => b.relevanceScore - a.relevanceScore); // Log scoring for debugging (first few bills) if (scoredBills.length > 0) { console.error( `Congress Bills Relevance Scores (top 5): ${scoredBills .slice(0, 5) .map( (b: any) => `${b.title.substring(0, 40)}... (score: ${b.relevanceScore})`, ) .join(", ")}`, ); } // Use smart filtering: prioritize high-scoring bills but always return top results // Only filter if we have many high-scoring options const highRelevanceBills = scoredBills.filter( (bill: any) => bill.relevanceScore >= 5, ); if (highRelevanceBills.length >= limit) { // We have enough high-relevance results, return those return highRelevanceBills .slice(0, limit) .map(({ relevanceScore, ...bill }: any) => bill); } else if (scoredBills.length > 0) { // Return top results sorted by relevance, even if scores are low // This ensures we always return something if the API returned results return scoredBills .slice(0, limit) .map(({ relevanceScore, ...bill }: any) => bill); } return []; } catch (error) { console.error("Congress API error:", error); return []; } } async getBillDetails( congress: number, billType: string, billNumber: number, ): Promise<CongressBill | null> { try { const params = new URLSearchParams({ format: "json", ...(this.apiKey && { api_key: this.apiKey }), }); const response = await axios.get( `${API_ENDPOINTS.CONGRESS}/bill/${congress}/${billType}/${billNumber}?${params}`, ); const bill = response.data.bill; return { congress: bill.congress, type: bill.type, number: bill.number, title: bill.title, shortTitle: bill.shortTitle, summary: bill.summary?.text, url: bill.url, introducedDate: bill.introducedDate, latestAction: bill.latestAction ? { actionDate: bill.latestAction.actionDate, text: bill.latestAction.text, } : undefined, subjects: bill.subjects?.map((s: any) => s.name), sponsors: bill.sponsors?.map((s: any) => ({ bioguideId: s.bioguideId, firstName: s.firstName, lastName: s.lastName, party: s.party, state: s.state, })), }; } catch (error) { console.error("Congress API error:", error); return null; } } async getRecentBills( congress?: number, limit: number = 20, ): Promise<CongressBill[]> { try { const params = new URLSearchParams({ limit: limit.toString(), format: "json", ...(this.apiKey && { api_key: this.apiKey }), ...(congress && { congress: congress.toString() }), }); const response = await axios.get( `${API_ENDPOINTS.CONGRESS}/bill?${params}`, ); return ( response.data.bills?.map((bill: any) => ({ congress: bill.congress, type: bill.type, number: bill.number, title: bill.title, shortTitle: bill.shortTitle, summary: bill.summary?.text, url: bill.url, introducedDate: bill.introducedDate, latestAction: bill.latestAction ? { actionDate: bill.latestAction.actionDate, text: bill.latestAction.text, } : undefined, subjects: bill.subjects?.map((s: any) => s.name), sponsors: bill.sponsors?.map((s: any) => ({ bioguideId: s.bioguideId, firstName: s.firstName, lastName: s.lastName, party: s.party, state: s.state, })), })) || [] ); } catch (error) { console.error("Congress API error:", error); return []; } } async searchVotes( congress?: number, chamber?: "House" | "Senate", limit: number = 20, ): Promise<CongressVote[]> { try { const params = new URLSearchParams({ limit: limit.toString(), format: "json", ...(this.apiKey && { api_key: this.apiKey }), ...(congress && { congress: congress.toString() }), ...(chamber && { chamber }), }); const response = await axios.get( `${API_ENDPOINTS.CONGRESS}/vote?${params}`, ); return ( response.data.votes?.map((vote: any) => ({ rollNumber: vote.rollNumber, url: vote.url, voteDate: vote.voteDate, voteQuestion: vote.voteQuestion, voteResult: vote.voteResult, voteTitle: vote.voteTitle, voteType: vote.voteType, chamber: vote.chamber, congress: vote.congress, session: vote.session, members: vote.members?.map((m: any) => ({ member: { bioguideId: m.member?.bioguideId, firstName: m.member?.firstName, lastName: m.member?.lastName, party: m.member?.party, state: m.member?.state, }, votePosition: m.votePosition, })), })) || [] ); } catch (error: any) { if (error.response?.status === 403) { console.error( "Congress API: API key required for votes/committees. Get one at https://api.congress.gov/", ); } else { console.error("Congress API error:", error.message || error); } return []; } } async getCommittees( congress?: number, chamber?: "House" | "Senate", ): Promise<Committee[]> { try { const params = new URLSearchParams({ format: "json", ...(this.apiKey && { api_key: this.apiKey }), ...(congress && { congress: congress.toString() }), ...(chamber && { chamber }), }); const response = await axios.get( `${API_ENDPOINTS.CONGRESS}/committee?${params}`, ); return ( response.data.committees?.map((committee: any) => ({ systemCode: committee.systemCode, name: committee.name, url: committee.url, chamber: committee.chamber, committeeType: committee.committeeType, subcommittees: committee.subcommittees?.map((sub: any) => ({ systemCode: sub.systemCode, name: sub.name, url: sub.url, })), })) || [] ); } catch (error: any) { if (error.response?.status === 403) { console.error( "Congress API: API key required for votes/committees. Get one at https://api.congress.gov/", ); } else { console.error("Congress API error:", error.message || error); } return []; } } } // Federal Register API Functions export class FederalRegisterAPI { async searchDocuments( query: string, limit: number = 20, ): Promise<FederalRegisterDocument[]> { try { // Get more results than needed for filtering const fetchLimit = Math.min(limit * 3, 100); const params = new URLSearchParams({ q: query, per_page: fetchLimit.toString(), order: "relevance", }); const response = await axios.get( `${API_ENDPOINTS.FEDERAL_REGISTER}/documents?${params}`, ); const documents = (response.data.results || []).map((doc: any) => ({ document_number: doc.document_number, title: doc.title, abstract: doc.abstract, publication_date: doc.publication_date, effective_date: doc.effective_date, agency_names: doc.agency_names, document_type: doc.document_type, pdf_url: doc.pdf_url, html_url: doc.html_url, json_url: doc.json_url, })); // Score and sort by relevance const scoredDocs = documents.map((doc: any) => ({ ...doc, relevanceScore: scoreDocumentRelevance(doc, query), })); // Sort by relevance (highest first) scoredDocs.sort((a: any, b: any) => b.relevanceScore - a.relevanceScore); // Filter out very low relevance results (score < 5) and return top results const relevantDocs = scoredDocs .filter((doc: any) => doc.relevanceScore >= 5) .slice(0, limit) .map(({ relevanceScore, ...doc }: any) => doc); // Remove score from output // If we filtered out too many, return the top results even if low score if (relevantDocs.length < limit && scoredDocs.length > 0) { return scoredDocs .slice(0, limit) .map(({ relevanceScore, ...doc }: any) => doc); } return relevantDocs; } catch (error) { console.error("Federal Register API error:", error); return []; } } async getRecentDocuments( limit: number = 20, ): Promise<FederalRegisterDocument[]> { try { const params = new URLSearchParams({ per_page: limit.toString(), order: "newest", }); const response = await axios.get( `${API_ENDPOINTS.FEDERAL_REGISTER}/documents?${params}`, ); return ( response.data.results?.map((doc: any) => ({ document_number: doc.document_number, title: doc.title, abstract: doc.abstract, publication_date: doc.publication_date, effective_date: doc.effective_date, agency_names: doc.agency_names, document_type: doc.document_type, pdf_url: doc.pdf_url, html_url: doc.html_url, json_url: doc.json_url, })) || [] ); } catch (error) { console.error("Federal Register API error:", error); return []; } } async getDocumentDetails( documentNumber: string, ): Promise<FederalRegisterDocument | null> { try { const response = await axios.get( `${API_ENDPOINTS.FEDERAL_REGISTER}/documents/${documentNumber}.json`, ); const doc = response.data; return { document_number: doc.document_number, title: doc.title, abstract: doc.abstract, publication_date: doc.publication_date, effective_date: doc.effective_date, agency_names: doc.agency_names, document_type: doc.document_type, pdf_url: doc.pdf_url, html_url: doc.html_url, json_url: doc.json_url, sections: doc.sections?.map((s: any) => ({ title: s.title, content: s.content, })), }; } catch (error) { console.error("Federal Register API error:", error); return null; } } } // US Code API Functions export class USCodeAPI { async searchCode( query: string, title?: number, limit: number = 20, ): Promise<USCodeSection[]> { try { const params = new URLSearchParams({ q: query, limit: limit.toString(), ...(title && { title: title.toString() }), }); // Add timeout and retry logic for US Code API const response = await axios.get( `${API_ENDPOINTS.US_CODE}/search?${params}`, { timeout: 30000, // 30 second timeout validateStatus: (status) => status < 500, // Don't throw on 4xx }, ); // Handle API errors gracefully if (response.status >= 400) { console.error( `US Code API error: ${response.status} - ${response.statusText}`, ); return []; } return ( response.data.results?.map((section: any) => ({ title: section.title, section: section.section, text: section.text, url: section.url, last_updated: section.last_updated, source: section.source, })) || [] ); } catch (error: any) { if (error.code === "ETIMEDOUT" || error.code === "ECONNABORTED") { console.error( "US Code API: Connection timeout. The API may be temporarily unavailable.", ); } else if (error.code === "ECONNREFUSED") { console.error( "US Code API: Connection refused. The API endpoint may be down.", ); } else { console.error("US Code API error:", error.message || error); } return []; } } async getSection( title: number, section: string, ): Promise<USCodeSection | null> { try { const response = await axios.get( `${API_ENDPOINTS.US_CODE}/title/${title}/section/${section}`, ); const data = response.data; return { title: data.title, section: data.section, text: data.text, url: data.url, last_updated: data.last_updated, source: data.source, }; } catch (error) { console.error("US Code API error:", error); return null; } } } // CourtListener API Functions export class CourtListenerAPI { private apiKey?: string; constructor(apiKey?: string) { // CourtListener API doesn't require key but has rate limits without one this.apiKey = apiKey || process.env.COURT_LISTENER_API_KEY; } async searchOpinions( query: string, court?: string, limit: number = 20, ): Promise<CourtOpinion[]> { try { const params = new URLSearchParams({ q: query, type: "o", // 'o' for opinions page_size: limit.toString(), ordering: "-date_filed", ...(court && { court: court }), }); const headers: Record<string, string> = { Accept: "application/json", }; if (this.apiKey) { headers["Authorization"] = `Token ${this.apiKey}`; } const response = await axios.get( `${API_ENDPOINTS.COURT_LISTENER}/search/`, { params, headers }, ); return ( response.data.results?.map((opinion: any) => { const absoluteUrl = opinion.absoluteUrl || opinion.absolute_url || (opinion.id ? `https://www.courtlistener.com/opinion/${opinion.id}/` : undefined); return { id: opinion.id, case_name: opinion.caseName || opinion.case_name, case_name_full: opinion.caseNameFull || opinion.case_name_full, date_filed: opinion.dateFiled || opinion.date_filed, date_modified: opinion.dateModified || opinion.date_modified, court: opinion.court || opinion.court_name, court_id: opinion.courtId || opinion.court_id, jurisdiction: opinion.jurisdiction, citation: opinion.citation, citation_count: opinion.citationCount || opinion.citation_count, precedential_status: opinion.precedentialStatus || opinion.precedential_status, url: absoluteUrl, absolute_url: absoluteUrl, download_url: opinion.downloadUrl || opinion.download_url, plain_text: opinion.plainText || opinion.plain_text, html: opinion.html, html_lawbox: opinion.htmlLawbox || opinion.html_lawbox, html_columbia: opinion.htmlColumbia || opinion.html_columbia, html_anon_2020: opinion.htmlAnon2020 || opinion.html_anon_2020, judges: opinion.judges, docket: opinion.docket, docket_number: opinion.docketNumber || opinion.docket_number, slug: opinion.slug, }; }) || [] ); } catch (error: any) { if (error.response?.status === 403) { console.error( "CourtListener API: API key required. Get one at https://www.courtlistener.com/api/", ); } else { console.error("CourtListener API error:", error.message || error); } return []; } } async getRecentOpinions( court?: string, limit: number = 20, ): Promise<CourtOpinion[]> { try { const params = new URLSearchParams({ type: "o", // 'o' for opinions page_size: limit.toString(), ordering: "-date_filed", ...(court && { court: court }), }); const headers: Record<string, string> = { Accept: "application/json", }; if (this.apiKey) { headers["Authorization"] = `Token ${this.apiKey}`; } const response = await axios.get( `${API_ENDPOINTS.COURT_LISTENER}/search/`, { params, headers }, ); return ( response.data.results?.map((opinion: any) => { const absoluteUrl = opinion.absoluteUrl || opinion.absolute_url || (opinion.id ? `https://www.courtlistener.com/opinion/${opinion.id}/` : undefined); return { id: opinion.id, case_name: opinion.caseName || opinion.case_name, case_name_full: opinion.caseNameFull || opinion.case_name_full, date_filed: opinion.dateFiled || opinion.date_filed, date_modified: opinion.dateModified || opinion.date_modified, court: opinion.court || opinion.court_name, court_id: opinion.courtId || opinion.court_id, jurisdiction: opinion.jurisdiction, citation: opinion.citation, citation_count: opinion.citationCount || opinion.citation_count, precedential_status: opinion.precedentialStatus || opinion.precedential_status, url: absoluteUrl, absolute_url: absoluteUrl, download_url: opinion.downloadUrl || opinion.download_url, plain_text: opinion.plainText || opinion.plain_text, html: opinion.html, html_lawbox: opinion.htmlLawbox || opinion.html_lawbox, html_columbia: opinion.htmlColumbia || opinion.html_columbia, html_anon_2020: opinion.htmlAnon2020 || opinion.html_anon_2020, judges: opinion.judges, docket: opinion.docket, docket_number: opinion.docketNumber || opinion.docket_number, slug: opinion.slug, }; }) || [] ); } catch (error: any) { if (error.response?.status === 403) { console.error( "CourtListener API: API key required. Get one at https://www.courtlistener.com/api/", ); } else { console.error("CourtListener API error:", error.message || error); } return []; } } async getOpinion(opinionId: number): Promise<CourtOpinion | null> { try { const headers: Record<string, string> = {}; if (this.apiKey) { headers["Authorization"] = `Token ${this.apiKey}`; } const response = await axios.get( `${API_ENDPOINTS.COURT_LISTENER}/search/${opinionId}/`, { headers }, ); const opinion = response.data; return { id: opinion.id, case_name: opinion.caseName, case_name_full: opinion.caseNameFull, date_filed: opinion.dateFiled, date_modified: opinion.dateModified, court: opinion.court, court_id: opinion.courtId, jurisdiction: opinion.jurisdiction, citation: opinion.citation, citation_count: opinion.citationCount, precedential_status: opinion.precedentialStatus, url: opinion.absoluteUrl, absolute_url: opinion.absoluteUrl, download_url: opinion.downloadUrl, plain_text: opinion.plainText, html: opinion.html, html_lawbox: opinion.htmlLawbox, html_columbia: opinion.htmlColumbia, html_anon_2020: opinion.htmlAnon2020, judges: opinion.judges, docket: opinion.docket, docket_number: opinion.docketNumber, slug: opinion.slug, }; } catch (error) { console.error("CourtListener API error:", error); return null; } } } // Regulations.gov API Functions export class RegulationsGovAPI { private apiKey: string; constructor(apiKey?: string) { this.apiKey = apiKey || process.env.REGULATIONS_GOV_API_KEY || ""; } async searchComments( query: string, limit: number = 20, ): Promise<RegulationComment[]> { try { const params = new URLSearchParams({ q: query, "page[size]": limit.toString(), sort: "-postedDate", ...(this.apiKey && { api_key: this.apiKey }), }); const response = await axios.get( `${API_ENDPOINTS.REGULATIONS_GOV}/comments?${params}`, ); return ( response.data.data?.map((comment: any) => ({ id: comment.id, comment: comment.attributes.comment, posted_date: comment.attributes.postedDate, agency_id: comment.attributes.agencyId, document_id: comment.attributes.documentId, submitter_name: comment.attributes.submitterName, organization: comment.attributes.organization, })) || [] ); } catch (error) { console.error("Regulations.gov API error:", error); return []; } } } // Main US Legal API Class export class USLegalAPI { public congress: CongressAPI; public federalRegister: FederalRegisterAPI; public usCode: USCodeAPI; public regulations: RegulationsGovAPI; public courtListener: CourtListenerAPI; constructor(apiKeys?: { congress?: string; regulationsGov?: string; courtListener?: string; }) { this.congress = new CongressAPI(apiKeys?.congress); this.federalRegister = new FederalRegisterAPI(); this.usCode = new USCodeAPI(); this.regulations = new RegulationsGovAPI(apiKeys?.regulationsGov); this.courtListener = new CourtListenerAPI(apiKeys?.courtListener); } // Comprehensive search across all sources async searchAll( query: string, limit: number = 20, ): Promise<{ bills: CongressBill[]; regulations: FederalRegisterDocument[]; codeSections: USCodeSection[]; comments: RegulationComment[]; }> { const [bills, regulations, codeSections, comments] = await Promise.all([ this.congress.searchBills(query, undefined, Math.ceil(limit / 4)), this.federalRegister.searchDocuments(query, Math.ceil(limit / 4)), this.usCode.searchCode(query, undefined, Math.ceil(limit / 4)), this.regulations.searchComments(query, Math.ceil(limit / 4)), ]); return { bills, regulations, codeSections, comments, }; } } export default USLegalAPI;

Latest Blog Posts

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/JamesANZ/us-legal-mcp'

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