- Add get_pypi_package_reviews: Aggregates community feedback from GitHub issues, Stack Overflow, and social media with sentiment analysis - Add manage_pypi_package_discussions: Future-ready discussion management with GitHub Discussions integration - Add get_pypi_maintainer_contacts: Privacy-respecting maintainer contact discovery with communication guidelines - Integrate all tools with MCP server endpoints and comprehensive error handling - Add extensive test coverage for all community functionality - Follow existing code patterns and async/await best practices - Include future-ready implementations for when PyPI adds native community features
2587 lines
95 KiB
Python
2587 lines
95 KiB
Python
"""FastMCP server for PyPI package queries."""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
import click
|
|
from fastmcp import FastMCP
|
|
|
|
from .core.exceptions import InvalidPackageNameError, NetworkError, PackageNotFoundError, SearchError
|
|
from .prompts import (
|
|
analyze_daily_trends,
|
|
analyze_environment_dependencies,
|
|
analyze_package_quality,
|
|
audit_security_risks,
|
|
check_outdated_packages,
|
|
compare_packages,
|
|
find_trending_packages,
|
|
generate_migration_checklist,
|
|
generate_update_plan,
|
|
plan_package_migration,
|
|
plan_version_upgrade,
|
|
resolve_dependency_conflicts,
|
|
suggest_alternatives,
|
|
track_package_updates,
|
|
)
|
|
from .tools import (
|
|
analyze_pypi_competition,
|
|
check_pypi_credentials,
|
|
check_python_compatibility,
|
|
delete_pypi_release,
|
|
download_package_with_dependencies,
|
|
find_alternatives,
|
|
get_compatible_python_versions,
|
|
get_package_download_stats,
|
|
get_package_download_trends,
|
|
get_pypi_account_info,
|
|
get_pypi_package_analytics,
|
|
get_pypi_package_rankings,
|
|
get_pypi_security_alerts,
|
|
get_pypi_upload_history,
|
|
get_top_packages_by_downloads,
|
|
get_trending_packages,
|
|
manage_package_keywords,
|
|
manage_package_urls,
|
|
manage_pypi_maintainers,
|
|
query_package_dependencies,
|
|
query_package_info,
|
|
query_package_versions,
|
|
resolve_package_dependencies,
|
|
search_by_category,
|
|
search_packages,
|
|
set_package_visibility,
|
|
update_package_metadata,
|
|
upload_package_to_pypi,
|
|
get_pypi_package_reviews,
|
|
manage_pypi_package_discussions,
|
|
get_pypi_maintainer_contacts,
|
|
)
|
|
from .tools.discovery import (
|
|
get_pypi_package_recommendations,
|
|
get_pypi_trending_today,
|
|
monitor_pypi_new_releases,
|
|
search_pypi_by_maintainer,
|
|
)
|
|
|
|
# Configure logging
|
|
logging.basicConfig(
|
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
)
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Create FastMCP application
|
|
mcp = FastMCP("PyPI Query MCP Server")
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_package_info(package_name: str) -> dict[str, Any]:
|
|
"""Query comprehensive information about a PyPI package.
|
|
|
|
This tool retrieves detailed information about a Python package from PyPI,
|
|
including metadata, description, author information, dependencies, and more.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to query (e.g., 'requests', 'django')
|
|
|
|
Returns:
|
|
Dictionary containing comprehensive package information including:
|
|
- Basic metadata (name, version, summary, description)
|
|
- Author and maintainer information
|
|
- License and project URLs
|
|
- Python version requirements
|
|
- Dependencies and classifiers
|
|
- Version history summary
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Querying package info for {package_name}")
|
|
result = await query_package_info(package_name)
|
|
logger.info(f"Successfully retrieved info for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error querying package {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error querying package {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_package_versions(package_name: str) -> dict[str, Any]:
|
|
"""Get version information for a PyPI package.
|
|
|
|
This tool retrieves comprehensive version information for a Python package,
|
|
including all available versions, release details, and distribution formats.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to query (e.g., 'requests', 'numpy')
|
|
|
|
Returns:
|
|
Dictionary containing version information including:
|
|
- Latest version and total version count
|
|
- List of all available versions (sorted)
|
|
- Recent versions with release details
|
|
- Distribution format information (wheel, source)
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Querying versions for {package_name}")
|
|
result = await query_package_versions(package_name)
|
|
logger.info(f"Successfully retrieved versions for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error querying versions for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error querying versions for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_package_dependencies(
|
|
package_name: str,
|
|
version: str | None = None,
|
|
include_transitive: bool = False,
|
|
max_depth: int = 5,
|
|
python_version: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Get dependency information for a PyPI package.
|
|
|
|
This tool retrieves comprehensive dependency information for a Python package,
|
|
including runtime dependencies, development dependencies, and optional dependencies.
|
|
When include_transitive=True, provides complete dependency tree analysis.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to query (e.g., 'django', 'flask')
|
|
version: Specific version to query (optional, defaults to latest version)
|
|
include_transitive: Whether to include transitive dependencies (default: False)
|
|
max_depth: Maximum recursion depth for transitive dependencies (default: 5)
|
|
python_version: Target Python version for dependency filtering (optional)
|
|
|
|
Returns:
|
|
Dictionary containing dependency information including:
|
|
- Runtime dependencies and development dependencies
|
|
- Optional dependency groups
|
|
- Python version requirements
|
|
- Dependency counts and summary statistics
|
|
- Transitive dependency tree (if include_transitive=True)
|
|
- Circular dependency detection
|
|
- Performance impact analysis
|
|
- Complexity scoring
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(
|
|
f"MCP tool: Querying dependencies for {package_name}"
|
|
+ (f" version {version}" if version else " (latest)")
|
|
+ (
|
|
f" with transitive dependencies (max depth: {max_depth})"
|
|
if include_transitive
|
|
else " (direct only)"
|
|
)
|
|
)
|
|
result = await query_package_dependencies(
|
|
package_name, version, include_transitive, max_depth, python_version
|
|
)
|
|
logger.info(f"Successfully retrieved dependencies for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error querying dependencies for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"version": version,
|
|
"include_transitive": include_transitive,
|
|
"max_depth": max_depth,
|
|
"python_version": python_version,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error querying dependencies for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"version": version,
|
|
"include_transitive": include_transitive,
|
|
"max_depth": max_depth,
|
|
"python_version": python_version,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def check_package_python_compatibility(
|
|
package_name: str, target_python_version: str, use_cache: bool = True
|
|
) -> dict[str, Any]:
|
|
"""Check if a package is compatible with a specific Python version.
|
|
|
|
This tool analyzes a package's Python version requirements and determines
|
|
if it's compatible with your target Python version.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to check (e.g., 'django', 'requests')
|
|
target_python_version: Target Python version to check (e.g., '3.9', '3.10.5', '3.11')
|
|
use_cache: Whether to use cached package data (default: True)
|
|
|
|
Returns:
|
|
Dictionary containing detailed compatibility information including:
|
|
- Compatibility status (True/False)
|
|
- Source of compatibility information (requires_python or classifiers)
|
|
- Detailed analysis and suggestions
|
|
- Package version requirements
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(
|
|
f"MCP tool: Checking Python {target_python_version} compatibility for {package_name}"
|
|
)
|
|
result = await check_python_compatibility(
|
|
package_name, target_python_version, use_cache
|
|
)
|
|
logger.info(f"Compatibility check completed for {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error checking compatibility for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"target_python_version": target_python_version,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error checking compatibility for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"target_python_version": target_python_version,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_package_compatible_python_versions(
|
|
package_name: str, python_versions: list[str] | None = None, use_cache: bool = True
|
|
) -> dict[str, Any]:
|
|
"""Get all Python versions compatible with a package.
|
|
|
|
This tool analyzes a package and returns which Python versions are
|
|
compatible with it, along with recommendations.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to analyze (e.g., 'numpy', 'pandas')
|
|
python_versions: List of Python versions to check (optional, defaults to common versions)
|
|
use_cache: Whether to use cached package data (default: True)
|
|
|
|
Returns:
|
|
Dictionary containing compatibility information including:
|
|
- List of compatible Python versions
|
|
- List of incompatible versions with reasons
|
|
- Compatibility rate and recommendations
|
|
- Package version requirements
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Getting compatible Python versions for {package_name}")
|
|
result = await get_compatible_python_versions(
|
|
package_name, python_versions, use_cache
|
|
)
|
|
logger.info(f"Compatible versions analysis completed for {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error getting compatible versions for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Unexpected error getting compatible versions for {package_name}: {e}"
|
|
)
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def resolve_dependencies(
|
|
package_name: str,
|
|
python_version: str | None = None,
|
|
include_extras: list[str] | None = None,
|
|
include_dev: bool = False,
|
|
max_depth: int = 5,
|
|
) -> dict[str, Any]:
|
|
"""Resolve all dependencies for a PyPI package recursively.
|
|
|
|
This tool performs comprehensive dependency resolution for a Python package,
|
|
analyzing the complete dependency tree including transitive dependencies.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to analyze (e.g., 'pyside2', 'django')
|
|
python_version: Target Python version for dependency filtering (e.g., '3.10', '3.11')
|
|
include_extras: List of extra dependency groups to include. These are optional
|
|
dependency groups defined by the package (e.g., ['socks'] for requests,
|
|
['argon2', 'bcrypt'] for django, ['test', 'doc'] for setuptools). Check the
|
|
package's PyPI page or use the provides_extra field to see available extras.
|
|
include_dev: Whether to include development dependencies (default: False)
|
|
max_depth: Maximum recursion depth for dependency resolution (default: 5)
|
|
|
|
Returns:
|
|
Dictionary containing comprehensive dependency analysis including:
|
|
- Complete dependency tree with all transitive dependencies
|
|
- Dependency categorization (runtime, development, extras)
|
|
- Package metadata for each dependency
|
|
- Summary statistics and analysis
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(
|
|
f"MCP tool: Resolving dependencies for {package_name} "
|
|
f"(Python {python_version}, extras: {include_extras})"
|
|
)
|
|
result = await resolve_package_dependencies(
|
|
package_name=package_name,
|
|
python_version=python_version,
|
|
include_extras=include_extras,
|
|
include_dev=include_dev,
|
|
max_depth=max_depth,
|
|
)
|
|
logger.info(f"Successfully resolved dependencies for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error resolving dependencies for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"python_version": python_version,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error resolving dependencies for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"python_version": python_version,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def download_package(
|
|
package_name: str,
|
|
download_dir: str = "./downloads",
|
|
python_version: str | None = None,
|
|
include_extras: list[str] | None = None,
|
|
include_dev: bool = False,
|
|
prefer_wheel: bool = True,
|
|
verify_checksums: bool = True,
|
|
max_depth: int = 5,
|
|
) -> dict[str, Any]:
|
|
"""Download a PyPI package and all its dependencies to local directory.
|
|
|
|
This tool downloads a Python package and all its dependencies, providing
|
|
comprehensive package collection for offline installation or analysis.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to download (e.g., 'pyside2', 'requests')
|
|
download_dir: Local directory to download packages to (default: './downloads')
|
|
python_version: Target Python version for compatibility (e.g., '3.10', '3.11')
|
|
include_extras: List of extra dependency groups to include. These are optional
|
|
dependency groups defined by the package (e.g., ['socks'] for requests,
|
|
['argon2', 'bcrypt'] for django). Check the package's PyPI page to see available extras.
|
|
include_dev: Whether to include development dependencies (default: False)
|
|
prefer_wheel: Whether to prefer wheel files over source distributions (default: True)
|
|
verify_checksums: Whether to verify downloaded file checksums (default: True)
|
|
max_depth: Maximum dependency resolution depth (default: 5)
|
|
|
|
Returns:
|
|
Dictionary containing download results including:
|
|
- Download statistics and file information
|
|
- Dependency resolution results
|
|
- File verification results
|
|
- Success/failure summary for each package
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(
|
|
f"MCP tool: Downloading {package_name} and dependencies to {download_dir} "
|
|
f"(Python {python_version})"
|
|
)
|
|
result = await download_package_with_dependencies(
|
|
package_name=package_name,
|
|
download_dir=download_dir,
|
|
python_version=python_version,
|
|
include_extras=include_extras,
|
|
include_dev=include_dev,
|
|
prefer_wheel=prefer_wheel,
|
|
verify_checksums=verify_checksums,
|
|
max_depth=max_depth,
|
|
)
|
|
logger.info(f"Successfully downloaded {package_name} and dependencies")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error downloading {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"download_dir": download_dir,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error downloading {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"download_dir": download_dir,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_download_statistics(
|
|
package_name: str, period: str = "month", use_cache: bool = True
|
|
) -> dict[str, Any]:
|
|
"""Get download statistics for a PyPI package.
|
|
|
|
This tool retrieves comprehensive download statistics for a Python package,
|
|
including recent download counts, trends, and analysis.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to analyze (e.g., 'requests', 'numpy')
|
|
period: Time period for recent downloads ('day', 'week', 'month', default: 'month')
|
|
use_cache: Whether to use cached data for faster responses (default: True)
|
|
|
|
Returns:
|
|
Dictionary containing download statistics including:
|
|
- Recent download counts (last day/week/month)
|
|
- Package metadata and repository information
|
|
- Download trends and growth analysis
|
|
- Data source and timestamp information
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(
|
|
f"MCP tool: Getting download statistics for {package_name} (period: {period})"
|
|
)
|
|
result = await get_package_download_stats(package_name, period, use_cache)
|
|
logger.info(
|
|
f"Successfully retrieved download statistics for package: {package_name}"
|
|
)
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error getting download statistics for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"period": period,
|
|
}
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Unexpected error getting download statistics for {package_name}: {e}"
|
|
)
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"period": period,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_download_trends(
|
|
package_name: str, include_mirrors: bool = False, use_cache: bool = True
|
|
) -> dict[str, Any]:
|
|
"""Get download trends and time series for a PyPI package.
|
|
|
|
This tool retrieves detailed download trends and time series data for a Python package,
|
|
providing insights into download patterns over the last 180 days.
|
|
|
|
Args:
|
|
package_name: The name of the PyPI package to analyze (e.g., 'django', 'flask')
|
|
include_mirrors: Whether to include mirror downloads in analysis (default: False)
|
|
use_cache: Whether to use cached data for faster responses (default: True)
|
|
|
|
Returns:
|
|
Dictionary containing download trends including:
|
|
- Time series data for the last 180 days
|
|
- Trend analysis (increasing/decreasing/stable)
|
|
- Peak download periods and statistics
|
|
- Average daily downloads and growth indicators
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is empty or invalid
|
|
PackageNotFoundError: If package is not found on PyPI
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(
|
|
f"MCP tool: Getting download trends for {package_name} "
|
|
f"(include_mirrors: {include_mirrors})"
|
|
)
|
|
result = await get_package_download_trends(
|
|
package_name, include_mirrors, use_cache
|
|
)
|
|
logger.info(
|
|
f"Successfully retrieved download trends for package: {package_name}"
|
|
)
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error getting download trends for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"include_mirrors": include_mirrors,
|
|
}
|
|
except Exception as e:
|
|
logger.error(
|
|
f"Unexpected error getting download trends for {package_name}: {e}"
|
|
)
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"include_mirrors": include_mirrors,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_top_downloaded_packages(
|
|
period: str = "month", limit: int = 20
|
|
) -> dict[str, Any]:
|
|
"""Get the most downloaded PyPI packages.
|
|
|
|
This tool retrieves a list of the most popular Python packages by download count,
|
|
helping you discover trending and widely-used packages in the Python ecosystem.
|
|
|
|
Args:
|
|
period: Time period for download ranking ('day', 'week', 'month', default: 'month')
|
|
limit: Maximum number of packages to return (default: 20, max: 50)
|
|
|
|
Returns:
|
|
Dictionary containing top packages information including:
|
|
- Ranked list of packages with download counts
|
|
- Package metadata and repository links
|
|
- Period and ranking information
|
|
- Data source and limitations
|
|
|
|
Note:
|
|
Due to API limitations, this tool provides results based on known popular packages.
|
|
For comprehensive data analysis, consider using Google BigQuery with PyPI datasets.
|
|
"""
|
|
try:
|
|
# Limit the maximum number of packages to prevent excessive API calls
|
|
actual_limit = min(limit, 50)
|
|
|
|
logger.info(
|
|
f"MCP tool: Getting top {actual_limit} packages for period: {period}"
|
|
)
|
|
result = await get_top_packages_by_downloads(period, actual_limit)
|
|
logger.info("Successfully retrieved top packages list")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error getting top packages: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"period": period,
|
|
"limit": limit,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def search_pypi_packages(
|
|
query: str,
|
|
limit: int = 20,
|
|
python_versions: list[str] | None = None,
|
|
licenses: list[str] | None = None,
|
|
categories: list[str] | None = None,
|
|
min_downloads: int | None = None,
|
|
maintenance_status: str | None = None,
|
|
has_wheels: bool | None = None,
|
|
sort_by: str = "relevance",
|
|
sort_desc: bool = True,
|
|
semantic_search: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Search PyPI packages with advanced filtering and sorting.
|
|
|
|
This tool provides comprehensive search functionality for PyPI packages with
|
|
advanced filtering options, multiple sorting criteria, and semantic search capabilities.
|
|
|
|
Args:
|
|
query: Search query string (required)
|
|
limit: Maximum number of results to return (default: 20, max: 100)
|
|
python_versions: Filter by Python versions (e.g., ["3.9", "3.10", "3.11"])
|
|
licenses: Filter by license types (e.g., ["mit", "apache", "bsd", "gpl"])
|
|
categories: Filter by categories (e.g., ["web", "data-science", "testing"])
|
|
min_downloads: Minimum monthly downloads threshold
|
|
maintenance_status: Filter by maintenance status ("active", "maintained", "stale", "abandoned")
|
|
has_wheels: Filter packages that have wheel distributions (true/false)
|
|
sort_by: Sort field ("relevance", "popularity", "recency", "quality", "name", "downloads")
|
|
sort_desc: Sort in descending order (default: true)
|
|
semantic_search: Use semantic search on package descriptions (default: false)
|
|
|
|
Returns:
|
|
Dictionary containing search results with packages, metadata, and filtering info
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If search query is empty or invalid
|
|
SearchError: If search operation fails
|
|
"""
|
|
try:
|
|
return await search_packages(
|
|
query=query,
|
|
limit=limit,
|
|
python_versions=python_versions,
|
|
licenses=licenses,
|
|
categories=categories,
|
|
min_downloads=min_downloads,
|
|
maintenance_status=maintenance_status,
|
|
has_wheels=has_wheels,
|
|
sort_by=sort_by,
|
|
sort_desc=sort_desc,
|
|
semantic_search=semantic_search,
|
|
)
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError):
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error searching packages for '{query}': {e}")
|
|
return {
|
|
"error": f"Search failed: {e}",
|
|
"error_type": "SearchError",
|
|
"query": query,
|
|
"limit": limit,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def search_packages_by_category(
|
|
category: str,
|
|
limit: int = 20,
|
|
sort_by: str = "popularity",
|
|
python_version: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Search packages by category with popularity sorting.
|
|
|
|
This tool searches for packages in specific categories, making it easy to discover
|
|
relevant packages for particular use cases or domains.
|
|
|
|
Args:
|
|
category: Category to search ("web", "data-science", "database", "testing", "cli",
|
|
"security", "networking", "dev-tools", "cloud", "gui")
|
|
limit: Maximum number of results to return (default: 20)
|
|
sort_by: Sort field (default: "popularity")
|
|
python_version: Filter by Python version compatibility (e.g., "3.10")
|
|
|
|
Returns:
|
|
Dictionary containing categorized search results
|
|
|
|
Raises:
|
|
SearchError: If category search fails
|
|
"""
|
|
try:
|
|
return await search_by_category(
|
|
category=category,
|
|
limit=limit,
|
|
sort_by=sort_by,
|
|
python_version=python_version,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error searching category '{category}': {e}")
|
|
return {
|
|
"error": f"Category search failed: {e}",
|
|
"error_type": "SearchError",
|
|
"category": category,
|
|
"limit": limit,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def find_package_alternatives(
|
|
package_name: str,
|
|
limit: int = 10,
|
|
include_similar: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Find alternative packages to a given package.
|
|
|
|
This tool analyzes a package's functionality and finds similar or alternative
|
|
packages that could serve the same purpose, useful for evaluating options
|
|
or finding replacements.
|
|
|
|
Args:
|
|
package_name: Name of the package to find alternatives for
|
|
limit: Maximum number of alternatives to return (default: 10)
|
|
include_similar: Include packages with similar functionality (default: true)
|
|
|
|
Returns:
|
|
Dictionary containing alternative packages with analysis and recommendations
|
|
|
|
Raises:
|
|
PackageNotFoundError: If the target package is not found
|
|
SearchError: If alternatives search fails
|
|
"""
|
|
try:
|
|
return await find_alternatives(
|
|
package_name=package_name,
|
|
limit=limit,
|
|
include_similar=include_similar,
|
|
)
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError):
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error finding alternatives for '{package_name}': {e}")
|
|
return {
|
|
"error": f"Alternatives search failed: {e}",
|
|
"error_type": "SearchError",
|
|
"package_name": package_name,
|
|
"limit": limit,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_trending_pypi_packages(
|
|
category: str | None = None,
|
|
time_period: str = "week",
|
|
limit: int = 20,
|
|
) -> dict[str, Any]:
|
|
"""Get trending packages based on recent download activity.
|
|
|
|
This tool identifies packages that are gaining popularity or have high
|
|
recent download activity, useful for discovering emerging trends in the
|
|
Python ecosystem.
|
|
|
|
Args:
|
|
category: Optional category filter ("web", "data-science", "database", etc.)
|
|
time_period: Time period for trending analysis ("day", "week", "month")
|
|
limit: Maximum number of packages to return (default: 20)
|
|
|
|
Returns:
|
|
Dictionary containing trending packages with analysis and metrics
|
|
|
|
Raises:
|
|
SearchError: If trending analysis fails
|
|
"""
|
|
try:
|
|
return await get_trending_packages(
|
|
category=category,
|
|
time_period=time_period,
|
|
limit=limit,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error getting trending packages (category: {category}): {e}")
|
|
return {
|
|
"error": f"Trending analysis failed: {e}",
|
|
"error_type": "SearchError",
|
|
"category": category,
|
|
"time_period": time_period,
|
|
"limit": limit,
|
|
}
|
|
|
|
|
|
# PyPI Publishing and Account Management Tools
|
|
|
|
|
|
@mcp.tool()
|
|
async def upload_package_to_pypi_tool(
|
|
distribution_paths: list[str],
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
skip_existing: bool = True,
|
|
verify_uploads: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Upload package distributions to PyPI or TestPyPI.
|
|
|
|
This tool uploads Python package distributions (.whl, .tar.gz files) to PyPI,
|
|
providing comprehensive upload management with safety checks and verification.
|
|
|
|
Args:
|
|
distribution_paths: List of paths to distribution files (.whl, .tar.gz)
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to upload to TestPyPI instead of production PyPI
|
|
skip_existing: Skip files that already exist on PyPI
|
|
verify_uploads: Verify uploads after completion
|
|
|
|
Returns:
|
|
Dictionary containing upload results and metadata
|
|
|
|
Raises:
|
|
PyPIAuthenticationError: If authentication fails
|
|
PyPIUploadError: If upload operations fail
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Uploading {len(distribution_paths)} distributions to {'TestPyPI' if test_pypi else 'PyPI'}")
|
|
result = await upload_package_to_pypi(
|
|
distribution_paths=distribution_paths,
|
|
api_token=api_token,
|
|
test_pypi=test_pypi,
|
|
skip_existing=skip_existing,
|
|
verify_uploads=verify_uploads,
|
|
)
|
|
logger.info(f"Upload completed: {result.get('summary', {})}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error uploading packages: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"distribution_paths": distribution_paths,
|
|
"test_pypi": test_pypi,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def check_pypi_credentials_tool(
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Validate PyPI API token and credentials.
|
|
|
|
This tool checks if your PyPI API token is valid and provides information
|
|
about your account permissions and capabilities.
|
|
|
|
Args:
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to check against TestPyPI instead of production PyPI
|
|
|
|
Returns:
|
|
Dictionary containing credential validation results
|
|
|
|
Raises:
|
|
PyPIAuthenticationError: If credential validation fails
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Checking {'TestPyPI' if test_pypi else 'PyPI'} credentials")
|
|
result = await check_pypi_credentials(api_token=api_token, test_pypi=test_pypi)
|
|
logger.info(f"Credential check completed: valid={result.get('valid', False)}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error checking credentials: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"test_pypi": test_pypi,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_pypi_upload_history_tool(
|
|
package_name: str,
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
limit: int = 50,
|
|
) -> dict[str, Any]:
|
|
"""Get upload history for a PyPI package.
|
|
|
|
This tool retrieves the upload history for a package, showing all versions,
|
|
files, and upload metadata with statistics and analysis.
|
|
|
|
Args:
|
|
package_name: Name of the package to get upload history for
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to check TestPyPI instead of production PyPI
|
|
limit: Maximum number of uploads to return
|
|
|
|
Returns:
|
|
Dictionary containing upload history and metadata
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Getting upload history for {package_name}")
|
|
result = await get_pypi_upload_history(
|
|
package_name=package_name,
|
|
api_token=api_token,
|
|
test_pypi=test_pypi,
|
|
limit=limit,
|
|
)
|
|
upload_count = result.get('statistics', {}).get('total_uploads', 0)
|
|
logger.info(f"Retrieved {upload_count} upload records for {package_name}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error getting upload history for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"test_pypi": test_pypi,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def delete_pypi_release_tool(
|
|
package_name: str,
|
|
version: str,
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
confirm_deletion: bool = False,
|
|
dry_run: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Delete a specific release from PyPI (with safety checks).
|
|
|
|
This tool provides safe deletion of PyPI releases with multiple safety checks,
|
|
dry-run capability, and comprehensive validation. Note that PyPI deletion is
|
|
very restricted and typically only available to package owners within a limited
|
|
time window after upload.
|
|
|
|
Args:
|
|
package_name: Name of the package
|
|
version: Version to delete
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to use TestPyPI instead of production PyPI
|
|
confirm_deletion: Explicit confirmation required for actual deletion
|
|
dry_run: If True, only simulate the deletion without actually performing it
|
|
|
|
Returns:
|
|
Dictionary containing deletion results and safety information
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package/version is not found
|
|
PyPIPermissionError: If deletion is not permitted
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: {'DRY RUN: ' if dry_run else ''}Deleting {package_name}=={version}")
|
|
result = await delete_pypi_release(
|
|
package_name=package_name,
|
|
version=version,
|
|
api_token=api_token,
|
|
test_pypi=test_pypi,
|
|
confirm_deletion=confirm_deletion,
|
|
dry_run=dry_run,
|
|
)
|
|
action = result.get('action', 'unknown')
|
|
logger.info(f"Deletion operation completed: {action}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error deleting release {package_name}=={version}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"version": version,
|
|
"test_pypi": test_pypi,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def manage_pypi_maintainers_tool(
|
|
package_name: str,
|
|
action: str,
|
|
username: str | None = None,
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Manage package maintainers (add/remove/list).
|
|
|
|
This tool helps manage package maintainers and collaborators. Note that maintainer
|
|
management typically requires package owner permissions and may need to be done
|
|
through the PyPI web interface.
|
|
|
|
Args:
|
|
package_name: Name of the package
|
|
action: Action to perform ('list', 'add', 'remove')
|
|
username: Username to add/remove (required for add/remove actions)
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to use TestPyPI instead of production PyPI
|
|
|
|
Returns:
|
|
Dictionary containing maintainer management results
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
PyPIPermissionError: If action is not permitted
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Managing maintainers for {package_name}: {action}")
|
|
result = await manage_pypi_maintainers(
|
|
package_name=package_name,
|
|
action=action,
|
|
username=username,
|
|
api_token=api_token,
|
|
test_pypi=test_pypi,
|
|
)
|
|
maintainer_count = result.get('maintainer_count', 0)
|
|
logger.info(f"Maintainer management completed: {maintainer_count} maintainers")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error managing maintainers for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"action": action,
|
|
"test_pypi": test_pypi,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_pypi_account_info_tool(
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Get PyPI account information, quotas, and limits.
|
|
|
|
This tool retrieves information about your PyPI account including permissions,
|
|
limitations, quotas, and provides recommendations for account security and usage.
|
|
|
|
Args:
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to use TestPyPI instead of production PyPI
|
|
|
|
Returns:
|
|
Dictionary containing account information and limitations
|
|
|
|
Raises:
|
|
PyPIAuthenticationError: If authentication fails
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Getting account information for {'TestPyPI' if test_pypi else 'PyPI'}")
|
|
result = await get_pypi_account_info(api_token=api_token, test_pypi=test_pypi)
|
|
logger.info("Account information retrieved successfully")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error getting account information: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"test_pypi": test_pypi,
|
|
}
|
|
|
|
|
|
# Metadata Management Tools
|
|
@mcp.tool()
|
|
async def update_package_metadata_tool(
|
|
package_name: str,
|
|
description: str | None = None,
|
|
keywords: list[str] | None = None,
|
|
classifiers: list[str] | None = None,
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
dry_run: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Update package metadata including description, keywords, and classifiers.
|
|
|
|
This tool helps manage PyPI package metadata by validating changes and providing
|
|
guidance on how to update metadata through package uploads.
|
|
|
|
Args:
|
|
package_name: Name of the package to update
|
|
description: New package description
|
|
keywords: List of keywords for the package
|
|
classifiers: List of PyPI classifiers (e.g., programming language, license)
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to use TestPyPI instead of production PyPI
|
|
dry_run: If True, only validate changes without applying them
|
|
|
|
Returns:
|
|
Dictionary containing metadata update results and recommendations
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
PyPIPermissionError: If user lacks permission to modify package
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Updating metadata for {package_name} (dry_run={dry_run})")
|
|
result = await update_package_metadata(
|
|
package_name=package_name,
|
|
description=description,
|
|
keywords=keywords,
|
|
classifiers=classifiers,
|
|
api_token=api_token,
|
|
test_pypi=test_pypi,
|
|
dry_run=dry_run,
|
|
)
|
|
logger.info(f"Metadata update completed for {package_name}: {result.get('success', 'analysis_complete')}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error updating metadata for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"dry_run": dry_run,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def manage_package_urls_tool(
|
|
package_name: str,
|
|
homepage: str | None = None,
|
|
documentation: str | None = None,
|
|
repository: str | None = None,
|
|
download_url: str | None = None,
|
|
bug_tracker: str | None = None,
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
validate_urls: bool = True,
|
|
dry_run: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Manage package URLs including homepage, documentation, and repository links.
|
|
|
|
This tool validates and manages package URLs, providing guidance on proper
|
|
URL configuration and accessibility checking.
|
|
|
|
Args:
|
|
package_name: Name of the package to update
|
|
homepage: Package homepage URL
|
|
documentation: Documentation URL
|
|
repository: Source code repository URL
|
|
download_url: Package download URL
|
|
bug_tracker: Bug tracker URL
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to use TestPyPI instead of production PyPI
|
|
validate_urls: Whether to validate URL accessibility
|
|
dry_run: If True, only validate changes without applying them
|
|
|
|
Returns:
|
|
Dictionary containing URL management results and validation
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
PyPIPermissionError: If user lacks permission to modify package
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Managing URLs for {package_name} (dry_run={dry_run})")
|
|
result = await manage_package_urls(
|
|
package_name=package_name,
|
|
homepage=homepage,
|
|
documentation=documentation,
|
|
repository=repository,
|
|
download_url=download_url,
|
|
bug_tracker=bug_tracker,
|
|
api_token=api_token,
|
|
test_pypi=test_pypi,
|
|
validate_urls=validate_urls,
|
|
dry_run=dry_run,
|
|
)
|
|
logger.info(f"URL management completed for {package_name}: quality_score={result.get('url_quality_score', 0)}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error managing URLs for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"dry_run": dry_run,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def set_package_visibility_tool(
|
|
package_name: str,
|
|
visibility: str,
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
confirm_action: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Set package visibility (private/public) for organization packages.
|
|
|
|
This tool provides guidance on package visibility management, which is primarily
|
|
available for PyPI organizations with special permissions.
|
|
|
|
Args:
|
|
package_name: Name of the package to modify
|
|
visibility: Visibility setting ("public" or "private")
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to use TestPyPI instead of production PyPI
|
|
confirm_action: Explicit confirmation required for visibility changes
|
|
|
|
Returns:
|
|
Dictionary containing visibility management results and limitations
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
PyPIPermissionError: If user lacks permission to modify package
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Setting visibility for {package_name} to {visibility}")
|
|
result = await set_package_visibility(
|
|
package_name=package_name,
|
|
visibility=visibility,
|
|
api_token=api_token,
|
|
test_pypi=test_pypi,
|
|
confirm_action=confirm_action,
|
|
)
|
|
logger.info(f"Visibility analysis completed for {package_name}: {result.get('success', 'analysis_complete')}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error setting visibility for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"visibility": visibility,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def manage_package_keywords_tool(
|
|
package_name: str,
|
|
action: str,
|
|
keywords: list[str] | None = None,
|
|
api_token: str | None = None,
|
|
test_pypi: bool = False,
|
|
dry_run: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Manage package keywords and search tags.
|
|
|
|
This tool provides comprehensive keyword management including validation,
|
|
quality analysis, and recommendations for better package discoverability.
|
|
|
|
Args:
|
|
package_name: Name of the package to modify
|
|
action: Action to perform ("add", "remove", "replace", "list")
|
|
keywords: List of keywords to add/remove/replace
|
|
api_token: PyPI API token (or use PYPI_API_TOKEN env var)
|
|
test_pypi: Whether to use TestPyPI instead of production PyPI
|
|
dry_run: If True, only simulate changes without applying them
|
|
|
|
Returns:
|
|
Dictionary containing keyword management results and recommendations
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
PyPIPermissionError: If user lacks permission to modify package
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Managing keywords for {package_name}: {action} (dry_run={dry_run})")
|
|
result = await manage_package_keywords(
|
|
package_name=package_name,
|
|
action=action,
|
|
keywords=keywords,
|
|
api_token=api_token,
|
|
test_pypi=test_pypi,
|
|
dry_run=dry_run,
|
|
)
|
|
logger.info(f"Keyword management completed for {package_name}: {result.get('success', 'analysis_complete')}")
|
|
return result
|
|
except Exception as e:
|
|
logger.error(f"Error managing keywords for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"action": action,
|
|
}
|
|
|
|
|
|
# PyPI Development Workflow Tools
|
|
@mcp.tool()
|
|
async def validate_package_name_pypi(package_name: str) -> dict[str, Any]:
|
|
"""Check if a package name is available and valid on PyPI.
|
|
|
|
This tool validates package name format according to PyPI standards and checks
|
|
availability on PyPI. It provides recommendations for improvement and suggests
|
|
alternatives if the name is already taken.
|
|
|
|
Args:
|
|
package_name: Name to validate and check for availability
|
|
|
|
Returns:
|
|
Dictionary containing validation results including:
|
|
- Format validation results and PyPI standards compliance
|
|
- Availability status on PyPI
|
|
- Recommendations for improvement
|
|
- Similar existing packages (if any)
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name format is severely invalid
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Validating package name: {package_name}")
|
|
from .tools.workflow import validate_pypi_package_name
|
|
result = await validate_pypi_package_name(package_name)
|
|
logger.info(f"Successfully validated package name: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, NetworkError) as e:
|
|
logger.error(f"Error validating package name {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error validating package name {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def preview_package_page_pypi(
|
|
package_name: str,
|
|
version: str = "1.0.0",
|
|
summary: str = "",
|
|
description: str = "",
|
|
author: str = "",
|
|
license_name: str = "MIT",
|
|
home_page: str = "",
|
|
keywords: list[str] = None,
|
|
classifiers: list[str] = None,
|
|
) -> dict[str, Any]:
|
|
"""Generate a preview of how a package page would look on PyPI.
|
|
|
|
This tool creates a preview of the PyPI package page based on the provided
|
|
metadata, helping developers visualize their package before upload and
|
|
optimize their package presentation.
|
|
|
|
Args:
|
|
package_name: Name of the package
|
|
version: Package version (default: "1.0.0")
|
|
summary: Short package description
|
|
description: Long package description
|
|
author: Package author name
|
|
license_name: License type (default: "MIT")
|
|
home_page: Project homepage URL
|
|
keywords: List of keywords for the package
|
|
classifiers: List of PyPI classifiers
|
|
|
|
Returns:
|
|
Dictionary containing preview information including:
|
|
- Formatted package metadata and rendered page sections
|
|
- Validation warnings and SEO recommendations
|
|
- Completeness and discoverability scores
|
|
- Upload readiness assessment
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Generating preview for package: {package_name}")
|
|
from .tools.workflow import preview_pypi_package_page
|
|
result = await preview_pypi_package_page(
|
|
package_name=package_name,
|
|
version=version,
|
|
summary=summary,
|
|
description=description,
|
|
author=author,
|
|
license_name=license_name,
|
|
home_page=home_page,
|
|
keywords=keywords or [],
|
|
classifiers=classifiers or [],
|
|
)
|
|
logger.info(f"Successfully generated preview for package: {package_name}")
|
|
return result
|
|
except InvalidPackageNameError as e:
|
|
logger.error(f"Error generating preview for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error generating preview for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def check_package_upload_requirements(
|
|
package_name: str,
|
|
version: str = "1.0.0",
|
|
author: str = "",
|
|
author_email: str = "",
|
|
description: str = "",
|
|
long_description: str = "",
|
|
license_name: str = "",
|
|
home_page: str = "",
|
|
classifiers: list[str] = None,
|
|
requires_python: str = "",
|
|
) -> dict[str, Any]:
|
|
"""Check if package metadata meets PyPI upload requirements.
|
|
|
|
This tool validates all required and recommended metadata fields for PyPI
|
|
package upload, following setup.py and setuptools standards. It provides
|
|
a comprehensive readiness assessment and actionable next steps.
|
|
|
|
Args:
|
|
package_name: Name of the package
|
|
version: Package version (default: "1.0.0")
|
|
author: Package author name
|
|
author_email: Author email address
|
|
description: Short package description
|
|
long_description: Detailed package description
|
|
license_name: License identifier
|
|
home_page: Project homepage URL
|
|
classifiers: List of PyPI classifiers
|
|
requires_python: Python version requirements
|
|
|
|
Returns:
|
|
Dictionary containing upload readiness assessment including:
|
|
- Required and recommended fields validation
|
|
- Compliance with PyPI standards and upload checklist
|
|
- Specific issues, warnings, and suggestions
|
|
- Actionable next steps for preparation
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Checking upload requirements for package: {package_name}")
|
|
from .tools.workflow import check_pypi_upload_requirements
|
|
result = await check_pypi_upload_requirements(
|
|
package_name=package_name,
|
|
version=version,
|
|
author=author,
|
|
author_email=author_email,
|
|
description=description,
|
|
long_description=long_description,
|
|
license_name=license_name,
|
|
home_page=home_page,
|
|
classifiers=classifiers or [],
|
|
requires_python=requires_python,
|
|
)
|
|
logger.info(f"Successfully checked upload requirements for package: {package_name}")
|
|
return result
|
|
except InvalidPackageNameError as e:
|
|
logger.error(f"Error checking upload requirements for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error checking upload requirements for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_package_build_logs(
|
|
package_name: str,
|
|
version: str | None = None,
|
|
platform: str = "all",
|
|
include_details: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Retrieve and analyze PyPI build logs and distribution information.
|
|
|
|
This tool fetches information about package builds, wheel distributions,
|
|
and build-related warnings or errors from PyPI. It provides comprehensive
|
|
analysis of build quality and platform support.
|
|
|
|
Args:
|
|
package_name: Name of the package to analyze
|
|
version: Specific version to check (optional, defaults to latest)
|
|
platform: Platform filter ("all", "windows", "macos", "linux")
|
|
include_details: Whether to include detailed file analysis
|
|
|
|
Returns:
|
|
Dictionary containing build information including:
|
|
- Available distributions (wheels, source) and build status
|
|
- Platform support and Python version coverage
|
|
- File sizes, checksums, and build quality analysis
|
|
- Build warnings, recommendations, and health assessment
|
|
|
|
Raises:
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Analyzing build logs for package: {package_name}")
|
|
from .tools.workflow import get_pypi_build_logs
|
|
result = await get_pypi_build_logs(
|
|
package_name=package_name,
|
|
version=version,
|
|
platform=platform,
|
|
include_details=include_details,
|
|
)
|
|
logger.info(f"Successfully analyzed build logs for package: {package_name}")
|
|
return result
|
|
except (PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error analyzing build logs for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"version": version,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error analyzing build logs for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"version": version,
|
|
}
|
|
|
|
|
|
# Register prompt templates following standard MCP workflow:
|
|
# 1. User calls tool → MCP client sends request
|
|
# 2. Tool function executes → Collects necessary data and parameters
|
|
# 3. Call Prompt generator → Pass parameters to corresponding generator
|
|
# 4. Load template → Get template with {{parameter}} placeholders
|
|
# 5. Parameter replacement → Replace {{parameter_name}} with actual values
|
|
# 6. Environment variable customization → Apply user's custom prompt words
|
|
# 7. Return final prompt → As tool's response back to AI
|
|
|
|
|
|
@mcp.prompt()
|
|
async def analyze_package_quality_prompt(
|
|
package_name: str, version: str | None = None
|
|
) -> str:
|
|
"""Generate a comprehensive quality analysis prompt for a PyPI package."""
|
|
# Step 3: Call Prompt generator
|
|
template = await analyze_package_quality(package_name, version)
|
|
|
|
# Step 5: Parameter replacement - replace {{parameter_name}} with actual values
|
|
result = template.replace("{{package_name}}", package_name)
|
|
|
|
# Handle version parameter
|
|
if version:
|
|
version_text = f"version {version}"
|
|
else:
|
|
version_text = ""
|
|
result = result.replace("{{version_text}}", version_text)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
@mcp.prompt()
|
|
async def compare_packages_prompt(
|
|
packages: list[str], use_case: str, criteria: list[str] | None = None
|
|
) -> str:
|
|
"""Generate a detailed comparison prompt for multiple PyPI packages."""
|
|
# Step 3: Call Prompt generator
|
|
template = await compare_packages(packages, use_case, criteria)
|
|
|
|
# Step 5: Parameter replacement
|
|
packages_text = ", ".join(f"'{pkg}'" for pkg in packages)
|
|
result = template.replace("{{packages_text}}", packages_text)
|
|
result = result.replace("{{use_case}}", use_case)
|
|
|
|
# Handle criteria parameter
|
|
if criteria:
|
|
criteria_text = (
|
|
f"\n\nFocus particularly on these criteria: {', '.join(criteria)}"
|
|
)
|
|
else:
|
|
criteria_text = ""
|
|
result = result.replace("{{criteria_text}}", criteria_text)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
@mcp.prompt()
|
|
async def suggest_alternatives_prompt(
|
|
package_name: str, reason: str, requirements: str | None = None
|
|
) -> str:
|
|
"""Generate a prompt for finding package alternatives."""
|
|
# Step 3: Call Prompt generator
|
|
template = await suggest_alternatives(package_name, reason, requirements)
|
|
|
|
# Step 5: Parameter replacement
|
|
result = template.replace("{{package_name}}", package_name)
|
|
|
|
# Handle reason parameter with context mapping
|
|
reason_context = {
|
|
"deprecated": "the package is deprecated or no longer maintained",
|
|
"security": "security vulnerabilities or concerns",
|
|
"performance": "performance issues or requirements",
|
|
"licensing": "licensing conflicts or restrictions",
|
|
"maintenance": "poor maintenance or lack of updates",
|
|
"features": "missing features or functionality gaps",
|
|
}
|
|
reason_text = reason_context.get(reason, reason)
|
|
result = result.replace("{{reason_text}}", reason_text)
|
|
|
|
# Handle requirements parameter
|
|
if requirements:
|
|
requirements_text = f"\n\nSpecific requirements: {requirements}"
|
|
else:
|
|
requirements_text = ""
|
|
result = result.replace("{{requirements_text}}", requirements_text)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
@mcp.prompt()
|
|
async def resolve_dependency_conflicts_prompt(
|
|
conflicts: list[str],
|
|
python_version: str | None = None,
|
|
project_context: str | None = None,
|
|
) -> str:
|
|
"""Generate a prompt for resolving dependency conflicts."""
|
|
messages = await resolve_dependency_conflicts(
|
|
conflicts, python_version, project_context
|
|
)
|
|
return messages[0].text
|
|
|
|
|
|
@mcp.prompt()
|
|
async def plan_version_upgrade_prompt(
|
|
package_name: str,
|
|
current_version: str,
|
|
target_version: str | None = None,
|
|
project_size: str | None = None,
|
|
) -> str:
|
|
"""Generate a prompt for planning package version upgrades."""
|
|
messages = await plan_version_upgrade(
|
|
package_name, current_version, target_version, project_size
|
|
)
|
|
return messages[0].text
|
|
|
|
|
|
@mcp.prompt()
|
|
async def audit_security_risks_prompt(
|
|
packages: list[str],
|
|
environment: str | None = None,
|
|
compliance_requirements: str | None = None,
|
|
) -> str:
|
|
"""Generate a prompt for security risk auditing of packages."""
|
|
messages = await audit_security_risks(
|
|
packages, environment, compliance_requirements
|
|
)
|
|
return messages[0].text
|
|
|
|
|
|
@mcp.prompt()
|
|
async def plan_package_migration_prompt(
|
|
from_package: str,
|
|
to_package: str,
|
|
codebase_size: str = "medium",
|
|
timeline: str | None = None,
|
|
team_size: int | None = None,
|
|
) -> str:
|
|
"""Generate a comprehensive package migration plan prompt."""
|
|
messages = await plan_package_migration(
|
|
from_package, to_package, codebase_size, timeline, team_size
|
|
)
|
|
return messages[0].text
|
|
|
|
|
|
@mcp.prompt()
|
|
async def generate_migration_checklist_prompt(
|
|
migration_type: str, packages_involved: list[str], environment: str = "all"
|
|
) -> str:
|
|
"""Generate a detailed migration checklist prompt."""
|
|
messages = await generate_migration_checklist(
|
|
migration_type, packages_involved, environment
|
|
)
|
|
return messages[0].text
|
|
|
|
|
|
# Environment Analysis Prompts
|
|
@mcp.prompt()
|
|
async def analyze_environment_dependencies_prompt(
|
|
environment_type: str = "local",
|
|
python_version: str | None = None,
|
|
project_path: str | None = None,
|
|
) -> str:
|
|
"""Generate a prompt for analyzing environment dependencies."""
|
|
# Step 3: Call Prompt generator
|
|
template = await analyze_environment_dependencies(
|
|
environment_type, python_version, project_path
|
|
)
|
|
|
|
# Step 5: Parameter replacement
|
|
result = template.replace("{{environment_type}}", environment_type)
|
|
|
|
# Handle environment info
|
|
env_info = f"({environment_type} environment)"
|
|
if python_version:
|
|
env_info += f" with Python {python_version}"
|
|
if project_path:
|
|
env_info += f" at {project_path}"
|
|
result = result.replace("{{environment_info}}", env_info)
|
|
|
|
# Handle command prefix based on environment
|
|
command_prefix = "uvx " if environment_type in ["virtual", "uv"] else ""
|
|
result = result.replace("{{command_prefix}}", command_prefix)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
@mcp.prompt()
|
|
async def check_outdated_packages_prompt(
|
|
package_filter: str | None = None,
|
|
severity_level: str = "all",
|
|
include_dev_dependencies: bool = True,
|
|
) -> str:
|
|
"""Generate a prompt for checking outdated packages."""
|
|
# Step 3: Call Prompt generator
|
|
template = await check_outdated_packages(
|
|
package_filter, severity_level, include_dev_dependencies
|
|
)
|
|
|
|
# Step 5: Parameter replacement
|
|
result = template.replace("{{severity_level}}", severity_level)
|
|
|
|
# Handle filter info
|
|
if package_filter:
|
|
filter_info = f" (filtering by: {package_filter})"
|
|
else:
|
|
filter_info = ""
|
|
result = result.replace("{{filter_info}}", filter_info)
|
|
|
|
# Handle dev dependencies
|
|
if include_dev_dependencies:
|
|
dev_deps_text = " including development dependencies"
|
|
else:
|
|
dev_deps_text = " excluding development dependencies"
|
|
result = result.replace("{{dev_deps_text}}", dev_deps_text)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
@mcp.prompt()
|
|
async def generate_update_plan_prompt(
|
|
update_strategy: str = "balanced",
|
|
environment_constraints: str | None = None,
|
|
testing_requirements: str | None = None,
|
|
) -> str:
|
|
"""Generate a prompt for creating package update plans."""
|
|
# Step 3: Call Prompt generator
|
|
template = await generate_update_plan(
|
|
update_strategy, environment_constraints, testing_requirements
|
|
)
|
|
|
|
# Step 5: Parameter replacement
|
|
result = template.replace("{{strategy}}", update_strategy)
|
|
|
|
# Handle constraints
|
|
if environment_constraints:
|
|
constraints_text = f"\n\nEnvironment constraints: {environment_constraints}"
|
|
else:
|
|
constraints_text = ""
|
|
result = result.replace("{{constraints_text}}", constraints_text)
|
|
|
|
# Handle testing requirements
|
|
if testing_requirements:
|
|
testing_text = f"\n\nTesting requirements: {testing_requirements}"
|
|
else:
|
|
testing_text = ""
|
|
result = result.replace("{{testing_text}}", testing_text)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
# Trending Analysis Prompts
|
|
@mcp.prompt()
|
|
async def analyze_daily_trends_prompt(
|
|
date: str = "today", category: str | None = None, limit: int = 20
|
|
) -> str:
|
|
"""Generate a prompt for analyzing daily PyPI trends."""
|
|
# Step 3: Call Prompt generator
|
|
template = await analyze_daily_trends(date, category, limit)
|
|
|
|
# Step 5: Parameter replacement
|
|
result = template.replace("{{date}}", date)
|
|
result = result.replace("{{limit}}", str(limit))
|
|
|
|
# Handle category filter
|
|
if category:
|
|
category_filter = f" focusing on {category} packages"
|
|
else:
|
|
category_filter = ""
|
|
result = result.replace("{{category_filter}}", category_filter)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
@mcp.prompt()
|
|
async def find_trending_packages_prompt(
|
|
time_period: str = "weekly", trend_type: str = "rising", domain: str | None = None
|
|
) -> str:
|
|
"""Generate a prompt for finding trending packages."""
|
|
# Step 3: Call Prompt generator
|
|
template = await find_trending_packages(time_period, trend_type, domain)
|
|
|
|
# Step 5: Parameter replacement
|
|
result = template.replace("{{time_period}}", time_period)
|
|
result = result.replace("{{trend_type}}", trend_type)
|
|
|
|
# Handle domain filter
|
|
if domain:
|
|
domain_filter = f" in the {domain} domain"
|
|
else:
|
|
domain_filter = ""
|
|
result = result.replace("{{domain_filter}}", domain_filter)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
@mcp.prompt()
|
|
async def track_package_updates_prompt(
|
|
time_range: str = "today", update_type: str = "all", popular_only: bool = False
|
|
) -> str:
|
|
"""Generate a prompt for tracking recent package updates."""
|
|
# Step 3: Call Prompt generator
|
|
template = await track_package_updates(time_range, update_type, popular_only)
|
|
|
|
# Step 5: Parameter replacement
|
|
result = template.replace("{{time_range}}", time_range)
|
|
result = result.replace("{{update_type}}", update_type)
|
|
|
|
# Handle popularity filter
|
|
if popular_only:
|
|
popularity_filter = " (popular packages only)"
|
|
popularity_description = "Popular packages with >1M downloads"
|
|
else:
|
|
popularity_filter = ""
|
|
popularity_description = "All packages in the ecosystem"
|
|
result = result.replace("{{popularity_filter}}", popularity_filter)
|
|
result = result.replace("{{popularity_description}}", popularity_description)
|
|
|
|
# Step 7: Return final prompt
|
|
return result
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_package_analytics(
|
|
package_name: str,
|
|
time_period: str = "month",
|
|
include_historical: bool = True,
|
|
include_platform_breakdown: bool = True,
|
|
include_version_analytics: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Get comprehensive analytics for a PyPI package including advanced metrics.
|
|
|
|
This tool provides detailed download analytics, trend analysis, geographic
|
|
distribution, platform breakdown, and version adoption patterns.
|
|
|
|
Args:
|
|
package_name: Name of the package to analyze
|
|
time_period: Time period for analysis ('day', 'week', 'month', 'year')
|
|
include_historical: Whether to include historical trend analysis
|
|
include_platform_breakdown: Whether to include platform/OS breakdown
|
|
include_version_analytics: Whether to include version-specific analytics
|
|
|
|
Returns:
|
|
Dictionary containing comprehensive analytics including:
|
|
- Download statistics and trends
|
|
- Platform and Python version breakdown
|
|
- Geographic distribution
|
|
- Version adoption patterns
|
|
- Quality metrics and indicators
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Generating comprehensive analytics for {package_name}")
|
|
result = await get_pypi_package_analytics(
|
|
package_name=package_name,
|
|
time_period=time_period,
|
|
include_historical=include_historical,
|
|
include_platform_breakdown=include_platform_breakdown,
|
|
include_version_analytics=include_version_analytics,
|
|
)
|
|
logger.info(f"Successfully generated analytics for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error generating analytics for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error generating analytics for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_security_alerts(
|
|
package_name: str,
|
|
include_dependencies: bool = True,
|
|
severity_filter: str | None = None,
|
|
include_historical: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""Get security alerts and vulnerability information for a PyPI package.
|
|
|
|
This tool queries multiple security databases including OSV (Open Source
|
|
Vulnerabilities), PyUp.io Safety DB, and GitHub Security Advisories to provide
|
|
comprehensive security information.
|
|
|
|
Args:
|
|
package_name: Name of the package to check for vulnerabilities
|
|
include_dependencies: Whether to check dependencies for vulnerabilities
|
|
severity_filter: Filter by severity ('LOW', 'MEDIUM', 'HIGH', 'CRITICAL')
|
|
include_historical: Whether to include historical vulnerabilities
|
|
|
|
Returns:
|
|
Dictionary containing security information including:
|
|
- Active vulnerabilities and CVEs
|
|
- Security scores and risk assessment
|
|
- Dependency vulnerability analysis
|
|
- Remediation recommendations
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Checking security alerts for {package_name}")
|
|
result = await get_pypi_security_alerts(
|
|
package_name=package_name,
|
|
include_dependencies=include_dependencies,
|
|
severity_filter=severity_filter,
|
|
include_historical=include_historical,
|
|
)
|
|
logger.info(f"Successfully checked security alerts for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error checking security alerts for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error checking security alerts for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_package_rankings(
|
|
package_name: str,
|
|
search_terms: list[str] | None = None,
|
|
competitor_packages: list[str] | None = None,
|
|
ranking_metrics: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Analyze package rankings and visibility in PyPI search results.
|
|
|
|
This tool analyzes how well a package ranks for relevant search terms,
|
|
compares it to competitor packages, and provides insights into search
|
|
visibility and discoverability.
|
|
|
|
Args:
|
|
package_name: Name of the package to analyze rankings for
|
|
search_terms: List of search terms to test rankings against
|
|
competitor_packages: List of competitor packages to compare against
|
|
ranking_metrics: Specific metrics to focus on ('relevance', 'popularity', 'downloads', 'quality')
|
|
|
|
Returns:
|
|
Dictionary containing ranking analysis including:
|
|
- Search position for various terms
|
|
- Competitor comparison matrix
|
|
- Visibility and discoverability metrics
|
|
- SEO and keyword optimization suggestions
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Analyzing search rankings for {package_name}")
|
|
result = await get_pypi_package_rankings(
|
|
package_name=package_name,
|
|
search_terms=search_terms,
|
|
competitor_packages=competitor_packages,
|
|
ranking_metrics=ranking_metrics,
|
|
)
|
|
logger.info(f"Successfully analyzed rankings for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error analyzing rankings for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error analyzing rankings for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def analyze_package_competition(
|
|
package_name: str,
|
|
competitor_packages: list[str] | None = None,
|
|
analysis_depth: str = "comprehensive",
|
|
include_market_share: bool = True,
|
|
include_feature_comparison: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Perform comprehensive competitive analysis against similar packages.
|
|
|
|
This tool analyzes a package against its competitors, providing insights
|
|
into market positioning, feature gaps, adoption trends, and competitive
|
|
advantages.
|
|
|
|
Args:
|
|
package_name: Name of the package to analyze
|
|
competitor_packages: List of competitor packages (auto-detected if not provided)
|
|
analysis_depth: Depth of analysis ('basic', 'comprehensive', 'detailed')
|
|
include_market_share: Whether to include market share analysis
|
|
include_feature_comparison: Whether to include feature comparison
|
|
|
|
Returns:
|
|
Dictionary containing competitive analysis including:
|
|
- Market positioning and share
|
|
- Feature comparison matrix
|
|
- Adoption and growth trends
|
|
- Competitive advantages and weaknesses
|
|
- Strategic recommendations
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Analyzing competition for {package_name}")
|
|
result = await analyze_pypi_competition(
|
|
package_name=package_name,
|
|
competitor_packages=competitor_packages,
|
|
analysis_depth=analysis_depth,
|
|
include_market_share=include_market_share,
|
|
include_feature_comparison=include_feature_comparison,
|
|
)
|
|
logger.info(f"Successfully analyzed competition for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error analyzing competition for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error analyzing competition for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
# PyPI Discovery & Monitoring Tools
|
|
|
|
@mcp.tool()
|
|
async def monitor_pypi_new_releases_tool(
|
|
categories: list[str] | None = None,
|
|
hours: int = 24,
|
|
min_downloads: int | None = None,
|
|
maintainer_filter: str | None = None,
|
|
enable_notifications: bool = False,
|
|
cache_ttl: int = 300,
|
|
) -> dict[str, Any]:
|
|
"""Track new releases in specified categories over a time period.
|
|
|
|
This tool monitors PyPI for new package releases, providing comprehensive tracking
|
|
and analysis of recent activity in the Python ecosystem.
|
|
|
|
Args:
|
|
categories: List of categories to monitor (e.g., ["web", "data-science", "ai", "cli"])
|
|
hours: Number of hours to look back for new releases (default: 24)
|
|
min_downloads: Minimum monthly downloads to include (filters out very new packages)
|
|
maintainer_filter: Filter releases by specific maintainer names
|
|
enable_notifications: Whether to enable alert system for monitoring
|
|
cache_ttl: Cache time-to-live in seconds (default: 300)
|
|
|
|
Returns:
|
|
Dictionary containing new releases with metadata, analysis, and alerts
|
|
|
|
Raises:
|
|
NetworkError: If unable to fetch release data
|
|
SearchError: If category filtering fails
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Monitoring new PyPI releases for {hours}h, categories: {categories}")
|
|
result = await monitor_pypi_new_releases(
|
|
categories=categories,
|
|
hours=hours,
|
|
min_downloads=min_downloads,
|
|
maintainer_filter=maintainer_filter,
|
|
enable_notifications=enable_notifications,
|
|
cache_ttl=cache_ttl,
|
|
)
|
|
logger.info(f"Successfully monitored releases: {result['total_releases_found']} found")
|
|
return result
|
|
except (NetworkError, SearchError) as e:
|
|
logger.error(f"Error monitoring new releases: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"categories": categories,
|
|
"hours": hours,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error monitoring new releases: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"categories": categories,
|
|
"hours": hours,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_pypi_trending_today_tool(
|
|
category: str | None = None,
|
|
min_downloads: int = 1000,
|
|
limit: int = 50,
|
|
include_new_packages: bool = True,
|
|
trending_threshold: float = 1.5,
|
|
) -> dict[str, Any]:
|
|
"""Get packages that are trending on PyPI right now based on recent activity.
|
|
|
|
This tool analyzes current PyPI trends to identify packages gaining popularity
|
|
or showing significant activity increases today.
|
|
|
|
Args:
|
|
category: Optional category filter ("web", "ai", "data-science", etc.)
|
|
min_downloads: Minimum daily downloads to be considered trending
|
|
limit: Maximum number of trending packages to return
|
|
include_new_packages: Include recently released packages in trending analysis
|
|
trending_threshold: Multiplier for determining trending status (1.5 = 50% increase)
|
|
|
|
Returns:
|
|
Dictionary containing trending packages with activity metrics and market insights
|
|
|
|
Raises:
|
|
SearchError: If trending analysis fails
|
|
NetworkError: If unable to fetch trending data
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Analyzing today's PyPI trends, category: {category}")
|
|
result = await get_pypi_trending_today(
|
|
category=category,
|
|
min_downloads=min_downloads,
|
|
limit=limit,
|
|
include_new_packages=include_new_packages,
|
|
trending_threshold=trending_threshold,
|
|
)
|
|
logger.info(f"Successfully analyzed trends: {result['total_trending']} packages found")
|
|
return result
|
|
except (SearchError, NetworkError) as e:
|
|
logger.error(f"Error analyzing trending packages: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"category": category,
|
|
"limit": limit,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error analyzing trends: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"category": category,
|
|
"limit": limit,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def search_pypi_by_maintainer_tool(
|
|
maintainer: str,
|
|
include_email: bool = False,
|
|
sort_by: str = "popularity",
|
|
limit: int = 50,
|
|
include_stats: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Find all packages maintained by a specific maintainer or organization.
|
|
|
|
This tool searches PyPI to find all packages associated with a particular
|
|
maintainer, providing comprehensive portfolio analysis.
|
|
|
|
Args:
|
|
maintainer: Maintainer name or email to search for
|
|
include_email: Whether to search by email addresses too
|
|
sort_by: Sort results by ("popularity", "recent", "name", "downloads")
|
|
limit: Maximum number of packages to return
|
|
include_stats: Include download and popularity statistics
|
|
|
|
Returns:
|
|
Dictionary containing packages by the maintainer with detailed portfolio analysis
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If maintainer name is invalid
|
|
SearchError: If maintainer search fails
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Searching packages by maintainer: '{maintainer}'")
|
|
result = await search_pypi_by_maintainer(
|
|
maintainer=maintainer,
|
|
include_email=include_email,
|
|
sort_by=sort_by,
|
|
limit=limit,
|
|
include_stats=include_stats,
|
|
)
|
|
logger.info(f"Successfully found {result['total_packages']} packages for maintainer")
|
|
return result
|
|
except (InvalidPackageNameError, SearchError) as e:
|
|
logger.error(f"Error searching by maintainer {maintainer}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"maintainer": maintainer,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error searching by maintainer {maintainer}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"maintainer": maintainer,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_pypi_package_recommendations_tool(
|
|
package_name: str,
|
|
recommendation_type: str = "similar",
|
|
limit: int = 20,
|
|
include_alternatives: bool = True,
|
|
user_context: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Get PyPI's algorithm-based package recommendations and suggestions.
|
|
|
|
This tool provides intelligent package recommendations using advanced algorithms
|
|
that consider functionality, popularity, and user context.
|
|
|
|
Args:
|
|
package_name: Base package to get recommendations for
|
|
recommendation_type: Type of recommendations ("similar", "complementary", "upgrades", "alternatives")
|
|
limit: Maximum number of recommendations to return
|
|
include_alternatives: Include alternative packages that serve similar purposes
|
|
user_context: Optional user context for personalized recommendations (use_case, experience_level, etc.)
|
|
|
|
Returns:
|
|
Dictionary containing personalized package recommendations with detailed analysis
|
|
|
|
Raises:
|
|
PackageNotFoundError: If base package is not found
|
|
SearchError: If recommendation generation fails
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Generating recommendations for package: '{package_name}'")
|
|
result = await get_pypi_package_recommendations(
|
|
package_name=package_name,
|
|
recommendation_type=recommendation_type,
|
|
limit=limit,
|
|
include_alternatives=include_alternatives,
|
|
user_context=user_context,
|
|
)
|
|
logger.info(f"Successfully generated {result['total_recommendations']} recommendations")
|
|
return result
|
|
except (PackageNotFoundError, SearchError) as e:
|
|
logger.error(f"Error generating recommendations for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"recommendation_type": recommendation_type,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error generating recommendations for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"recommendation_type": recommendation_type,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_pypi_package_reviews_tool(
|
|
package_name: str,
|
|
include_ratings: bool = True,
|
|
include_community_feedback: bool = True,
|
|
sentiment_analysis: bool = False,
|
|
max_reviews: int = 50,
|
|
) -> dict[str, Any]:
|
|
"""Get community reviews and feedback for a PyPI package.
|
|
|
|
This tool aggregates community feedback from various sources including
|
|
GitHub discussions, issues, Stack Overflow mentions, and social media to
|
|
provide comprehensive community sentiment analysis.
|
|
|
|
Note: This is a future-ready implementation as PyPI doesn't currently have
|
|
a native review system. The function prepares for when such features become
|
|
available while providing useful community sentiment analysis from existing sources.
|
|
|
|
Args:
|
|
package_name: Name of the package to get reviews for
|
|
include_ratings: Whether to include numerical ratings (when available)
|
|
include_community_feedback: Whether to include textual feedback analysis
|
|
sentiment_analysis: Whether to perform sentiment analysis on feedback
|
|
max_reviews: Maximum number of reviews to return
|
|
|
|
Returns:
|
|
Dictionary containing review and feedback information including:
|
|
- Community sentiment and ratings
|
|
- Feedback from GitHub issues and discussions
|
|
- Social media mentions and sentiment
|
|
- Quality indicators and community health metrics
|
|
- Future-ready structure for native PyPI reviews
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Getting community reviews for package: {package_name}")
|
|
result = await get_pypi_package_reviews(
|
|
package_name=package_name,
|
|
include_ratings=include_ratings,
|
|
include_community_feedback=include_community_feedback,
|
|
sentiment_analysis=sentiment_analysis,
|
|
max_reviews=max_reviews,
|
|
)
|
|
logger.info(f"Successfully retrieved community reviews for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error getting reviews for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error getting reviews for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def manage_pypi_package_discussions_tool(
|
|
package_name: str,
|
|
action: str = "get_status",
|
|
discussion_settings: dict[str, Any] | None = None,
|
|
moderator_controls: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Manage and interact with PyPI package discussions.
|
|
|
|
This tool provides management capabilities for package discussions,
|
|
including enabling/disabling discussions, setting moderation policies,
|
|
and retrieving discussion status and metrics.
|
|
|
|
Note: This is a future-ready implementation as PyPI doesn't currently have
|
|
native discussion features. The function prepares for when such features
|
|
become available while providing integration with existing discussion platforms.
|
|
|
|
Args:
|
|
package_name: Name of the package to manage discussions for
|
|
action: Action to perform ('get_status', 'enable', 'disable', 'configure', 'moderate')
|
|
discussion_settings: Settings for discussions (when enabling/configuring)
|
|
moderator_controls: Moderation settings and controls
|
|
|
|
Returns:
|
|
Dictionary containing discussion management results including:
|
|
- Current discussion status and settings
|
|
- Available discussion platforms and integration status
|
|
- Moderation settings and community guidelines
|
|
- Discussion metrics and engagement data
|
|
- Future-ready structure for native PyPI discussions
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Managing discussions for package: {package_name}, action: {action}")
|
|
result = await manage_pypi_package_discussions(
|
|
package_name=package_name,
|
|
action=action,
|
|
discussion_settings=discussion_settings,
|
|
moderator_controls=moderator_controls,
|
|
)
|
|
logger.info(f"Successfully managed discussions for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error managing discussions for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
"action": action,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error managing discussions for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
"action": action,
|
|
}
|
|
|
|
|
|
@mcp.tool()
|
|
async def get_pypi_maintainer_contacts_tool(
|
|
package_name: str,
|
|
contact_types: list[str] | None = None,
|
|
include_social_profiles: bool = False,
|
|
include_contribution_guidelines: bool = True,
|
|
respect_privacy_settings: bool = True,
|
|
) -> dict[str, Any]:
|
|
"""Get contact information and communication channels for package maintainers.
|
|
|
|
This tool retrieves publicly available contact information for package
|
|
maintainers while respecting privacy settings and providing appropriate
|
|
communication channels for different types of inquiries.
|
|
|
|
Args:
|
|
package_name: Name of the package to get maintainer contacts for
|
|
contact_types: Types of contact info to retrieve ('email', 'github', 'social', 'support')
|
|
include_social_profiles: Whether to include social media profiles
|
|
include_contribution_guidelines: Whether to include contribution guidelines
|
|
respect_privacy_settings: Whether to respect maintainer privacy preferences
|
|
|
|
Returns:
|
|
Dictionary containing maintainer contact information including:
|
|
- Publicly available contact methods
|
|
- Communication preferences and guidelines
|
|
- Support channels and community resources
|
|
- Privacy-respecting contact recommendations
|
|
- Contribution guidelines and community standards
|
|
|
|
Raises:
|
|
InvalidPackageNameError: If package name is invalid
|
|
PackageNotFoundError: If package is not found
|
|
NetworkError: For network-related errors
|
|
"""
|
|
try:
|
|
logger.info(f"MCP tool: Getting maintainer contacts for package: {package_name}")
|
|
result = await get_pypi_maintainer_contacts(
|
|
package_name=package_name,
|
|
contact_types=contact_types,
|
|
include_social_profiles=include_social_profiles,
|
|
include_contribution_guidelines=include_contribution_guidelines,
|
|
respect_privacy_settings=respect_privacy_settings,
|
|
)
|
|
logger.info(f"Successfully retrieved maintainer contacts for package: {package_name}")
|
|
return result
|
|
except (InvalidPackageNameError, PackageNotFoundError, NetworkError) as e:
|
|
logger.error(f"Error getting maintainer contacts for {package_name}: {e}")
|
|
return {
|
|
"error": str(e),
|
|
"error_type": type(e).__name__,
|
|
"package_name": package_name,
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error getting maintainer contacts for {package_name}: {e}")
|
|
return {
|
|
"error": f"Unexpected error: {e}",
|
|
"error_type": "UnexpectedError",
|
|
"package_name": package_name,
|
|
}
|
|
|
|
|
|
@click.command()
|
|
@click.option(
|
|
"--log-level",
|
|
default="INFO",
|
|
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"]),
|
|
help="Logging level",
|
|
)
|
|
def main(log_level: str) -> None:
|
|
"""Start the PyPI Query MCP Server."""
|
|
# Set logging level
|
|
logging.getLogger().setLevel(getattr(logging, log_level))
|
|
|
|
logger.info("Starting PyPI Query MCP Server")
|
|
logger.info(f"Log level set to: {log_level}")
|
|
|
|
# Run the FastMCP server (uses STDIO transport by default)
|
|
mcp.run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|