Skip to main content
Glama
eliotk

NPI Registry MCP Server

search_npi_registry

Search the U.S. National Provider Identifier registry to find healthcare providers and organizations by name, location, specialty, or NPI number using flexible criteria.

Instructions

Search the National Provider Identifier (NPI) registry.

The NPI registry contains information about healthcare providers and organizations in the United States. You can search by various criteria including name, NPI number, location, and specialty.

WILDCARD SUPPORT: Most text fields support wildcard searches using '' after at least 2 characters for fuzzy matching (e.g., 'smith', 'hosp*', 'cardi*').

Args: first_name: Provider's first name (supports wildcards: 'john*' matches 'John', 'Johnny', etc.) last_name: Provider's last name (supports wildcards: 'smith*' matches 'Smith', 'Smithson', etc.) organization_name: Organization name (supports wildcards: 'hosp*' matches 'Hospital', 'Hospice', etc.) npi: Specific 10-digit NPI number to look up (exact match only) city: City name (supports wildcards: 'san*' matches 'San Francisco', 'San Diego', etc.) state: State abbreviation (e.g., 'CA', 'NY', 'TX') - exact match only postal_code: ZIP/postal code (supports wildcards: '902*' matches '90210', '90211', etc.) specialty: Provider specialty or taxonomy (supports wildcards: 'cardi*' matches 'Cardiology', 'Cardiac Surgery', etc.) limit: Maximum number of results to return (1-200, default: 10)

Examples: - Find all Smiths: last_name='smith*' - Find hospitals: organization_name='hosp*' - Find cardiologists: specialty='cardio*' - Find providers in San cities: city='san*' - Find providers in 90210 area: postal_code='902*'

Returns: Dictionary containing search results with provider information

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
first_nameNo
last_nameNo
organization_nameNo
npiNo
cityNo
stateNo
postal_codeNo
specialtyNo
limitNo

Implementation Reference

  • The main handler function for the 'search_npi_registry' tool. Decorated with @mcp.tool() for registration. Handles input validation, invokes NPIRegistryClient for API search, parses results into structured format, and returns a dictionary with search results.
    @mcp.tool()
    async def search_npi_registry(
        first_name: Optional[str] = None,
        last_name: Optional[str] = None,
        organization_name: Optional[str] = None,
        npi: Optional[str] = None,
        city: Optional[str] = None,
        state: Optional[str] = None,
        postal_code: Optional[str] = None,
        specialty: Optional[str] = None,
        limit: int = 10,
    ) -> Dict[str, Any]:
        """Search the National Provider Identifier (NPI) registry.
    
        The NPI registry contains information about healthcare providers and organizations
        in the United States. You can search by various criteria including name, NPI number,
        location, and specialty.
    
        WILDCARD SUPPORT: Most text fields support wildcard searches using '*' after at least
        2 characters for fuzzy matching (e.g., 'smith*', 'hosp*', 'cardi*').
    
        Args:
            first_name: Provider's first name (supports wildcards: 'john*' matches 'John', 'Johnny', etc.)
            last_name: Provider's last name (supports wildcards: 'smith*' matches 'Smith', 'Smithson', etc.)
            organization_name: Organization name (supports wildcards: 'hosp*' matches 'Hospital', 'Hospice', etc.)
            npi: Specific 10-digit NPI number to look up (exact match only)
            city: City name (supports wildcards: 'san*' matches 'San Francisco', 'San Diego', etc.)
            state: State abbreviation (e.g., 'CA', 'NY', 'TX') - exact match only
            postal_code: ZIP/postal code (supports wildcards: '902*' matches '90210', '90211', etc.)
            specialty: Provider specialty or taxonomy (supports wildcards: 'cardi*' matches 'Cardiology', 'Cardiac Surgery', etc.)
            limit: Maximum number of results to return (1-200, default: 10)
    
        Examples:
            - Find all Smiths: last_name='smith*'
            - Find hospitals: organization_name='hosp*'
            - Find cardiologists: specialty='cardio*'
            - Find providers in San cities: city='san*'
            - Find providers in 90210 area: postal_code='902*'
    
        Returns:
            Dictionary containing search results with provider information
        """
        try:
            # Validate input parameters
            if limit < 1 or limit > 200:
                return {
                    "error": "Limit must be between 1 and 200",
                    "results": []
                }
    
            # Validate NPI format if provided
            if npi and (not npi.isdigit() or len(npi) != 10):
                return {
                    "error": "NPI must be exactly 10 digits",
                    "results": []
                }
    
            # Validate state format if provided
            if state and len(state) != 2:
                return {
                    "error": "State must be a 2-letter abbreviation (e.g., 'CA', 'NY')",
                    "results": []
                }
    
            # Create search parameters
            params = NPISearchParams(
                first_name=first_name,
                last_name=last_name,
                organization_name=organization_name,
                npi=npi,
                city=city,
                state=state,
                postal_code=postal_code,
                specialty=specialty,
                limit=limit,
            )
    
            # Perform search
            providers = await npi_client.search(params)
    
            # Format results
            results = []
            for provider in providers:
                result = {
                    "npi": provider.npi,
                    "entity_type": provider.entity_type,
                    "is_organization": provider.is_organization,
                    "status": provider.status,
                    "enumeration_date": provider.enumeration_date,
                    "last_updated": provider.last_updated,
                }
    
                if provider.is_organization:
                    result.update({
                        "organization_name": provider.organization_name,
                        "organization_subpart": provider.organization_subpart,
                        "authorized_official": {
                            "first_name": provider.authorized_official_first_name,
                            "last_name": provider.authorized_official_last_name,
                            "title": provider.authorized_official_title,
                            "telephone": provider.authorized_official_telephone,
                        } if provider.authorized_official_first_name else None,
                    })
                else:
                    result.update({
                        "name": {
                            "first": provider.first_name,
                            "last": provider.last_name,
                            "middle": provider.middle_name,
                            "prefix": provider.name_prefix,
                            "suffix": provider.name_suffix,
                            "credential": provider.credential,
                        },
                        "gender": provider.gender,
                        "sole_proprietor": provider.sole_proprietor,
                    })
    
                # Add addresses
                if provider.addresses:
                    result["addresses"] = provider.addresses
    
                # Add practice locations
                if provider.practice_locations:
                    result["practice_locations"] = provider.practice_locations
    
                # Add taxonomies (specialties)
                if provider.taxonomies:
                    result["taxonomies"] = provider.taxonomies
    
                # Add other identifiers
                if provider.identifiers:
                    result["identifiers"] = provider.identifiers
    
                results.append(result)
    
            return {
                "success": True,
                "count": len(results),
                "results": results,
            }
    
        except Exception as e:
            return {
                "success": False,
                "error": str(e),
                "results": []
            }
  • Pydantic BaseModel defining the input schema/parameters for the search_npi_registry tool, matching the function arguments with descriptions and constraints.
    class NPISearchParams(BaseModel):
        """Parameters for NPI registry search."""
    
        first_name: Optional[str] = Field(None, description="Provider's first name")
        last_name: Optional[str] = Field(None, description="Provider's last name")
        organization_name: Optional[str] = Field(None, description="Organization name")
        npi: Optional[str] = Field(None, description="Specific NPI number (10 digits)")
        city: Optional[str] = Field(None, description="City name")
        state: Optional[str] = Field(None, description="State abbreviation (e.g., 'CA', 'NY')")
        postal_code: Optional[str] = Field(None, description="ZIP/postal code")
        specialty: Optional[str] = Field(None, description="Provider specialty or taxonomy")
        limit: int = Field(10, description="Maximum number of results to return (1-200)", ge=1, le=200)
  • Pydantic BaseModel used internally to parse and structure NPI provider data from the API response, defining the output schema shape.
    class NPIProvider(BaseModel):
        """NPI provider information."""
    
        npi: str
        entity_type: str
        replacement_npi: Optional[str] = None
        ein: Optional[str] = None
        is_organization: bool
    
        # Basic information
        first_name: Optional[str] = None
        last_name: Optional[str] = None
        middle_name: Optional[str] = None
        name_prefix: Optional[str] = None
        name_suffix: Optional[str] = None
        credential: Optional[str] = None
        sole_proprietor: Optional[str] = None
        gender: Optional[str] = None
        enumeration_date: Optional[str] = None
        last_updated: Optional[str] = None
        status: Optional[str] = None
    
        # Organization information
        organization_name: Optional[str] = None
        organization_subpart: Optional[str] = None
        parent_organization_lbn: Optional[str] = None
        parent_organization_tin: Optional[str] = None
        authorized_official_first_name: Optional[str] = None
        authorized_official_last_name: Optional[str] = None
        authorized_official_title: Optional[str] = None
        authorized_official_telephone: Optional[str] = None
    
        # Addresses
        addresses: List[Dict[str, Any]] = Field(default_factory=list)
    
        # Practice locations
        practice_locations: List[Dict[str, Any]] = Field(default_factory=list)
    
        # Taxonomies (specialties)
        taxonomies: List[Dict[str, Any]] = Field(default_factory=list)
    
        # Other identifiers
        identifiers: List[Dict[str, Any]] = Field(default_factory=list)
  • Supporting class that handles HTTP requests to the NPI Registry API, parameter building, response parsing via _parse_provider, and returns structured List[NPIProvider]. Used directly by the handler.
    class NPIRegistryClient:
        """Client for interacting with the NPI Registry API."""
    
        BASE_URL = "https://npiregistry.cms.hhs.gov/api/"
    
        def __init__(self):
            self.client = httpx.AsyncClient(timeout=30.0)
    
        async def close(self):
            """Close the HTTP client."""
            await self.client.aclose()
    
        async def search(self, params: NPISearchParams) -> List[NPIProvider]:
            """Search the NPI registry."""
            # Build query parameters
            query_params = {"version": "2.1", "limit": str(params.limit)}
    
            if params.npi:
                query_params["number"] = params.npi
            if params.first_name:
                query_params["first_name"] = params.first_name
            if params.last_name:
                query_params["last_name"] = params.last_name
            if params.organization_name:
                query_params["organization_name"] = params.organization_name
            if params.city:
                query_params["city"] = params.city
            if params.state:
                query_params["state"] = params.state
            if params.postal_code:
                query_params["postal_code"] = params.postal_code
            if params.specialty:
                query_params["taxonomy_description"] = params.specialty
    
            try:
                response = await self.client.get(
                    f"{self.BASE_URL}",
                    params=query_params
                )
                response.raise_for_status()
                data = response.json()
    
                results = []
                if "results" in data:
                    for result in data["results"]:
                        provider = self._parse_provider(result)
                        results.append(provider)
    
                return results
    
            except httpx.HTTPError as e:
                raise Exception(f"Error searching NPI registry: {str(e)}")
            except Exception as e:
                raise Exception(f"Unexpected error: {str(e)}")
    
        def _parse_provider(self, data: Dict[str, Any]) -> NPIProvider:
            """Parse NPI provider data from API response."""
            basic = data.get("basic", {})
    
            # Determine if this is an organization based on enumeration_type
            # NPI-1 = Individual, NPI-2 = Organization
            enumeration_type = data.get("enumeration_type", "")
            is_org = enumeration_type == "NPI-2"
    
            provider_data = {
                "npi": data.get("number", ""),
                "entity_type": "Organization" if is_org else "Individual",
                "replacement_npi": data.get("replacement_npi"),
                "ein": basic.get("ein"),
                "is_organization": is_org,
                "enumeration_date": basic.get("enumeration_date"),
                "last_updated": basic.get("last_updated"),
                "status": basic.get("status"),
                "sole_proprietor": basic.get("sole_proprietor"),
                "gender": basic.get("gender"),
            }
    
            if is_org:
                # Organization fields
                provider_data.update({
                    "organization_name": basic.get("organization_name"),
                    "organization_subpart": basic.get("organization_subpart"),
                    "parent_organization_lbn": basic.get("parent_organization_lbn"),
                    "parent_organization_tin": basic.get("parent_organization_tin"),
                    "authorized_official_first_name": basic.get("authorized_official_first_name"),
                    "authorized_official_last_name": basic.get("authorized_official_last_name"),
                    "authorized_official_title": basic.get("authorized_official_title"),
                    "authorized_official_telephone": basic.get("authorized_official_telephone"),
                })
            else:
                # Individual fields
                provider_data.update({
                    "first_name": basic.get("first_name"),
                    "last_name": basic.get("last_name"),
                    "middle_name": basic.get("middle_name"),
                    "name_prefix": basic.get("name_prefix"),
                    "name_suffix": basic.get("name_suffix"),
                    "credential": basic.get("credential"),
                })
    
            # Add addresses
            provider_data["addresses"] = data.get("addresses", [])
    
            # Add practice locations
            provider_data["practice_locations"] = data.get("practice_locations", [])
    
            # Add taxonomies
            provider_data["taxonomies"] = data.get("taxonomies", [])
    
            # Add other identifiers
            provider_data["identifiers"] = data.get("identifiers", [])
    
            return NPIProvider(**provider_data)
  • The @mcp.tool() decorator on the search_npi_registry function registers it as an MCP tool with FastMCP.
    @mcp.tool()
Behavior4/5

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

With no annotations provided, the description carries the full burden of behavioral disclosure. It effectively adds context beyond basic functionality by detailing wildcard support rules (e.g., '*' after at least 2 characters), exact vs. fuzzy matching for parameters like NPI and state, and the default limit of 10 results. It doesn't cover aspects like rate limits or authentication needs, but provides substantial operational guidance.

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 clear sections (overview, wildcard support, args, examples, returns) and front-loaded key information. It's appropriately sized but could be slightly more concise by integrating some details more tightly. Every sentence adds value, though minor trimming might improve flow.

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

Completeness4/5

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

For a search tool with 9 parameters, 0% schema coverage, no annotations, and no output schema, the description is highly complete. It covers purpose, usage, parameter details, examples, and return format. It lacks some advanced behavioral traits like error handling or pagination, but provides enough context for effective use given the complexity.

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?

Given 0% schema description coverage and 9 parameters, the description compensates fully by explaining each parameter's purpose, wildcard support, and constraints (e.g., NPI is exact match, state abbreviations, limit range 1-200). Examples illustrate usage, adding significant value beyond the bare schema. This exceeds the baseline expectation for such low coverage.

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

Purpose5/5

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

The description clearly states the tool searches the NPI registry for healthcare providers and organizations in the US, specifying the resource (NPI registry) and action (search). It distinguishes the tool's purpose by listing specific search criteria like name, NPI number, location, and specialty, making it highly specific and informative even without sibling tools for comparison.

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

Usage Guidelines4/5

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

The description provides clear context on when to use the tool by listing searchable criteria and examples, such as finding providers by name, location, or specialty. However, it lacks explicit guidance on when not to use it or alternatives, which prevents a perfect score. No sibling tools are mentioned, so no comparison is needed.

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/eliotk/npi-registry-mcp-server'

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