BioMCP

// Define API constants export const USER_AGENT = 'BioMCP/0.1.0'; export const DEFAULT_API_TIMEOUT = 30000; // 30 seconds export const RCSB_PDB_DATA_API = 'https://data.rcsb.org/rest/v1'; export const RCSB_PDB_SEARCH_API = 'https://search.rcsb.org/rcsbsearch/v2/query'; export const PDBE_API_BASE = 'https://www.ebi.ac.uk/pdbe/api'; export const UNIPROT_API_BASE = 'https://rest.uniprot.org/uniprotkb'; /** * Make an API request with retry logic and error handling * * @param url The URL to make the request to * @param method The HTTP method to use (GET, POST, etc.) * @param body Optional request body for POST requests * @param timeout Timeout in milliseconds * @returns The parsed JSON response, or null if the request failed */ export async function makeApiRequest( url: string, method: string = 'GET', body?: Record<string, unknown>, timeout: number = DEFAULT_API_TIMEOUT, testMode: boolean = false ): Promise<unknown> { const headers: Record<string, string> = { "User-Agent": USER_AGENT, "Accept": "application/json", }; if (body) { headers["Content-Type"] = "application/json"; } const options: RequestInit = { method, headers, }; if (body) { options.body = JSON.stringify(body); } try { console.error(`Making ${method} request to: ${url}`); if (body) { console.error(`Request body: ${JSON.stringify(body).substring(0, 200)}...`); } // Maximum of 3 retry attempts for transient network issues let response: Response | undefined; let retries = 0; const maxRetries = 3; while (retries < maxRetries) { try { // Create AbortController for timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); options.signal = controller.signal; try { response = await fetch(url, options); } finally { clearTimeout(timeoutId); // In test mode, we need to release all resources explicitly if (testMode && response) { // Force the response body to complete processing await response.text().catch(() => {}); } } break; } catch (fetchError: unknown) { retries++; // Check if this was a timeout if (fetchError instanceof Error && fetchError.name === "AbortError") { console.error(`Request timeout (attempt ${retries}/${maxRetries}) for: ${url}`); } else { console.error(`Fetch error (attempt ${retries}/${maxRetries}):`, fetchError); } if (retries >= maxRetries) throw fetchError; // Exponential backoff const backoffTime = 1000 * Math.pow(2, retries - 1); console.error(`Retrying after ${backoffTime}ms`); await new Promise(resolve => setTimeout(resolve, backoffTime)); } } if (!response) { throw new Error("Failed to get a response after retries"); } console.error(`Response status: ${response.status}`); // For 404 errors with specific PDB IDs, try an alternative URL format if (response.status === 404 && url.includes('/core/entry/')) { const pdbId = url.split('/').pop()?.toUpperCase(); // Try alternative endpoint for older entries if (pdbId && pdbId.length === 4) { console.error(`Entry not found, trying alternative GraphQL approach for PDB ID: ${pdbId}`); const graphqlUrl = 'https://data.rcsb.org/graphql'; const graphqlQuery = { query: `{ entry(entry_id:"${pdbId}") { rcsb_id struct { title } } }` }; // Create new AbortController for this request const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); const graphqlResponse = await fetch(graphqlUrl, { method: 'POST', headers: { "User-Agent": USER_AGENT, "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify(graphqlQuery), signal: controller.signal }); clearTimeout(timeoutId); if (graphqlResponse.ok) { const graphqlData = await graphqlResponse.json(); if (graphqlData?.data?.entry) { console.error(`Successfully retrieved data via GraphQL`); return graphqlData.data.entry; } } console.error(`GraphQL approach also failed`); } } if (!response.ok) { const errorText = await response.text(); console.error(`Error response body: ${errorText.substring(0, 200)}`); throw new Error(`HTTP error! status: ${response.status}`); } const responseText = await response.text(); console.error(`Response body first 300 chars: ${responseText.substring(0, 300)}`); try { const data = JSON.parse(responseText); return data; } catch (parseError) { console.error(`JSON parse error: ${parseError}`); // If parsing fails but we have response text, try to salvage the data if (responseText && responseText.length > 0) { console.error(`Attempting to salvage partial data`); try { // Create minimal structure with title from response text const titleMatch = responseText.match(/"title"\s*:\s*"([^"]+)"/); if (titleMatch && titleMatch[1]) { return { struct: { title: titleMatch[1] } }; } } catch (salvageError) { console.error(`Failed to salvage data:`, salvageError); } } return null; } } catch (error) { console.error(`Error making API request to ${url}:`, error); return null; } }