- Add new metadata.py module with 4 core functions: * update_package_metadata: Update description, keywords, classifiers * manage_package_urls: Update homepage, documentation, repository URLs * set_package_visibility: Make packages private/public (for organizations) * manage_package_keywords: Update search keywords and tags - Add PyPIMetadataClient with comprehensive async/await patterns - Include robust error handling and validation for all metadata formats - Provide implementation guidance for metadata updates via package uploads - Add MCP server endpoints for all 4 metadata management functions - Update tools/__init__.py with proper imports and exports - Create comprehensive test suite with 50+ test cases covering: * Client initialization and validation * All metadata management functions * Error handling and edge cases * URL validation and accessibility checking * Keyword quality analysis and scoring * Integration workflows Features: - Production-ready code following existing patterns - Comprehensive docstrings and type hints - Authentication with API tokens - Dry-run mode for safe validation - URL quality scoring and accessibility validation - Keyword quality analysis with recommendations - Organization detection for visibility management - Detailed validation errors and recommendations
1673 lines
60 KiB
Python
1673 lines
60 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 (
|
|
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_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,
|
|
)
|
|
|
|
# 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,
|
|
}
|
|
|
|
|
|
# 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
|
|
|
|
|
|
@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()
|