# Directory Structure ``` ├── .github │ ├── dependabot.yml │ └── workflows │ └── ruff_check.yml ├── .gitignore ├── .python-version ├── demo.png ├── LICENSE ├── pyproject.toml ├── README.md ├── src │ └── mcp_server_ipinfo │ ├── __init__.py │ ├── cache.py │ ├── ipinfo.py │ ├── models.py │ └── server.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- ``` 3.13 ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Python-generated files __pycache__/ *.py[oc] build/ dist/ wheels/ *.egg-info # Virtual environments .venv # Other files .DS_Store .env ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # IP Geolocation MCP Server This is a simple [Model Context Protocol](https://modelcontextprotocol.io) server that uses the [ipinfo.io](https://ipinfo.io) API to get detailed information about an IP address. This can be used to determine where the user is located (approximately) and what network they are used. <a href="https://glama.ai/mcp/servers/pll7u5ak1h"> <img width="380" height="200" src="https://glama.ai/mcp/servers/pll7u5ak1h/badge" alt="IP Geolocation Server MCP server" /> </a>  ## Installation You'll need to create a token to use the IPInfo API. If you don't already have one, you can sign up for a free account at https://ipinfo.io/signup. While each client has its own way of specifying, you'll generally use the following values: | Field | Value | |-------|-------| | **Command** | `uvx` | | **Arguments** | `mcp-server-ipinfo` | | **Environment** | `IPINFO_API_TOKEN` = `<YOUR TOKEN>` | ### Development Version If you'd like to use the latest and greatest, the server can be pulled straight from GitHub. Just add an additional `--from` argument: | Field | Value | |-------|-------| | **Command** | `uvx` | | **Arguments** | `--from`, `git+https://github.com/briandconnelly/mcp-server-ipinfo`, `mcp-server-ipinfo` | | **Environment** | `IPINFO_API_TOKEN` = `<YOUR TOKEN>` | ## Components ### Tools - `get_ip_details`: This tool is used to get detailed information about an IP address. - **Input:** `ip`: The IP address to get information about. - **Output:** `IPDetails`: A Pydantic model containing detailed information about the IP, including location, organization, and country details. ### Resources _No custom resources are included_ ### Prompts _No custom prompts are included_ ## License MIT License - See [LICENSE](LICENSE) file for details. ## Disclaimer This project is not affiliated with [IPInfo](https://ipinfo.io). ``` -------------------------------------------------------------------------------- /src/mcp_server_ipinfo/__init__.py: -------------------------------------------------------------------------------- ```python ``` -------------------------------------------------------------------------------- /.github/workflows/ruff_check.yml: -------------------------------------------------------------------------------- ```yaml name: Check lints and formatting with Ruff on: push: branches: [main] pull_request: branches: [main] permissions: read-all jobs: ruff: runs-on: ubuntu-latest steps: - name: Get repo uses: actions/checkout@v4 - name: Get ruff uses: astral-sh/ruff-action@v3 - run: ruff check - run: ruff format --check ``` -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- ```yaml version: 2 updates: # Enable version updates for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" labels: - "ci" - "dependencies" # Python dependency updates - package-ecosystem: "pip" directory: "/" schedule: interval: "weekly" labels: - "python" - "dependencies" ``` -------------------------------------------------------------------------------- /src/mcp_server_ipinfo/cache.py: -------------------------------------------------------------------------------- ```python from datetime import datetime, timedelta from .models import IPDetails class IPInfoCache: """ A cache for IPInfo API responses. """ def __init__(self, ttl_seconds: int = 3600): self.cache = {} self.ttl_seconds = ttl_seconds def get(self, ip: str) -> IPDetails | None: if ip in self.cache: data, timestamp = self.cache[ip] if datetime.now() - timestamp < timedelta(seconds=self.ttl_seconds): return data return None def set(self, ip: str, data: IPDetails): self.cache[ip] = (data, datetime.now()) ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml [project] name = "mcp-server-ipinfo" version = "0.2.0" description = "IP Geolocation Server for MCP" readme = "README.md" license = { file = "LICENSE" } authors = [ { name = "Brian Connelly", email = "[email protected]" } ] classifiers = [ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.13", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Scientific/Engineering :: Artificial Intelligence", "Topic :: System :: Networking", ] requires-python = ">=3.13" dependencies = [ "fastmcp>=2.9.0", "ipinfo>=5.1.1", "pydantic>=2.10.6", ] [project.urls] Repository = "https://github.com/briandconnelly/mcp-server-ipinfo.git" Issues = "https://github.com/briandconnelly/mcp-server-ipinfo/issues" [project.scripts] mcp-server-ipinfo = "mcp_server_ipinfo.server:mcp.run" [build-system] requires = ["hatchling"] build-backend = "hatchling.build" ``` -------------------------------------------------------------------------------- /src/mcp_server_ipinfo/ipinfo.py: -------------------------------------------------------------------------------- ```python import os from datetime import datetime, timezone import ipinfo from .models import IPDetails def ipinfo_lookup(ip: str | None, **kwargs) -> IPDetails: """ Retrieve detailed information about an IP address using the ipinfo.io service. This function fetches comprehensive information about the specified IP address, including geolocation data, ISP details, and country information. If no IP is provided, it returns information about the client's current IP address. Args: ip: The IP address to look up. If None, returns information about the client's current IP address. **kwargs: Additional arguments to pass to the ipinfo handler. These can include timeout settings, cache settings, or other ipinfo.io options. Returns: IPDetails: A Pydantic model containing detailed information about the IP, including location, organization, and country details. Raises: ipinfo.exceptions.RequestQuotaExceededError: If the API request quota is exceeded ipinfo.exceptions.RequestFailedError: If the API request fails ValueError: If the provided IP address is invalid Example: >>> details = ipinfo_lookup("8.8.8.8") >>> print(details.country) 'US' >>> print(details.org) 'Google LLC' """ handler = ipinfo.getHandler( access_token=os.environ.get("IPINFO_API_TOKEN", None), headers={"user-agent": "mcp-server-ipinfo", "custom_header": "yes"}, **kwargs, ) details = handler.getDetails(ip_address=ip) return IPDetails(**details.all, ts_retrieved=str(datetime.now(timezone.utc))) ``` -------------------------------------------------------------------------------- /src/mcp_server_ipinfo/models.py: -------------------------------------------------------------------------------- ```python from pydantic import BaseModel, condecimal, constr from pydantic.networks import HttpUrl, IPvAnyAddress class IPDetails(BaseModel): """ A Pydantic model representing detailed information about an IP address. This model contains geographical, network, and additional metadata about an IP address, including location coordinates, country information, ISP details, and timezone data. """ ip: IPvAnyAddress = None # type: ignore """The IP address (supports both IPv4 and IPv6 formats)""" hostname: str | None = None """The hostname associated with the IP address, if available""" city: str | None = None """The city where the IP address is located""" region: str | None = None """The region/state where the IP address is located""" country: constr(pattern=r"^[A-Z]{2}$") | None = None """The two-letter ISO country code (e.g., 'US', 'GB', 'DE')""" loc: str | None = None """The geographical coordinates in the format 'latitude,longitude'""" org: str | None = None """The organization/ISP associated with the IP address (free plan only; paid plan: see `asn` field)""" postal: str | None = None """The postal/ZIP code of the IP address location""" timezone: str | None = None """The timezone of the IP address location (e.g., 'America/New_York')""" country_name: str | None = None """The full name of the country""" isEU: bool | None = None """Boolean indicating if the country is in the European Union""" country_flag_url: HttpUrl | None = None """URL to the country's flag image""" country_flag: dict | None = None """Dictionary containing country flag information""" country_currency: dict | None = None """Dictionary containing country currency information with fields: - code: str - The three-letter currency code (e.g., 'USD', 'EUR', 'GBP') - symbol: str - The currency symbol (e.g., '$', '€', '£')""" continent: dict | None = None """Dictionary containing continent information with fields: - code: str - The two-letter continent code (e.g., 'NA', 'EU', 'AS') - name: str - The full continent name (e.g., 'North America', 'Europe', 'Asia')""" latitude: condecimal(ge=-90, le=90) | None = None """The latitude coordinate, ranging from -90 to 90 degrees""" longitude: condecimal(ge=-180, le=180) | None = None """The longitude coordinate, ranging from -180 to 180 degrees""" asn: dict | None = None """Dictionary containing ASN information with fields (Basic, Standard, Business, and Enterprise plans only): - asn: str - The ASN number - name: str - The name of the ASN - domain: str - The domain of the ASN - route: str - The route of the ASN - type: str - The type of the ASN""" privacy: dict | None = None """Dictionary containing privacy information with fields (Standard, Business, and Enterprise plans only): - vpn: bool - Whether the IP address is in a VPN - proxy: bool - Whether the IP address is in a proxy - tor: bool - Whether the IP address is in a Tor exit node - relay: bool - Whether the IP address is in a relay node - hosting: bool - Whether the IP address is in a hosting provider - service: bool - Whether the IP address is in a service provider""" carrier: dict | None = None """Dictionary containing mobile operator information with fields (Business and Enterprise plans only): - name: str - The name of the mobile operator - mcc: str - The Mobile Country Code of the mobile operator - mnc: str - The Mobile Network Code of the mobile operator""" company: dict | None = None """Dictionary containing company information with fields (Business and Enterprise plans only): - name: str - The name of the company - domain: HttpUrl - The domain of the company - type: str - The type of the company""" domains: dict | None = None """Dictionary containing domains information with fields (Business and Enterprise plans only): - ip: IPvAnyAddress - The IP address of the domain - total: int - The total number of domains associated with the IP address - domains: list[HttpUrl] - The list of domains associated with the IP address""" abuse: dict | None = None """Dictionary containing abuse contact information with fields (Business and Enterprise plans only): - address: str - The address of the abuse contact - country: str - The country of the abuse contact - email: str - The email of the abuse contact - phone: str - The phone number of the abuse contact - network: str - The network of the abuse contact""" bogon: bool | None = None """Boolean indicating if the IP address is a bogon IP address. A bogon IP address is an IP address that is not assigned to a network and is used for testing or other purposes. This is not a reliable indicator of the IP address's location. """ anycast: bool | None = None """Boolean indicating if the IP address is an anycast IP address""" ts_retrieved: str | None = None """The timestamp of the IP address lookup""" ``` -------------------------------------------------------------------------------- /src/mcp_server_ipinfo/server.py: -------------------------------------------------------------------------------- ```python import ipaddress import os from typing import Annotated from fastmcp import Context, FastMCP from fastmcp.exceptions import ToolError from pydantic import Field from .cache import IPInfoCache from .ipinfo import ipinfo_lookup from .models import IPDetails cache = IPInfoCache() # Create an MCP server mcp = FastMCP( name="IP Address Geolocation and Internet Service Provider Lookup", instructions=""" This MCP server provides tools to look up IP address information using the IPInfo API. For a given IPv4 or IPv6 address, it provides information about the geographic location of that device, the internet service provider, and additional information about the connection. If we assume that the user is physically using that device, the location of that user is the location of the device. The IPInfo API is free to use, but it has a rate limit. Paid plans provide more information, but are not required for basic use. The IPINFO_API_TOKEN environment variable with a valid API key can be set to enable paid features. The accuracy of the location determined by IP geolocation can vary. Generally, the country is accurate, but the city and region may not be. If a user is using a VPN, Proxy, Tor, or hosting provider, the location returned will be the location of that service's exit point, not the user's actual location. If the user is using a mobile/cellular connection, the location returned may differ from the user's actual location. If anycast is true, the location returned may differ from the user's actual location. In any of these cases, if the user's location is important, you should ask the user for their location. An IPv4 address consists of four decimal numbers separated by dots (.), known as octets. An IPv6 address consists of eight groups of four hexadecimal numbers separated by colons (:). Recommended companion servers: - unifi-network-mcp: Provides information about the devices, configuration, and performance of the user's local area network (LAN), their Wi-Fi network, and their connection to the internet. - cloudflare: Provides information about historical internet speed/quality summaries for a given internet service provider or location. For example, we can provide Cloudflare with the internet service provider or location determined using get_ip_details to obtain information about the historical and competitiveperformance of the user's internet service provider. """, ) @mcp.tool() async def get_ip_details( ip: Annotated[ str | None, Field( description="The IP address to analyze (IPv4 or IPv6). If None or not provided, analyzes the requesting client's IP address.", examples=["8.8.8.8", "2001:4860:4860::8888", None], ), ] = None, ctx: Context = None, ) -> IPDetails: """Get detailed information about an IP address including location, ISP, and network details. This tool provides comprehensive IP address analysis including geographic location, internet service provider information, network details, and security context. Use when you need to understand the user's location, ISP, and network details or those of a given IP address. Common use cases: - Analyze user's current location and connection details (leave ip parameter blank) - Investigate suspicious IP addresses for security analysis - Determine geographic distribution of website visitors or API users - Look up ISP and hosting provider information for network troubleshooting - Get timezone information for scheduling or time-sensitive operations - Verify if an IP belongs to a VPN, proxy, or hosting provider - Check country-specific compliance requirements (EU, etc.) Args: ip: The IP address to analyze (IPv4 or IPv6). If None or not provided, analyzes the requesting client's IP address. ctx: The MCP request context. Returns: IPDetails: Comprehensive IP information including: Basic Info: - ip: The IP address that was analyzed - hostname: Associated hostname/domain name - org: Organization/ISP name (e.g., "Google LLC", "Comcast Cable") - ts_retrieved: The timestamp when the IP address was looked up (UTC) Geographic Location: - city: City name - region: State/province/region name - country: Two-letter ISO country code (e.g., "US", "GB") - country_name: Full country name - postal: ZIP/postal code - loc: Coordinates as "latitude,longitude" string - latitude/longitude: Separate coordinate values - timezone: IANA timezone identifier (e.g., "America/New_York") Regional Info: - continent: Continent information dictionary - country_flag: Country flag image data - country_flag_url: URL to country flag image - country_currency: Currency information for the country - isEU: True if country is in European Union Network/Security Info (some features require paid API plan): - asn: Autonomous System Number details - privacy: VPN/proxy/hosting detection data - carrier: Mobile network operator info (for cellular IPs) - company: Company/organization details - domains: Associated domain names - abuse: Abuse contact information - bogon: True if IP is in bogon/reserved range - anycast: True if IP uses anycast routing Examples: # Get your own IP details my_info = get_ip_details() # Analyze a specific IP server_info = get_ip_details("8.8.8.8") # Check if IP is from EU for GDPR compliance details = get_ip_details("192.168.1.1") is_eu_user = details.isEU Note: Some advanced features (ASN, privacy detection, carrier info) require an IPINFO_API_TOKEN environment variable with a paid API plan. Basic location and ISP info works without authentication. """ ctx.info(f"Getting details for IP address {ip}") if "IPINFO_API_TOKEN" not in os.environ: ctx.warning("IPINFO_API_TOKEN is not set") if ip in ("null", "", "undefined", "0.0.0.0", "::"): ip = None # If IP address given, check cache first if ip and (cached := cache.get(ip)): ctx.debug(f"Returning cached result for {ip}") return cached if ip: try: parsed_ip = ipaddress.ip_address(ip) if parsed_ip.is_private: raise ToolError( f"{ip} is a private IP address. Geolocation may not be meaningful." ) elif parsed_ip.is_loopback: raise ToolError( f"{ip} is a loopback IP address. Geolocation may not be meaningful." ) elif parsed_ip.is_multicast: raise ToolError( f"{ip} is a multicast IP address. Geolocation may not be meaningful." ) elif parsed_ip.is_reserved: raise ToolError( f"{ip} is a reserved IP address. Geolocation may not be meaningful." ) except ValueError: ctx.error(f"Got an invalid IP address: {ip}") raise ToolError(f"{ip} is not a valid IP address") try: result = ipinfo_lookup(ip) cache.set(result.ip, result) return result except Exception as e: ctx.error(f"Failed to look up IP details: {str(e)}") raise ToolError(f"Lookup failed for IP address {str(e)}") @mcp.tool() def get_ipinfo_api_token(ctx: Context) -> str | None: """Check if the IPINFO_API_TOKEN environment variable is configured for enhanced IP lookups. This tool verifies whether the IPInfo API token is properly configured in the environment. The token enables access to premium features like ASN information, privacy detection, carrier details, and enhanced accuracy for IP geolocation analysis. Common use cases: - Verify API token configuration before performing advanced IP analysis - Troubleshoot why certain IP lookup features are unavailable - Check system configuration for applications requiring premium IP data - Validate environment setup during deployment or testing - Determine available feature set for IP analysis workflows Args: ctx: The MCP request context. Returns: bool: True if IPINFO_API_TOKEN environment variable is set and configured, False if the token is missing or not configured. Examples: # Check if premium features are available has_token = get_ipinfo_api_token() if has_token: # Safe to use advanced IP analysis features details = get_ip_details("8.8.8.8") # Will include ASN, privacy data else: # Limited to basic IP information only details = get_ip_details("8.8.8.8") # Basic location/ISP only # Use in conditional workflows if get_ipinfo_api_token(): # Perform advanced IP geolocation analysis pass else: # Fall back to basic analysis or prompt for token configuration pass Note: The IPInfo API provides basic location and ISP information without authentication, but premium features (ASN details, VPN/proxy detection, carrier information, enhanced accuracy) require a valid API token from https://ipinfo.io/. """ return os.environ.get("IPINFO_API_TOKEN") ```