Skip to content

About

MCP server exposing 141 functions across 14 astronomical data services (SIMBAD, NED, MAST, ADS, Gaia, JWST, and more) to AI assistants — data-access backend for the AKD Astro Data Search Agent

Resources

Stars

3 stars

Watchers

0 watching

Forks

Repository files navigation

Astroquery MCP Server

A Model Context Protocol (MCP) server that dynamically exposes all astroquery functionality to AI assistants. This server uses introspection to discover and expose Python package functionality through MCP tools, making it a reusable template for creating MCP servers for any Python library.

Part of AI Agents for Science (Accelerated Knowledge Discovery — AKD). This MCP server is the data-access backend for the AKD Astro Data Search Agent, a natural-language astrophysics dataset-discovery system, also packaged as a Claude Code plugin.

What is This?

This is an MCP server that:

  • Dynamically discovers all functions from astroquery modules using Python introspection
  • Exposes them as MCP tools that AI assistants can call
  • Handles parameter conversion (e.g., coordinates, units) automatically
  • Serializes results (Astropy Tables → JSON) for AI consumption
  • Provides a reusable pattern for wrapping any Python package as an MCP server

Available Astroquery Modules

This server provides access to 14 astronomical data services with 141 functions total:

Module Functions Description
SIMBAD 18 Astronomical database for object resolution and crossmatch
NED 15 NASA/IPAC Extragalactic Database for galaxies and extragalactic objects
VizieR 9 Access to astronomical catalogs and data tables
ADS 1 NASA Astrophysics Data System for literature search
MAST 19 Mikulski Archive for Space Telescopes (HST, JWST, etc.)
MAST Catalogs 12 Query catalogs in MAST (HSC, PanSTARRS, etc.)
HEASARC 9 High Energy Astrophysics archive (X-ray, gamma-ray)
IRSA 7 NASA/IPAC Infrared Science Archive
NASA Exoplanet Archive 7 Exoplanet data and confirmed planets
Gaia 13 ESA Gaia mission astrometric catalog
SDSS 16 Sloan Digital Sky Survey optical imaging and spectroscopy
ALMA 16 Atacama Large Millimeter/submillimeter Array data
ESA Hubble 21 ESA Hubble Space Telescope Archive
ESA JWST 16 ESA James Webb Space Telescope Archive

Example queries:

  • Query SIMBAD for object data: astroquery_execute("simbad", "query_object", {"object_name": "M31"})
  • Search MAST for observations: astroquery_execute("mast", "query_region", {"coordinates": "M31", "radius": 0.1})
  • Get exoplanet data: astroquery_execute("nea", "query_object", {"object_name": "Kepler-22 b"})
  • Run TAP/ADQL queries: astroquery_execute("simbad", "query_tap", {"query": "SELECT TOP 10 * FROM basic"})

For the complete, always-current list of MCP tools, archives and functions with full parameters, see the documentation site or the wiki.

How It Works

Architecture Overview

┌─────────────────┐
│  AI Assistant   │
└────────┬────────┘
         │ MCP Protocol
┌────────▼────────────────────────────────────────┐
│  FastMCP Server (server.py)                     │
│  ┌────────────────────────────────────────┐     │
│  │ MCP Tools:                             │     │
│  │ - astroquery_list_modules()            │     │
│  │ - astroquery_list_functions(module)    │     │
│  │ - astroquery_get_function_info(...)    │     │
│  │ - astroquery_execute(module, func, params) │ │
│  └─────────┬──────────────────────────────┘     │
└────────────┼──────────────────────────────────┘
             │
┌────────────▼──────────────────────────────────┐
│  Introspection Layer (introspection.py)       │
│  - Discovers Python classes/modules           │
│  - Extracts method signatures                 │
│  - Parses docstrings                          │
└────────────┬──────────────────────────────────┘
             │
┌────────────▼──────────────────────────────────┐
│  Executor (executor.py)                       │
│  - Dynamically imports and calls functions    │
│  - Converts parameters (dict → SkyCoord, etc) │
│  - Serializes results (Table → dict)          │
└────────────┬──────────────────────────────────┘
             │
┌────────────▼──────────────────────────────────┐
│  Python Package (astroquery)                  │
│  - SIMBAD, MAST, ADS, Vizier, etc.           │
└───────────────────────────────────────────────┘

Key Components

  1. server.py - FastMCP server with 4 core tools
  2. introspection.py - Discovers functions from Python modules
  3. executor.py - Dynamically executes discovered functions
  4. config.py - Configuration management
  5. auth.py - Authentication handling
  6. utils/ - Serialization helpers (Table → dict, etc.)
  7. models/ - Type definitions and error handling

Using This as a Template

This codebase can be adapted for any Python package with a similar structure. Here's how:

Step 1: Define Your Modules

Edit introspection.py to define which classes/modules to expose:

# introspection.py
YOUR_PACKAGE_MODULES = {
    "service1": "your_package.service1.ServiceClass",
    "service2": "your_package.service2.AnotherClass",
    # Add more modules here
}

Examples for different packages:

# For scikit-learn
SKLEARN_MODULES = {
    "linear_model": "sklearn.linear_model",
    "tree": "sklearn.tree",
    "ensemble": "sklearn.ensemble",
}

# For requests
REQUESTS_MODULES = {
    "requests": "requests",
}

# For pandas
PANDAS_MODULES = {
    "dataframe": "pandas.DataFrame",
    "series": "pandas.Series",
    "io": "pandas",  # For read_csv, etc.
}

Step 2: Customize Parameter Handling

Edit executor.py's prepare_argument() function to handle package-specific types:

def prepare_argument(value: Any, param_name: str) -> Any:
    """Convert argument values to appropriate types."""

    # Example: Handle file paths
    if param_name.lower() in {"filepath", "path", "filename"}:
        return Path(value)

    # Example: Handle dataframes from dicts
    if param_name.lower() in {"data", "df"}:
        if isinstance(value, dict):
            import pandas as pd
            return pd.DataFrame(value)

    # Add your package-specific conversions here
    return value

For astroquery, we handle:

  • Coordinates: dict → SkyCoord
  • Radius: number → Quantity
  • Units: string → astropy.units

Step 3: Customize Result Serialization

Edit executor.py's serialize_result() to convert package-specific types to JSON:

def serialize_result(result: Any) -> Any:
    """Serialize results to JSON-compatible format."""

    # Example: Handle pandas DataFrames
    if isinstance(result, pd.DataFrame):
        return result.to_dict(orient="records")

    # Example: Handle numpy arrays
    if isinstance(result, np.ndarray):
        return result.tolist()

    # Add your package-specific serialization here
    return result

For astroquery, we serialize:

  • astropy.Table → dict
  • SkyCoord → {ra_deg, dec_deg, frame}
  • Quantity → {value, unit}

Step 4: Update Server Metadata

Edit server.py to customize server name and instructions:

mcp = FastMCP(
    "your-package-mcp",  # Change this
    instructions="""MCP server for [your package].

    Use `list_modules` to see available modules
    Use `list_functions` to see available functions
    Use `get_function_info` to get parameter details
    Use `execute` to call any function

    Example workflow:
    1. list_modules()
    2. list_functions("module_name")
    3. get_function_info("module_name", "function_name")
    4. execute("module_name", "function_name", {"param": "value"})
    """,
)

# Update tool names
@mcp.tool()
def your_package_execute(...):
    ...

Step 5: Handle Authentication (if needed)

If your package requires API keys:

  1. Edit config.py to define auth requirements:
"auth": {
    "your_service": {"token_env": "YOUR_SERVICE_TOKEN"},
}
  1. Edit auth.py to configure the package:
def configure_your_package_auth():
    token = get_token("your_service")
    if token:
        import your_package
        your_package.api_key = token

Step 6: Update pyproject.toml

[project]
name = "your-package-mcp"
version = "0.1.0"
description = "MCP server for your package"
dependencies = [
    "your-package>=1.0.0",
    "fastmcp>=2.14.2",
    "mcp>=1.25.0",
]

[project.scripts]
your-package-mcp = "server:main"

[tool.fastmcp]
name = "your-package-mcp"

Installation & Usage

Requirements

Python 3.10 or newer. Direct dependencies and the oldest versions CI tests against:

Package Minimum Notes
astropy 6.1 Python 3.10 resolves to astropy 6.1.x; 3.11+ gets 7.x
astroquery 0.4.10
fastmcp 2.14.2
mcp 1.25.0
numpy 1.24
httpx 0.28.1 Required by fastmcp 2.14.2
pydantic 2.11.7 Required by fastmcp 2.14.2
pyyaml 6.0
tenacity 8.2

Every name the code uses from these packages is checked against the minimum versions by scripts/check_api_compat.py (see Testing).

Install Dependencies

# Using uv (recommended)
uv pip install -e .

# Or using pip
pip install -e .

Run the Server

# Run directly
python server.py

# Or using fastmcp deploy (for MCP client integration)
fastmcp deploy server.py

Configure Authentication (Optional)

For services requiring authentication:

export API_DEV_KEY="your-ads-token"
export MAST_TOKEN="your-mast-token"

Result Size (max_rows)

astroquery_execute keeps at most max_rows rows (default 20) in every table of a result, for all modules: a single table, a list of tables (VizieR), or tables nested in a dict (Gaia load_data). Each table reports row_count (rows returned), total_rows and truncated. Raise max_rows or narrow the query (smaller radius, filters, TOP in ADQL) to get more rows. For example, a MAST get_product_list with 6,342 products drops from about 4.1 million characters to about 14,000 with the default.

Science Products (Artifacts)

Functions are labeled by astroquery_list_functions as either:

  • metadata: catalog rows, product lists, URLs. Results are returned inline, sized for the model's context. Use these to discover what exists (e.g. MAST get_product_list, which lists every product with its dataURI).
  • retrieval: download_*, get_images, get_spectra, get_product, ... These fetch files. File contents are never returned to the model.

Each file a retrieval call produces is saved in the artifact store and described in the response's artifacts list:

{
  "artifact_id": "c1931e4c4cf75a42",
  "archive": "sdss",
  "product_id": "frame-r-000756-2-0427",
  "filename": "frame-r-000756-2-0427.fits",
  "content_type": "application/fits",
  "content_encoding": null,
  "size_bytes": 12447360,
  "sha256": "c1931e4c4cf75a42...",
  "download_url": "https://data.sdss.org/sas/dr17/eboss/photoObj/frames/301/756/2/frame-r-000756-2-0427.fits.bz2",
  "local_path": "~/Library/Caches/astroquery-mcp/artifacts/downloads/.../frame-r-000756-2-0427.fits",
  "resource_uri": "astroquery://artifacts/c1931e4c4cf75a42",
  "fits_summary": {"hdus": [{"index": 0, "name": "PRIMARY", "type": "PrimaryHDU", "shape": [1489, 2048]}], "header": {"TELESCOP": "..."}}
}
  • download_url is the archive's original URL when the archive reports it. MAST download_products does not, but the dataURI from get_product_list identifies the file.
  • resource_uri is an MCP resource: MCP clients read the file's bytes through it, outside the model's context. Works whether the server runs locally or remotely.
  • fits_summary (FITS files only) lists HDU names, types and shapes plus a few primary-header cards (TELESCOP, INSTRUME, OBJECT, DATE-OBS, EXPTIME, ...). Only headers are read.
  • In-memory products are stored the same way: FITS HDULists, binary payloads, and numpy arrays over 1000 elements (saved as .npy). Text payloads up to 4 KB stay inline.
  • For NED/SDSS get_images/get_spectra, the server calls the _async variant so the original URL is kept.

Where files go. Each retrieval call writes into its own new folder: <store>/downloads/<UTC time>-<archive>-<id>/. The call runs in that folder, and output-folder parameters (download_dir, savedir, folder, location) point there unless you pass your own. The store is, in order of priority:

  1. ASTROQUERY_MCP_ARTIFACT_DIR environment variable
  2. artifacts.dir in the YAML config (ASTROQUERY_MCP_CONFIG)
  3. The per-user cache dir: ~/Library/Caches/astroquery-mcp/artifacts (macOS), $XDG_CACHE_HOME or ~/.cache/astroquery-mcp/artifacts (Linux), %LOCALAPPDATA%\astroquery-mcp\artifacts (Windows)

Files are not deleted automatically; remove old folders under downloads/ when needed. Because every call gets a fresh folder, astroquery's "already downloaded" cache does not apply across calls.

Testing

uv run pytest              # offline tests: no network, about 2 seconds
uv run pytest -m network   # live tests against ADS, SIMBAD, XMM, ... (needs API_DEV_KEY)

CI (.github/workflows/tests.yml) runs on every pull request into main and again on main after each merge. Merging is blocked unless the Offline tests check passes, which requires:

  • the offline tests passing on Python 3.10, 3.11, 3.12 and 3.13, and
  • a minimum versions job: install the oldest dependency versions allowed by pyproject.toml on Python 3.10, run scripts/check_api_compat.py, then the offline tests.

scripts/check_api_compat.py reads the source without running it, collects every third-party import, attribute (u.arcmin, SkyCoord.from_name) and keyword argument the code uses, and confirms each one exists in the installed versions. To check locally against the minimum versions:

uv pip compile pyproject.toml --group dev --resolution lowest-direct -p 3.10 -o /tmp/lowest.txt
uv venv -p 3.10 /tmp/lowest && VIRTUAL_ENV=/tmp/lowest uv pip install -r /tmp/lowest.txt
/tmp/lowest/bin/python scripts/check_api_compat.py && /tmp/lowest/bin/python -m pytest

Live tests run only when started by hand: Actions → Tests → Run workflow → tick live. They need the API_DEV_KEY (and optionally MAST_TOKEN) repository secrets.

Use with Claude Desktop

Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "astroquery": {
      "command": "python",
      "args": ["/path/to/server.py"],
      "env": {
        "API_DEV_KEY": "your-token-here"
      }
    }
  }
}

Example: Creating an MCP Server for Pandas

Here's a complete minimal example for pandas:

# introspection.py
PANDAS_MODULES = {
    "io": "pandas",  # read_csv, read_json, etc.
    "dataframe": "pandas.DataFrame",
}

# executor.py - parameter handling
def prepare_argument(value: Any, param_name: str) -> Any:
    if param_name == "filepath_or_buffer":
        return value  # Already a string path
    return value

# executor.py - result serialization
def serialize_result(result: Any) -> Any:
    if isinstance(result, pd.DataFrame):
        return {
            "columns": result.columns.tolist(),
            "data": result.to_dict(orient="records"),
            "shape": result.shape,
        }
    return result

# server.py
mcp = FastMCP(
    "pandas-mcp",
    instructions="MCP server for pandas data operations"
)

Now AI assistants can call:

execute("io", "read_csv", {"filepath_or_buffer": "data.csv"})
execute("dataframe", "describe", {})

Documentation

The documentation site (Sphinx, Read the Docs theme) and the GitHub wiki are generated from the source docstrings by scripts/gen_docs.py and republished automatically on every GitHub release (.github/workflows/docs.yml).

Build locally:

uv run --group docs sphinx-build -b html docs docs/_build/html

This writes the site to docs/_build/html and the wiki pages to build/wiki.

Advanced Features

Method Filtering

By default, introspection looks for methods starting with common patterns:

QUERY_METHOD_PREFIXES = (
    "query_", "get_", "list_", "search_", "download_",
    "fetch_", "resolve_", "cone_search",
)

Customize this in introspection.py for your package:

# For a REST API wrapper
API_METHOD_PREFIXES = (
    "get_", "post_", "put_", "delete_", "fetch_",
)

# For a data processing library
PROCESSING_METHOD_PREFIXES = (
    "transform_", "process_", "compute_", "analyze_",
)

Error Handling

The template includes structured error handling in models/errors.py:

class MCPError(Exception):
    def __init__(
        self,
        code: ErrorCode,
        message: str,
        service: str,
        recoverable: bool = True,
        suggestion: str = "",
        details: dict = None,
    ):
        ...

Use it in your code:

raise MCPError(
    code=ErrorCode.VALIDATION_ERROR,
    message="Invalid parameter",
    service="your_module",
    recoverable=False,
    suggestion="Check the function signature",
)

Retry Logic

The template includes configurable retry logic in retry.py for handling:

  • Network errors
  • Rate limiting
  • Temporary service failures

Customize in config.py:

defaults:
  timeout: 60
  max_retries: 3
  backoff_factor: 2.0

Project Structure

astroquery_mcp/
├── server.py              # Main MCP server (FastMCP)
├── introspection.py       # Function discovery via reflection
├── executor.py            # Dynamic function execution
├── config.py             # Configuration management
├── auth.py               # Authentication handling
├── http_client.py        # HTTP client with retry logic
├── retry.py              # Retry decorators
├── models/
│   ├── errors.py         # Error types
│   └── results.py        # Result types
├── utils/
│   ├── table_utils.py    # Table serialization
│   └── param_utils.py    # Parameter conversion
└── tools/
    ├── discovery.py      # Service discovery
    ├── simbad.py         # Service-specific helpers
    └── ...

Key Design Patterns

1. Dynamic Discovery

Uses Python's inspect and importlib to discover functions at runtime:

def introspect_class(module_name: str, class_path: str):
    module = importlib.import_module(module_path)
    cls = getattr(module, class_name)

    for method_name in dir(cls):
        method = getattr(cls, method_name)
        sig = inspect.signature(method)
        # Extract parameters, docstrings, etc.

2. Generic Execution

Single execute() function handles all discovered functions:

def execute_function(module_name, function_name, params):
    cls = get_class_instance(module_name)
    method = getattr(cls, function_name)
    prepared_params = {k: prepare_argument(v, k) for k, v in params.items()}
    result = method(**prepared_params)
    return serialize_result(result)

3. Type Conversion

Bidirectional conversion between JSON and package-specific types:

JSON dict → Python objects → Function call → Result → JSON dict
  ↓             ↓               ↓            ↓         ↓
{ra: 10,     SkyCoord(...)   method()    Table     {columns: [],
 dec: 41}                                           data: [...]}

Benefits of This Approach

  1. Zero boilerplate - No need to manually write MCP tools for each function
  2. Automatic updates - New package functions are automatically exposed
  3. Type safety - Introspection captures parameter types and defaults
  4. Discoverable - AI can explore available functions via list_functions
  5. Reusable - Same pattern works for any Python package
  6. Maintainable - Changes to the package are automatically reflected

When to Use This Pattern

This pattern works well for packages that:

  • ✅ Have class-based or module-based APIs
  • ✅ Use standard Python types or have serializable custom types
  • ✅ Have functions with clear parameters (not *args/**kwargs heavy)
  • ✅ Have docstrings for parameters

May need customization for:

  • ⚠️ Packages with heavy use of callbacks/generators
  • ⚠️ Packages requiring complex state management
  • ⚠️ Packages with binary data (images, audio, etc.)
  • ⚠️ Packages with streaming APIs

Contributing

To add support for a new astroquery module:

  1. Add to ASTROQUERY_MODULES in introspection.py
  2. Add type conversions if needed in executor.py
  3. Add auth config if needed in config.py

License

MIT License - feel free to use this template for your own MCP servers!

Related Projects

  • FastMCP - FastAPI-like framework for MCP servers
  • Astroquery - Python package for astronomical data queries
  • MCP - Model Context Protocol specification

Questions?

This README explains the architecture and how to adapt it. Key files to study:

  • server.py:1-171 - MCP server setup and tool definitions
  • introspection.py:109-198 - How function discovery works
  • executor.py:66-118 - Parameter conversion examples
  • executor.py:121-165 - Result serialization examples

Complete Function Reference

Below is a comprehensive list of all available modules and functions with their complete signatures.

Summary

  • Total Modules: 14
  • Total Functions: 141

Module List

  1. SIMBAD - 18 functions
  2. NED - 15 functions
  3. VizieR - 9 functions
  4. ADS - 1 function
  5. MAST - 19 functions
  6. MAST Catalogs - 12 functions
  7. HEASARC - 9 functions
  8. IRSA - 7 functions
  9. NASA Exoplanet Archive - 7 functions
  10. Gaia - 13 functions
  11. SDSS - 16 functions
  12. ALMA - 16 functions
  13. ESA Hubble - 21 functions
  14. ESA JWST - 16 functions

Module Details

1. SIMBAD

Class: astroquery.simbad.Simbad

SIMBAD is an astronomical database providing basic data, cross-identifications, bibliography, and measurements for astronomical objects outside the solar system.

Functions (18):

1.1 simbad_query_object(object_name, wildcard=False, criteria=None, get_query_payload=False, async_job=False, verbose=False)

Query SIMBAD for the given object.

Parameters:

  • object_name (Any, required): Name of object to be queried
  • wildcard (Any, optional, default=False): When True, object specified with wildcards
  • criteria (Any, optional): Criteria in ADQL syntax
  • get_query_payload (Any, optional, default=False): When True, returns HTTP request parameters
  • async_job (Any, optional, default=False): Execute in asynchronous mode
  • verbose (Any, optional, default=False): Verbose output flag
1.2 simbad_query_region(coordinates, radius='2.0 arcmin', criteria=None, get_query_payload=False, async_job=False, equinox=None, epoch=None, cache=None)

Query SIMBAD in a cone around the specified coordinates.

Parameters:

  • coordinates (Any, required): Identifier or coordinates to query around
  • radius (Any, optional, default='2.0 arcmin'): Radius of the region
  • criteria (Any, optional): Criteria in ADQL syntax
  • get_query_payload (Any, optional, default=False): When True, returns HTTP request parameters
  • async_job (Any, optional, default=False): Execute in asynchronous mode
  • equinox (Any, optional): Equinox for coordinates
  • epoch (Any, optional): Epoch for coordinates
  • cache (Any, optional): Cache results flag
1.3 simbad_query_tap(query, maxrec=10000, async_job=False, get_query_payload=False, *uploads)

Query SIMBAD TAP service.

Parameters:

  • query (str, required): ADQL query string
  • maxrec (Any, optional, default=10000): Maximum number of records to return
  • async_job (Any, optional, default=False): Execute in asynchronous mode
  • get_query_payload (Any, optional, default=False): When True, returns HTTP request parameters
  • uploads (Any, required): Local tables to be used in query

For the complete list of all 141 functions across all 14 modules, see the full scope documentation or use the astroquery_list_functions() MCP tool.

Key Function Patterns

Most modules support these common query patterns:

  • Object queries: query_object(object_name, ...)
  • Region queries: query_region(coordinates, radius, ...)
  • TAP/ADQL queries: query_tap(query, ...) for advanced SQL-like queries
  • Criteria queries: query_criteria(**filters) for filtering by attributes
  • Download functions: download_*() for retrieving data products
  • List functions: list_*() for discovering available catalogs, tables, columns

Using the Functions

All functions are called through the astroquery_execute MCP tool:

# Basic syntax
astroquery_execute(module_name, function_name, params)

# Examples
astroquery_execute("simbad", "query_object", {"object_name": "M31"})
astroquery_execute("mast", "query_region", {"coordinates": "M31", "radius": 0.1})
astroquery_execute("heasarc", "query_tap", {"query": "SELECT * FROM chandra_observation"})

Discovery Workflow

  1. List modules: astroquery_list_modules() - See all 14 available modules
  2. List functions: astroquery_list_functions("simbad") - See all functions for SIMBAD
  3. Get function info: astroquery_get_function_info("simbad", "query_object") - See detailed parameters
  4. Execute: astroquery_execute("simbad", "query_object", {"object_name": "M31"}) - Run the query

Citation

If you use this software in your research, please cite it as:

Astroquery MCP Server. NASA IMPACT.
https://github.com/NASA-IMPACT/astroquery-mcp

Contributors

  • Gaurab Panthee (@igaurab) — original author
  • Ashkbiz Danehkar (@danehkar) — astrophysics domain expertise; Universities Space Research Association
  • Nish (@NISH1001) — NASA IMPACT / UAH
  • Muthukumaran R (@muthukumaranR) — NASA IMPACT

Acknowledgments

Originally developed by Gaurab Panthee in collaboration with astrophysicist Ashkbiz Danehkar (Universities Space Research Association). This repository is the canonical home for continued development under NASA IMPACT.

Built on astroquery, an Astropy-affiliated package, and served over MCP with FastMCP.

License

Licensed under the Apache License, Version 2.0. See LICENSE for details.

About

MCP server exposing 141 functions across 14 astronomical data services (SIMBAD, NED, MAST, ADS, Gaia, JWST, and more) to AI assistants — data-access backend for the AKD Astro Data Search Agent

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages