diff --git a/eng/pypi/README.md b/eng/pypi/README.md new file mode 100644 index 0000000000..f59e44e537 --- /dev/null +++ b/eng/pypi/README.md @@ -0,0 +1,177 @@ +# PyPI Packaging for Azure MCP Server + +This document describes how the Azure MCP Server is packaged for distribution on [PyPI](https://pypi.org), enabling installation via `pip`, `pipx`, or execution via `uvx`. + +## Package Architecture + +The Azure MCP Server is published as a single package (`msmcp-azure`) with platform-specific wheels. Each wheel contains the pre-compiled binary for a specific OS and architecture combination. + +### Wheel Naming Convention + +Wheels follow PyPI's platform tag conventions: + +- `msmcp_azure-1.0.0-py3-none-win_amd64.whl` - Windows x64 +- `msmcp_azure-1.0.0-py3-none-win_arm64.whl` - Windows ARM64 +- `msmcp_azure-1.0.0-py3-none-macosx_11_0_x86_64.whl` - macOS x64 (Intel) +- `msmcp_azure-1.0.0-py3-none-macosx_11_0_arm64.whl` - macOS ARM64 (Apple Silicon) +- `msmcp_azure-1.0.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl` - Linux x64 +- `msmcp_azure-1.0.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl` - Linux ARM64 + +When you install with `pip install msmcp-azure`, pip automatically selects the correct wheel for your platform. + +## Installation Methods + +### Using pip + +```bash +# Install - pip automatically selects the correct wheel for your platform +pip install msmcp-azure + +# Run +azmcp server start +``` + +### Using uvx (recommended for MCP servers) + +```bash +# Run directly without installation +uvx msmcp-azure server start +``` + +### Using pipx + +```bash +# Install as a global tool +pipx install msmcp-azure + +# Run +azmcp server start +``` + +## Configuration with MCP Clients + +### VS Code / GitHub Copilot + +```json +{ + "mcpServers": { + "azure": { + "command": "uvx", + "args": ["msmcp-azure", "server", "start"] + } + } +} +``` + +### Claude Desktop + +```json +{ + "mcpServers": { + "azure": { + "command": "uvx", + "args": ["msmcp-azure", "server", "start"] + } + } +} +``` + +## Building PyPI Packages + +### Prerequisites + +1. Python 3.10+ with `pip` and `build` package +2. .NET SDK for building server binaries +3. PowerShell 7+ + +### Build Steps + +```powershell +# 1. Create build info +./eng/scripts/New-BuildInfo.ps1 -PublishTarget internal + +# 2. Build the server binaries for all platforms +./eng/scripts/Build-Code.ps1 -OperatingSystems windows,linux,macos -Architectures x64,arm64 + +# 3. Create PyPI packages +./eng/scripts/Pack-Pypi.ps1 + +# Output will be in .work/packages_pypi/ +``` + +### Local Development + +For quick local testing: + +```powershell +# Build for current platform only +./eng/scripts/Build-Code.ps1 + +# Create PyPI packages +./eng/scripts/Pack-Pypi.ps1 -UsePaths + +# Install locally for testing +pip install .work/packages_pypi/Azure.Mcp.Server/*.whl +``` + +## Publishing to PyPI + +### Test PyPI (recommended for testing) + +```bash +# Upload to Test PyPI +twine upload --repository testpypi .work/packages_pypi/Azure.Mcp.Server/*.whl .work/packages_pypi/Azure.Mcp.Server/*.tar.gz + +# Test installation +pip install --index-url https://test.pypi.org/simple/ msmcp-azure +``` + +### Production PyPI + +```bash +# Upload to production PyPI +twine upload .work/packages_pypi/Azure.Mcp.Server/*.whl .work/packages_pypi/Azure.Mcp.Server/*.tar.gz +``` + +## Project Structure + +``` +eng/ +├── pypi/ +│ ├── __init__.py # Package entry point with binary execution +│ ├── pyproject.toml.template # Template for pyproject.toml +│ └── README.md # This file +└── scripts/ + └── Pack-Pypi.ps1 # Packaging script +``` + +## Debugging + +Set the `DEBUG` environment variable to enable verbose logging: + +```bash +# Enable debug output +DEBUG=true azmcp server start + +# Or +DEBUG=mcp azmcp server start +``` + +## Troubleshooting + +### Unsupported platform + +If you see an error about an unsupported platform, check that your OS and architecture combination is in the supported list above. + +### Permission denied on Unix + +The package sets executable permissions during installation, but if you encounter issues: + +```bash +chmod +x $(python -c "import msmcp_azure; print(msmcp_azure.get_executable_path())") +``` + +### Reporting Issues + +If you encounter issues not covered here, please report them at: +https://github.com/microsoft/mcp/issues diff --git a/eng/pypi/__init__.py b/eng/pypi/__init__.py new file mode 100644 index 0000000000..35838553bb --- /dev/null +++ b/eng/pypi/__init__.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Azure MCP Server - PyPI package. + +This module provides the entry point for the Azure MCP Server CLI. +The binary is bundled directly in the wheel for the target platform. +""" + +import os +import platform +import subprocess +import sys +from pathlib import Path + +__version__ = "0.0.0" # Will be replaced during packaging + +# Debug mode check +DEBUG = os.environ.get("DEBUG", "").lower() in ("true", "1", "*") or "mcp" in os.environ.get("DEBUG", "") + + +def debug_log(*args, **kwargs): + """Print debug messages to stderr if DEBUG is enabled.""" + if DEBUG: + print(*args, file=sys.stderr, **kwargs) + + +def get_executable_path(): + """Get the path to the platform-specific executable.""" + # The binary is located in the bin subdirectory of this package + package_dir = Path(__file__).parent + bin_dir = package_dir / "bin" + + # Determine the executable name based on platform + system = platform.system().lower() + if system == "windows": + executable_name = "azmcp.exe" + else: + executable_name = "azmcp" + + executable_path = bin_dir / executable_name + + debug_log(f"Package directory: {package_dir}") + debug_log(f"Binary directory: {bin_dir}") + debug_log(f"Executable path: {executable_path}") + + return executable_path + + +def run_executable(args=None): + """ + Run the platform-specific executable with the given arguments. + + Args: + args: List of command-line arguments to pass to the executable. + Defaults to sys.argv[1:] if not provided. + + Returns: + The exit code from the executable. + """ + if args is None: + args = sys.argv[1:] + + executable_path = get_executable_path() + + if not executable_path.exists(): + print(f"Error: Executable not found at {executable_path}", file=sys.stderr) + print(f"This may indicate a packaging issue or unsupported platform.", file=sys.stderr) + return 1 + + debug_log(f"Running: {executable_path} {' '.join(args)}") + + try: + result = subprocess.run( + [str(executable_path)] + list(args), + stdin=sys.stdin, + stdout=sys.stdout, + stderr=sys.stderr, + ) + return result.returncode + except PermissionError: + print(f"Error: Permission denied executing {executable_path}", file=sys.stderr) + print("Try: chmod +x " + str(executable_path), file=sys.stderr) + return 126 + except OSError as e: + print(f"Error executing {executable_path}: {e}", file=sys.stderr) + return 1 + + +def main(): + """Main entry point for the CLI.""" + sys.exit(run_executable()) + + +if __name__ == "__main__": + main() diff --git a/eng/pypi/pyproject.toml.template b/eng/pypi/pyproject.toml.template new file mode 100644 index 0000000000..7f56401a70 --- /dev/null +++ b/eng/pypi/pyproject.toml.template @@ -0,0 +1,51 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "{{PACKAGE_NAME}}" +version = "{{VERSION}}" +description = "{{DESCRIPTION}}" +readme = "README.md" +license = "MIT" +authors = [ + { name = "Microsoft", email = "azuremcp@microsoft.com" } +] +keywords = [{{KEYWORDS}}] +classifiers = [ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: {{OS_CLASSIFIER}}", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", +] +requires-python = ">=3.10" +dependencies = [] + +[project.scripts] +{{CLI_NAME}} = "{{MODULE_NAME}}:main" + +[project.urls] +Homepage = "{{HOMEPAGE}}" +Documentation = "{{HOMEPAGE}}" +Repository = "https://github.com/microsoft/mcp" +Issues = "https://github.com/microsoft/mcp/issues" + +[tool.hatch.build.targets.wheel] +packages = ["src/{{MODULE_NAME}}"] +artifacts = ["src/{{MODULE_NAME}}/bin/*"] + +[tool.hatch.build.targets.sdist] +include = [ + "src/{{MODULE_NAME}}/**/*.py", + "src/{{MODULE_NAME}}/bin/**/*", + "README.md", + "LICENSE", + "NOTICE.txt", +] diff --git a/eng/pypi/wrapper/__init__.py b/eng/pypi/wrapper/__init__.py new file mode 100644 index 0000000000..fa3e5c6b21 --- /dev/null +++ b/eng/pypi/wrapper/__init__.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +""" +Azure MCP Server - Cross-platform wrapper package. + +This module provides a cross-platform entry point for the Azure MCP Server. +It automatically detects the current platform and delegates to the appropriate +platform-specific package. +""" + +import os +import platform +import subprocess +import sys +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as get_version + +__version__ = "0.0.0" # Will be replaced during packaging + +# Debug mode check +DEBUG = os.environ.get("DEBUG", "").lower() in ("true", "1", "*") or "mcp" in os.environ.get("DEBUG", "") + + +def debug_log(*args, **kwargs): + """Print debug messages to stderr if DEBUG is enabled.""" + if DEBUG: + print(*args, file=sys.stderr, **kwargs) + + +def get_platform_info(): + """Get the current platform and architecture.""" + system = platform.system().lower() + machine = platform.machine().lower() + + # Map OS names to PyPI conventions + os_map = { + "windows": "win32", + "darwin": "darwin", + "linux": "linux", + } + + # Map architecture names + arch_map = { + "x86_64": "x64", + "amd64": "x64", + "aarch64": "arm64", + "arm64": "arm64", + } + + pypi_os = os_map.get(system, system) + pypi_arch = arch_map.get(machine, machine) + + return pypi_os, pypi_arch + + +def find_platform_package(): + """Find and return the platform-specific package module.""" + pypi_os, pypi_arch = get_platform_info() + + # Get the base package name from this package's name + try: + # Try to determine the base package name + base_name = __name__.replace("_", "-") + if base_name.endswith("-"): + base_name = base_name[:-1] + except Exception: + base_name = "msmcp-azure" + + platform_package_name = f"{base_name}-{pypi_os}-{pypi_arch}" + module_name = platform_package_name.replace("-", "_") + + debug_log(f"Looking for platform package: {platform_package_name}") + debug_log(f"Module name: {module_name}") + + try: + # Try to import the platform-specific package + platform_module = __import__(module_name) + debug_log(f"Successfully loaded {platform_package_name}") + return platform_module + except ImportError as e: + debug_log(f"Failed to import {module_name}: {e}") + return None + + +def install_platform_package(): + """Attempt to install the platform-specific package.""" + pypi_os, pypi_arch = get_platform_info() + + try: + base_name = __name__.replace("_", "-") + if base_name.endswith("-"): + base_name = base_name[:-1] + except Exception: + base_name = "msmcp-azure" + + platform_package_name = f"{base_name}-{pypi_os}-{pypi_arch}" + + print(f"Installing missing platform package: {platform_package_name}", file=sys.stderr) + + try: + subprocess.check_call( + [sys.executable, "-m", "pip", "install", platform_package_name], + stdout=subprocess.DEVNULL if not DEBUG else None, + stderr=subprocess.DEVNULL if not DEBUG else None, + ) + print(f"✅ Successfully installed {platform_package_name}", file=sys.stderr) + return True + except subprocess.CalledProcessError as e: + debug_log(f"pip install failed: {e}") + return False + + +def print_troubleshooting(): + """Print troubleshooting steps for users.""" + pypi_os, pypi_arch = get_platform_info() + + try: + base_name = __name__.replace("_", "-") + if base_name.endswith("-"): + base_name = base_name[:-1] + except Exception: + base_name = "msmcp-azure" + + platform_package_name = f"{base_name}-{pypi_os}-{pypi_arch}" + + print(f""" +❌ Failed to load platform-specific package '{platform_package_name}' + +🔍 Troubleshooting steps: + +1. Install the platform-specific package manually: + pip install {platform_package_name} + +2. If using uvx, try with the platform extra: + uvx --with {base_name}[{pypi_os}-{pypi_arch}] {base_name} + +3. Check if your platform is supported: + - Windows x64: {base_name}-win32-x64 + - Windows ARM64: {base_name}-win32-arm64 + - macOS x64: {base_name}-darwin-x64 + - macOS ARM64: {base_name}-darwin-arm64 + - Linux x64: {base_name}-linux-x64 + - Linux ARM64: {base_name}-linux-arm64 + +4. If the issue persists, please report it at: + https://github.com/microsoft/mcp/issues +""", file=sys.stderr) + + +def main(): + """Main entry point for the CLI.""" + debug_log("\nWrapper package starting") + debug_log(f"Python version: {sys.version}") + debug_log(f"Platform: {platform.system()} {platform.machine()}") + debug_log(f"Arguments: {sys.argv[1:]}") + + # Try to find the platform package + platform_module = find_platform_package() + + if platform_module is None: + # Try to install it + if install_platform_package(): + platform_module = find_platform_package() + + if platform_module is None: + print_troubleshooting() + sys.exit(1) + + # Run the executable from the platform package + try: + exit_code = platform_module.run_executable(sys.argv[1:]) + sys.exit(exit_code) + except AttributeError: + print(f"Error: Platform package does not have run_executable function", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error running executable: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/eng/scripts/Get-ProjectProperties.ps1 b/eng/scripts/Get-ProjectProperties.ps1 index 5fcbe179e4..5108b06725 100644 --- a/eng/scripts/Get-ProjectProperties.ps1 +++ b/eng/scripts/Get-ProjectProperties.ps1 @@ -54,6 +54,10 @@ $propertyList = @( 'DnxDescription', 'DnxToolCommandName', + 'PypiPackageName', + 'PypiPackageKeywords', + 'PypiDescription', + 'IsAotCompatible', 'McpRepositoryName' diff --git a/eng/scripts/New-BuildInfo.ps1 b/eng/scripts/New-BuildInfo.ps1 index 80a279a024..10b1852530 100644 --- a/eng/scripts/New-BuildInfo.ps1 +++ b/eng/scripts/New-BuildInfo.ps1 @@ -510,9 +510,12 @@ function Get-ServerDetails { dnxDescription = $props.DnxDescription dnxToolCommandName = $props.DnxToolCommandName dnxPackageTags = @($props.DnxPackageTags -split '[;,] *' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) + pypiPackageName = $props.PypiPackageName + pypiDescription = $props.PypiDescription + pypiPackageKeywords = @($props.PypiPackageKeywords -split '[;,] *' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) + platforms = $platforms mcpRepositoryName = $props.McpRepositoryName serverJsonPath = $props.ServerJsonPath | Get-RepoRelativePath -NormalizeSeparators - platforms = $platforms } } diff --git a/eng/scripts/Pack-Pypi.ps1 b/eng/scripts/Pack-Pypi.ps1 new file mode 100644 index 0000000000..602b88112a --- /dev/null +++ b/eng/scripts/Pack-Pypi.ps1 @@ -0,0 +1,384 @@ +#!/bin/env pwsh +#Requires -Version 7 + +<# +.SYNOPSIS + Packs Azure MCP Server binaries into PyPI wheels for distribution. + +.DESCRIPTION + This script creates platform-specific PyPI wheels for the Azure MCP Server. + Each wheel contains the binary for a specific platform (OS + architecture). + + The wheels are named according to PyPI conventions: + - msmcp_azure-1.0.0-py3-none-win_amd64.whl + - msmcp_azure-1.0.0-py3-none-macosx_11_0_arm64.whl + - msmcp_azure-1.0.0-py3-none-manylinux_2_17_x86_64.whl + etc. + + Users can install with: + - pip install msmcp-azure + - uvx msmcp-azure + - pipx install msmcp-azure + +.PARAMETER ArtifactsPath + Path to the build artifacts containing the server binaries. + +.PARAMETER BuildInfoPath + Path to the build_info.json file containing server and platform details. + +.PARAMETER OutputPath + Path where the PyPI packages will be created. + +.PARAMETER UsePaths + Switch to use default paths for local development. + +.EXAMPLE + ./Pack-Pypi.ps1 -UsePaths + Creates PyPI packages using default local paths. + +.EXAMPLE + ./Pack-Pypi.ps1 -ArtifactsPath ".work/build" -BuildInfoPath ".work/build_info.json" + Creates PyPI packages using specified artifact and build info paths. +#> + +[CmdletBinding()] +param( + [string] $ArtifactsPath, + [string] $BuildInfoPath, + [string] $OutputPath, + [switch] $UsePaths +) + +$ErrorActionPreference = "Stop" +. "$PSScriptRoot/../common/scripts/common.ps1" +$RepoRoot = $RepoRoot.Path.Replace('\', '/') + +$pypiSourcePath = "$RepoRoot/eng/pypi" + +# When running locally, ignore missing artifacts instead of failing +$ignoreMissingArtifacts = $env:TF_BUILD -ne 'true' +$exitCode = 0 + +if (!$ArtifactsPath) { + $ArtifactsPath = "$RepoRoot/.work/build" +} + +if (!$BuildInfoPath) { + $BuildInfoPath = "$RepoRoot/.work/build_info.json" +} + +if (!$OutputPath) { + $OutputPath = "$RepoRoot/.work/packages_pypi" + Remove-Item -Path $OutputPath -Recurse -Force -ErrorAction SilentlyContinue -ProgressAction SilentlyContinue +} + +if (!(Test-Path $ArtifactsPath)) { + LogError "Artifacts path $ArtifactsPath does not exist." + $exitCode = 1 +} + +if (!(Test-Path $BuildInfoPath)) { + LogError "Build info file $BuildInfoPath does not exist. Run eng/scripts/New-BuildInfo.ps1 to create it." + $exitCode = 1 +} + +if ($exitCode -ne 0) { + exit $exitCode +} + +$buildInfo = Get-Content $BuildInfoPath -Raw | ConvertFrom-Json -AsHashtable + +$tempFolder = "$RepoRoot/.work/temp_pypi" + +# Map node OS names to PyPI wheel platform tags +# See: https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/ +$wheelPlatformMap = @{ + 'win32-x64' = 'win_amd64' + 'win32-arm64' = 'win_arm64' + 'darwin-x64' = 'macosx_11_0_x86_64' + 'darwin-arm64' = 'macosx_11_0_arm64' + 'linux-x64' = 'manylinux_2_17_x86_64.manylinux2014_x86_64' + 'linux-arm64' = 'manylinux_2_17_aarch64.manylinux2014_aarch64' +} + +# Map OS names to Python classifier OS names +$osClassifierMap = @{ + 'win32' = 'Microsoft :: Windows' + 'darwin' = 'MacOS' + 'linux' = 'POSIX :: Linux' +} + +function Get-ModuleName($packageName) { + return $packageName.Replace('-', '_') +} + +function Get-KeywordsString($keywords) { + return ($keywords | ForEach-Object { "`"$_`"" }) -join ', ' +} + +function BuildServerPackages([hashtable] $server, [bool] $native) { + $serverDirectory = "$ArtifactsPath/$($server.artifactPath)" + + if (!(Test-Path $serverDirectory)) { + $message = "Server directory $serverDirectory does not exist." + if ($ignoreMissingArtifacts) { + Write-Warning $message + } + else { + Write-Error $message + } + return + } + + $filteredPlatforms = $server.platforms | Where-Object { $_.native -eq $native -and -not $_.specialPurpose } + if ($filteredPlatforms.Count -eq 0) { + Write-Host "No platforms to build for server $($server.name) with native=$native" + return + } + + $serverOutputPath = "$OutputPath/$($server.artifactPath)" + New-Item -ItemType Directory -Force -Path $serverOutputPath | Out-Null + + # Use PyPI package name from csproj + $basePackageName = $server.pypiPackageName ?? $server.cliName + $description = $server.pypiDescription ?? $server.description + $cliName = $server.cliName + $keywords = @($server.pypiPackageKeywords ?? $server.npmPackageKeywords) + $moduleName = Get-ModuleName $basePackageName + + if ($native) { + $basePackageName += "-native" + $description += " with native dependencies" + $keywords += "native" + $moduleName = Get-ModuleName $basePackageName + } + + $builtPlatforms = @() + + # Build a wheel for each platform + foreach ($platform in $filteredPlatforms) { + $platformDirectory = "$ArtifactsPath/$($platform.artifactPath)" + + if (!(Test-Path $platformDirectory)) { + $errorMessage = "Platform directory $platformDirectory does not exist." + if ($ignoreMissingArtifacts) { + Write-Warning $errorMessage + continue + } + + Write-Error $errorMessage + return + } + + $pypiOs = $platform.nodeOs + $arch = $platform.architecture + $platformKey = "$pypiOs-$arch" + $wheelPlatformTag = $wheelPlatformMap[$platformKey] + + if (!$wheelPlatformTag) { + Write-Warning "Unknown platform: $platformKey, skipping" + continue + } + + $osClassifier = $osClassifierMap[$pypiOs] + $extension = $platform.extension + + Write-Host "`nBuilding wheel for $basePackageName ($platformKey)" -ForegroundColor Cyan + + # Clean temp folder + Remove-Item -Path $tempFolder -Recurse -Force -ErrorAction SilentlyContinue -ProgressAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $tempFolder | Out-Null + New-Item -ItemType Directory -Force -Path "$tempFolder/src/$moduleName/bin" | Out-Null + + # Copy binary files + Write-Host " Copying binaries from $platformDirectory" + Copy-Item -Path "$platformDirectory/*" -Destination "$tempFolder/src/$moduleName/bin" -Recurse -Force + + # Copy package __init__.py + Copy-Item -Path "$pypiSourcePath/__init__.py" -Destination "$tempFolder/src/$moduleName/__init__.py" -Force + + # Remove symbols files before packing + Write-Host " Removing symbol files" + Get-ChildItem -Path $tempFolder -Recurse -Include "*.pdb", "*.dSYM", "*.dbg" | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue -ProgressAction SilentlyContinue + + # Read template and replace placeholders + $pyprojectTemplate = Get-Content "$pypiSourcePath/pyproject.toml.template" -Raw + + $pyprojectContent = $pyprojectTemplate ` + -replace '{{PACKAGE_NAME}}', $basePackageName ` + -replace '{{VERSION}}', $server.version ` + -replace '{{DESCRIPTION}}', $description ` + -replace '{{KEYWORDS}}', (Get-KeywordsString $keywords) ` + -replace '{{OS_CLASSIFIER}}', $osClassifier ` + -replace '{{CLI_NAME}}', $cliName ` + -replace '{{MODULE_NAME}}', $moduleName ` + -replace '{{HOMEPAGE}}', $server.readmeUrl + + $pyprojectPath = "$tempFolder/pyproject.toml" + Write-Host " Writing pyproject.toml" + $pyprojectContent | Out-File -FilePath $pyprojectPath -Encoding utf8 -Force + + # Update version in __init__.py + $initPyPath = "$tempFolder/src/$moduleName/__init__.py" + $initPyContent = Get-Content $initPyPath -Raw + $initPyContent = $initPyContent -replace '__version__ = "0\.0\.0"', "__version__ = `"$($server.version)`"" + $initPyContent | Out-File -FilePath $initPyPath -Encoding utf8 -Force + + # Set executable permissions on non-Windows + $binPath = "bin/$cliName$extension" + if (!$IsWindows) { + Write-Host " Setting executable permissions" -ForegroundColor Yellow + $binFullPath = "$tempFolder/src/$moduleName/$binPath" + if (Test-Path $binFullPath) { + Invoke-LoggedCommand "chmod +x `"$binFullPath`"" + } + } + else { + Write-Warning " Executable permissions are not set when packing on a Windows agent." + } + + # Process and copy README + & "$RepoRoot/eng/scripts/Process-PackageReadMe.ps1" ` + -Command "extract" ` + -InputReadMePath "$RepoRoot/$($server.readmePath)" ` + -PackageType "pypi" ` + -InsertPayload @{ ToolTitle = 'PyPI Package' } ` + -OutputDirectory $tempFolder + + Write-Host " Copying LICENSE and NOTICE.txt" + Copy-Item -Path "$RepoRoot/LICENSE" -Destination $tempFolder -Force + Copy-Item -Path "$RepoRoot/NOTICE.txt" -Destination $tempFolder -Force + + # Build the wheel with platform-specific tag + Write-Host " Building wheel" -ForegroundColor Green + Push-Location $tempFolder + try { + $pythonCmd = if (Get-Command python3 -ErrorAction SilentlyContinue) { "python3" } else { "python" } + + Invoke-LoggedCommand "$pythonCmd -m pip install --quiet build wheel" + + # Build wheel only (no sdist for platform packages) + # We use --wheel and then rename to set the correct platform tag + Invoke-LoggedCommand "$pythonCmd -m build --wheel" + + # Rename the wheel to include the correct platform tag + $distPath = "$tempFolder/dist" + if (Test-Path $distPath) { + $wheels = Get-ChildItem -Path $distPath -Filter "*.whl" + foreach ($wheel in $wheels) { + # The default wheel name is like: msmcp_azure-1.0.0-py3-none-any.whl + # We need to change it to: msmcp_azure-1.0.0-py3-none-.whl + $newName = $wheel.Name -replace '-py3-none-any\.whl$', "-py3-none-$wheelPlatformTag.whl" + $newPath = Join-Path $distPath $newName + + Write-Host " Renaming wheel to $newName" + Move-Item -Path $wheel.FullName -Destination $newPath -Force + + # Copy to output + Copy-Item -Path $newPath -Destination $serverOutputPath -Force + Write-Host " ✅ Created: $newName" -ForegroundColor Green + } + } + } + finally { + Pop-Location + } + + $builtPlatforms += $platformKey + } + + # Build a source distribution (sdist) - just once, platform-independent + # The sdist doesn't include binaries, it's for reference/transparency only + Write-Host "`nBuilding source distribution for $basePackageName" -ForegroundColor Cyan + + Remove-Item -Path $tempFolder -Recurse -Force -ErrorAction SilentlyContinue -ProgressAction SilentlyContinue + New-Item -ItemType Directory -Force -Path $tempFolder | Out-Null + New-Item -ItemType Directory -Force -Path "$tempFolder/src/$moduleName" | Out-Null + + # Copy package __init__.py (without binaries) + Copy-Item -Path "$pypiSourcePath/__init__.py" -Destination "$tempFolder/src/$moduleName/__init__.py" -Force + + # Create pyproject.toml for sdist (OS Independent) + $pyprojectTemplate = Get-Content "$pypiSourcePath/pyproject.toml.template" -Raw + + $pyprojectContent = $pyprojectTemplate ` + -replace '{{PACKAGE_NAME}}', $basePackageName ` + -replace '{{VERSION}}', $server.version ` + -replace '{{DESCRIPTION}}', $description ` + -replace '{{KEYWORDS}}', (Get-KeywordsString $keywords) ` + -replace '{{OS_CLASSIFIER}}', 'OS Independent' ` + -replace '{{CLI_NAME}}', $cliName ` + -replace '{{MODULE_NAME}}', $moduleName ` + -replace '{{HOMEPAGE}}', $server.readmeUrl + + # Remove the artifacts line for sdist since there are no binaries + $pyprojectContent = $pyprojectContent -replace 'artifacts = \["src/{{MODULE_NAME}}/bin/\*"\]\n', '' + $pyprojectContent = $pyprojectContent -replace 'artifacts = \["src/[^"]+/bin/\*"\]\n', '' + + $pyprojectPath = "$tempFolder/pyproject.toml" + $pyprojectContent | Out-File -FilePath $pyprojectPath -Encoding utf8 -Force + + # Update version in __init__.py + $initPyPath = "$tempFolder/src/$moduleName/__init__.py" + $initPyContent = Get-Content $initPyPath -Raw + $initPyContent = $initPyContent -replace '__version__ = "0\.0\.0"', "__version__ = `"$($server.version)`"" + $initPyContent | Out-File -FilePath $initPyPath -Encoding utf8 -Force + + # Process and copy README + & "$RepoRoot/eng/scripts/Process-PackageReadMe.ps1" ` + -Command "extract" ` + -InputReadMePath "$RepoRoot/$($server.readmePath)" ` + -PackageType "pypi" ` + -InsertPayload @{ ToolTitle = 'PyPI Package' } ` + -OutputDirectory $tempFolder + + Copy-Item -Path "$RepoRoot/LICENSE" -Destination $tempFolder -Force + Copy-Item -Path "$RepoRoot/NOTICE.txt" -Destination $tempFolder -Force + + Push-Location $tempFolder + try { + $pythonCmd = if (Get-Command python3 -ErrorAction SilentlyContinue) { "python3" } else { "python" } + + Invoke-LoggedCommand "$pythonCmd -m pip install --quiet build" + Invoke-LoggedCommand "$pythonCmd -m build --sdist" + + $distPath = "$tempFolder/dist" + if (Test-Path $distPath) { + Copy-Item -Path "$distPath/*.tar.gz" -Destination $serverOutputPath -Force + Write-Host " ✅ Created source distribution" -ForegroundColor Green + } + } + finally { + Pop-Location + } + + Write-Host "`n✅ PyPI packages built successfully for $($server.name)" -ForegroundColor Green + Write-Host " Package: $basePackageName" + Write-Host " Platforms: $($builtPlatforms -join ', ')" +} + +# Main execution +foreach ($server in $buildInfo.servers) { + Write-Host "`n========================================" -ForegroundColor Cyan + Write-Host "Building PyPI packages for $($server.name)" -ForegroundColor Cyan + Write-Host "========================================" -ForegroundColor Cyan + + # Build non-native packages + BuildServerPackages $server $false + + # Build native packages if available + $hasNative = ($server.platforms | Where-Object { $_.native -eq $true }).Count -gt 0 + if ($hasNative) { + BuildServerPackages $server $true + } +} + +# Cleanup temp folder +Remove-Item -Path $tempFolder -Recurse -Force -ErrorAction SilentlyContinue -ProgressAction SilentlyContinue + +Write-Host "`n========================================" -ForegroundColor Green +Write-Host "PyPI packaging complete!" -ForegroundColor Green +Write-Host "Output: $OutputPath" -ForegroundColor Green +Write-Host "========================================" -ForegroundColor Green + +exit $exitCode diff --git a/eng/scripts/Process-PackageReadMe.ps1 b/eng/scripts/Process-PackageReadMe.ps1 index d21b29afd9..cdfd51db91 100644 --- a/eng/scripts/Process-PackageReadMe.ps1 +++ b/eng/scripts/Process-PackageReadMe.ps1 @@ -44,7 +44,7 @@ param ( [string] $Command, [string] $InputReadMePath, [string] $OutputDirectory, - [ValidateSet('nuget','npm','vsix')] + [ValidateSet('nuget','npm','vsix','pypi')] [string] $PackageType, [hashtable] $InsertPayload = @{} ) @@ -101,7 +101,7 @@ function Extract-PackageSpecificReadMe { [Parameter(Mandatory=$true)] [string] $OutputDirectory, [Parameter(Mandatory=$true)] - [ValidateSet('nuget','npm','vsix')] + [ValidateSet('nuget','npm','vsix','pypi')] [string] $PackageType, [hashtable] $InsertPayload = @{} ) diff --git a/servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj b/servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj index 7ccc36c4ba..b592138ace 100644 --- a/servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj +++ b/servers/Azure.Mcp.Server/src/Azure.Mcp.Server.csproj @@ -23,6 +23,10 @@ Azure.Mcp $(CliName) azure,mcp + + + msmcp-azure + azure,mcp,model-context-protocol,ai,llm