Skip to main content
Glama
jbdamask

NIH RePORTER MCP

by jbdamask

search_projects

Search NIH-funded research projects using filters like fiscal years, PI names, organization details, funding amounts, and COVID-19 response categories to find specific studies.

Instructions

Search for NIH funded projects with advanced criteria

Args:
    fiscal_years: Comma-separated list of fiscal years (e.g., "2022,2023")
    pi_names: Comma-separated list of PI names (will match any of the names)
    organization: Name of the organization
    org_state: Two-letter state code (e.g., "CA", "NY")
    org_city: City name
    org_type: Organization type
    org_department: Department name
    min_amount: Minimum award amount
    max_amount: Maximum award amount
    covid_response: COVID-19 response category (options: "Reg-CV", "CV", "C3", "C4", "C5", "C6")
    funding_mechanism: Type of funding (e.g., "R01", "F32", "K99")
    ic_code: Institute or Center code (e.g., "NCI", "NIMH")
    rcdc_terms: Comma-separated RCDC terms for research categorization
    start_date: Project start date (YYYY-MM-DD)
    end_date: Project end date (YYYY-MM-DD)
    newly_added_only: Only show recently added projects
    include_abstracts: Include project abstracts in results
    limit: Maximum number of results to return (default: 10, max: 50)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
fiscal_yearsNo
pi_namesNo
organizationNo
org_stateNo
org_cityNo
org_typeNo
org_departmentNo
min_amountNo
max_amountNo
covid_responseNo
funding_mechanismNo
ic_codeNo
rcdc_termsNo
start_dateNo
end_dateNo
newly_added_onlyNo
include_abstractsNo
limitNo

Implementation Reference

  • Primary handler function for 'search_projects' tool. Registered via @mcp.tool() decorator. Parses input parameters into API criteria, calls NIHReporterClient.get_projects(), formats results as markdown, handles errors.
    @mcp.tool()
    async def search_projects(
        fiscal_years: Optional[str] = None,
        pi_names: Optional[str] = None,
        organization: Optional[str] = None,
        org_state: Optional[str] = None,
        org_city: Optional[str] = None,
        org_type: Optional[str] = None,
        org_department: Optional[str] = None,
        min_amount: Optional[float] = None,
        max_amount: Optional[float] = None,
        covid_response: Optional[str] = None,
        funding_mechanism: Optional[str] = None,
        ic_code: Optional[str] = None,
        rcdc_terms: Optional[str] = None,
        start_date: Optional[str] = None,
        end_date: Optional[str] = None,
        newly_added_only: Optional[bool] = False,
        include_abstracts: Optional[bool] = True,
        limit: Optional[int] = 10
    ) -> str:
        """
        Search for NIH funded projects with advanced criteria
        
        Args:
            fiscal_years: Comma-separated list of fiscal years (e.g., "2022,2023")
            pi_names: Comma-separated list of PI names (will match any of the names)
            organization: Name of the organization
            org_state: Two-letter state code (e.g., "CA", "NY")
            org_city: City name
            org_type: Organization type
            org_department: Department name
            min_amount: Minimum award amount
            max_amount: Maximum award amount
            covid_response: COVID-19 response category (options: "Reg-CV", "CV", "C3", "C4", "C5", "C6")
            funding_mechanism: Type of funding (e.g., "R01", "F32", "K99")
            ic_code: Institute or Center code (e.g., "NCI", "NIMH")
            rcdc_terms: Comma-separated RCDC terms for research categorization
            start_date: Project start date (YYYY-MM-DD)
            end_date: Project end date (YYYY-MM-DD)
            newly_added_only: Only show recently added projects
            include_abstracts: Include project abstracts in results
            limit: Maximum number of results to return (default: 10, max: 50)
        """
        try:
            logger.info(f"Advanced search request received with parameters: {locals()}")
            
            criteria = {}
            
            # Basic criteria
            if fiscal_years:
                try:
                    # Handle escaped quotes and clean the input string
                    years_str = fiscal_years.replace('\\"', '').replace('"', '').strip()
                    years = [int(year.strip()) for year in years_str.split(",") if year.strip()]
                    if not years:
                        raise ValueError("No valid years found after parsing")
                    criteria["fiscal_years"] = years
                except ValueError as e:
                    logger.error(f"Invalid fiscal years format: {fiscal_years}, error: {str(e)}")
                    return f"Error: Invalid fiscal years format. Please provide comma-separated years (e.g., 2020,2021)"
            
            # Handle multiple PI names
            if pi_names:
                try:
                    # Handle escaped quotes and clean the input string
                    names_str = pi_names.replace('\\"', '').replace('"', '').strip()
                    names = [name.strip() for name in names_str.split(",") if name.strip()]
                    if not names:
                        raise ValueError("No valid names found after parsing")
                    criteria["pi_names"] = [{"any_name": name} for name in names]
                except Exception as e:
                    logger.error(f"Invalid PI names format: {pi_names}, error: {str(e)}")
                    return f"Error: Invalid PI names format. Please provide comma-separated names"
            
            # Organization criteria
            org_criteria = {}
            if organization:
                org_criteria["org_names"] = [organization.strip().strip('"').strip("'")]
            if org_state:
                org_criteria["org_states"] = [org_state.strip().strip('"').strip("'").upper()]
            if org_city:
                org_criteria["org_cities"] = [org_city.strip().strip('"').strip("'")]
            if org_type:
                org_criteria["org_types"] = [org_type.strip().strip('"').strip("'")]
            if org_department:
                org_criteria["org_depts"] = [org_department.strip().strip('"').strip("'")]
            if org_criteria:
                criteria.update(org_criteria)
            
            # Award amount range
            if min_amount is not None or max_amount is not None:
                criteria["award_amount_range"] = {
                    "min_amount": min_amount if min_amount is not None else 0,
                    "max_amount": max_amount if max_amount is not None else float('inf')
                }
            
            # COVID response
            if covid_response:
                criteria["covid_response"] = [covid_response]
            
            # Funding mechanism
            if funding_mechanism:
                criteria["funding_mechanism"] = funding_mechanism.strip().strip('"').strip("'")
                
            # Institute/Center code
            if ic_code:
                criteria["agency_ic_admin"] = ic_code.strip().strip('"').strip("'").upper()
                
            # RCDC terms
            if rcdc_terms:
                try:
                    terms_str = rcdc_terms.strip().strip('"').strip("'")
                    terms = [term.strip() for term in terms_str.split(",")]
                    criteria["rcdc_terms"] = terms
                except Exception as e:
                    logger.error(f"Invalid RCDC terms format: {rcdc_terms}")
                    return f"Error: Invalid RCDC terms format. Please provide comma-separated terms without quotes"
            
            # Date criteria
            if start_date or end_date:
                criteria["date_range"] = {
                    "start_date": start_date.strip().strip('"').strip("'") if start_date else None,
                    "end_date": end_date.strip().strip('"').strip("'") if end_date else None
                }
            
            # Other filters
            if newly_added_only:
                criteria["newly_added_projects_only"] = True
            
            # Include fields
            include_fields = [
                "project_num", "project_title", "principal_investigators",
                "organization", "fiscal_year", "award_amount",
                "project_start_date", "project_end_date", "funding_mechanism",
                "agency_ic_admin", "rcdc_terms"
            ]
            if include_abstracts:
                include_fields.extend(["abstract_text", "phr_text"])
            
            # Ensure limit is within bounds
            try:
                criteria["limit"] = min(max(1, int(limit)), 50)
            except (ValueError, TypeError):
                logger.error(f"Invalid limit value: {limit}")
                return f"Error: Invalid limit value. Please provide a number between 1 and 50"
            
            logger.info(f"Constructed search criteria: {json.dumps(criteria, indent=2)}")
            
            results = await api_client.get_projects(criteria)
            return api_client.format_project_results(results)
            
        except Exception as e:
            logger.error(f"Project search failed: {str(e)}", exc_info=True)
            return f"Project search failed: {str(e)}\nPlease check the logs for more details."
  • NIHReporterClient class providing helper methods get_projects (API call) and format_project_results (markdown formatting) used by the search_projects tool.
    class NIHReporterClient:
        """Client for interacting with the NIH RePORTER API"""
        
        def __init__(self):
            self.headers = {
                "Content-Type": "application/json",
            }
        
        async def get_projects(self, criteria: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
            """Get projects from NIH RePORTER API"""
            logger.info(f"Fetching projects from NIH RePORTER with criteria: {criteria}")
            async with httpx.AsyncClient() as client:
                payload = {
                    "criteria": criteria or {},
                    "limit": criteria.get("limit", 50),
                    "offset": criteria.get("offset", 0),
                    "sort_field": criteria.get("sort_field", "project_start_date"),
                    "sort_order": criteria.get("sort_order", "desc")
                }
                logger.debug(f"Sending payload to NIH API: {json.dumps(payload, indent=2)}")
                
                try:
                    response = await client.post(
                        f"{API_BASE}/projects/search",
                        headers=self.headers,
                        json=payload
                    )
                    response.raise_for_status()
                    response_data = response.json()
                    logger.debug(f"Received response: {json.dumps(response_data, indent=2)}")
                    return response_data
                except httpx.HTTPStatusError as e:
                    logger.error(f"HTTP error occurred: {e.response.status_code} - {e.response.text}")
                    raise
                except json.JSONDecodeError as e:
                    logger.error(f"Failed to parse API response: {e}")
                    logger.error(f"Raw response: {response.text}")
                    raise
                except Exception as e:
                    logger.error(f"Unexpected error during API call: {str(e)}")
                    raise
    
        def format_project_results(self, results: Dict[str, Any], include_publications: bool = False) -> str:
            """Format project results into markdown string with optional publication links"""
            logger.debug(f"Formatting results: {json.dumps(results, indent=2)}")
            
            if not results.get("results"):
                logger.info("No results found in API response")
                return "No projects found."
            
            try:
                formatted_results = []
                for project in results["results"]:
                    # Format amount safely
                    award_amount = project.get('award_amount')
                    amount_str = f"${award_amount:,.2f}" if award_amount is not None else "N/A"
                    
                    # Get organization details safely
                    org = project.get('organization', {})
                    org_parts = []
                    if org.get('org_name'):
                        org_parts.append(org.get('org_name'))
                    if org.get('org_city'):
                        org_parts.append(org.get('org_city'))
                    if org.get('org_state'):
                        org_parts.append(org.get('org_state'))
                    org_details = ", ".join(org_parts) if org_parts else "N/A"
                    
                    # Format dates safely
                    start_date = project.get('project_start_date') or 'N/A'
                    end_date = project.get('project_end_date') or 'N/A'
                    
                    # Get study section info safely
                    study_section = project.get('study_section', {})
                    study_name = study_section.get('study_section_name', 'N/A')
                    srg_code = study_section.get('srg_code', '')
                    study_info = f"{study_name} ({srg_code})" if srg_code else study_name
                    
                    # Handle PIs safely
                    pis = project.get('principal_investigators', [])
                    pi_names = [pi.get('full_name', 'N/A') for pi in pis if pi.get('full_name')]
                    pi_str = ", ".join(pi_names) if pi_names else "N/A"
                    
                    # Build project info with markdown formatting
                    project_info = [
                        f"### {project.get('project_title', 'Untitled Project')}",
                        "",
                        f"**Project Number:** `{project.get('project_num', 'N/A')}`",
                        f"**Principal Investigator(s):** {pi_str}",
                        f"**Organization:** {org_details}",
                        f"**Fiscal Year:** {project.get('fiscal_year', 'N/A')}",
                        f"**Award Amount:** {amount_str}",
                        f"**Project Period:** {start_date} to {end_date}",
                        f"**Study Section:** {study_info}"
                    ]
                    
                    # Add funding mechanism if available
                    mechanism = project.get('funding_mechanism')
                    if mechanism:
                        project_info.append(f"**Funding Mechanism:** {mechanism}")
                    
                    # Add IC Code if available
                    ic_code = project.get('agency_ic_admin')
                    if ic_code:
                        project_info.append(f"**Institute/Center:** {ic_code}")
                    
                    # Add RCDC terms if available
                    rcdc_terms = project.get('rcdc_terms', [])
                    if rcdc_terms:
                        terms_str = ", ".join(f"`{term}`" for term in rcdc_terms if term)
                        if terms_str:
                            project_info.append(f"**RCDC Terms:** {terms_str}")
                    
                    # Add abstract if it exists
                    abstract = project.get('abstract_text')
                    if abstract:
                        project_info.extend([
                            "",
                            "#### Abstract",
                            abstract
                        ])
                    
                    # Add PHR if it exists
                    phr = project.get('phr_text')
                    if phr:
                        project_info.extend([
                            "",
                            "#### Public Health Relevance",
                            phr
                        ])
                    
                    # Add publications section if available
                    if include_publications and project.get('related_publications'):
                        project_info.extend([
                            "",
                            "#### Related Publications"
                        ])
                        
                        for pub in project.get('related_publications', []):
                            pmid = pub.get('pmid')
                            title = pub.get('title', 'Untitled Publication')
                            
                            pub_info = [""]
                            
                            # Always show the PMID if we have it
                            if pmid:
                                pub_info.append(f"##### {title} (PMID: [{pmid}](https://pubmed.ncbi.nlm.nih.gov/{pmid}/))")
                            else:
                                pub_info.append(f"##### {title}")
                            
                            # Add other details if we have them
                            if pub.get('authors'):
                                author_str = ", ".join(pub['authors'])
                                pub_info.append(f"**Authors:** {author_str}")
                            
                            if pub.get('journal_title'):
                                pub_info.append(f"**Journal:** {pub['journal_title']}")
                                
                            if pub.get('publication_year'):
                                pub_info.append(f"**Year:** {pub['publication_year']}")
                                
                            if pub.get('doi'):
                                pub_info.append(f"**DOI:** [{pub['doi']}](https://doi.org/{pub['doi']})")
                            
                            project_info.extend(pub_info)
                    
                    project_info.extend(["", "---", ""])
                    formatted_results.append("\n".join(filter(None, project_info)))
                
                total = f"# NIH RePORTER Search Results\n\n**Total matching projects:** {results.get('meta', {}).get('total', 0)}"
                return f"{total}\n\n" + "\n".join(formatted_results)
                
            except Exception as e:
                logger.error(f"Error formatting results: {str(e)}")
                logger.error(f"Results that caused error: {json.dumps(results, indent=2)}")
                raise
  • @mcp.tool() decorator registers search_projects as an MCP tool in FastMCP server.
    @mcp.tool()
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries full burden for behavioral disclosure. While it mentions the tool searches with advanced criteria and provides parameter details, it doesn't describe important behavioral aspects like authentication requirements, rate limits, pagination behavior, error handling, or what the search results actually contain beyond the mention of abstracts.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is well-structured with a clear opening sentence followed by a comprehensive parameter list. While lengthy due to the many parameters, every sentence serves a purpose. The parameter explanations are efficient and avoid redundancy, though the overall length might benefit from some grouping or categorization.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex search tool with 18 parameters and no output schema, the description provides excellent parameter documentation but lacks information about the search results format, pagination, sorting options, and how multiple criteria interact. Without annotations or output schema, the agent won't know what fields are returned or how results are structured beyond the mention of abstracts.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters5/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description provides extensive parameter documentation with clear examples and explanations for all 18 parameters, compensating completely for the 0% schema description coverage. It adds significant value beyond the basic schema by explaining format requirements (e.g., 'comma-separated list', 'YYYY-MM-DD'), providing examples, and clarifying parameter behavior (e.g., 'will match any of the names', 'default: 10, max: 50').

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool searches for NIH funded projects with advanced criteria, providing a specific verb ('search') and resource ('NIH funded projects'). However, it doesn't differentiate from sibling tools like search_combined or search_publications, which appear to be related search functions.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives like search_combined or search_publications. There's no mention of prerequisites, use cases, or exclusions that would help an agent choose between these similar-sounding search tools.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/jbdamask/mcp-nih-reporter'

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